I have this code that I adapted for QGraphicsSvgItem from one of the question on SO.
The issue is that when I drag left side towards right it feels as if actually the right side is moving toward the left side. Same with top side to bottom.
In nutshell, moving left or top sides feel like the rect is resizing in reverse. I think this is because the rect is drawn from top-left. I am not able to fix it. Can someone please help me.
Thanks.
import sys
from PyQt6.QtCore import Qt, QRectF, QPointF
from PyQt6.QtGui import QBrush, QPainterPath, QPainter, QColor, QPen, QTransform, QPixmap
from PyQt6.QtWidgets import QApplication, QGraphicsView, QGraphicsScene, QGraphicsItem
from PyQt6.QtSvgWidgets import QGraphicsSvgItem
from PyQt6.QtSvg import QSvgRenderer
class ResizableSvgItem(QGraphicsSvgItem):
handleTopLeft = 1
handleTopMiddle = 2
handleTopRight = 3
handleMiddleLeft = 4
handleMiddleRight = 5
handleBottomLeft = 6
handleBottomMiddle = 7
handleBottomRight = 8
handleSize = 12.0
handleSpace = -8.0
handleCursors = {
handleTopLeft: Qt.CursorShape.SizeFDiagCursor,
handleTopMiddle: Qt.CursorShape.SizeVerCursor,
handleTopRight: Qt.CursorShape.SizeBDiagCursor,
handleMiddleLeft: Qt.CursorShape.SizeHorCursor,
handleMiddleRight: Qt.CursorShape.SizeHorCursor,
handleBottomLeft: Qt.CursorShape.SizeBDiagCursor,
handleBottomMiddle: Qt.CursorShape.SizeVerCursor,
handleBottomRight: Qt.CursorShape.SizeFDiagCursor,
}
def __init__(self, svg_file, *args):
super().__init__(svg_file, *args)
self.handles = {}
self.handleSelected = None
self.mousePressPos = None
self.mousePressRect = None
self.setAcceptHoverEvents(True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsFocusable, True)
self.updateHandlesPos()
def boundingRect(self):
""" Return the bounding rect of the SVG item. """
# Get the original bounding rect of the SVG item
original_rect = super().boundingRect()
o = self.handleSize + self.handleSpace
return original_rect.adjusted(-o, -o, o, o)
def handleAt(self, point):
# for k, v in self.handles.items():
# if v.contains(point):
# return k
# Check if the point is on the border
rect = self.boundingRect()
border_width = self.handleSize + self.handleSpace
if (
point.x() >= rect.left() - border_width and point.x() <= rect.left() + border_width and
point.y() >= rect.top() - border_width and point.y() <= rect.top() + border_width
):
return self.handleTopLeft
elif (
point.x() >= rect.right() - border_width and point.x() <= rect.right() + border_width and
point.y() >= rect.top() - border_width and point.y() <= rect.top() + border_width
):
return self.handleTopRight
elif (
point.x() >= rect.right() - border_width and point.x() <= rect.right() + border_width and
point.y() >= rect.bottom() - border_width and point.y() <= rect.bottom() + border_width
):
return self.handleBottomRight
elif (
point.x() >= rect.left() - border_width and point.x() <= rect.left() + border_width and
point.y() >= rect.bottom() - border_width and point.y() <= rect.bottom() + border_width
):
return self.handleBottomLeft
elif (
point.x() >= rect.left() - border_width and point.x() <= rect.right() + border_width and
point.y() >= rect.top() - border_width and point.y() <= rect.top() + border_width
):
return self.handleTopMiddle
elif (
point.x() >= rect.right() - border_width and point.x() <= rect.right() + border_width and
point.y() >= rect.top() - border_width and point.y() <= rect.bottom() + border_width
):
return self.handleMiddleRight
elif (
point.x() >= rect.left() - border_width and point.x() <= rect.right() + border_width and
point.y() >= rect.bottom() - border_width and point.y() <= rect.bottom() + border_width
):
return self.handleBottomMiddle
elif (
point.x() >= rect.left() - border_width and point.x() <= rect.left() + border_width and
point.y() >= rect.top() - border_width and point.y() <= rect.bottom() + border_width
):
return self.handleMiddleLeft
else:
# Check if the point is inside the SVG item
if super().contains(point):
return None
else:
return self.handleTopLeft # or any other handle, since the point is not on the border
def hoverMoveEvent(self, moveEvent):
if self.isSelected():
handle = self.handleAt(moveEvent.pos())
cursor = Qt.CursorShape.ArrowCursor if handle is None else self.handleCursors[handle]
self.setCursor(cursor)
super().hoverMoveEvent(moveEvent)
def hoverLeaveEvent(self, moveEvent):
self.setCursor(Qt.CursorShape.ArrowCursor)
super().hoverLeaveEvent(moveEvent)
def mousePressEvent(self, mouseEvent):
self.handleSelected = self.handleAt(mouseEvent.pos())
if self.handleSelected:
self.mousePressPos = mouseEvent.pos()
self.mousePressRect = self.boundingRect()
super().mousePressEvent(mouseEvent)
def mouseMoveEvent(self, mouseEvent):
if self.handleSelected is not None:
self.interactiveResize(mouseEvent.pos())
else:
super().mouseMoveEvent(mouseEvent)
def mouseReleaseEvent(self, mouseEvent):
super().mouseReleaseEvent(mouseEvent)
self.handleSelected = None
self.mousePressPos = None
self.mousePressRect = None
self.update()
def updateHandlesPos(self):
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. """
boundingRect = self.boundingRect()
rect = boundingRect
self.prepareGeometryChange()
if self.handleSelected is not None:
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()
rect.setLeft(toX)
rect.setTop(toY)
elif self.handleSelected == self.handleTopMiddle:
fromY = self.mousePressRect.top()
toY = fromY + mousePos.y() - self.mousePressPos.y()
rect.setTop(toY)
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()
rect.setRight(toX)
rect.setTop(toY)
elif self.handleSelected == self.handleMiddleLeft:
fromX = self.mousePressRect.left()
toX = fromX + mousePos.x() - self.mousePressPos.x()
rect.setLeft(toX)
elif self.handleSelected == self.handleMiddleRight:
fromX = self.mousePressRect.right()
toX = fromX + mousePos.x() - self.mousePressPos.x()
rect.setRight(toX)
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()
rect.setLeft(toX)
rect.setBottom(toY)
elif self.handleSelected == self.handleBottomMiddle:
fromY = self.mousePressRect.bottom()
toY = fromY + mousePos.y() - self.mousePressPos.y()
rect.setBottom(toY)
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()
rect.setRight(toX)
rect.setBottom(toY)
# Calculate the new scale
newWidth = rect.width()
newHeight = rect.height()
scaleX = newWidth / self.mousePressRect.width()
scaleY = newHeight / self.mousePressRect.height()
# Apply the scaling transformation
self.setTransform(QTransform().scale(scaleX, scaleY), True)
self.updateHandlesPos()
def shape(self):
""" Returns the shape of this item as a QPainterPath in local coordinates. """
path = QPainterPath()
path.addRect(self.boundingRect()) # Use boundingRect instead of rect
if self.isSelected():
for shape in self.handles.values():
path.addEllipse(shape)
return path
def paint(self, painter, option, widget=None):
""" Paint the SVG item and its resize handles. """
# Draw the SVG item
super().paint(painter, option, widget)
# Draw the handles
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setBrush(QBrush(QColor(255, 0, 0, 255)))
painter.setPen(QPen(QColor(0, 0, 0, 255), 1.0, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
for handle, rect in self.handles.items():
if self.handleSelected is None or handle == self.handleSelected:
painter.drawEllipse(rect)
Finally got it fixed. Now the resizing works as expected.
def interactiveResize(self, mousePos):
""" Perform shape interactive resize. """
boundingRect = self.boundingRect()
rect = boundingRect
self.prepareGeometryChange()
if self.handleSelected is not None:
dx = mousePos.x() - self.mousePressPos.x()
dy = mousePos.y() - self.mousePressPos.y()
if self.handleSelected == self.handleTopLeft:
self.setTransform(QTransform().translate(dx, dy).scale(
(self.mousePressRect.width() - dx) / self.mousePressRect.width(),
(self.mousePressRect.height() - dy) / self.mousePressRect.height()
), True)
elif self.handleSelected == self.handleTopMiddle:
self.setTransform(QTransform().translate(0, dy).scale(
1, (self.mousePressRect.height() - dy) / self.mousePressRect.height()
), True)
elif self.handleSelected == self.handleTopRight:
self.setTransform(QTransform().translate(0, dy).scale(
(self.mousePressRect.width() + dx) / self.mousePressRect.width(),
(self.mousePressRect.height() - dy) / self.mousePressRect.height()
), True)
elif self.handleSelected == self.handleMiddleLeft:
self.setTransform(QTransform().translate(dx, 0).scale(
(self.mousePressRect.width() - dx) / self.mousePressRect.width(),
1
), True)
elif self.handleSelected == self.handleMiddleRight:
self.setTransform(QTransform().translate(0, 0).scale(
(self.mousePressRect.width() + dx) / self.mousePressRect.width(),
1
), True)
elif self.handleSelected == self.handleBottomLeft:
self.setTransform(QTransform().translate(dx, 0).scale(
(self.mousePressRect.width() - dx) / self.mousePressRect.width(),
(self.mousePressRect.height() + dy) / self.mousePressRect.height()
), True)
elif self.handleSelected == self.handleBottomMiddle:
self.setTransform(QTransform().translate(0, 0).scale(
1, (self.mousePressRect.height() + dy) / self.mousePressRect.height()
), True)
elif self.handleSelected == self.handleBottomRight:
self.setTransform(QTransform().translate(0, 0).scale(
(self.mousePressRect.width() + dx) / self.mousePressRect.width(),
(self.mousePressRect.height() + dy) / self.mousePressRect.height()
), True)
self.updateHandlesPos()