PyQtricks

Advanced tricks and hacks for PyQt (and PySide)


What is a QWidget?

2021-11-15 | Categories: QWidget, Guide lines

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:

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.

Painting

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:

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.

How does painting work?

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.

Event handling

TBC


Project maintained by MaurizioB Hosted on GitHub Pages — Theme by mattgraham