Using QSignalMapper for communication between QGraphicsItems

267 Views Asked by At

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

There are 2 best solutions below

5
On BEST ANSWER

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

Here is the solution that I came up with. Like @eyllanesc's first solution it uses a signaller which I call a Broadcaster instead of QSignalMapper which is now obsolete/deprecated. Here are the relevant changes:

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

    def __init__(self, x , y0 , y1 , parent=None):
        ...
        self.index = -1
        ...

    def updateX( self , id , x ):
        if id is not self.index:
            # Disconnect and reconnect to avoid a signal cycle
            self.onXMove.disconnect()
            self.setX( x )
            self.onXMove.connect( self.sender().onXMove )


# Alternative to signal mapper
class Broadcaster( QtCore.QObject ):
    onXMove = QtCore.Signal( int , int )    


class CustomScene(QtGui.QGraphicsScene):
    def __init__(self , parent=None):
        super(CustomScene, self).__init__(parent)
        self.broadcaster = Broadcaster()
        self.count = 0

    def addItem( self , item ):
        item.index = self.count
        self.count = self.count + 1
        item.onXMove.connect( self.broadcaster.onXMove )
        self.broadcaster.onXMove.connect( item.updateX )
        return QtGui.QGraphicsScene.addItem(self,item)              

And here is the complete program

from PySide import QtGui, QtCore
import sys

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

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

        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( self.index , value.x() )
            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 , x ):
        if id is not self.index:
            self.onXMove.disconnect()
            self.setX( x )
            self.onXMove.connect( self.sender().onXMove )


class Broadcaster( QtCore.QObject ):
    onXMove = QtCore.Signal( int , int )


class CustomScene(QtGui.QGraphicsScene):
    def __init__(self , parent=None):
        super(CustomScene, self).__init__(parent)
        self.broadcaster = Broadcaster()
        self.count = 0

    def addItem( self , item ):
        item.index = self.count
        self.count = self.count + 1
        item.onXMove.connect( self.broadcaster.onXMove )
        self.broadcaster.onXMove.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_())