python-3.xpyqtpyqt6

resize QGraphicsSvgItem


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)
   

Solution

  • 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()