pythonpysideqgraphicsitemqsignalmapper

Using QSignalMapper for communication between QGraphicsItems


The code below shows my attempt to get several movable VerticalLineSegment objects (derived from QGraphicsLineItem and QObject) to signal one (using a QSignalMapper) another when they move. I'd appreciate help as to why the VerticalLineSegment slot updateX is not triggered.

(Going forward the goal will be to have the VerticalLineSegments in different QGraphicsScenes but I thought it best to keep it simple for now.)

from PySide import QtGui, QtCore
import sys


class VerticalLineSegment( QtCore.QObject , QtGui.QGraphicsLineItem ):
    onXMove = QtCore.Signal()

    def __init__(self, x , y0 , y1 , parent=None):
        QtCore.QObject.__init__(self)
        QtGui.QGraphicsLineItem.__init__( self , x , y0 , x , y1 , parent)

        self.setFlag(QtGui.QGraphicsLineItem.ItemIsMovable)
        self.setFlag(QtGui.QGraphicsLineItem.ItemSendsGeometryChanges)
        self.setCursor(QtCore.Qt.SizeAllCursor)

    def itemChange( self , change , value ):
        if change is QtGui.QGraphicsItem.ItemPositionChange:
            self.onXMove.emit()
            value.setY(0)  # Restrict movements along horizontal direction
            return value
        return QtGui.QGraphicsLineItem.itemChange(self, change , value )

    def shape(self):
        path = super(VerticalLineSegment, self).shape()
        stroker = QtGui.QPainterPathStroker()
        stroker.setWidth(5)
        return stroker.createStroke(path)

    def boundingRect(self):
        return self.shape().boundingRect()

    # slot
    def updateX(self , object ):
        print "slot"        


class CustomScene(QtGui.QGraphicsScene):
    def __init__(self , parent=None):
        super(CustomScene, self).__init__(parent)
        self.signalMapper = QtCore.QSignalMapper()

    def addItem( self , item ):
        self.signalMapper.setMapping( item , item )
        item.onXMove.connect(self.signalMapper.map )
        self.signalMapper.mapped.connect(item.updateX)
        return QtGui.QGraphicsScene.addItem(self,item)


class Editor(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(Editor, self).__init__(parent)

        scene = CustomScene()

        line0 = VerticalLineSegment( 10 , 210 , 300 )
        line1 = VerticalLineSegment( 10 , 110 , 200 )
        line2 = VerticalLineSegment( 10 ,  10 , 100 )

        scene.addItem( line0 )
        scene.addItem( line1 )
        scene.addItem( line2 )

        view = QtGui.QGraphicsView()
        view.setScene( scene )

        self.setGeometry( 250 , 250 , 600 , 600 )
        self.setCentralWidget(view)
        self.show()


if __name__=="__main__":
    app=QtGui.QApplication(sys.argv)
    myapp = Editor()
    sys.exit(app.exec_())

Solution

  • In PySide (and also in PySide2, PyQt4 and PyQt5) it is not possible to inherit from QGraphicsItem and QObject (only double inheritance is allowed in special cases)

    So a possible solution is to use the composition, that is, to have a QObject as an attribute and that this has the signal:

    import sys
    import uuid
    from PySide import QtGui, QtCore
    
    
    class Signaller(QtCore.QObject):
        onXMove = QtCore.Signal()
    
    
    class VerticalLineSegment(QtGui.QGraphicsLineItem):
        def __init__(self, _id, x, y0, y1, parent=None):
            super(VerticalLineSegment, self).__init__(x, y0, x, y1, parent)
            self._id = _id
            self.signaller = Signaller()
            self.setFlag(QtGui.QGraphicsLineItem.ItemIsMovable)
            self.setFlag(QtGui.QGraphicsLineItem.ItemSendsGeometryChanges)
            self.setCursor(QtCore.Qt.SizeAllCursor)
    
        def itemChange(self, change, value):
            if change is QtGui.QGraphicsItem.ItemPositionChange:
                self.signaller.onXMove.emit()
                value.setY(0)  # Restrict movements along horizontal direction
                return value
            return QtGui.QGraphicsLineItem.itemChange(self, change, value)
    
        def shape(self):
            path = super(VerticalLineSegment, self).shape()
            stroker = QtGui.QPainterPathStroker()
            stroker.setWidth(5)
            return stroker.createStroke(path)
    
        def boundingRect(self):
            return self.shape().boundingRect()
    
        def updateX(self, _id):
            print("slot", _id)
    
    
    class CustomScene(QtGui.QGraphicsScene):
        def __init__(self, parent=None):
            super(CustomScene, self).__init__(parent)
            self.signalMapper = QtCore.QSignalMapper(self)
    
        def addItem(self, item):
            if hasattr(item, "_id"):
                item.signaller.onXMove.connect(self.signalMapper.map)
                self.signalMapper.setMapping(item.signaller, item._id)
                self.signalMapper.mapped[str].connect(item.updateX)
            super(CustomScene, self).addItem(item)
    
    
    class Editor(QtGui.QMainWindow):
        def __init__(self, parent=None):
            super(Editor, self).__init__(parent)
    
            scene = CustomScene()
    
            line0 = VerticalLineSegment(str(uuid.uuid4()), 10.0, 210.0, 300.0)
            line1 = VerticalLineSegment(str(uuid.uuid4()), 10.0, 110.0, 200.0)
            line2 = VerticalLineSegment(str(uuid.uuid4()), 10.0, 10.0, 100.0)
    
            scene.addItem(line0)
            scene.addItem(line1)
            scene.addItem(line2)
    
            view = QtGui.QGraphicsView()
            view.setScene(scene)
    
            self.setGeometry(250, 250, 600, 600)
            self.setCentralWidget(view)
            self.show()
    

    Or use QGraphicsObject:

    import sys
    from PySide import QtCore, QtGui
    
    class VerticalLineSegment(QtGui.QGraphicsObject):
        onXMove = QtCore.Signal()
    
        def __init__(self, x, y0, y1, parent=None):
            super(VerticalLineSegment, self).__init__(parent)
            self._line = QtCore.QLineF(x, y0, x, y1)
            self.setFlag(QtGui.QGraphicsLineItem.ItemIsMovable)
            self.setFlag(QtGui.QGraphicsLineItem.ItemSendsGeometryChanges)
            self.setCursor(QtCore.Qt.SizeAllCursor)
    
        def paint(self, painter, option, widget=None):
            painter.drawLine(self._line)
    
        def shape(self):
            path = QtGui.QPainterPath()
            path.moveTo(self._line.p1())
            path.lineTo(self._line.p2())
            stroker = QtGui.QPainterPathStroker()
            stroker.setWidth(5)
            return stroker.createStroke(path)
    
        def boundingRect(self):
            return self.shape().boundingRect()
    
        def itemChange(self, change, value):
            if change is QtGui.QGraphicsItem.ItemPositionChange:
                self.onXMove.emit()
                value.setY(0)  # Restrict movements along horizontal direction
                return value
            return QtGui.QGraphicsLineItem.itemChange(self, change, value)
    
        def updateX(self , obj):
            print("slot", obj) 
    
    class CustomScene(QtGui.QGraphicsScene):
        def __init__(self, parent=None):
            super(CustomScene, self).__init__(parent)
            self.signalMapper = QtCore.QSignalMapper(self)
    
        def addItem(self, item):
            if isinstance(item, QtCore.QObject):
                item.onXMove.connect(self.signalMapper.map)
                self.signalMapper.setMapping(item, item)
                self.signalMapper.mapped[QtCore.QObject].connect(item.updateX)
            super(CustomScene, self).addItem(item)
    
    
    class Editor(QtGui.QMainWindow):
        def __init__(self, parent=None):
            super(Editor, self).__init__(parent)
    
            scene = CustomScene()
    
            line0 = VerticalLineSegment(10.0, 210.0, 300.0)
            line1 = VerticalLineSegment(10.0, 110.0, 200.0)
            line2 = VerticalLineSegment(10.0, 10.0, 100.0)
    
            scene.addItem(line0)
            scene.addItem(line1)
            scene.addItem(line2)
    
            view = QtGui.QGraphicsView()
            view.setScene(scene)
    
            self.setGeometry(250, 250, 600, 600)
            self.setCentralWidget(view)
            self.show()