PyQt5中QGraphicsScene多选中元素移动的Undo/Redo实现问题
解决QGraphicsScene多选元素移动的Undo/Redo问题
针对QGraphicsScene中多选元素移动时无法在操作完成后触发单次Undo命令的问题,核心思路是捕捉拖动的开始与结束时机,分别记录选中元素的初始位置和最终位置,仅在拖动完成后创建一次Undo命令。以下是完整的Python实现方案:
import sys from typing import Dict from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QPen, QColor, QBrush from PyQt5.QtWidgets import QApplication, QGraphicsItem, QMainWindow, QGraphicsScene, QGraphicsView, \ QGraphicsEllipseItem, QUndoStack, QUndoCommand import random class CustomUndoRedoStack(QUndoStack): instance = None def __init__(self): if not CustomUndoRedoStack.instance: CustomUndoRedoStack.instance = self super(CustomUndoRedoStack, self).__init__() @staticmethod def get_instance(): return CustomUndoRedoStack.instance class MovingMultiItemsCommand(QUndoCommand): def __init__(self, new_items_pos: Dict[QGraphicsItem, QPoint], old_items_pos: Dict[QGraphicsItem, QPoint]): super(MovingMultiItemsCommand, self).__init__() self.new_items_pos = new_items_pos self.old_items_pos = old_items_pos def undo(self) -> None: for item, pos in self.old_items_pos.items(): item.setPos(pos) super().undo() def redo(self) -> None: for item, pos in self.new_items_pos.items(): item.setPos(pos) super().redo() class CustomQGraphicsEllipseItem(QGraphicsEllipseItem): def __init__(self, x, y): super(CustomQGraphicsEllipseItem, self).__init__(x, y, 20, 20) self.setFlag(QGraphicsItem.ItemIsSelectable) self.setFlag(QGraphicsItem.ItemIsMovable) self.setPen(QPen(QColor(255, 128, 0), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)) self.setBrush(QBrush(QColor(255, 128, 20, 128))) class MyGraphicsView(QGraphicsView): def __init__(self): super(MyGraphicsView, self).__init__() self.setDragMode(QGraphicsView.RubberBandDrag) self._isPanning = False self._mousePressed = False self.setCacheMode(QGraphicsView.CacheBackground) self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) class MyGraphicsScene(QGraphicsScene): def __init__(self): super(MyGraphicsScene, self).__init__() self.setBackgroundBrush(QBrush(QColor(50, 50, 50))) self.dragging = False self.old_positions = {} def mousePressEvent(self, event): super().mousePressEvent(event) # 左键按下且有选中元素时,记录所有选中项的初始位置 if event.button() == Qt.LeftButton and self.selectedItems(): self.dragging = True self.old_positions = {item: item.pos() for item in self.selectedItems()} def mouseReleaseEvent(self, event): super().mouseReleaseEvent(event) if self.dragging: self.dragging = False # 记录移动完成后的位置 new_positions = {item: item.pos() for item in self.selectedItems()} # 检查是否有元素位置发生实际变化 position_changed = any( self.old_positions[item] != new_positions[item] for item in self.old_positions ) if position_changed: # 创建并推送Undo命令到栈中 cmd = MovingMultiItemsCommand(new_positions, self.old_positions) CustomUndoRedoStack.get_instance().push(cmd) # 清空临时存储的位置数据 self.old_positions.clear() class MyMainWindow(QMainWindow): def __init__(self): super(MyMainWindow, self).__init__() self.setWindowTitle("Test") self.resize(800, 600) self.gv = MyGraphicsView() self.gv.setScene(MyGraphicsScene()) self.setCentralWidget(self.gv) self.populate() def populate(self): scene = self.gv.scene() for i in range(500): x = random.randint(0, 1000) y = random.randint(0, 1000) rect = CustomQGraphicsEllipseItem(x, y) scene.addItem(rect) def main(): app = QApplication(sys.argv) # 初始化全局Undo栈实例 CustomUndoRedoStack() ex = MyMainWindow() ex.show() sys.exit(app.exec_()) if __name__ == '__main__': main()
关键修改说明
- 拖动时机捕捉:重写
MyGraphicsScene的mousePressEvent和mouseReleaseEvent,分别在拖动开始时记录选中元素初始位置,拖动结束时记录最终位置,确保仅在操作完成后触发命令。 - 无效命令过滤:添加位置变化检查,避免因误操作(如点击未移动)创建无意义的Undo命令。
- 命令逻辑优化:移除
MovingMultiItemsCommand构造函数中自动推送栈的代码,改为在场景的鼠标释放事件中手动推送,逻辑更清晰可控。 - 全局栈初始化:在
main函数中显式初始化单例Undo栈,确保实例正常创建。
内容的提问来源于stack exchange,提问作者Uncle Bob
相关产品推荐
相关产品推荐

