QGraphicsItem не меняйте перо родителя при чтении ребенка

#python #python-3.x #pyside2 #qgraphicsitem

Вопрос:

У меня есть несколько QGraphicsItem s, которые находятся в иерархии «родитель-потомок». Я пытаюсь заставить подсветку элемента при наведении мыши работать на основе элемента, что означает, что если мышь наведена на элемент, он должен выделяться.

Подсветка работает нормально, но если я выполняю подсветку на дочернем элементе, то подсветка также автоматически происходит на родительском элементе, что нежелательно. Вот пример кода проблемы

 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_())
 

При наведении курсора на строку (которая является родительской) подсвечивается только строка. Однако при наведении курсора мыши на прямоугольник оба они подсвечиваются. Я предполагаю, что это как-то связано с тем, что прямоугольник является дочерним элементом элемента строки.

Я хотел бы сохранить иерархию «родитель-потомок», потому что мне нужно вычислять дочерние позиции на основе родительских позиций, что проще таким образом.

Есть ли способ не каскадировать выделение дочернего элемента до родительского как?

Ответ №1:

Возможное решение состоит в том , чтобы проверить, действительно ли позиция события находится внутри элемента boundingRect() , но это будет работать только для элементов, которые простираются только по вертикали и горизонтали. Поскольку линии также могут иметь определенный угол, лучше shape() вместо этого свериться с:

 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)
 

Однако часть «оставить» немного сложнее: родитель не получает событие «оставить» при наведении курсора на ребенка, и для этого требуется добавить фильтр событий сцены для всех детей.

Поскольку фильтр событий сцены может быть установлен только тогда, когда элемент находится на сцене, вы должны попытаться установить фильтр как при добавлении дочернего элемента, так и при добавлении родителя в сцену.

Чтобы упростить ситуацию, я создал две похожие ручки разной ширины, так что вам нужно только использовать setPen() вместо того, чтобы постоянно получать текущую и менять ее.

 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)
 

Комментарии:

1. Что меня смущает, так это то, что другие события, например mouseDoubleClickEvent , работают нормально, они отправляются только соответствующему дочернему элементу, а не родителю. Почему событие наведения отличается?

2. @wasp256 события наведения не совпадают с событиями щелчка; рассмотрим события ввода/вывода QWidget: когда вы вводите ребенка, вы также вводите родителя, и это довольно очевидно, так как ребенок всегда находится внутри родительского rect() . QGraphicsItem немного отличается тем фактом, что дочерние элементы могут иметь геометрию, которая находится за пределами геометрии родительского элемента, но концепция остается, даже если она кажется немного нелогичной: если мышь вводит дочерний элемент, она считается также введенной в родительский элемент. События щелчка отличаются тем, что они не распространяются, если они обрабатываются, и они строго зависят->

3. -> на форме/геометрии элемента (виджета или графического элемента), на котором произошло событие кнопки. Вы можете обрабатывать события щелчка от родителя, даже если они находятся за пределами родительской формы, и для этого вы должны реализовать shape() родительскую форму, включив в нее события всех ее дочерних элементов (рекурсивно создавая суммы дочерних shape() элементов).