Search code examples
pythonc++qtpyqtqgraphicsitem

Update position of custom QGraphicsItem on connected item change


I have two subclassed QGraphicsRectItems that are supposed to be connected with a line that adjusts based on the position of the textboxes.

In the diagramscene example of the Qt docus the itemChanged method of a subclassed QGraphicsPolygonItem calls a updatePosition method of the connected arrow which calls setLine to update the arrow's position. In my case I cannot call setLine as I am subclassing QGraphicsItem instead of QGraphicsLineItem.

How should I implement updatePosition method in the Arrow class below to update the position of my QGraphicsItem? The following is a runnable example that shows what happens currently when the textboxes are clicked and moved.

enter image description here

import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *


class Arrow(QGraphicsItem):

    def __init__(self, startItem, endItem, parent=None, scene=None):
        super().__init__(parent, scene)

        self.startItem = startItem
        self.endItem = endItem

    def boundingRect(self):
        p1 = self.startItem.pos() + self.startItem.rect().center()
        p3 = self.endItem.pos() + self.endItem.rect().center()
        bounds = p3 - p1
        size = QSizeF(abs(bounds.x()), abs(bounds.y()))
        return QRectF(p1, size)

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

        p1 = self.startItem.pos() + self.startItem.rect().center()
        p3 = self.endItem.pos() + self.endItem.rect().center()

        pen = QPen()
        pen.setWidth(1)
        painter.setRenderHint(QPainter.Antialiasing)

        if self.isSelected():
            pen.setStyle(Qt.DashLine)
        else:
            pen.setStyle(Qt.SolidLine)

        pen.setColor(Qt.black)
        painter.setPen(pen)
        painter.drawLine(QLineF(p1, p3))
        painter.setBrush(Qt.NoBrush)

    def updatePosition(self):
        #Not sure what to do here...


class TextBox(QGraphicsRectItem):

    def __init__(self, text, position, rect=QRectF(0, 0, 200, 100),
                 parent=None, scene=None):
        super().__init__(rect, parent, scene)

        self.setFlags(QGraphicsItem.ItemIsFocusable |
                      QGraphicsItem.ItemIsMovable |
                      QGraphicsItem.ItemIsSelectable)

        self.text = QGraphicsTextItem(text, self)  

        self.setPos(position)

        self.arrows = []

    def paint(self, painter, option, widget=None):
        painter.setPen(Qt.black)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setBrush(Qt.white)
        painter.drawRect(self.rect())

    def addArrow(self, arrow):
        self.arrows.append(arrow)

    def itemChange(self, change, value):
        if change == QGraphicsItem.ItemPositionChange:
            for arrow in self.arrows:
                arrow.updatePosition()

        return value


if __name__ == "__main__":

    app = QApplication(sys.argv)

    view = QGraphicsView()
    scene = QGraphicsScene()
    scene.setSceneRect(0, 0, 500, 1000)
    view.setScene(scene)

    textbox1 = TextBox("item 1", QPointF(50, 50), scene=scene)
    textbox1.setZValue(1)
    textbox2 = TextBox("item 2", QPointF(100, 500), scene=scene)
    textbox2.setZValue(1)

    arrow = Arrow(textbox1, textbox2, scene=scene)
    arrow.setZValue(0)

    textbox1.addArrow(arrow)
    textbox2.addArrow(arrow)

    view.show()

    sys.exit(app.exec_())

Solution

  • The position of the item doesn't actually matter - it can remain at 0,0 - providing the bounding box is correct (which it will be according to your Arrow::boundingBox implementation). Hence, I think if you simply trigger a bounding box change, and a redraw in updatePosition, everything will work as you want.

    Of course, if you care about the position of the arrow being at the head or tail of the line, you can move it in updatePosition, and adjust the bounding box / paint coordinates accordingly - but that's entirely up to you if that makes sense or not.