PyQt6 – handle events on QGraphicsItem

  Kiến thức lập trình

I’m fairly new to PyQt6, and I’m interested in event handling in QGraphicsItems.

I have QGraphicsItem(s), drawn on a QGraphicsScene and a view with a QGraphicsView. I’m mainly drawing two types of object on my scene:

  • a fixed-size background (a bitmap via a QGraphicsPixmapItem)
  • polygons that must react to the mouse (via custom item). I want the polygons to highlight if the mouse hovers over them.

I’ve discovered hoverEvent, which is very useful in my case, but when I draw my scene, not all the polygons are visible. However, they should all react to the mouse.

My desired behavior is :

  • If the poly is visible, when the mouse enters, it highlights, when the mouse leaves, it reverts to normal.

  • If the poly is not visible, when the mouse enters, it highlights, when the mouse leaves, it reverts to invisible.

The problem is that events are not distributed to invisible or disable items. How can I force hoverEvent propagation?
Similarly, I can’t handle mouseEvent for Release and Move (even when polygons are visible).

I’d also be very happy if an expert could explain how QEvent are propagated in general, as I haven’t found any very detailed (or well explained) documentation.
Reviews and comments on implementation choices are also welcome.

Here’s my example code (change default_visibility to test it)

from PyQt6.QtCore import Qt, QRectF, QSize, QPointF
from PyQt6.QtGui import QImage, QPixmap, QColor, QPen, QBrush, QPainter, QPainterPath, QPolygonF
from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QApplication, QGraphicsItem, QGraphicsPixmapItem


class QGraphicsCustomPolygonItem(QGraphicsItem):
    def __init__(self, polygon, default_visibility):
        super().__init__()

        self.default_visibility = default_visibility
        self.polygon = polygon

        self.main_color = QColor(255, 0, 0)

        self.pen_color = self.main_color
        self.pen_color.setAlpha(255)
        self.pen = QPen(self.pen_color)
        self.pen.setWidthF(0.5)

        self.brush_color = self.main_color
        self.brush_color.setAlpha(32)
        self.brush = QBrush(self.brush_color)

        self._highlight = False

        self.setVisible(default_visibility)
        self.setAcceptHoverEvents(True)

    @property
    def highlight(self) -> bool:
        return self._highlight

    @highlight.setter
    def highlight(self, value: bool) -> None:
        assert isinstance(value, bool)
        self._highlight = value
        self.brush_color = self.main_color
        if value is True:
            self.brush_color.setAlpha(64)
            self.setVisible(True)
        else:
            self.brush_color.setAlpha(32)
            self.setVisible(self.default_visibility)
        self.brush = QBrush(self.brush_color)
        self.update(self.boundingRect())

    def boundingRect(self) -> QRectF:
        return self.polygon.boundingRect()

    def paint(self, painter: QPainter, option, widget=None):
        painter.setRenderHint(QPainter.RenderHint.Antialiasing)
        painter.setPen(self.pen)
        painter.setBrush(self.brush)
        painter.drawPolygon(self.polygon, Qt.FillRule.OddEvenFill)

    def shape(self):
        path = QPainterPath()
        path.addPolygon(self.polygon)
        return path

    def mouseMoveEvent(self, event):
        print("mouse move")

    def mousePressEvent(self, event):
        print("mouse press")

    def mouseReleaseEvent(self, event):
        print("mouse release")

    def mouseDoubleClickEvent(self, event):
        print("mouse double click")

    def hoverMoveEvent(self, event):
        print("hover move")

    def hoverEnterEvent(self, event):
        print("hover enter")
        self.highlight = True

    def hoverLeaveEvent(self, event):
        print("hover leave")
        self.highlight = False


if __name__ == '__main__':
    app = QApplication([])

    scene = QGraphicsScene()

    # draw a background image
    background_image = QImage(QSize(300, 300), QImage.Format.Format_RGB888)
    for x in range(background_image.width()):
        for y in range(background_image.height()):
            if (x + y) % 2 == 0:
                background_image.setPixelColor(x, y, Qt.GlobalColor.white)
            else:
                background_image.setPixelColor(x, y, Qt.GlobalColor.black)
    background_item = QGraphicsPixmapItem(QPixmap.fromImage(background_image))
    scene.addItem(background_item)

    # draw a custom polygon
    poly = QPolygonF([QPointF(x, y) for x, y in [(20, 40), (200, 60), (120, 250), (50, 200)]])
    custom_polygon_item = QGraphicsCustomPolygonItem(poly, default_visibility=True)
    scene.addItem(custom_polygon_item)

    # init the viewport
    view = QGraphicsView(scene)

    view.setGeometry(0, 0, 600, 600)
    view.show()
    app.exec()

I tried to subclass the QGraphicsScene to manage event propagation manually. From what I understood, it’s the scene’s role to reformat events and distribute them to items, but I didn’t succeed and anyway, I don’t think it was the right way to do it.

New contributor

Maxe is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.

LEAVE A COMMENT