pythonpyqt5rotationqgraphicsitem

QGraphicsItem Rotation handle


I'm new to PyQt5. I'm trying to implement item control like thisexample

there you can rotate item by dragging the rotation handle. What a have for now:

import math
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from math import sqrt, acos


class QDMRotationHandle(QGraphicsPixmapItem):
    def __init__(self, item):
        super().__init__(item)
        self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)
        self.setPixmap(QPixmap('rotation_handle.png').scaledToWidth(20))
        self.setTransformOriginPoint(10, 10)
        self.item = item
        self.item.boundingRect().moveCenter(QPointF(50, 50))
        self.hypot = self.parentItem().boundingRect().height()
        self.setPos(self.item.transformOriginPoint().x()-10, self.item.transformOriginPoint().y() - self.hypot)

    def getSecVector(self, sx, sy, ex, ey):
        return {"x": ex - sx, "y": 0}

    def getVector(self, sx, sy, ex, ey):
        if ex == sx or ey == sy:
            return 0
        return {"x": ex - sx, "y": ey - sy}

    def getVectorAngleCos(self, ax, ay, bx, by):
        ma = sqrt(ax * ax + ay * ay)
        mb = sqrt(bx * bx + by * by)
        sc = ax * bx + ay * by
        res = sc / ma / mb
        return res

    def mouseMoveEvent(self, event):
        pos = self.mapToParent(event.pos())
       # parent_pos = self.mapToScene(self.parent.transformOriginPoint())

        parent_pos = self.parentItem().pos()
        parent_center_pos = self.item.boundingRect().center()


        parent_pos_x = parent_pos.x() + self.item.transformOriginPoint().x()
        parent_pos_y = parent_pos.y() + self.item.transformOriginPoint().y()
        print("mouse: ", pos.x(), pos.y())
        print("handle: ", self.pos().x(), self.pos().y())
        print("item: ", parent_pos.x(), parent_pos.y())
        #print(parent_center_pos.x(), parent_center_pos.y())
        vecA = self.getVector(parent_pos_x, parent_pos_y, pos.x(), pos.y())
        vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
        #
        vect.setLine(parent_pos_x, parent_pos_y, pos.x(), pos.y())
        if pos.x() > parent_pos_x:
            #
            secVect.setLine(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)

            vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)
        elif pos.x() < parent_pos_x:
            #
            secVect.setLine(parent_pos_x, parent_pos_y, pos.x(), parent_pos_y)

            vecB = self.getSecVector(parent_pos_x, parent_pos_y, pos.x(), -parent_pos_y)
        if vecA != 0:
            cos = self.getVectorAngleCos(vecA["x"], vecA["y"], vecB["x"], vecB["y"])
            cos = abs(cos)
            if cos > 1:
                cos = 1
            sin = abs(sqrt(1 - cos ** 2))
            lc = self.hypot * cos
            ld = self.hypot * sin
            #self.ell = scene.addRect(parent_pos_x, parent_pos_y, 5, 5)
            if pos.x() < parent_pos_x and pos.y() < parent_pos_y:
                print(parent_pos_x, parent_pos_y )
                #self.ell.setPos(parent_pos.x(), parent_pos.y())
                self.setPos(parent_pos_x - lc, parent_pos_y - ld)
            elif pos.x() > parent_pos_x and pos.y() < parent_pos_y:
                #self.ell.setPos(parent_pos_x, parent_pos_y)
                self.setPos(parent_pos_x + lc, parent_pos_y - ld)
            elif pos.x() > parent_pos_x and pos.y() > parent_pos_y:
                #self.ell.setPos(parent_pos_x, parent_pos_y)
                self.setPos(parent_pos_x + lc, parent_pos_y + ld)
            elif pos.x() < parent_pos_x and pos.y() > parent_pos_y:
                #self.ell.setPos(parent_pos_x, parent_pos_y)
                self.setPos(parent_pos_x - lc, parent_pos_y + ld)
        else:
            if pos.x() == parent_pos_x and pos.y() < parent_pos_y:
                self.setPos(parent_pos_x, parent_pos_y - self.hypot)
            elif pos.x() == parent_pos_x and pos.y() > parent_pos_y:
                self.setPos(parent_pos_x, parent_pos_y + self.hypot)
            elif pos.y() == parent_pos_x and pos.x() > parent_pos_y:
                self.setPos(parent_pos_x + self.hypot, parent_pos_y)
            elif pos.y() == parent_pos_x and pos.x() < parent_pos_y:
                self.setPos(parent_pos_x - self.hypot, parent_pos_y)

        item_position = self.item.transformOriginPoint()
        handle_pos = self.pos()
        #print(item_position.y())
        angle = math.atan2(item_position.y() - handle_pos.y(),
                           item_position.x() - handle_pos.x()) / math.pi * 180 - 90
        self.item.setRotation(angle)
        self.setRotation(angle)


class QDMBoundingRect(QGraphicsRectItem):
    def __init__(self, item, handle):
        super().__init__()
        self.item = item
        self.handle = handle
        item.setParentItem(self)
        handle.setParentItem(self)
        self.setRect(0, 0, item.boundingRect().height(), item.boundingRect().width())
        self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable)




app = QtWidgets.QApplication([])
scene = QtWidgets.QGraphicsScene()



item = scene.addRect(0, 0, 100, 100)
item.setTransformOriginPoint(50, 50)

handle_item = QDMRotationHandle(item)
#handle_item.setParentItem(item)
# handle_item.setOffset(10, 10)
#handle_item.setPos(40, -40)

scene.addItem(handle_item)
vect = scene.addLine(0, 0, 100, 100)
secVect = scene.addLine(50, 50, 100, 100)
secVect.setPen(QPen(Qt.green))
boundingRect = QDMBoundingRect(item, handle_item)

scene.addItem(boundingRect)
view = QtWidgets.QGraphicsView(scene)
view.setFixedSize(500, 500)
view.show()

app.exec_()

It works fine when item is in the initial position, but if you move it, the rotation stops working as it should. It seems like i do something wrong with coordinates, but i cant understand what. Please help...


Solution

  • Your object structure is a bit disorganized and unnecessarily convoluted.

    For instance:

    What you could do, instead, is to use a single "bounding rect item", and add controls as its children. Then, by filtering mouse events of those children, you can then alter the rotation based on the scene position of those events.

    In the following example, I took care of the above, considering:

    Also, by calling setFiltersChildEvents(True) the sceneEventFilter() receives any scene event from its children, allowing us to track mouse events; we return True for all the events that we handle so that they are not propagated to the parent, and also because move events can only received if mouse press events are accepted (the default implementation ignores them, unless the item is movable).

    class QDMBoundingRect(QGraphicsRectItem):
        _startPos = QPointF()
        def __init__(self, item):
            super().__init__()
            self.setRect(item.boundingRect())
            self.setFlags(self.ItemIsMovable)
            self.setFiltersChildEvents(True)
            self.item = item
    
            self.center = QGraphicsEllipseItem(-5, -5, 10, 10, self)
            self.handle = QGraphicsRectItem(-10, -10, 20, 20, self)
            self.vect = QGraphicsLineItem(self)
            self.secVect = QGraphicsLineItem(self)
            self.secVect.setPen(Qt.green)
            self.secVect.setFlags(self.ItemIgnoresTransformations)
    
            self.setCenter(item.transformOriginPoint())
    
        def setCenter(self, center):
            self.center.setPos(center)
            self.handle.setPos(center.x(), -40)
            self.vect.setLine(QLineF(center, self.handle.pos()))
            self.secVect.setPos(center)
            self.setTransformOriginPoint(center)
    
        def sceneEventFilter(self, item, event):
            if item == self.handle:
                if (event.type() == event.GraphicsSceneMousePress 
                    and event.button() == Qt.LeftButton):
                        self._startPos = event.pos()
                        return True
                elif (event.type() == event.GraphicsSceneMouseMove 
                    and self._startPos is not None):
                        centerPos = self.center.scenePos()
                        line = QLineF(centerPos, event.scenePos())
                        self.setRotation(90 - line.angle())
                        diff = self.handle.scenePos() - centerPos
                        self.secVect.setLine(0, 0, diff.x(), 0)
                        return True
            if (event.type() == event.GraphicsSceneMouseRelease
                and self._startPos is not None):
                    self._startPos = None
                    return True
            return super().sceneEventFilter(item, event)
    
    
    app = QApplication([])
    scene = QGraphicsScene()
    
    item = scene.addRect(0, 0, 100, 100)
    item.setPen(Qt.red)
    item.setTransformOriginPoint(50, 50)
    
    boundingRect = QDMBoundingRect(item)
    
    scene.addItem(boundingRect)
    view = QGraphicsView(scene)
    view.setFixedSize(500, 500)
    view.show()
    
    app.exec_()
    

    With the above code you can also implement the movement of the "center" handle, allowing to rotate around a different position:

        def sceneEventFilter(self, item, event):
            if item == self.handle:
                # ... as above
            elif item == self.center:
                if (event.type() == event.GraphicsSceneMousePress 
                    and event.button() == Qt.LeftButton):
                        self._startPos = event.pos()
                        return True
                elif (event.type() == event.GraphicsSceneMouseMove 
                    and self._startPos is not None):
                        newPos = self.mapFromScene(
                            event.scenePos() - self._startPos)
                        self.setCenter(newPos)
                        return True
            if (event.type() == event.GraphicsSceneMouseRelease
                and self._startPos is not None):
                    self._startPos = None
                    return True
            return super().sceneEventFilter(item, event)