Rimuto
Rimuto

Reputation: 33

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...

Upvotes: 0

Views: 476

Answers (1)

musicamante
musicamante

Reputation: 48231

Your object structure is a bit disorganized and unnecessarily convoluted.

For instance:

  • you're adding handle_item to the scene, but since you've made it a child of item, you shall not try to add it to the scene again;
  • the ItemIsMovable is useless if you don't call the default implementation in mouseMoveEvent, but for your purpose you actually don't need to make it movable at all;
  • the whole computation is unnecessarily complex (and prone to bugs, since you're both setting the object position and rotation);
  • using a pixmap for the handle seems quite unnecessary, use Qt primitives if you can, to avoid graphical artifacts and unnecessary transform computations;

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:

  • control/handle items should always be positioned at 0, 0, so that the computation required for getting/setting their position is much easier;
  • since items (including vector items) are actually children of the QDMBoundingRect, you don't need to compute coordinates for each point of the vectors: the rotation of the parent will be applied to them automatically;
  • the secVect (still a child item) is simplified by just setting its x offset using the mapped scene positions of center and handle;

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)

Upvotes: 2

Related Questions