pythonpysideqgraphicsviewqgraphicssceneqgraphicspathitem

Mouse hover over a PySide QGraphicsPathItem


I'm experiencing strange behaviour when I attempt to register mouse hover events on a QGraphicsPathItem.

In the example code (which I hacked from the qt5 elastic nodes example) I draw a simple curve. Hovering over the curve will change the colour of the curve but the hover events aren't registered on the lower right half of the curve. Additionally, only the top left half of the curve actually changes colour.

I added a QPainterPathStroker thinking that the hover events would register within this area but this is not the case.

My goal is to have mouse movements into, within and out of the QPainterPathStroker area register as hover events on the QGraphicsPathItem.

Any help is appreciated. Thanks

from PySide import QtCore, QtGui

class Edge(QtGui.QGraphicsPathItem):

    def __init__(self):
        QtGui.QGraphicsPathItem.__init__(self)
        self.setAcceptsHoverEvents(True)
        path = QtGui.QPainterPath()
        x1 = -100
        x2 = 120
        y1 = -100
        y2 = 120
        dx = abs(x1-x2)/2
        dy = abs(y1-y2)/2
        a = QtCore.QPointF(x1, y1)
        b = QtCore.QPointF(x1+dx, y1)
        c = QtCore.QPointF(x2-dy, y2)
        d = QtCore.QPointF(x2, y2)
        path.moveTo(a)
        path.cubicTo(b,c,d)
        self.setPath(path)

        self.hover = False

    def hoverEnterEvent(self, event):

        self.hover = True
        QtGui.QGraphicsPathItem.hoverEnterEvent(self, event)

    def hoverMoveEvent(self, event):

        QtGui.QGraphicsPathItem.hoverMoveEvent(self, event)

    def hoverLeaveEvent(self, event):

        self.hover = False
        QtGui.QGraphicsPathItem.hoverLeaveEvent(self, event)        

    def boundingRect(self):
        return QtCore.QRectF(-100,-100,120,120)

    def paint(self, painter, option, widget):

        if self.hover:
            c = QtCore.Qt.red
        else:
            c = QtCore.Qt.black
        painter.setPen(QtGui.QPen(c, 10, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
        painter.drawPath(self.path())

        painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
        painter.drawPath(self.shape())

    def shape(self):
        s = QtGui.QPainterPathStroker()    
        s.setWidth(30)
        s.setCapStyle(QtCore.Qt.RoundCap)
        path = s.createStroke(self.path())
        return path


class GraphWidget(QtGui.QGraphicsView):
    def __init__(self):
        QtGui.QGraphicsView.__init__(self)

        scene = QtGui.QGraphicsScene(self)
        scene.setItemIndexMethod(QtGui.QGraphicsScene.NoIndex)
        scene.setSceneRect(-200, -200, 400, 400)
        self.setScene(scene)
        self.setRenderHint(QtGui.QPainter.Antialiasing)
        self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse)
        self.setResizeAnchor(QtGui.QGraphicsView.AnchorViewCenter)

        self.edge = Edge()
        scene.addItem(self.edge)

        self.scale(0.8, 0.8)
        self.setMinimumSize(400, 400)
        self.setWindowTitle(self.tr("Elastic Nodes"))

    def wheelEvent(self, event):
        self.scaleView(math.pow(2.0, -event.delta() / 240.0))

    def drawBackground(self, painter, rect):
        sceneRect = self.sceneRect()
        rightShadow = QtCore.QRectF(sceneRect.right(), sceneRect.top() + 5, 5, sceneRect.height())
        bottomShadow = QtCore.QRectF(sceneRect.left() + 5, sceneRect.bottom(), sceneRect.width(), 5)
        if rightShadow.intersects(rect) or rightShadow.contains(rect):
            painter.fillRect(rightShadow, QtCore.Qt.darkGray)
        if bottomShadow.intersects(rect) or bottomShadow.contains(rect):
            painter.fillRect(bottomShadow, QtCore.Qt.darkGray)

        gradient = QtGui.QLinearGradient(sceneRect.topLeft(), sceneRect.bottomRight())
        gradient.setColorAt(0, QtCore.Qt.white)
        gradient.setColorAt(1, QtCore.Qt.lightGray)
        painter.fillRect(rect.intersect(sceneRect), QtGui.QBrush(gradient))
        painter.setBrush(QtCore.Qt.NoBrush)
        painter.drawRect(sceneRect)

    def scaleView(self, scaleFactor):
        factor = self.matrix().scale(scaleFactor, scaleFactor).mapRect(QtCore.QRectF(0, 0, 1, 1)).width()

        if factor < 0.07 or factor > 100:
            return

        self.scale(scaleFactor, scaleFactor)

    def mouseMoveEvent(self, event):

        self.edge.update()
        QtGui.QGraphicsView.mouseMoveEvent(self, event)


widget = GraphWidget()
widget.show()

Solution

  • The problem in this case is the boundingRect() that does not cover the complete path(), and this is used by the paint() method, the solution is to return self.shape().boundingRect():

    class Edge(QtGui.QGraphicsPathItem):
        def __init__(self):
            QtGui.QGraphicsPathItem.__init__(self)
            self.setAcceptsHoverEvents(True)
            path = QtGui.QPainterPath()
            x1 = -100
            x2 = 120
            y1 = -100
            y2 = 120
            dx = abs(x1-x2)/2
            dy = abs(y1-y2)/2
            a = QtCore.QPointF(x1, y1)
            b = a + QtCore.QPointF(dx, 0)
            d = QtCore.QPointF(x2, y2)
            c = d - QtCore.QPointF(dy, 0)
            path.moveTo(a)
            path.cubicTo(b,c,d)
            self.setPath(path)
    
            self.hover = False
    
        def hoverEnterEvent(self, event):
            QtGui.QGraphicsPathItem.hoverEnterEvent(self, event)
            self.hover = True
            self.update()
    
        def hoverMoveEvent(self, event):
            # print(event)
            QtGui.QGraphicsPathItem.hoverMoveEvent(self, event)
    
        def hoverLeaveEvent(self, event):
            QtGui.QGraphicsPathItem.hoverLeaveEvent(self, event)
            self.hover = False
            self.update()        
    
        def boundingRect(self):
            return self.shape().boundingRect()
    
        def paint(self, painter, option, widget):
            c = QtCore.Qt.red if self.hover else QtCore.Qt.black
            painter.setPen(QtGui.QPen(c, 10, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
            painter.drawPath(self.path())
    
            painter.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin))
            painter.drawPath(self.shape())
    
        def shape(self):
            s = QtGui.QPainterPathStroker()    
            s.setWidth(30)
            s.setCapStyle(QtCore.Qt.RoundCap)
            path = s.createStroke(self.path())
            return path
    

    enter image description here

    enter image description here