在小部件上绘制形状

Drawing a shape over a widget

我需要能够在另一个小部件之上绘制 circle/line,但每次我尝试时,它都会落后。我已经阅读了很多关于在小部件上使用 QPainter 的帖子,但我仍然无法让它工作。

以下是我的应用程序的一个最小示例,我只是想弄清楚将 paintevent 函数放在哪里才能使其正常工作。

我的最终目标是让用户画出这样的热数独 - thermo

但我认为,如果我能想出如何在我的数独网格上绘制任何东西,我就能解决剩下的问题

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import *


class SudokuCell(QLineEdit):
    def __init__(self, cell_size, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cell_size = cell_size
        font = self.font()
        font.setPointSize(24)
        self.setFont(font)
        self.setAlignment(Qt.AlignCenter)
        self.setFixedSize(cell_size, cell_size)
        self.setAutoFillBackground(True)

class SudokuRegion(QWidget):
    def __init__(self, cell_size, narrow_line_width, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cell_size = cell_size
        self.narrow_line_width = narrow_line_width

        layout = QGridLayout()
        layout.setSpacing(narrow_line_width)
        layout.setContentsMargins(0,0,0,0)
        self.setLayout(layout)

        for i in range(3):
            for j in range(3):
                new_cell = SudokuCell(cell_size, objectName=f"C{i}{j}")
                layout.addWidget(new_cell, i, j)
                               
class SudokuGrid(QWidget):
    def __init__(self, cell_size, wide_line_width, narrow_line_width, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cell_size = cell_size
        self.wide_line_width = wide_line_width
        self.narrow_line_width = narrow_line_width

        layout = QGridLayout()
        layout.setContentsMargins(wide_line_width,wide_line_width, 0, 0)
        layout.setSpacing(wide_line_width)
        self.setLayout(layout)

        for i in range(3):
            for j in range(3):
                new_region = SudokuRegion(cell_size, narrow_line_width, objectName=f"Region{i * 3 + j}")
                layout.addWidget(new_region, i, j)

class MainWindow(QMainWindow):
    def __init__(self, window_w, window_h, orthogonal_intersection_size, cell_size, wide_line_width, narrow_line_width):
        super().__init__()
        self.window_w = window_w
        self.window_h = window_h
        self.orthogonal_intersection_size = orthogonal_intersection_size
        self.cell_size = cell_size
        self.wide_line_width = wide_line_width
        self.narrow_line_width = narrow_line_width

        # a sudoku grid is exactly this large. Google a sudoku grid if you dont understand
        self.frame_size = 9 * cell_size + 4 * wide_line_width + 6 * narrow_line_width
        self.initUi()

    def initUi(self):
        self.setWindowTitle("Sudoku Solver")
        self.setGeometry(100, 100, self.window_w, self.window_h)

        widget = QWidget(self)
        self.setCentralWidget(widget)

        hor_box = QHBoxLayout()
        widget.setLayout(hor_box)

        self.frame = QFrame(widget)
        self.frame.setFixedSize(self.frame_size, self.frame_size)
        self.frame.setStyleSheet(".QFrame {background-color: black}")

        self.grid = SudokuGrid(self.cell_size, self.wide_line_width, self.narrow_line_width, self.frame)
        hor_box.addWidget(self.frame)

        # other widgets are added to the hor_box later but arent important for this question
        self.show()

def main():
    app = QApplication(sys.argv)
    window = MainWindow(
        window_w=1000,
        window_h=750,
        orthogonal_intersection_size=25,
        cell_size=80,
        wide_line_width=8,
        narrow_line_width=2
    )
    window.show()
    app.exec_()

if __name__ == "__main__":
    main()

您可以在 SudokuCell 或 SudokuGrid 中重新实现 paintEvent,但在任何一种情况下,SudokuCell 都需要具有透明背景,以便热绘图将绘制在除 QLineEdit 文本编辑器之外的所有内容之上。我选择了数独单元格。

import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *

class SudokuCell(QLineEdit):
    def __init__(self, cell_size, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.cell_size = cell_size
        font = self.font()
        font.setPointSize(24)
        self.setFont(font)
        self.setAlignment(Qt.AlignCenter)
        self.setFixedSize(cell_size, cell_size)
        self.setAutoFillBackground(True)

        self.setStyleSheet('''
        SudokuCell {
            background-color: rgba(0, 0, 0, 0);
            border: none;
        }''')
        self.line = QPainterPath()
        self.ellipse = False
        
    def paintEvent(self, event):
        qp = QPainter(self)
        qp.setPen(Qt.NoPen)
        qp.setBrush(Qt.white)
        qp.drawRect(self.rect())
        s = self.cell_size
        if self.ellipse:
            qp.setRenderHint(QPainter.Antialiasing)
            qp.setBrush(Qt.gray)
            qp.drawEllipse(self.rect().center(), s / 2.5, s / 2.5)
        qp.setPen(QPen(Qt.gray, s / 2, Qt.SolidLine, Qt.FlatCap, Qt.MiterJoin))
        qp.drawPath(self.line)
        super().paintEvent(event)

请注意 super().paintEvent(event) 在 自定义绘画之后被调用 ,因此文本编辑器将在任何绘画之上可见。如果背景保持白色,它将覆盖自定义绘画。 SudokuGrid class 具有向行和列指定的任何单元格添加直线或椭圆的方法。

class SudokuGrid(QWidget):
    ...

    def cell(self, row, col):
        region = self.layout().itemAtPosition(row // 3, col // 3).widget()
        return region.layout().itemAtPosition(row % 3, col % 3).widget()

    def add_ellipse(self, row, col):
        self.cell(row, col).ellipse = True

    def add_line(self, row, col, *line):
        path = QPainterPath(QPointF(*line[0]) * self.cell_size)
        for point in line[1:]:
            path.lineTo(QPointF(*point) * self.cell_size)
        self.cell(row, col).line = path

SudokuGrid.add_line()*line 参数旨在成为 0-1 范围内的元组,作为定义 QPainterPath 行元素的简单键(类似于渐变的行元素) — 0 = left/top, 0.5 = 中心, 1 = right/bottom).

class MainWindow(QMainWindow):
    def __init__(self, window_w, window_h, orthogonal_intersection_size, cell_size, wide_line_width, narrow_line_width):
        super().__init__()
        self.window_w = window_w
        self.window_h = window_h
        self.orthogonal_intersection_size = orthogonal_intersection_size
        self.cell_size = cell_size
        self.wide_line_width = wide_line_width
        self.narrow_line_width = narrow_line_width

        # a sudoku grid is exactly this large. Google a sudoku grid if you dont understand
        self.frame_size = 9 * cell_size + 4 * wide_line_width + 6 * narrow_line_width
        self.initUi()

        self.grid.add_line(0, 0, (0.25, 0.5), (1, 0.5))
        self.grid.add_line(0, 1, (0, 0.5), (1, 0.5))
        self.grid.add_line(0, 2, (0, 0.5), (0.5, 0.5), (0.5, 1))
        self.grid.add_line(1, 2, (0.5, 0), (0.5, 1))
        self.grid.add_line(2, 2, (0.5, 0), (0.5, 0.5))
        self.grid.add_ellipse(2, 2)

下面是它在 MainWindow 中的使用方式。当然,您可能会以更适合您的程序的完全不同的方式将线条和椭圆添加到网格,主要目的是展示如何在“小部件顶部”实现绘画。