I create a resizable QGraphicsRectItem
, I am able to resize it just fine, but I am not able to update the position of the new resized item in its scene
import typing
import sys
from PyQt5.QtGui import QPen, QBrush, QColor, QResizeEvent
from PyQt5.QtCore import QRectF, QSize
from PyQt5.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem, QGraphicsRectItem, QMainWindow, QVBoxLayout, QWidget
class ResizableRect(QGraphicsRectItem):
def __init__(self, *args):
super().__init__(*args)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setPen(QPen(QBrush(QColor('blue')), 5))
self.selected_edge = None
self.click_pos = self.click_rect = None
def mousePressEvent(self, event):
""" The mouse is pressed, start tracking movement. """
self.click_pos = event.pos()
self.newY = self.pos().y()
rect = self.rect()
if abs(rect.left() - self.click_pos.x()) < 5:
self.selected_edge = 'left'
elif abs(rect.right() - self.click_pos.x()) < 5:
self.selected_edge = 'right'
elif abs(rect.top() - self.click_pos.y()) < 5:
self.selected_edge = 'top'
elif abs(rect.bottom() - self.click_pos.y()) < 5:
self.selected_edge = 'bottom'
else:
self.selected_edge = None
self.click_pos = event.pos()
self.click_rect = rect
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
""" Continue tracking movement while the mouse is pressed. """
# Calculate how much the mouse has moved since the click.
pos = event.pos()
x_diff = pos.x() - self.click_pos.x()
y_diff = pos.y() - self.click_pos.y()
# Start with the rectangle as it was when clicked.
rect = QRectF(self.click_rect)
# Then adjust by the distance the mouse moved.
if self.selected_edge is None:
rect.translate(x_diff, y_diff)
elif self.selected_edge == 'top':
rect.adjust(0, y_diff, 0, 0)
# Test when resize rectangle upward; not working properly for now
if y_diff < 0:
newCenter = (rect.bottom() - pos.y()) / 2
self.newY = self.pos().y() - newCenter
elif self.selected_edge == 'left':
rect.adjust(x_diff, 0, 0, 0)
elif self.selected_edge == 'bottom':
rect.adjust(0, 0, 0, y_diff)
elif self.selected_edge == 'right':
rect.adjust(0, 0, x_diff, 0)
# Also check if the rectangle has been dragged inside out.
if rect.width() < 5:
if self.selected_edge == 'left':
rect.setLeft(rect.right() - 5)
else:
rect.setRight(rect.left() + 5)
if rect.height() < 5:
if self.selected_edge == 'top':
rect.setTop(rect.bottom() - 5)
else:
rect.setBottom(rect.top() + 5)
# Finally, update the rect that is now guaranteed to stay in bounds.
self.setY(self.newY)
self.setRect(rect)
def mouseReleaseEvent(self, event): # for printing only i.e., after resizing
print(f"item.pos(): {self.pos()}")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget(self)
self.setCentralWidget(central)
self.rect = ResizableRect(-100, -50, 200, 100)
scene = QGraphicsScene(0, 0, 300, 300)
scene.addItem(self.rect)
self.view = QGraphicsView(central)
self.view.setScene(scene)
layout = QVBoxLayout(central)
layout.addWidget(self.view)
def main():
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
main()
For now, I am testing updating item.pos()
when resize upward only, It is not working properly and I need advice to correct this implementation. In mouseMoveEvent()
, when self.selected_edge == top
, I calculate the center of the new rectangle. Then I compute the newY
value that I will use to update the item's position in scene later on, i.e., self.setY(self.newY)
. The result is that the item keeps moving upward as I resize. Where did I do wrong?
Thank you for helping!
An important aspect that is often misunderstood is the coordinate system used for graphics items.
While the documentation addresses three "major" coordinate systems, it's important to understand that:
pos()
relative to their parent;The documentation addresses this in the primitive functions (like
addRect()
):This is extremely important to understand, especially when dealing with mouse events.
Let's suppose you make a QGraphicsRectItem subclass and create two instances of them:
itemA
: created with a simplemyRectItem(100, 50, 300, 200)
;itemB
: created withmyRectItem(0, 0, 300, 200)
and then moved withitemB.setPos(100, 50)
;If you implement the
mousePressEvent()
in that class and print theevent.pos()
, you will see two very different results. Suppose you click in the center of those items:itemA
will showQPointF(250, 150)
;itemB
will showQPointF(150, 100)
;This is because the position is in item coordinates: while the rectangle of
itemB
is always starting from(0, 0)
(the origin point of the item), the rectangle ofitemB
is actually "translated" from the item position, so you get the point relative to the rectangle, added by the position of that rectangle.If you want to allow resizing on all edges of an item, you must consider these aspects, and also decide if the resizing should actually change the item position or the geometry of its rectangle.
The most common and suggested way is to use the first approach, as it's usually more consistent and immediate.
The only difference is about choosing the origin point of the item, which just depends on your needs: generally you just have contents that start from the item origin point and go bottom-right (similarly to how windows behave), but in some cases the contents should be "around" the center of the item position (common for "control points").
Finally, resizing of a rectangle should normally consider its corners. A good approach doesn't use strings to identify the "side", but integer values, or, even better, bitwise values. Qt provides some basic enums that allow
OR
combinations, and we can use theQt.Edges
flag for our needs.In this way not only we can provide resizing from corners, but also set appropriate cursors for the item that add visual hints about the resizing feature.
In the following code I've implemented all of the above and further more, considering:
ItemIsMovable
(which you ignored by completely overriding it in themouseMoveEvent()
);(0, 0)
or around the center);