Search code examples
pythonpyqtpyqt5qgraphicsitemqgraphicspathitem

Problem in shape method of QGraphicsPathItem


In below figure I have QGraphicsPathItem on scene as red portion and override it's shape as blue portion. I want when the red space is dragged and moved then the item is lengthened or shortened linearly, and when the blue space is dragged then the entire item must be moved. Here is what I tried...

import sys

from PyQt5.QtCore import QRectF, Qt, QPointF
from PyQt5.QtGui import QPainterPath, QPen, QPainterPathStroker, QPainter
from PyQt5.QtWidgets import QApplication, QMainWindow, QGraphicsScene, QGraphicsView, QGraphicsPathItem, QGraphicsItem

class Item(QGraphicsPathItem):
    circle = QPainterPath()
    circle.addEllipse(QRectF(-5, -5, 10, 10))

    def __init__(self):
        super(Item, self).__init__()
        self.setPath(Item.circle)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)

    def paint(self, painter, option, widget):
        color = Qt.red if self.isSelected() else Qt.black
        painter.setPen(QPen(color, 2, Qt.SolidLine))
        painter.drawPath(self.path())

        # To paint path of shape
        painter.setPen(QPen(Qt.blue, 1, Qt.SolidLine))
        painter.drawPath(self.shape())

    def shape(self):
        startPoint = self.mapFromScene(self.pos())
        endPoint = self.mapFromScene(QPointF(10, 10))
        path = QPainterPath(startPoint)
        path.lineTo(endPoint)
        stroke = QPainterPathStroker()
        stroke.setWidth(10)
        return stroke.createStroke(path)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = QMainWindow()
    window.show()
    scene = QGraphicsScene()
    scene.setSceneRect(0, 0, 200, 200)
    view = QGraphicsView()
    view.setScene(scene)
    window.setCentralWidget(view)
    scene.addItem(Item())
    sys.exit(app.exec_())

I am getting output as disturbed path


Solution

  • Handling the task of resizing and stretching in the same item is complicated, so to avoid it I have used 2 items: A handle and a Pipe. Thus each one manages his own task and updates the position of the other elements:

    import sys
    
    from PyQt5 import QtCore, QtGui, QtWidgets
    
    
    class HandleItem(QtWidgets.QGraphicsPathItem):
        def __init__(self, parent=None):
            super().__init__(parent)
            path = QtGui.QPainterPath()
            path.addEllipse(QtCore.QRectF(-5, -5, 10, 10))
            self.setPath(path)
    
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
    
            self._pipe_item = None
    
        @property
        def pipe_item(self):
            return self._pipe_item
    
        @pipe_item.setter
        def pipe_item(self, item):
            self._pipe_item = item
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
                ip = self.pipe_item.mapFromScene(value)
                self.pipe_item.end_pos = ip
            elif change == QtWidgets.QGraphicsItem.ItemSelectedChange:
                color = QtCore.Qt.red if value else QtCore.Qt.black
                self.setPen(QtGui.QPen(color, 2, QtCore.Qt.SolidLine))
            return super().itemChange(change, value)
    
    
    class PipeItem(QtWidgets.QGraphicsPathItem):
        def __init__(self, parent=None):
            super().__init__(parent)
    
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
    
            self._end_pos = QtCore.QPointF()
    
            self._handle = HandleItem()
            self.handle.pipe_item = self
    
            self.end_pos = QtCore.QPointF(10, 10)
            self.handle.setPos(self.end_pos)
    
            self.setPen(QtGui.QPen(QtCore.Qt.blue, 1, QtCore.Qt.SolidLine))
    
        @property
        def handle(self):
            return self._handle
    
        @property
        def end_pos(self):
            return self._end_pos
    
        @end_pos.setter
        def end_pos(self, p):
            path = QtGui.QPainterPath()
            path.lineTo(p)
            stroke = QtGui.QPainterPathStroker()
            stroke.setWidth(10)
            self.setPath(stroke.createStroke(path))
            self._end_pos = p
    
        def paint(self, painter, option, widget):
            option.state &= ~QtWidgets.QStyle.State_Selected
            super().paint(painter, option, widget)
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemSceneHasChanged:
                if self.scene():
                    self.scene().addItem(self.handle)
            elif change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
                p = self.mapToScene(self.end_pos)
                self.handle.setPos(p)
            return super().itemChange(change, value)
    
    
    if __name__ == "__main__":
        app = QtWidgets.QApplication(sys.argv)
        scene = QtWidgets.QGraphicsScene(sceneRect=QtCore.QRectF(0, 0, 200, 200))
        item = PipeItem()
        scene.addItem(item)
        view = QtWidgets.QGraphicsView(scene)
        window = QtWidgets.QMainWindow()
        window.setCentralWidget(view)
        window.resize(640, 480)
        window.show()
        sys.exit(app.exec_())
    

    UPDATE:

    If you want the logic you want to be implemented then it is more complicated. The cause of the error is that the paint() method uses the boundingRect() to set the paint area, but in your case it does not take into account that it varies, a possible solution is the following:

    class Item(QGraphicsPathItem):
        circle = QPainterPath()
        circle.addEllipse(QRectF(-5, -5, 10, 10))
    
        # ...
    
        def boundingRect(self):
            return self.shape().boundingRect()