Resize a QGraphicsItem with the mouse
Here is mine with a less code :
class BoxItem(QGraphicsObject):
def __init__(self):
super().__init__()
self.mousePressPos = None
self.mousePressRect = None
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
# self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
# self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.rect = QRect(0, 0, 100, 100)
self.moving = False
self.origin = QPoint()
# # Resizer actions
# self.resizer.setPos(self.boundingRect().bottomRight() - self.r_offset)
# self.resizer.resizeSignal.connect(self.resize)
def corner_rect(self) -> QRect:
""" Return corner rect geometry """
return QRect(self.rect.right() - 10, self.rect.bottom() - 10, 10, 10)
def boundingRect(self) -> QRectF:
""" Override boundingRect """
return self.rect.adjusted(-10, -10, 10, 10)
def paint(self, painter, option, widget=None):
""" OVerride paint """
brush = QBrush(QColor(255, 100, 100, 200))
brush.setStyle(Qt.Dense7Pattern)
painter.setBrush(brush)
painter.drawRect(self.rect)
if self.isSelected():
painter.setBrush(QBrush(QColor(Qt.red)))
painter.setPen(Qt.NoPen)
painter.drawRect(self.corner_rect())
# Draw selection
pen = QPen(QColor(Qt.green))
pen.setStyle(Qt.DotLine)
painter.setPen(pen)
painter.setBrush(Qt.NoBrush)
painter.drawRect(self.rect)
self.update()
def hoverMoveEvent(self, event: QMouseEvent):
""" Override hover move Event : Display cursor """
if self.isSelected() & self.corner_rect().contains(event.pos().toPoint()):
self.setCursor(Qt.SizeFDiagCursor)
else:
self.setCursor(Qt.ArrowCursor)
super().hoverMoveEvent(event)
def mousePressEvent(self, event: QMouseEvent):
""" override mouse Press Event """
if self.isSelected() & self.corner_rect().contains(
QPoint(event.pos().toPoint())
):
self.moving = True
self.origin = self.rect.topLeft()
else:
super().mousePressEvent(event)
def mouseReleaseEvent(self, event: QMouseEvent):
""" Override mouse release event """
self.moving = False
super().mouseReleaseEvent(event)
def mouseMoveEvent(self, event: QMouseEvent):
""" Override mouse move event """
if self.moving:
# If moving is set from mousePressEvent , change geometry
self.prepareGeometryChange()
pos = event.pos().toPoint()
if pos.x() >= self.origin.x():
self.rect.setRight(pos.x())
else:
self.rect.setLeft(pos.x())
if pos.y() >= self.origin.y():
self.rect.setBottom(pos.y())
else:
self.rect.setTop(pos.y())
self.rect = self.rect.normalized()
self.update()
return
else:
super().mouseMoveEvent(event)
Daniele's code was very helpful, and I particularly like the resizing handles. I didn't like the fact that you could move the rectangle outside the window limits or resize it until it had negative width or height.
This code enforces bounds on the moving and resizing, so you can't cause those problems. I left off the resizing handles to keep this example simple.
import typing
from PySide6.QtCore import QRectF, QSize
from PySide6.QtGui import QPen, QBrush, QColor, QResizeEvent
from PySide6.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()
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)
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)
# Figure out the limits of movement. I did it by updating the scene's
# rect after the window resizes.
scene_rect = self.scene().sceneRect()
view_left = scene_rect.left()
view_top = scene_rect.top()
view_right = scene_rect.right()
view_bottom = scene_rect.bottom()
# Next, check if the rectangle has been dragged out of bounds.
if rect.top() < view_top:
if self.selected_edge is None:
rect.translate(0, view_top-rect.top())
else:
rect.setTop(view_top)
if rect.left() < view_left:
if self.selected_edge is None:
rect.translate(view_left-rect.left(), 0)
else:
rect.setLeft(view_left)
if view_bottom < rect.bottom():
if self.selected_edge is None:
rect.translate(0, view_bottom - rect.bottom())
else:
rect.setBottom(view_bottom)
if view_right < rect.right():
if self.selected_edge is None:
rect.translate(view_right - rect.right(), 0)
else:
rect.setRight(view_right)
# 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.setRect(rect)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
central = QWidget(self)
self.setCentralWidget(central)
self.rect = ResizableRect()
scene = QGraphicsScene(0, 0, 300, 300)
scene.addItem(self.rect)
self.view = QGraphicsView(central)
self.view.setScene(scene)
layout = QVBoxLayout(central)
self.setLayout(layout)
layout.addWidget(self.view)
self.old_size: typing.Optional[QSize] = None
def show(self):
super().show()
self.resize_scene()
def resizeEvent(self, event: QResizeEvent):
super().resizeEvent(event)
self.resize_scene()
def resize_scene(self):
if not self.isVisible():
# Viewport size isn't set yet, so calculation won't work.
return
size = self.view.maximumViewportSize()
if self.old_size is None:
new_rect = QRectF(size.width()/4, size.height()/4,
size.width()/2, size.height()/2)
else:
old_rect = QRectF(self.rect.rect())
x_scale = size.width() / self.old_size.width()
y_scale = size.height() / self.old_size.height()
new_rect = QRectF(old_rect.left()*x_scale, old_rect.top()*y_scale,
old_rect.width()*x_scale, old_rect.height()*y_scale)
self.rect.setRect(new_rect)
self.view.scene().setSceneRect(0, 0, size.width(), size.height())
self.old_size = size
def main():
app = QApplication()
window = MainWindow()
window.show()
app.exec_()
main()
You would need to implement the resizing yourself. To do that you need to subclass QGraphicsRectItem
and implement mouse event handlers: mousePressEvent
, mouseMoveEvent
, mouseReleaseEvent
.
The general idea is the following:
- You create a set of handles that you can use to resize the shape.
- By tracking handles position change you calculate the new geometry of the shape.
I wrote some code times ago which was perfectly working with PyQt5, but it should work with PyQt4 too. You should be able to adapt it to you needs:
import sys
from PyQt5.QtCore import Qt, QRectF, QPointF
from PyQt5.QtGui import QBrush, QPainterPath, QPainter, QColor, QPen, QPixmap
from PyQt5.QtWidgets import QGraphicsRectItem, QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
class GraphicsRectItem(QGraphicsRectItem):
handleTopLeft = 1
handleTopMiddle = 2
handleTopRight = 3
handleMiddleLeft = 4
handleMiddleRight = 5
handleBottomLeft = 6
handleBottomMiddle = 7
handleBottomRight = 8
handleSize = +8.0
handleSpace = -4.0
handleCursors = {
handleTopLeft: Qt.SizeFDiagCursor,
handleTopMiddle: Qt.SizeVerCursor,
handleTopRight: Qt.SizeBDiagCursor,
handleMiddleLeft: Qt.SizeHorCursor,
handleMiddleRight: Qt.SizeHorCursor,
handleBottomLeft: Qt.SizeBDiagCursor,
handleBottomMiddle: Qt.SizeVerCursor,
handleBottomRight: Qt.SizeFDiagCursor,
}
def __init__(self, *args):
"""
Initialize the shape.
"""
super().__init__(*args)
self.handles = {}
self.handleSelected = None
self.mousePressPos = None
self.mousePressRect = None
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.updateHandlesPos()
def handleAt(self, point):
"""
Returns the resize handle below the given point.
"""
for k, v, in self.handles.items():
if v.contains(point):
return k
return None
def hoverMoveEvent(self, moveEvent):
"""
Executed when the mouse moves over the shape (NOT PRESSED).
"""
if self.isSelected():
handle = self.handleAt(moveEvent.pos())
cursor = Qt.ArrowCursor if handle is None else self.handleCursors[handle]
self.setCursor(cursor)
super().hoverMoveEvent(moveEvent)
def hoverLeaveEvent(self, moveEvent):
"""
Executed when the mouse leaves the shape (NOT PRESSED).
"""
self.setCursor(Qt.ArrowCursor)
super().hoverLeaveEvent(moveEvent)
def mousePressEvent(self, mouseEvent):
"""
Executed when the mouse is pressed on the item.
"""
self.handleSelected = self.handleAt(mouseEvent.pos())
if self.handleSelected:
self.mousePressPos = mouseEvent.pos()
self.mousePressRect = self.boundingRect()
super().mousePressEvent(mouseEvent)
def mouseMoveEvent(self, mouseEvent):
"""
Executed when the mouse is being moved over the item while being pressed.
"""
if self.handleSelected is not None:
self.interactiveResize(mouseEvent.pos())
else:
super().mouseMoveEvent(mouseEvent)
def mouseReleaseEvent(self, mouseEvent):
"""
Executed when the mouse is released from the item.
"""
super().mouseReleaseEvent(mouseEvent)
self.handleSelected = None
self.mousePressPos = None
self.mousePressRect = None
self.update()
def boundingRect(self):
"""
Returns the bounding rect of the shape (including the resize handles).
"""
o = self.handleSize + self.handleSpace
return self.rect().adjusted(-o, -o, o, o)
def updateHandlesPos(self):
"""
Update current resize handles according to the shape size and position.
"""
s = self.handleSize
b = self.boundingRect()
self.handles[self.handleTopLeft] = QRectF(b.left(), b.top(), s, s)
self.handles[self.handleTopMiddle] = QRectF(b.center().x() - s / 2, b.top(), s, s)
self.handles[self.handleTopRight] = QRectF(b.right() - s, b.top(), s, s)
self.handles[self.handleMiddleLeft] = QRectF(b.left(), b.center().y() - s / 2, s, s)
self.handles[self.handleMiddleRight] = QRectF(b.right() - s, b.center().y() - s / 2, s, s)
self.handles[self.handleBottomLeft] = QRectF(b.left(), b.bottom() - s, s, s)
self.handles[self.handleBottomMiddle] = QRectF(b.center().x() - s / 2, b.bottom() - s, s, s)
self.handles[self.handleBottomRight] = QRectF(b.right() - s, b.bottom() - s, s, s)
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()
def shape(self):
"""
Returns the shape of this item as a QPainterPath in local coordinates.
"""
path = QPainterPath()
path.addRect(self.rect())
if self.isSelected():
for shape in self.handles.values():
path.addEllipse(shape)
return path
def paint(self, painter, option, widget=None):
"""
Paint the node in the graphic view.
"""
painter.setBrush(QBrush(QColor(255, 0, 0, 100)))
painter.setPen(QPen(QColor(0, 0, 0), 1.0, Qt.SolidLine))
painter.drawRect(self.rect())
painter.setRenderHint(QPainter.Antialiasing)
painter.setBrush(QBrush(QColor(255, 0, 0, 255)))
painter.setPen(QPen(QColor(0, 0, 0, 255), 1.0, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
for handle, rect in self.handles.items():
if self.handleSelected is None or handle == self.handleSelected:
painter.drawEllipse(rect)
def main():
app = QApplication(sys.argv)
grview = QGraphicsView()
scene = QGraphicsScene()
scene.setSceneRect(0, 0, 680, 459)
scene.addPixmap(QPixmap('01.png'))
grview.setScene(scene)
item = GraphicsRectItem(0, 0, 300, 150)
scene.addItem(item)
grview.fitInView(scene.sceneRect(), Qt.KeepAspectRatio)
grview.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
I was also trying to do a similar task, except in my case I needed a reusable object that could inherit the parent item that needed to be resized. Here is an example of using this Resizer
class to add a resize anchor to its parent QGraphicsRectItem.
The Resizer
class inherits QGraphicsObject and reimplements the paint method. The itemChange
method of Resizer
emits a signal which is assigned to the resize
method in the parent Box
class. This in turn uses the position change information from the resizer to adjust the rect of the box.
This way, the resizer can be resused for other items as well, without having to re-implement mouse press/move/release events.
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class Box(QGraphicsRectItem):
def __init__(self, position, rect=QRectF(0, 0, 100, 50), parent=None, scene=None):
super().__init__(rect, parent, scene)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsFocusable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.setPos(position)
self.resizer = Resizer(parent=self)
resizerWidth = self.resizer.rect.width() / 2
resizerOffset = QPointF(resizerWidth, resizerWidth)
self.resizer.setPos(self.rect().bottomRight() - resizerOffset)
self.resizer.resizeSignal.connect(self.resize)
def paint(self, painter, option, widget=None):
pen = QPen()
pen.setColor(Qt.black)
painter.setPen(pen)
painter.setBrush(Qt.transparent)
painter.drawRect(self.rect())
@pyqtSlot()
def resize(self, change):
self.setRect(self.rect().adjusted(0, 0, change.x(), change.y()))
self.prepareGeometryChange()
self.update()
class Resizer(QGraphicsObject):
resizeSignal = pyqtSignal(QPointF)
def __init__(self, rect=QRectF(0, 0, 10, 10), parent=None):
super().__init__(parent)
self.setFlag(QGraphicsItem.ItemIsMovable, True)
self.setFlag(QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)
self.rect = rect
def boundingRect(self):
return self.rect
def paint(self, painter, option, widget=None):
if self.isSelected():
pen = QPen()
pen.setStyle(Qt.DotLine)
painter.setPen(pen)
painter.drawEllipse(self.rect)
def itemChange(self, change, value):
if change == QGraphicsItem.ItemPositionChange:
if self.isSelected():
self.resizeSignal.emit(value - self.pos())
return value
if __name__ == "__main__":
app = QApplication(sys.argv)
view = QGraphicsView()
scene = QGraphicsScene()
scene.setSceneRect(0, 0, 500, 1000)
view.setScene(scene)
box = Box(QPointF(50, 50), scene=scene)
view.show()
sys.exit(app.exec_())