-2

I have a graphic scene and 2 classes of a line and endpoint, but there is a problem when i move the endpoints of the line. When i move the line the endpoints will move, but when move the endpoints they just come off the line, even though i make it the parent. I want it to work both ways so when the endpoint is moved the line moves to it, so it can change length and slope.

class Lineitem(QGraphicsLineItem):
    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

        self.endpoints = []
        for i in range(2):
            endpoint = Endpoint(self)
            endpoint.setParentItem(self)
            self.endpoints.append(endpoint)

        self.endpoints[0].setPos(-5, -5)
        self.endpoints[1].setPos(495, -5)


class Endpoint(QGraphicsEllipseItem):
    def __init__(self, parent):
        super().__init__(parent)
        self.setRect(0, 0, 10, 10 )
        self.setBrush(QColor(0, 0, 255))

        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

Here is MainWindow:

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.gscene = QGraphicsScene(0, 0, 1000, 1000)
        
        self.line = Lineitem()
        self.line.setPos(300, 300)

        self.gview = QGraphicsView(self.gscene)
        self.gscene.addItem(self.line)

        self.setCentralWidget(self.gview)
musicamante
  • 41,230
  • 6
  • 33
  • 58
drivereye
  • 33
  • 5

2 Answers2

2

This is the expected behavior: a child item moves with its parent item, not the other way around. If you want to adjust the line according to the endpoints you need to write a function to do that.

class LineItem(QGraphicsLineItem):
    # ...
    def updateLine(self):
        p1, p2 = self.endpoints
        self.setLine(QLineF(p1.pos(), p2.pos()))

Now there are a few different options for how/where to call this function (they all achieve the same result but you may prefer one or find that one better suits your project).

IN THE CHILD SUBCLASS

Option 1) Reimplement mouseMoveEvent

class Endpoint(QGraphicsEllipseItem):

    def __init__(self, *args, **kwargs):
        super().__init__(-5, -5, 10, 10, *args, **kwargs)
        self.setBrush(QColor(0, 0, 255))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        self.parentItem().updateLine()

Also notice that I offset the rect so pos() is actually at the center of the item.

Option 2) Set the flag ItemSendsScenePositionChanges, reimplement itemChange to catch ItemScenePositionHasChanged

class Endpoint(QGraphicsEllipseItem):

    def __init__(self, *args, **kwargs):
        super().__init__(-5, -5, 10, 10, *args, **kwargs)
        self.setBrush(QColor(0, 0, 255))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
        self.setFlag(self.ItemSendsScenePositionChanges)

    def itemChange(self, change, value)
        if change == self.ItemScenePositionHasChanged:
            self.parentItem().updateLine()
        return super().itemChange(change, value)

IN THE PARENT SUBCLASS

Option 3) Call setFiltersChildEvents(True) and reimplement sceneEventFilter to catch QEvent.GraphicsSceneMouseMove

class LineItem(QGraphicsLineItem):

    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
        self.endpoints = [Endpoint(self) for i in range(2)]
        self.endpoints[1].setPos(500, 0)
        self.setFiltersChildEvents(True)
        
    def sceneEventFilter(self, obj, event):
        if event.type() == QEvent.GraphicsSceneMouseMove:
            obj.mouseMoveEvent(event)
            self.updateLine()
            return True # we handled the event, prevent further processing
        return super().sceneEventFilter(obj, event)
        
    def updateLine(self):
        p1, p2 = self.endpoints
        self.setLine(QLineF(p1.pos(), p2.pos()))

Note that setFiltersChildEvents will filter events from ALL child items and you may want to check if obj in self.endpoints. Alternatively call installSceneEventFilter(parent) on each child, which can only be used AFTER the parent is added to the scene. So it would be done in the scope where the line is added to the scene, such as:

line = LineItem()
scene.addItem(line)
for x in line.endpoints:
    x.installSceneEventFilter(line)

Also it may be useful to know in what order these methods are invoked in response to a mouse move:

sceneEventFilter -> mouseMoveEvent -> itemChange

  1. LineItem.sceneEventFilter intercepts the event before it gets dispatched to an event handler. If it returns False it will proceed through the event system.

  2. Endpoint.mouseMoveEvent the event is now received in the mouseMoveEvent handler. The default implementation or calling super will move the item and send enabled notifications:

  3. Endpoint.itemChange the item is notified of ItemScenePositionHasChanged here, (a read-only notification)

alec
  • 5,799
  • 1
  • 7
  • 20
  • Note that calling the super `__init__` of the QGraphicsLineItem with explicit coordinates *and* still adding the `args` is not a good idea: allowing the positional arguments means that you provide that functionality, and if you do add any of the standard arguments in the constructor you'll get a TypeError. – musicamante Oct 09 '21 at 21:42
  • It's also worth noticing that, while the use of the mouse event as trigger is a nice idea, it could be insufficient if the endpoints have to be positioned programmatically. – musicamante Oct 09 '21 at 21:45
  • @musicamante Good point. I've used args and kwargs so many times I often just throw it in without thinking – alec Oct 09 '21 at 21:48
1

The fact that the endpoints are children of the line doesn't mean anything: the parent doesn't magically know that you want it to change based on the position of its children.
In your case you only added two endpoints, but nothing stops you to add other child items: how could the parent know what to do whenever any of the children has been moved?

If you want to update the line based on the endpoints you need to implement it on your own, by:

  • intercept position changes, by setting the ItemSendsGeometryChanges flag and capture ItemPositionHasChanged overriding itemChange();
  • notify the parent about the position change;

Considering that basic QGraphicsItems like QGraphicsLineItem and QGraphicsEllipseItem do not inherit from QObject, you cannot use signals, but if you are certain that the items will always be children of your Lineitem class, then you can safely call a parent item function that will rebuild the new line:

class Lineitem(QGraphicsLineItem):
    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)
        self.endpoints = []
        
        for i in range(2):
            endpoint = Endpoint(self)
            endpoint.setParentItem(self)
            self.endpoints.append(endpoint)
        self.endpoints[1].setPos(500, 0)

    def updateLine(self):
        self.setLine(QLineF(self.endpoints[0].pos(), self.endpoints[1].pos()))


class Endpoint(QGraphicsEllipseItem):
    def __init__(self, parent):
        super().__init__(parent)
        self.setRect(-5, -5, 10, 10)
        self.setBrush(QColor(0, 0, 255))
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)
        self.setFlag(self.ItemSendsGeometryChanges)

    def itemChange(self, change, value):
        if change == self.ItemPositionHasChanged:
            self.parentItem().updateLine()
        return super().itemChange(change, value)

Note that I changed the rect of the ellipses in order to make them always centered at the origin point (0, 0) of the item coordinate system.
This ensures you that whenever you move them, they are always positioned at the correct coordinates, without trying to "fix" them by half of their size.

musicamante
  • 41,230
  • 6
  • 33
  • 58