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

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()

关键修改说明

  1. 拖动时机捕捉:重写MyGraphicsScene的mousePressEvent和mouseReleaseEvent,分别在拖动开始时记录选中元素初始位置,拖动结束时记录最终位置,确保仅在操作完成后触发命令。
  2. 无效命令过滤:添加位置变化检查,避免因误操作(如点击未移动)创建无意义的Undo命令。
  3. 命令逻辑优化:移除MovingMultiItemsCommand构造函数中自动推送栈的代码,改为在场景的鼠标释放事件中手动推送,逻辑更清晰可控。
  4. 全局栈初始化:在main函数中显式初始化单例Undo栈,确保实例正常创建。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 12:10:55