Drawing a shape over a widget

Tags: , ,



I need to be able to draw a circle/line on top of another widget, but every time I try, it goes behind. I have read lots of posts about using QPainter over widgets but I still can’t get it to work.

The following is a minimal example of my app, and I just want to figure out where to put a paintevent function for it to work properly.

My end goal is to allow users to draw thermo sudokus such as this – thermo

But I think that if I can work out how to draw anything on top of my SudokuGrid, I can work the rest out

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

Answer

You can reimplement the paintEvent in SudokuCell or SudokuGrid, but in either case SudokuCell needs to have a transparent background so the thermo drawings will be painted on top of everything except for the QLineEdit text editor. I chose SudokuCell.

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)

Notice that super().paintEvent(event) is called after the custom painting, so the text editor will be visible on top of anything painted. If the background was left white it would cover the custom painting. The SudokuGrid class has methods to add a line or ellipse to any cell specified by row and column.

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

The *line arguments for SudokuGrid.add_line() are intended to be tuples in the range 0-1 as a simple key to define the elements of the line for a QPainterPath (similar to that of a gradient — 0 = left/top, 0.5 = center, 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)

And here is how it might be used in MainWindow. Of course you may go about adding the lines and ellipses to the grid in an entirely different way that better suits your program, the primary purpose was to show how to achieve the painting “on top of the widget”.



Source: stackoverflow