解决QGraphicsObject按住Shift旋转时抖动不稳定的问题
问题修复:PySide6 ImageBox旋转抖动问题
问题根源
- 坐标系统混淆:旋转时使用
event.pos()(Item局部坐标),但Item旋转后局部坐标系跟着旋转,导致鼠标位置计算的角度不断跳变,引发抖动。 - 无初始角度偏移:直接用鼠标位置到中心的绝对角度设置旋转,没有记录旋转开始时的初始角度差,导致鼠标移动时角度频繁重置。
- 旋转与缩放逻辑冲突:Shift旋转时没有完全阻断缩放拖拽的逻辑分支,导致两种操作相互干扰。
- 无限重绘触发:
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)
关键修改点说明
- 坐标系统切换:旋转时使用
event.scenePos()(场景全局坐标)计算角度,彻底避免Item旋转后局部坐标系的干扰。 - 初始状态记录:在
mousePressEvent中记录旋转开始时的初始旋转角度和鼠标场景位置,后续旋转基于偏移量累加,避免角度跳变。 - 逻辑分支隔离:Shift旋转时直接返回,完全跳过缩放拖拽逻辑,消除操作冲突。
- 消除无限重绘:删除
paint方法中的self.update(),终止无限重绘循环。 - 补充依赖导入:添加
math模块导入,修复原代码的运行报错。
内容的提问来源于stack exchange,提问作者user21736604
相关产品推荐
相关产品推荐

