I'm new to PyQt5. I'm trying to implement item control like this
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...
Your object structure is a bit disorganized and unnecessarily convoluted.
For instance:
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;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;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:
0, 0
, so that the computation required for getting/setting their position is much easier;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)