PyQt5 QGraphicsItem旋转手柄移动后失效问题求助
问题描述
我是PyQt5新手,想要实现拖动旋转手柄旋转图形项的功能。目前编写的代码在图形项初始位置时旋转正常,但移动图形项后旋转功能失效,怀疑是坐标处理错误,求排查解决。
附上代码:
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_()
问题排查与修复
核心问题是坐标系统混淆,错误地使用父项的场景坐标计算手柄位置和旋转角度,导致移动父项后坐标完全错位。以下是具体修复点和优化后的代码:
关键错误点
- 手柄是父项的子元素,
pos()是相对于父项的局部坐标,却用场景坐标设置位置,导致移动父项后手柄脱离旋转原点 - 计算角度时,未将父项旋转原点转换到场景坐标系,忽略了父项的场景位置偏移
- 手动实现向量余弦计算冗余且易出错,PyQt自带角度计算方法可直接使用
修复后的代码
import math from PyQt5 import QtCore, QtWidgets from PyQt5.QtWidgets import * from PyQt5.QtCore import * from PyQt5.QtGui import * class QDMRotationHandle(QGraphicsPixmapItem): def __init__(self, item): super().__init__(item) # 让手柄不受父项旋转影响,保持原始朝向 self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIgnoresTransformations) # 用临时图标替代外部图片,确保代码可直接运行 pixmap = QPixmap(20, 20) pixmap.fill(Qt.red) painter = QPainter(pixmap) painter.setPen(Qt.white) painter.drawText(pixmap.rect(), Qt.AlignCenter, "R") painter.end() self.setPixmap(pixmap) self.setTransformOriginPoint(10, 10) self.item = item # 手柄到旋转原点的距离(取父项边界框半高) self.radius = self.item.boundingRect().height() / 2 # 初始位置:旋转原点正上方(局部坐标) self.update_handle_pos(0) def update_handle_pos(self, angle): # 根据旋转角度计算手柄的局部坐标 rad = math.radians(angle) x = self.item.transformOriginPoint().x() + self.radius * math.sin(rad) - 10 y = self.item.transformOriginPoint().y() - self.radius * math.cos(rad) - 10 self.setPos(x, y) def mouseMoveEvent(self, event): # 统一转换到场景坐标系计算 mouse_scene_pos = self.mapToScene(event.pos()) origin_scene_pos = self.item.mapToScene(self.item.transformOriginPoint()) # 用QLineF直接计算角度,避免手动向量计算误差 line = QLineF(origin_scene_pos, mouse_scene_pos) angle = line.angle() - 90 # 调整初始角度,使手柄在正上方时旋转角为0 # 更新父项旋转角度和手柄位置 self.item.setRotation(angle) self.update_handle_pos(angle) super().mouseMoveEvent(event) class QDMBoundingRect(QGraphicsRectItem): def __init__(self, item, handle): super().__init__() self.item = item self.handle = handle item.setParentItem(self) handle.setParentItem(self) # 修正边界框宽高顺序错误 rect = item.boundingRect() self.setRect(0, 0, rect.width(), rect.height()) self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable) # 初始位置居中 self.setPos(200, 200) app = QtWidgets.QApplication([]) scene = QtWidgets.QGraphicsScene() # 创建目标图形项 item = QGraphicsRectItem(0, 0, 100, 100) item.setTransformOriginPoint(50, 50) item.setBrush(Qt.lightGray) # 创建旋转手柄 handle_item = QDMRotationHandle(item) # 创建可移动的边界框容器 boundingRect = QDMBoundingRect(item, handle_item) scene.addItem(boundingRect) view = QtWidgets.QGraphicsView(scene) view.setFixedSize(500, 500) view.show() app.exec_()
修复说明
- 坐标统一:所有角度计算基于场景坐标系,彻底解决父项移动后的坐标偏移问题
- 简化逻辑:使用
QLineF.angle()替代手动向量计算,消除冗余代码和误差 - 自动更新位置:通过
update_handle_pos实时计算手柄局部坐标,确保始终保持在旋转原点的指定半径处 - 交互优化:添加
ItemIgnoresTransformations标志,让手柄不受父项旋转影响,提升操作体验 - 无依赖运行:替换临时图标,避免外部图片文件依赖
内容的提问来源于stack exchange,提问作者Rimuto
相关产品推荐
相关产品推荐

