I am new to Qt. I encountered a problem when resizing the QGraphicsItem. It has not been solved after a week.
The code refered the issue Resize a QGraphicsItem with the mouse, but when I resized the item, the local coordinate system of the QGraphicsItem is destructed. For example, the initial QGraphicsItem is (200,200,100,100), where the scene coordinate (200,200) is the origin point of the local coordinates. Now drag(resize) its upper left corner to the scene coordinate (100,100), that is, the QGraphicsItem becomes (100,100,200,200). At this time, the origin of the local coordinate system of QGraphicsItem has not changed, it is still (0,0)(scenePos:(200,200)), not the upper left corner (-100,-100)(scenePos:(100,100)). This causes QGraphicsItem.pos() and QGraphicsItem.scenePos() return wrong pos(200,200) instead of the position of the upper left corner (scenePos: (100,100)), and this pos information is very important to me. Does anyone can help me?
Here are the kernel code of resize:
def interactiveResize(self, mousePos):
"""
Perform shape interactive resize.
"""
offset = self.handleSize + self.handleSpace
boundingRect = self.boundingRect()
rect = self.rect()
diff = QPointF(0, 0)
self.prepareGeometryChange()
if self.handleSelected == self.handleTopLeft:
fromX = self.mousePressRect.left()
fromY = self.mousePressRect.top()
toX = fromX + mousePos.x() - self.mousePressPos.x()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setX(toX - fromX)
diff.setY(toY - fromY)
boundingRect.setLeft(toX)
boundingRect.setTop(toY)
rect.setLeft(boundingRect.left() + offset)
rect.setTop(boundingRect.top() + offset)
self.setRect(rect)
elif self.handleSelected == self.handleTopMiddle:
fromY = self.mousePressRect.top()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setY(toY - fromY)
boundingRect.setTop(toY)
rect.setTop(boundingRect.top() + offset)
self.setRect(rect)
elif self.handleSelected == self.handleTopRight:
fromX = self.mousePressRect.right()
fromY = self.mousePressRect.top()
toX = fromX + mousePos.x() - self.mousePressPos.x()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setX(toX - fromX)
diff.setY(toY - fromY)
boundingRect.setRight(toX)
boundingRect.setTop(toY)
rect.setRight(boundingRect.right() - offset)
rect.setTop(boundingRect.top() + offset)
self.setRect(rect)
elif self.handleSelected == self.handleMiddleLeft:
fromX = self.mousePressRect.left()
toX = fromX + mousePos.x() - self.mousePressPos.x()
diff.setX(toX - fromX)
boundingRect.setLeft(toX)
rect.setLeft(boundingRect.left() + offset)
self.setRect(rect)
elif self.handleSelected == self.handleMiddleRight:
print("MR")
fromX = self.mousePressRect.right()
toX = fromX + mousePos.x() - self.mousePressPos.x()
diff.setX(toX - fromX)
boundingRect.setRight(toX)
rect.setRight(boundingRect.right() - offset)
self.setRect(rect)
elif self.handleSelected == self.handleBottomLeft:
fromX = self.mousePressRect.left()
fromY = self.mousePressRect.bottom()
toX = fromX + mousePos.x() - self.mousePressPos.x()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setX(toX - fromX)
diff.setY(toY - fromY)
boundingRect.setLeft(toX)
boundingRect.setBottom(toY)
rect.setLeft(boundingRect.left() + offset)
rect.setBottom(boundingRect.bottom() - offset)
self.setRect(rect)
elif self.handleSelected == self.handleBottomMiddle:
fromY = self.mousePressRect.bottom()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setY(toY - fromY)
boundingRect.setBottom(toY)
rect.setBottom(boundingRect.bottom() - offset)
self.setRect(rect)
elif self.handleSelected == self.handleBottomRight:
fromX = self.mousePressRect.right()
fromY = self.mousePressRect.bottom()
toX = fromX + mousePos.x() - self.mousePressPos.x()
toY = fromY + mousePos.y() - self.mousePressPos.y()
diff.setX(toX - fromX)
diff.setY(toY - fromY)
boundingRect.setRight(toX)
boundingRect.setBottom(toY)
rect.setRight(boundingRect.right() - offset)
rect.setBottom(boundingRect.bottom() - offset)
self.setRect(rect)
self.updateHandlesPos()
The problem with the linked solution is that the resizing is always applied to the item's rect
, so its position is never changed when resizing is done on the left or top side of the rectangle.
The solution is to always change only the rectangle size, and then alter the item position based on the size difference if the handle requires it.
I don't really like the proposed implementation, as it seems overly complicated and changes aspects of the QGraphicsRectItem that don't need to (or shouldn't) be changed for this purpose.
I propose a different and more modular approach that uses child items for the handles, which will eventually trigger the resizing on the parent.
This implementation is far more simpler than the given one, since it doesn't change the standard and expected behavior of QGraphicsRectItem (such as its shape, bounding rect, and pen/brush capabilities) meaning that it can be easily used and even subclassed just like a standard QGraphicsRectItem.
class Handle(QGraphicsEllipseItem):
def __init__(self, parent, cursor, size=8, pen=None, brush=None):
super().__init__(-size / 2, -size / 2, size, size, parent)
self.parent = parent
self.setCursor(cursor)
if pen is None:
pen = QPen(Qt.black, 1)
if brush is None:
brush = QBrush(Qt.red)
self.setPen(pen)
self.setBrush(brush)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.startPos = event.pos()
else:
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
self.parent.handleResize(self, event.pos() - self.startPos)
class ResizableItem(QGraphicsRectItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlag(self.ItemIsMovable)
self.topLeftHandle = Handle(self, Qt.SizeFDiagCursor)
self.topMidHandle = Handle(self, Qt.SizeVerCursor)
self.topRightHandle = Handle(self, Qt.SizeBDiagCursor)
self.midLeftHandle = Handle(self, Qt.SizeHorCursor)
self.midRightHandle = Handle(self, Qt.SizeHorCursor)
self.bottomLeftHandle = Handle(self, Qt.SizeBDiagCursor)
self.bottomMidHandle = Handle(self, Qt.SizeVerCursor)
self.bottomRightHandle = Handle(self, Qt.SizeFDiagCursor)
self.updateHandles()
def setRect(self, *args):
super().setRect(*args)
self.updateHandles()
def updateHandles(self):
r = self.rect()
c = r.center()
self.topLeftHandle.setPos(r.topLeft())
self.topMidHandle.setPos(c.x(), r.y())
self.topRightHandle.setPos(r.topRight())
self.midLeftHandle.setPos(r.x(), c.y())
self.midRightHandle.setPos(r.right(), c.y())
self.bottomLeftHandle.setPos(r.bottomLeft())
self.bottomMidHandle.setPos(c.x(), r.bottom())
self.bottomRightHandle.setPos(r.bottomRight())
def handleResize(self, handle, delta):
rect = self.rect()
size = rect.size()
newPos = self.pos()
dx = delta.x()
dy = delta.y()
if handle == self.topLeftHandle:
rect.setSize(size - QSizeF(dx, dy))
newPos += delta
elif handle == self.topMidHandle:
rect.setHeight(size.height() - dy)
newPos.setY(newPos.y() + dy)
elif handle == self.topRightHandle:
rect.setSize(size + QSizeF(dx, -dy))
newPos.setY(newPos.y() + dy)
elif handle == self.midLeftHandle:
rect.setWidth(size.width() - dx)
newPos.setX(newPos.x() + dx)
elif handle == self.midRightHandle:
rect.setWidth(size.width() + dx)
elif handle == self.bottomLeftHandle:
rect.setSize(size + QSizeF(-dx, dy))
newPos.setX(newPos.x() + dx)
elif handle == self.bottomMidHandle:
rect.setHeight(size.height() + dy)
elif handle == self.bottomRightHandle:
rect.setSize(size + QSizeF(dx, dy))
if not rect.isValid():
# ensure that the rectangle has not a negative size
size = rect.size()
width = size.width()
if width < 0:
size.setWidth(0)
if self.x() != newPos.x():
newPos.setX(newPos.x() + width)
height = size.height()
if height < 0:
size.setHeight(0)
if self.y() != newPos.y():
newPos.setY(newPos.y() + height)
rect.setSize(size)
self.setRect(rect)
self.setPos(newPos)
In the following example you can see that the position is correctly updated when resizing is done on the top or left sides, and this also works correctly even for items that have rectangles created with an offset.
I added a blue circle to the second item to show its origin point.
class ResizableItemWithLabel(ResizableItem):
labelItem = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setFlag(self.ItemSendsGeometryChanges)
def itemChange(self, change, value):
if change == self.ItemPositionHasChanged:
self.updateHandles()
return super().itemChange(change, value)
def updateHandles(self):
super().updateHandles()
if not self.labelItem:
self.labelItem = QGraphicsTextItem(self)
r = self.rect()
self.labelItem.setHtml('''
<center>
pos: {px:.02f}, {py:.02f}<br>
rect: {rx:.02f}, {ry:.02f}, {rw:.02f}x{rh:.02f}
</center>
'''.format(px=self.x(), py=self.y(),
rx=r.x(), ry=r.y(), rw=r.width(), rh=r.height()))
self.labelItem.adjustSize()
br = self.labelItem.boundingRect()
br.moveCenter(r.center())
self.labelItem.setPos(br.topLeft())
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
scene = QGraphicsScene()
view = QGraphicsView(scene)
scene.setSceneRect(0, 0, 640, 480)
item1 = ResizableItem(0, 0, 200, 100)
item1.setPos(100, 50)
item2 = ResizableItemWithLabel(150, 200, 200, 100)
origin = QGraphicsEllipseItem(-5, -5, 10, 10, item2)
origin.setBrush(QColor(Qt.blue))
scene.addItem(item1)
scene.addItem(item2)
view.show()
sys.exit(app.exec_())