Skip to content
Advertisement

QGraphicsItem don’t change pen of parent when chaning child

I have a multiple QGraphicsItems that are in a parent-child hierarchy. I’m trying to get highlighting of an item on mouse hover to work on an item basis, meaning that if the mouse hovers over an item it should highlight.

The highlighting works fine, but if I’m performing the highlighting on a child, then the highlighting is also automatically happening on the parent of such, which is not desired. Here is a code example of the problem

from PySide2.QtCore import Qt
from PySide2.QtGui import QPen
from PySide2.QtWidgets import QGraphicsItem, 
    QGraphicsScene, QGraphicsView, QGraphicsLineItem, QGraphicsSceneHoverEvent, 
    QGraphicsRectItem, QMainWindow, QApplication

class TextBox(QGraphicsRectItem):
    def __init__(self, parent: QGraphicsItem, x: float, y: float, width: float, height: float):
        super().__init__(parent)
        self.setParentItem(parent)

        self.setAcceptHoverEvents(True)

        pen = QPen(
            Qt.white,
            2,
            Qt.SolidLine,
            Qt.RoundCap,
            Qt.RoundJoin
        )

        self.setPen(pen)
        self.setRect(x, y, width, height)

    def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverEnterEvent(event)

        current_pen = self.pen()
        current_pen.setWidth(5)
        self.setPen(current_pen)

    def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverLeaveEvent(event)

        current_pen = self.pen()
        current_pen.setWidth(2)
        self.setPen(current_pen)


class LineItem(QGraphicsLineItem):
    def __init__(
            self,
            x1_pos: float,
            x2_pos: float,
            y1_pos: float,
            y2_pos: float,
            parent: QGraphicsItem = None
    ):
        super().__init__()
        self.setParentItem(parent)

        self.setAcceptHoverEvents(True)

        pen = QPen(
            Qt.white,
            2,
            Qt.SolidLine,
            Qt.RoundCap,
            Qt.RoundJoin
        )

        self.setPen(pen)

        self.setLine(
            x1_pos,
            y1_pos,
            x2_pos,
            y2_pos
        )

    def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverEnterEvent(event)

        current_pen = self.pen()
        current_pen.setWidth(5)
        self.setPen(current_pen)

    def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverLeaveEvent(event)

        current_pen = self.pen()
        current_pen.setWidth(2)
        self.setPen(current_pen)


class DiagramScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.setBackgroundBrush(Qt.black)

        line_item = LineItem(0, 200, 0, 0)
        box = TextBox(line_item, 0, 0, 20, 20)

        self.addItem(line_item)


class GraphicsView(QGraphicsView):
    def __init__(self):
        self.scene = DiagramScene()
        super().__init__(self.scene)


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

        self.setCentralWidget(GraphicsView())


if __name__ == '__main__':

    import sys

    app = QApplication(sys.argv)

    mainWindow = MainWindow()
    mainWindow.setGeometry(100, 100, 800, 500)
    mainWindow.show()

    sys.exit(app.exec_())

When hovering over the line (which is the parent) then only the line gets highlighted. However, when hovering over the rectangle then both get highlighted. I assume it has something to do with that the rectangle is a child of the line item.

I would like to keep the parent-child hierarchy because I have to calculate child positions based on the parent positions which is easier that way.

Is there a way to not cascade the highlighting of the child item up to the parent as?

Advertisement

Answer

A possible solution is to check if the event position is actually within the item’s boundingRect(), but this would only work for items that extend only vertically and horizontally. Since lines can also have a certain angle, it’s better to check against the shape() instead:

def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
    super().hoverEnterEvent(event)

    if self.shape().contains(event.pos()):
        current_pen = self.pen()
        current_pen.setWidth(5)
        self.setPen(current_pen)

The “leave” part is a bit trickier, though: the parent doesn’t receive a leave event when hovering on a child, and it requires adding a scene event filter on all children.

Since a scene event filter can only be installed when an item is on a scene, you must try to install the filter both when a child is added or when the parent is added to a scene.

To simplify things, I created two similar pens with different widths, so that you only need to use setPen() instead of continuously getting the current one and change it.

class LineItem(QGraphicsLineItem):
    def __init__(
            self,
            x1_pos: float,
            x2_pos: float,
            y1_pos: float,
            y2_pos: float,
            parent: QGraphicsItem = None
    ):
        super().__init__()
        self.setParentItem(parent)

        self.setAcceptHoverEvents(True)

        self.normalPen = QPen(
            Qt.green,
            2,
            Qt.SolidLine,
            Qt.RoundCap,
            Qt.RoundJoin
        )

        self.hoverPen = QPen(self.normalPen)
        self.hoverPen.setWidth(5)

        self.setPen(self.normalPen)

        self.setLine(
            x1_pos,
            y1_pos,
            x2_pos,
            y2_pos
        )

    def hoverEnterEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverEnterEvent(event)
        if self.shape().contains(event.pos()):
            self.setPen(self.hoverPen)

    def hoverLeaveEvent(self, event: QGraphicsSceneHoverEvent):
        super().hoverLeaveEvent(event)
        self.setPen(self.normalPen)

    def itemChange(self, change, value):
        if change == self.ItemChildAddedChange and self.scene():
            value.installSceneEventFilter(self)
        elif change == self.ItemChildRemovedChange:
            value.removeSceneEventFilter(self)
        elif change == self.ItemSceneHasChanged and self.scene():
            for child in self.childItems():
                child.installSceneEventFilter(self)
        return super().itemChange(change, value)

    def sceneEventFilter(self, child, event):
        if event.type() == event.GraphicsSceneHoverEnter:
            self.setPen(self.normalPen)
        elif (event.type() == event.GraphicsSceneHoverLeave and 
            self.shape().contains(child.mapToParent(event.pos()))):
                self.setPen(self.hoverPen)
        return super().sceneEventFilter(child, event)
User contributions licensed under: CC BY-SA
1 People found this is helpful
Advertisement