jsaugustyn
jsaugustyn

Reputation: 53

Rotate QGraphicsPixmapItem with mouse

I'm writing an application in PyQt that will allow users to select images placed on a QGraphicsScene (using a custom QGraphicsPixmapItem). Upon selection I would like a rotation handle to appear on the image that the user can "grab" with the mouse and rotate, thus rotating the QGraphicsPixmapItem. Basically, I am looking for the rotation handle feature you get in PowerPoint upon selecting a shape. This seems like a really basic feature that many people would have implemented, but I cannot find any good examples on the web. Can anyone point me in the right direction?

Upvotes: 4

Views: 3103

Answers (1)

NoDataDumpNoContribution
NoDataDumpNoContribution

Reputation: 10860

Let's first divide the problem into smaller ones and then assemble everything again. I use PyQt5 in this solution.

1. Rotate a QGraphicsItem

For that you need to set a rotation angle in degree using setRotation on the item. The rotation will be around the point specified by setTransformOriginPoint. Usually one would take the center of a shape. If you do not specify this point, usually the upper, left corner of a shape is taken.

2. Drag a QGraphicsItem

For performance reasons QGraphicsItems aren't movable and do not send position changes to the event framework. By setting the appropriate flags QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges you can change that. Additionally QGraphicsItem does not inherit from QObject, so for using signals I usually have an additional object which inherits from QObject.

3. Draw a handle item and determine the rotation angle to rotate

In the example below I have a very small rectangle as handle, you can of course use any QGraphicsItem you like. My method make_GraphicsItem_draggable takes any QGraphicsItem derived class and makes it draggable. For determining the rotation angle given the current position of the draggable handle item and the transformation origin of the item to be rotated use math.atan2 and the differences in x and y coordinates of these positions.

Example

import math
from PyQt5 import QtCore, QtWidgets

class DraggableGraphicsItemSignaller(QtCore.QObject):

    positionChanged = QtCore.pyqtSignal(QtCore.QPointF)

    def __init__(self):
        super().__init__()

def make_GraphicsItem_draggable(parent):

    class DraggableGraphicsItem(parent):

        def __init__(self, *args, **kwargs):
            """
            By default QGraphicsItems are not movable and also do not emit signals when the position is changed for
            performance reasons. We need to turn this on.
            """
            parent.__init__(self, *args, **kwargs)
            self.parent = parent
            self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
            self.signaller = DraggableGraphicsItemSignaller()

        def itemChange(self, change, value):
            if change == QtWidgets.QGraphicsItem.ItemPositionChange:
                self.signaller.positionChanged.emit(value)

            return parent.itemChange(self, change, value)

    return DraggableGraphicsItem

def rotate_item(position):
    item_position = item.transformOriginPoint()
    angle = math.atan2(item_position.y() - position.y(), item_position.x() - position.x()) / math.pi * 180 - 45 # -45 because handle item is at upper left border, adjust to your needs
    print(angle)
    item.setRotation(angle)


DraggableRectItem = make_GraphicsItem_draggable(QtWidgets.QGraphicsRectItem)

app = QtWidgets.QApplication([])

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

handle_item = DraggableRectItem()
handle_item.signaller.positionChanged.connect(rotate_item)
handle_item.setRect(-40, -40, 20, 20)
scene.addItem(handle_item)

view = QtWidgets.QGraphicsView(scene)
view.setFixedSize(300, 200)
view.show()

app.exec_()

Start (item = big rectangle and handle = small rectangle)

Start

Rotated after dragging the handle (small rectangle)

Rotated

One thing that is missing: The handle does not have a fixed distance to the item position (i.e. instead of moving in a circle you can drag it further away or closer). While this does not change the rotation angle, it does not look perfect. But the main points are covered here and should set you on the right track.

Upvotes: 4

Related Questions