用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_WIDTH
和 WINDOW_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。
采用您的方法,您将面临两种可能性:
- 整个 QPixmap 的连续 重绘 :你不能轻易地 "clear" 已经绘制的表面上的东西,如果你有移动或消失的物体,你需要那个
- 添加必须手动移动的自定义小部件(并计算它们相对于像素图的位置将是一个严重的 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 处剪裁)。
我在使用 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_WIDTH
和 WINDOW_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。
采用您的方法,您将面临两种可能性:
- 整个 QPixmap 的连续 重绘 :你不能轻易地 "clear" 已经绘制的表面上的东西,如果你有移动或消失的物体,你需要那个
- 添加必须手动移动的自定义小部件(并计算它们相对于像素图的位置将是一个严重的 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 处剪裁)。