Search code examples
pythonpython-3.xpyqtpyqt5qgraphicsitem

Resize about center on a QGraphicsItem proportional to mouse speed?


I am sorry for repetitively asking questions regarding pyqt, but the limited resources on it force me to.

I have been trying to implement a resize function for my circle shape, that is an extension of QGraphicsItem, which has an ellipse and a centered text. I am able to resize my shape as desired, but the shape takes a while to catch up to the mouse, i.e on switching directions the circle continues to increase but takes a while to switch directions, moreover the circle resizes with an anchor at the top left corner of the bounding rect.

See this question for a bit of code background

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):
        self.prepareGeometryChange()
        if self.handleSelected in [self.handleTopLeft,
                                   self.handleTopRight,
                                   self.handleBottomLeft,
                                   self.handleBottomRight,
                                   self.handleTopMiddle,
                                   self.handleBottomMiddle,
                                   self.handleMiddleLeft,
                                   self.handleMiddleRight]:
            self.radius += (mousePos.y() + mousePos.x() + self.mousePressPos.x() - self.mousePressPos.y())/64
            self.setPos(self.x(),self.y())
        self.update()   
        self.updateHandlesPos()

Solution

  • Since the OP has not provided an MRE then I have created an example from scratch. The logic is to track the changes of the items and according to that calculate the new geometry and establish the new position of the other items.

    from PyQt5 import QtWidgets, QtGui, QtCore
    
    
    class GripItem(QtWidgets.QGraphicsPathItem):
        circle = QtGui.QPainterPath()
        circle.addEllipse(QtCore.QRectF(-5, -5, 10, 10))
        square = QtGui.QPainterPath()
        square.addRect(QtCore.QRectF(-10, -10, 20, 20))
    
        def __init__(self, annotation_item, index):
            super(GripItem, self).__init__()
            self.m_annotation_item = annotation_item
            self.m_index = index
    
            self.setPath(GripItem.circle)
            self.setBrush(QtGui.QColor("green"))
            self.setPen(QtGui.QPen(QtGui.QColor("green"), 2))
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
            self.setAcceptHoverEvents(True)
            self.setZValue(11)
            self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
    
        def hoverEnterEvent(self, event):
            self.setPath(GripItem.square)
            self.setBrush(QtGui.QColor("red"))
            super(GripItem, self).hoverEnterEvent(event)
    
        def hoverLeaveEvent(self, event):
            self.setPath(GripItem.circle)
            self.setBrush(QtGui.QColor("green"))
            super(GripItem, self).hoverLeaveEvent(event)
    
        def mouseReleaseEvent(self, event):
            self.setSelected(False)
            super(GripItem, self).mouseReleaseEvent(event)
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
                self.m_annotation_item.movePoint(self.m_index, value)
            return super(GripItem, self).itemChange(change, value)
    
    
    class DirectionGripItem(GripItem):
        def __init__(self, annotation_item, direction=QtCore.Qt.Horizontal, parent=None):
            super(DirectionGripItem, self).__init__(annotation_item, parent)
            self._direction = direction
    
        @property
        def direction(self):
            return self._direction
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionChange and self.isEnabled():
                p = QtCore.QPointF(self.pos())
                if self.direction == QtCore.Qt.Horizontal:
                    p.setX(value.x())
                elif self.direction == QtCore.Qt.Vertical:
                    p.setY(value.y())
                self.m_annotation_item.movePoint(self.m_index, p)
                return p
            return super(DirectionGripItem, self).itemChange(change, value)
    
    
    class CircleAnnotation(QtWidgets.QGraphicsEllipseItem):
        def __init__(self, radius=1, parent=None):
            super(CircleAnnotation, self).__init__(parent)
            self.setZValue(11)
            self.m_items = []
    
            self.setPen(QtGui.QPen(QtGui.QColor("green"), 4))
    
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
            self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True)
    
            self.setAcceptHoverEvents(True)
    
            self.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor))
    
            self._radius = radius
            self.update_rect()
    
        @property
        def radius(self):
            return self._radius
    
        @radius.setter
        def radius(self, r):
            if r <= 0:
                raise ValueError("radius must be positive")
            self._radius = r
            self.update_rect()
            self.add_grip_items()
            self.update_items_positions()
    
        def update_rect(self):
            rect = QtCore.QRectF(0, 0, 2 * self.radius, 2 * self.radius)
            rect.moveCenter(self.rect().center())
            self.setRect(rect)
    
        def add_grip_items(self):
            if self.scene() and not self.m_items:
                for i, (direction) in enumerate(
                    (
                        QtCore.Qt.Vertical,
                        QtCore.Qt.Horizontal,
                        QtCore.Qt.Vertical,
                        QtCore.Qt.Horizontal,
                    )
                ):
                    item = DirectionGripItem(self, direction, i)
                    self.scene().addItem(item)
                    self.m_items.append(item)
    
        def movePoint(self, i, p):
            if 0 <= i < min(4, len(self.m_items)):
                item_selected = self.m_items[i]
                lp = self.mapFromScene(p)
                self._radius = (lp - self.rect().center()).manhattanLength()
                k = self.indexOf(lp)
                if k is not None:
                    self.m_items = [item for item in self.m_items if not item.isSelected()]
                    self.m_items.insert(k, item_selected)
                    self.update_items_positions([k])
                    self.update_rect()
    
        def update_items_positions(self, index_no_updates=None):
            index_no_updates = index_no_updates or []
            for i, (item, direction) in enumerate(
                zip(
                    self.m_items,
                    (
                        QtCore.Qt.Vertical,
                        QtCore.Qt.Horizontal,
                        QtCore.Qt.Vertical,
                        QtCore.Qt.Horizontal,
                    ),
                ),
            ):
                item.m_index = i
                if i not in index_no_updates:
                    pos = self.mapToScene(self.point(i))
                    item = self.m_items[i]
                    item._direction = direction
                    item.setEnabled(False)
                    item.setPos(pos)
                    item.setEnabled(True)
    
        def indexOf(self, p):
            for i in range(4):
                if p == self.point(i):
                    return i
    
        def point(self, index):
            if 0 <= index < 4:
                return [
                    QtCore.QPointF(0, -self.radius),
                    QtCore.QPointF(self.radius, 0),
                    QtCore.QPointF(0, self.radius),
                    QtCore.QPointF(-self.radius, 0),
                ][index]
    
        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
                self.update_items_positions()
                return
            if change == QtWidgets.QGraphicsItem.ItemSceneHasChanged:
                self.add_grip_items()
                self.update_items_positions()
                return
            return super(CircleAnnotation, self).itemChange(change, value)
    
        def hoverEnterEvent(self, event):
            self.setBrush(QtGui.QColor(255, 0, 0, 100))
            super(CircleAnnotation, self).hoverEnterEvent(event)
    
        def hoverLeaveEvent(self, event):
            self.setBrush(QtGui.QBrush(QtCore.Qt.NoBrush))
            super(CircleAnnotation, self).hoverLeaveEvent(event)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
    
        scene = QtWidgets.QGraphicsScene()
        view = QtWidgets.QGraphicsView(scene)
        view.setRenderHints(QtGui.QPainter.Antialiasing)
        item = CircleAnnotation()
        item.radius = 100
        scene.addItem(item)
        view.showMaximized()
    
        sys.exit(app.exec_())