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

解决QGraphicsObject按住Shift旋转时抖动不稳定的问题

问题修复:PySide6 ImageBox旋转抖动问题

问题根源

  1. 坐标系统混淆:旋转时使用event.pos()(Item局部坐标),但Item旋转后局部坐标系跟着旋转,导致鼠标位置计算的角度不断跳变,引发抖动。
  2. 无初始角度偏移:直接用鼠标位置到中心的绝对角度设置旋转,没有记录旋转开始时的初始角度差,导致鼠标移动时角度频繁重置。
  3. 旋转与缩放逻辑冲突:Shift旋转时没有完全阻断缩放拖拽的逻辑分支,导致两种操作相互干扰。
  4. 无限重绘触发:paint方法中调用self.update(),引发无限重绘循环,加重画面抖动。

修复步骤

  • 改用场景坐标计算旋转角度,避免Item坐标系旋转带来的干扰。
  • 记录旋转开始时的初始角度和鼠标位置,通过计算角度偏移量更新旋转,而非直接设置绝对角度。
  • 旋转时完全隔离缩放/拖拽逻辑,避免操作冲突。
  • 移除paint中的self.update(),消除无限重绘。

修改后的完整代码

from PySide6.QtCore import QRect, QPoint, QRectF, Qt
from PySide6.QtGui import QMouseEvent, QBrush, QColor, QPen, QTransform, QGuiApplication
from PySide6.QtWidgets import QGraphicsObject, QGraphicsItem
import math  # 补充缺失的math模块导入


class ImageBox(QGraphicsObject):
    def __init__(self, pixmap):
        super().__init__()
        self.image = pixmap
        self.mousePressPos = None
        self.mousePressRect = None
        self.setAcceptHoverEvents(True)
        self.setFlag(QGraphicsItem.ItemIsMovable, True)
        self.setFlag(QGraphicsItem.ItemIsSelectable, True)
        self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True)

        self.isMirroredHorizontally = False
        self.isMirroredVertically = False
        self.rotate_op = False
        # 新增旋转状态记录变量
        self.initial_rotation = 0
        self.initial_mouse_scene_pos = None

        self.rect = self.image.rect()

        self.moving = []
        self.origin = QPoint()

        self.setTransformOriginPoint(self.rect.center())

    def getPixmap(self):
        return self.image

    def mirrorHorizontally(self):
        rect = self.boundingRect()
        self.isMirroredHorizontally = not self.isMirroredHorizontally
        scaleX = -1 if self.isMirroredHorizontally else 1
        scaleY = -1 if self.isMirroredVertically else 1
        transform = QTransform(scaleX, 0, 0, scaleY, 0, 0)
        self.setTransform(transform)
        if self.isMirroredHorizontally:
            self.moveBy(rect.width(), 0)
        else:
            self.moveBy(-rect.width(), 0)

    def mirrorVertically(self):
        rect = self.boundingRect()
        self.isMirroredVertically = not self.isMirroredVertically
        scaleX = -1 if self.isMirroredHorizontally else 1
        scaleY = -1 if self.isMirroredVertically else 1
        transform = QTransform(scaleX, 0, 0, scaleY, 0, 0)
        self.setTransform(transform)
        if self.isMirroredVertically:
            self.moveBy(0, rect.height())
        else:
            self.moveBy(0, -rect.height())

    def corners_rect(self) -> list:
        """ Return corner rect geometry for each corner"""
        size = 10  # 缩放控制点大小
        size2 = self.rect.width() - 40  # 边缘控制点宽度
        size3 = self.rect.height() - 40  # 边缘控制点高度
        half_size = size / 2  # 控制点半宽
        return [
            QRect(self.rect.left() - half_size, self.rect.top() - half_size, size, size),  # top left
            QRect(self.rect.right() - half_size, self.rect.top() - half_size, size, size),  # top right
            QRect(self.rect.left() - half_size, self.rect.bottom() - half_size, size, size),  # bottom left
            QRect(self.rect.right() - half_size, self.rect.bottom() - half_size, size, size),  # bottom right
            QRect(self.rect.left() + 20, self.rect.top() - half_size, size2, size),
            # top edge
            QRect(self.rect.right() - half_size, self.rect.top() + 20, size, size3),
            # right edge
            QRect(self.rect.left() + 20, self.rect.bottom() - half_size, size2, size),
            # bottom edge
            QRect(self.rect.left() - half_size, self.rect.top() + 20, size, size3),
            # left edge
        ]

    def boundingRect(self) -> QRectF:
        """ Override boundingRect """
        return self.rect.adjusted(-10, -10, 10, 10)

    def paint(self, painter, option, widget=None):
        painter.drawPixmap(self.rect, self.image)

        painter.drawRect(self.rect)

        point_list = self.corners_rect()

        if self.isSelected():
            pen = QPen(QColor("#00FFFF"))  # 边框颜色
            pen.setWidth(3)  # 边框宽度
            painter.setPen(pen)
            painter.drawRect(self.rect)

            painter.setBrush(QBrush(QColor("#00FFFF")))
            painter.setPen(Qt.NoPen)
            for rect in point_list[:4]:
                painter.drawEllipse(rect)

        # 移除self.update(),避免无限重绘
        # self.update()

    def mousePressEvent(self, event: QMouseEvent):
        """ override mouse Press Event """
        point_list = self.corners_rect()
        self.moving = [rect.contains(QPoint(event.pos().toPoint())) for rect in point_list]
        
        # 记录旋转初始状态
        if QGuiApplication.keyboardModifiers() == Qt.ShiftModifier and any(self.moving):
            self.initial_rotation = self.rotation()
            self.initial_mouse_scene_pos = event.scenePos()
            self.rotate_op = True
        elif any(self.moving):
            self.origin = self.rect.topLeft()
            self.rotate_op = False
        else:
            super().mousePressEvent(event)

    def mouseReleaseEvent(self, event: QMouseEvent):
        """ Override mouse release event """
        self.moving = [False]*8
        self.rotate_op = False
        super().mouseReleaseEvent(event)

    def rotateWithMouse(self, mouse_scene_pos):
        # 使用场景坐标计算,避免Item旋转后的坐标干扰
        center_scene = self.mapToScene(self.rect.center())
        # 计算初始鼠标位置与中心的角度
        initial_dx = self.initial_mouse_scene_pos.x() - center_scene.x()
        initial_dy = self.initial_mouse_scene_pos.y() - center_scene.y()
        initial_angle = math.degrees(math.atan2(initial_dy, initial_dx))
        
        # 计算当前鼠标位置与中心的角度
        current_dx = mouse_scene_pos.x() - center_scene.x()
        current_dy = mouse_scene_pos.y() - center_scene.y()
        current_angle = math.degrees(math.atan2(current_dy, current_dx))
        
        # 计算角度偏移,累加到初始旋转值上
        angle_offset = current_angle - initial_angle
        self.setRotation(self.initial_rotation + angle_offset)

    def mouseMoveEvent(self, event: QMouseEvent):
        """ Override mouse move event """
        if any(self.moving):
            self.prepareGeometryChange()
            
            if QGuiApplication.keyboardModifiers() == Qt.ShiftModifier:
                # 旋转模式:直接调用旋转逻辑,跳过缩放
                self.rotateWithMouse(event.scenePos())
                return
            
            # 缩放/拖拽模式
            pos = event.pos().toPoint()
            if self.moving[0] or self.moving[2]:  # top left or bottom left
                self.rect.setLeft(pos.x())
            if self.moving[0] or self.moving[1]:  # top left or top right
                self.rect.setTop(pos.y())
            if self.moving[1] or self.moving[3]:  # top right or bottom right
                self.rect.setRight(pos.x())
            if self.moving[2] or self.moving[3]:  # bottom left or bottom right
                self.rect.setBottom(pos.y())

            if self.moving[4]:  # top edge
                self.rect.setTop(pos.y())
            if self.moving[6]:  # bottom edge
                self.rect.setBottom(pos.y())
            if self.moving[5]:  # right edge
                self.rect.setRight(pos.x())
            if self.moving[7]:  # left edge
                self.rect.setLeft(pos.x())

            self.rect = self.rect.normalized()
            self.update()
            return
        else:
            super().mouseMoveEvent(event)

关键修改点说明

  1. 坐标系统切换:旋转时使用event.scenePos()(场景全局坐标)计算角度,彻底避免Item旋转后局部坐标系的干扰。
  2. 初始状态记录:在mousePressEvent中记录旋转开始时的初始旋转角度和鼠标场景位置,后续旋转基于偏移量累加,避免角度跳变。
  3. 逻辑分支隔离:Shift旋转时直接返回,完全跳过缩放拖拽逻辑,消除操作冲突。
  4. 消除无限重绘:删除paint方法中的self.update(),终止无限重绘循环。
  5. 补充依赖导入:添加math模块导入,修复原代码的运行报错。

内容的提问来源于stack exchange,提问作者user21736604

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 20:35:57