Draw a correct grid with PyQt5

谁说胖子不能爱 提交于 2020-07-10 07:00:09

问题


I'm struggling a bit with PyQt5: I have to implement Conway's Game of Life and I started out with the GUI general setup. I thought about stacking (vertically) two widgets, one aimed at displaying the game board and another one containing the buttons and sliders.

This is what I came up with (I'm a total noob)

I'd like to fit the grid correctly with respect to the edges. It looks like it builds the grid underneath the dedicated canvas: it would be great to fix the canvas first and then paint on it but this whole thing of layouts, widgets and all that blows my mind.

This is my (fastly and poorly written) code

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_()

Thank you for your help, have a nice day!


回答1:


The reason for the pixmap not being show at its full size is because you're using WINDOW_WIDTH and WINDOW_HEIGHT for both the window and the pixmap. Since the window also contains the toolbar and its own margins, you're forcing it to be smaller than it should, hence the "clipping out" of the board.

The simpler solution would be to set the scaledContents property of the label:

    self.label.setScaledContents(True)

But the result would be a bit ugly, as the label will have a size slightly smaller than the pixmap you drawn upon, making it blurry.

Another (and better) possibility would be to set the fixed size after the window has been shown, so that Qt will take care of the required size of all objects:

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_()

Even if it's not part of your question, I'm going to suggest you a slightly different concept, that doesn't involve a QLabel.

With your approach, you'll face two possibilities:

  1. continuous repainting of the whole QPixmap: you cannot easily "clear" something from an already painted surface, and if you'll have objects that move or disappear, you will need that
  2. adding custom widgets that will have to be manually moved (and computing their position relative to the pixmap will be a serious PITA)

A better solution would be to avoid at all the QLabel, and implement your own widget with custom painting.

Here's a simple example:

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))

Now you don't need make_grid anymore, and you can use Grid instead of the QLabel.

Note that I removed one pixel to compute the square size, otherwise the last row/column lines won't be shown, as happened in your pixmap (consider that in a 20x20 sided square, a 20px line starting from 0.5 would be clipped at pixel 19.5).



来源:https://stackoverflow.com/questions/59521183/draw-a-correct-grid-with-pyqt5

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!