用PyQt5绘制正确的网格

Draw a correct grid with PyQt5

我在使用 PyQt5 时遇到了一些困难:我必须实现 Conway 的生命游戏,并且我从 GUI 常规设置开始。我考虑过(垂直)堆叠两个小部件,一个用于显示游戏板,另一个包含按钮和滑块。

这是我想出来的(我是个菜鸟)

我想根据边缘正确调整网格。看起来它在专用 canvas 下方构建了网格:首先修复 canvas 然后在其上绘制会很棒,但是整个布局、小部件和所有这些都让我大吃一惊。

这是我的(写得又快又烂的)代码

import sys

from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton, QWidget
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QPixmap, QColor, QPainter

WINDOW_WIDTH, WINDOW_HEIGHT = 800, 600
SQUARE_SIDE = 20
ROWS, COLS = int(WINDOW_HEIGHT/SQUARE_SIDE), int(WINDOW_WIDTH/2*SQUARE_SIDE)

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        layout = QVBoxLayout()
        buttons_layout = QHBoxLayout()
        self.label = QLabel()
        self.label.setContentsMargins(0,0,0,0)
        self.label.setStyleSheet('background-color: white; ')
        self.label.setAlignment(Qt.AlignCenter)
        slider = QSlider(Qt.Horizontal)
        start_button = QPushButton('Start')
        pause_button = QPushButton('Pause')
        reset_button = QPushButton('Reset')
        load_button = QPushButton('Load')
        save_button = QPushButton('Save')
        layout.addWidget(self.label)
        buttons_layout.addWidget(start_button)
        buttons_layout.addWidget(pause_button)
        buttons_layout.addWidget(reset_button)
        buttons_layout.addWidget(load_button)
        buttons_layout.addWidget(save_button)
        buttons_layout.addWidget(slider)
        layout.addLayout(buttons_layout)
        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.make_grid()

    def make_grid(self):
        _canvas = QPixmap(WINDOW_WIDTH, WINDOW_HEIGHT)
        _canvas.fill(QColor("#ffffff"))
        self.label.setPixmap(_canvas)
        painter = QPainter(self.label.pixmap())
        for c in range(COLS):
            painter.drawLine(SQUARE_SIDE*c, WINDOW_HEIGHT, SQUARE_SIDE*c, 0)
        for r in range(ROWS):
            painter.drawLine(0, SQUARE_SIDE*r, WINDOW_WIDTH, SQUARE_SIDE*r)



if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT)
    window.setWindowTitle("Conway's Game of Life")
    window.show()
    app.exec_()

感谢您的帮助,祝您愉快!

像素图未以完整尺寸显示的原因是因为您对 window 和 [= 使用了 WINDOW_WIDTHWINDOW_HEIGHT 45=] 像素图。由于 window 还包含工具栏和它自己的边距,您强制它比它应该的要小,因此板的 "clipping out"。

更简单的解决方案是设置标签的 scaledContents 属性:

    self.label.setScaledContents(True)

但结果会有点难看,因为标签的尺寸会比您绘制的像素图略小,使其变得模糊。

另一种(更好的)可能性是在 显示 window 之后设置固定大小 ,这样 Qt 将处理所需的大小所有对象:

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
#    window.setFixedSize(WINDOW_WIDTH, WINDOW_HEIGHT)
    window.setWindowTitle("Conway's Game of Life")
    window.show()
    window.setFixedSize(window.size())
    app.exec_()

即使这不是您问题的一部分,我也会向您建议一个稍微不同的概念,它不涉及 QLabel。

采用您的方法,您将面临两种可能性:

  1. 整个 QPixmap 的连续 重绘 :你不能轻易地 "clear" 已经绘制的表面上的东西,如果你有移动或消失的物体,你需要那个
  2. 添加必须手动移动的自定义小部件(并计算它们相对于像素图的位置将是一个严重的 PITA)

更好的解决方案是完全避免 QLabel,并使用自定义绘画实现您自己的小部件。

这是一个简单的例子:

class Grid(QWidget):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setMinimumSize(800, 600)
        self.columns = 40
        self.rows = 30

        # some random objects
        self.objects = [
            (10, 20), 
            (11, 21), 
            (12, 20), 
            (12, 22), 
        ]

    def resizeEvent(self, event):
        # compute the square size based on the aspect ratio, assuming that the
        # column and row numbers are fixed
        reference = self.width() * self.rows / self.columns
        if reference > self.height():
            # the window is larger than the aspect ratio
            # use the height as a reference (minus 1 pixel)
            self.squareSize = (self.height() - 1) / self.rows
        else:
            # the opposite
            self.squareSize = (self.width() - 1) / self.columns

    def paintEvent(self, event):
        qp = QPainter(self)
        # translate the painter by half a pixel to ensure correct line painting
        qp.translate(.5, .5)
        qp.setRenderHints(qp.Antialiasing)

        width = self.squareSize * self.columns
        height = self.squareSize * self.rows
        # center the grid
        left = (self.width() - width) / 2
        top = (self.height() - height) / 2
        y = top
        # we need to add 1 to draw the topmost right/bottom lines too
        for row in range(self.rows + 1):
            qp.drawLine(left, y, left + width, y)
            y += self.squareSize
        x = left
        for column in range(self.columns + 1):
            qp.drawLine(x, top, x, top + height)
            x += self.squareSize

        # create a smaller rectangle
        objectSize = self.squareSize * .8
        margin = self.squareSize* .1
        objectRect = QRectF(margin, margin, objectSize, objectSize)

        qp.setBrush(Qt.blue)
        for col, row in self.objects:
            qp.drawEllipse(objectRect.translated(
                left + col * self.squareSize, top + row * self.squareSize))

现在您不再需要 make_grid,您可以使用 Grid 代替 QLabel。

请注意,我删除了一个像素来计算正方形大小,否则最后 row/column 行将不会显示,就像您的像素图中发生的那样(考虑在 20x20 边的正方形中,20px 行开始从 0.5 开始将在像素 19.5 处剪裁)。