How to enable mouse tracking on a QLabel that has RichText

Question:

I really want a single QLabel to have different colours for different parts of the string. Past questions have led me to the solution of using HTML4 rich text options within it, for example:

'<font color="red">I'm red! </font><font color="blue">I'm blue!</font>'

This solution works great visually when passed into QLabel.setText(), but for some reason I’m finding that mouse tracking completely breaks within the widget once it uses rich text.

Here’s the MRE, with a normal QLabel and empty background space as a control:

from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel
from PyQt5.QtGui import QMouseEvent
from PyQt5.QtCore import Qt, QEvent
import sys


class testWindow(QMainWindow):
    def __init__(self):
        super(testWindow, self).__init__()

        self.setWindowTitle('QLabel Test')
        self.setMouseTracking(True)
        self.resize(600, 200)

        # label that displays coordinates picked up from mouseMoveEvent
        self.coordLabel = QLabel(self)
        self.coordLabel.setText('Mouse at:')
        self.coordLabel.setStyleSheet('''QLabel {background-color:#088;}''')
        self.coordLabel.setGeometry(0, 0, 200, 200)
        self.coordLabel.setMouseTracking(True)

        # label with multiple colours for different sections of the string
        self.richTextLabel = QLabel(self)
        self.richTextLabel.setText('<font color="red">I'm red! </font><font color="blue">I'm blue!</font>')
        self.richTextLabel.setStyleSheet('''QLabel {background-color:#880;}''')
        self.richTextLabel.setTextFormat(Qt.RichText)  # text format is explicitly set to RichText
        self.richTextLabel.setGeometry(400, 0, 200, 200)
        self.richTextLabel.setMouseTracking(True)

        # everything has mouse tracking set to True
        # 3 blocks: coordinate label, empty space, rich text label
        self.show()

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if event.type() == QEvent.MouseMove:
            x, y = event.x(), event.y()  # coordinates of mouse
            self.coordLabel.setText('Mouse at: {}, {}'.format(x, y))  # set to coordLabel text


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = testWindow()
    sys.exit(app.exec_())

What I’m looking for is either a way to fix the mouse tracking on these QLabels. Although if there’s another way to make a QLabel with multiple colours in one string of text that would be helpful too.

Thanks 🙂

Asked By: mazelover1939

||

Answers:

By default, Qt widgets (anything that, directly or not, inherits from QWidget) do not accept mouse events.

Whenever an event is not accepted by a Qt object (aka, a QObject), it is automatically sent to its parent, up in the object tree hierarchy. As soon as an object in that hierarchy accepts the event, it’s not propagated anymore.

Consider that:

  • by default, events are normally accepted (see the documentation);
  • if the event was already accepted and isn’t explicitly ignored (or wasn’t previously ignored), the parent will not receive it;
  • an event could be handled by an object (because it reacts to it) but it could still be set as ignored, because it’s important that the parent receives it;

Whenever a QLabel has rich text content (either automatically detected or explicitly set using setTextFormat(Qt.RichText)), mouse events are automatically accepted anyway, and that’s because they’re normally handled; most importantly, because rich text can often include anchors (hyperlinks).

If you need to get any event received by a child widget, the only safe way to do so is by installing an event filter on it.

Now, the problem is that mouse events sent to a widget use local coordinates, so if you want to get absolute coordinates relative to the parent, you have to map them (see the map*() functions in the QWidget documentation).

This is a possible implementation that considers the above:

class testWindow(QMainWindow):
    def __init__(self):
        ...
        self.richTextLabel.installEventFilter(self)

    def updateCoordLabel(self, pos, obj=None):
        if isinstance(obj, QWidget) and obj != self:
            if not self.isAncestorOf(obj):
                return
            pos = obj.mapTo(self, pos)
        self.coordLabel.setText('Mouse at: {}, {}'.format(pos.x(), pos.y()))  # set to coordLabel text

    def eventFilter(self, obj, event):
        if obj == self.richTextLabel and event.type() == event.MouseMove:
            self.updateCoordLabel(event.pos(), obj)
        return super().eventFilter(obj, event)

    def mouseMoveEvent(self, event: QMouseEvent) -> None:
        if event.type() == QEvent.MouseMove:
            self.updateCoordLabel(event.pos())

Note that avoiding layout managers is almost always a bad idea, unless you really know what you’re doing. Layout management considers a lot of important aspects; the most important are the system font, OS font scaling, and screen DPI (including High DPI) settings; those aspects then result in changing the requirements, aspect, and, most importantly, usability of each widget. Ignoring all these aspects will almost always result in having a UI that for some reason becomes completely unusable:

  • widgets could overlap, resulting in:
    • making them partially invisible (because they’ve been hidden by others);
    • not usable, because mouse events are captured by widgets stacked above them (even if it doesn’t look like they are) which will get mouse events intended for the underlying widget;
  • contents could be clipped due to the forced geometry; this is specifically the case of QLabel: if the user uses a big font size or has huge font scaling set (for instance, because they have some visual impairment), the text will be only partially visible, if not completely hidden;
  • the screen might not be big enough to show all contents, hiding some widgets;

You may not care for the above aspects, but you must remember that what you see on your screen is almost NEVER what anybody else will see, which in some cases is a completely different look than the one you expected.
Ignoring those aspects will make your program unusable for some people, and for the wrong reasons.

If you do that because you want more control on the way the layout is managed, then it’s the wrong reason. Just learn how to deal with layout managers: which means understanding what sizeHint() is and how it’s used, the purpose of size policies, generic layout management (including spacing and stretch factors) and the purpose of layout items.

Answered By: musicamante
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.