1

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

Sumit
  • 83
  • 7
  • I do not understand you, explain better your requirement – eyllanesc May 15 '20 at 16:01
  • I have aa qgraphicspath item shown as red circle in image which is move able. I want item.isSelected() method return true when I click mouse between red circle and a fixed point on scene – Sumit May 15 '20 at 16:14
  • Okay, and why override the shape method? – eyllanesc May 15 '20 at 16:16
  • to detect mouse press in larger area – Sumit May 15 '20 at 16:18
  • What do you mean larger area? Please be precise and detailed. – eyllanesc May 15 '20 at 16:19
  • here by larger area I mean blue region shown in image. – Sumit May 15 '20 at 16:21
  • can you please run the code and tell me why blue part (shape) is not painting correctly. – Sumit May 15 '20 at 16:22
  • should I explain more? – Sumit May 15 '20 at 16:24
  • From what I understand you want the item to only drag if it is pressed in the red color and not in the blue color space, am I correct? – eyllanesc May 15 '20 at 16:25
  • I want also drag it if item is pressed in blue space – Sumit May 15 '20 at 16:26
  • 1
    Should the size of the space in blue change or should it always be the same size? – eyllanesc May 15 '20 at 16:27
  • and want change blue space dynamically from fixed point on scene to current position of item – Sumit May 15 '20 at 16:28
  • I'm getting a better understanding of what you want, let's assume you move from point (0, 0) to (100, 100), it is obvious that there are infinite paths so my question is Does the blue part depend on the path or should it only be a linear tube connecting those points? – eyllanesc May 15 '20 at 16:38
  • It should be linear tube – Sumit May 15 '20 at 16:43
  • Summarizing what I understand: You 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, am I correct ? – eyllanesc May 15 '20 at 16:45
  • yes ,you are right – Sumit May 15 '20 at 16:51
  • first problem I am getting is when moving red portion blue portion is not painting correctly. – Sumit May 15 '20 at 16:54
  • when we click inside bounding rect of an item then item get selected.similiarly I want blue portion to act as bounding rect for red portion. – Sumit May 15 '20 at 16:59
  • I know the cause of the error but your methodology is incorrect so I ask you to rewrite your question clearly explaining what you want (you can rely on my summaries) – eyllanesc May 15 '20 at 17:01
  • in my code in shape method if I return stroke.createStroke(self.path()) then it working fine.but on changing self.path() with path I have created then it doesn't work. – Sumit May 15 '20 at 17:04
  • okay I am rewriting question – Sumit May 15 '20 at 17:04
  • 1
    As I said I already understood what you want, and I know why it fails so you no longer say the same since otherwise the comments will be extended too much. Better invest your time improving your post clearly explaining what you want. – eyllanesc May 15 '20 at 17:06

1 Answers1

2

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()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • okay I agree with you but I want to implement it as a single item like we override shape method for collision detection – Sumit May 15 '20 at 18:05
  • I am not streaching item just streaching it's shape – Sumit May 15 '20 at 18:07
  • in my code if I make path in shape method constant then it going to work .you can try changing my code little by setting path as constant. I am asking why it is not working when that path is not constant. for example set startPoint = QPointF(0,-10) and endPoint=QPointF(0,10) . – Sumit May 15 '20 at 18:10
  • update section is what I want.It worked and strength my concept.thank you – Sumit May 15 '20 at 18:21