1

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_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
Olumide
  • 5,397
  • 10
  • 55
  • 104

2 Answers2

1

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • @eyllansec The line `self.signalMapper.mapped[str].connect(print)` in the first version generates the error `SyntaxError: invalid syntax`. Is there a typo somewhere? – Olumide Apr 10 '19 at 18:43
  • @Olumide What version of python and pyside do you use? – eyllanesc Apr 10 '19 at 18:45
  • @eyllansec My Python version is 2.7.15rc1, PySide 1.2.2, QtCore version 4.8.7. BTW, I came up with a somewhat similar solution. I'll post it in a moment. Please let me know what you think. – Olumide Apr 10 '19 at 18:49
  • @Olumide I modified the first code to be compatible with python2 and python3. The problem is that in python2 print is not a callable, on the other hand in python3 it is. Try with my new code – eyllanesc Apr 10 '19 at 18:52
  • I am marking this as the answer because, although my answer is simpler because it avoids the use of QSignalMapper, this answer addresses the question that I asked i.e. how to use a QSignalMapper to answer the question. – Olumide Apr 15 '19 at 07:52
0

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_())
Olumide
  • 5,397
  • 10
  • 55
  • 104
  • Your explanation has an error, in Qt5 >= 5.10 it is deprecated, but you are using PySide which is based on Qt4 so for that class it is not deprecated. – eyllanesc Apr 10 '19 at 19:02
  • Ah, true. ... But its future proof ;-) – Olumide Apr 10 '19 at 19:04
  • So far I have not had to analyze the time because it is deprecated, although for these cases I do not use QSignalMapper but a model that allows you to handle the simplest things. :-) – eyllanesc Apr 10 '19 at 19:07
  • I used to think signal mapper was "cool" but I now think its limiting because of the narrow range of data types it can re-emit. (BTW, I am trying to get your versions to update the positions of all line segments like my answer so that I can objectively compare them.) – Olumide Apr 10 '19 at 19:10
  • I will add a solution using models but later, maybe it will help you. What you think you want to do is have the items of several QGraphicsScene synchronized. But I have a question, why do you have several QGraphicsScene? a QGraphicsScene can belong to several QGraphicsView – eyllanesc Apr 10 '19 at 19:13
  • I'll gladly explain but should we do this in [chat](https://chat.stackoverflow.com/rooms/info/191620/using-qsignalmapper-in-to-signal-between-qgraphicsscene-objects-in-pyside?tab=general) so as not to create noise here? – Olumide Apr 10 '19 at 19:17
  • @eyllanesc Can you take a look at this [question](https://stackoverflow.com/questions/56768301/using-qt-model-view-framework-to-notify-qgraphicsitem-in-a-view-of-user-edit-mad) about models? Thanks – Olumide Jun 26 '19 at 14:16