Advanced tricks and hacks for PyQt (and PySide)
Almost any UI toolkit uses the concept of widget, which is a graphical element (usually rectangular shaped) used to show something to the user and possibly allow interaction with the program. A widget can be a button, a scroll bar or an input field used to type some text.
In Qt, the QWidget is the most basic element of any UI element, and it provides
functions and features that can be implemented to create more complex interface
elements. Most importantly:
paintEvent()
,
which is called whenever the widget has to be displayed;mousePressEvent()
)
that are triggered when the widget receives input events like mouse button presses,
keyboard strokes, wheel movements;setFixedSize()
)
which are used to get or set size constraints, change the position of a widget, or
know if the widget can be resized and how;In this post I’ll try to explain how widgets generally work, how it’s possible to alter the default behavior of existing widget, and how custom widgets can be created.
Any widget that has to display some content (which is, almost all), has to implement
the paintEvent()
.
This is, more or less, what happens whenever a widget is being displayed:
paintEvent()
with that event as argument;painter = QPainter(self)
);Note that a paint event can only be generated by Qt, and creating a QPainter on a widget
is only allowed as a direct consequence of such an event.
A common mistake from beginners is trying to do something like this:
def onClick(self):
painter = QPainter(self)
painter.drawRect(0, 0, 100, 100)
This will not only do absolutely nothing, but will also show a similar message in the debug/terminal output:
StdErr: QWidget::paintEngine: Should no longer be called
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::drawRects: Painter not active
The only possible way to “force” a repainting is by calling
update()
(and its related functions),
which will schedule a repaint, or repaint()
,
which will repaints the widget immediately (but, be aware, this is normally discouraged).
Painting, as said, has to be done exclusively on the paintEvent()
implementation.
A widget is usually drawn multiple times, not just once, and at least in the following cases:
Since any of the situations above can happen at any time, and even dozens of times in a matter of seconds, it is important to ensure that the painting is fast, it doesn’t require too much time/CPU to be completed, and that what is painting is always consistent.
NOTE: consider the last case above; since resizing causes repainting, this means that no change in geometry (most importantly, size) should ever happen in a paint event, as it might cause infinite recursion.
Let’s see a simple widget painting:
class MyWidget(QWidget):
def __init__(self):
super().__init__()
self.setMinimumSize(320, 240)
def paintEvent(self, event):
qp = QPainter(self)
qp.setPen(Qt.red)
qp.setBrush(Qt.green)
qp.drawRect(0, 0, self.width() - 1, self.height() - 1)
The code above is pretty self-explanatory, but let’s see what happens.
In the __init__
we call the super class (which is mandatory), and set a
minimum size for it (see the size section below about this).
In the paintEvent
we create a QPainter for the widget and begin it. Note that
the explicit form, but normally unnecessary, is the following:
qp = QPainter()
qp.begin(self)
We then set a pen for the shape we’re drawing, and a brush for the fill color; QPainter automatically creates QPen and QBrush objects, but a more explicit version of the code above would be:
pen = QPen(QColor(Qt.red))
qp.setPen(pen)
brush = QBrush(QColor(Qt.green))
qp.setBrush(brush)
Finally, we draw a rectangle for the full extent of the widget.
Note that I subtracted one pixel from both the width and height. This is required as
the painting starts at the first pixel and extends to the given width and height;
this means that painting a 2x2 rectangle actually starts at 0x0 and extends to 3x3,
because the width and height of the rectangle is considered from the “middle” of the
pixels.
TBC