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()
Advertisement
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”.