You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 16:05:24