本页内容

撤销框架示例

本示例演示了如何使用 Qt 撤销框架实现撤销/重做功能。

“撤销”图示例

在 Qt 撤销框架中,用户执行的所有操作都通过继承自 `QUndoCommand` 的类来实现。一个撤销命令类既知道如何通过 `redo()` 撤销操作(即仅执行首次操作),也知道如何通过 `undo()` 执行该操作。 对于用户执行的每项操作,都会将一个命令放入QUndoStack 中。由于该堆栈包含在文档上执行的所有命令(按时间顺序堆叠),因此可以通过撤销和重做这些命令来回滚文档的状态。有关撤销框架的高级介绍,请参阅概述文档。

“撤销”示例实现了一个简单的图表应用程序。用户可以添加和删除形状为方框或矩形的项目,并通过鼠标拖拽来移动这些项目。撤销堆栈显示在QUndoView 中,这是一个将命令作为列表项展示的列表。 “撤销”和“重做”功能可通过“编辑”菜单访问。用户还可以从撤销视图中选择命令。

我们使用图形视图框架来实现该图。由于该框架本身已提供示例(例如“Diagram Scene Example”),因此我们仅简要介绍相关代码。

该示例由以下类组成:

  • MainWindow 是主窗口,负责排列示例中的控件。它根据用户输入创建命令,并将这些命令保存在命令堆栈中。
  • AddCommand 将一个项目添加到场景中。
  • DeleteCommand 从场景中删除一个项目。
  • MoveCommand 当项目被移动时,`MoveCommand` 会记录移动的起始位置和结束位置,并在调用 `redo() ` 和 `undo() ` 时根据这些位置移动项目。
  • DiagramScene 继承QGraphicsScene ,并在项目被移动时为MoveComands 发出信号。
  • DiagramItem 继承QGraphicsPolygonItem ,并在图中表示一个项目。

MainWindow 类定义

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow();

public slots:
    void itemMoved(DiagramItem *movedDiagram, const QPointF &moveStartPosition);

private slots:
    void deleteItem();
    void addBox();
    void addTriangle();
    void about();
    void updateActions();

private:
    void createActions();
    void createMenus();
    void createToolBars();
    void createUndoView();

    QAction *deleteAction = nullptr;
    QAction *addBoxAction = nullptr;
    QAction *addTriangleAction = nullptr;
    QAction *undoAction = nullptr;
    QAction *redoAction = nullptr;
    QAction *exitAction = nullptr;
    QAction *aboutAction = nullptr;

    QMenu *fileMenu = nullptr;
    QMenu *editMenu = nullptr;
    QMenu *itemMenu = nullptr;
    QMenu *helpMenu = nullptr;

    DiagramScene *diagramScene = nullptr;
    QUndoStack *undoStack = nullptr;
    QUndoView *undoView = nullptr;
};

MainWindow 类负责维护撤销堆栈,即当它从undoAction 和redoAction 接收到triggered() 信号时,会创建QUndoCommand对象,并将它们压入或弹出堆栈。

MainWindow 类的实现

我们先来看看构造函数:

MainWindow::MainWindow()
{
    undoStack = new QUndoStack(this);
    diagramScene = new DiagramScene();

    const QBrush pixmapBrush(QPixmap(":/icons/cross.png").scaled(30, 30));
    diagramScene->setBackgroundBrush(pixmapBrush);
    diagramScene->setSceneRect(QRect(0, 0, 500, 500));

    createActions();
    createMenus();
    createToolBars();

    createUndoView();

    connect(diagramScene, &DiagramScene::itemMoved,
            this, &MainWindow::itemMoved);
    connect(diagramScene, &DiagramScene::selectionChanged,
            this, &MainWindow::updateActions);

    setWindowTitle("Undo Framework");
    QGraphicsView *view = new QGraphicsView(diagramScene);
    setCentralWidget(view);
    adjustSize();
}

在构造函数中,我们初始化了 DiagramScene 和QGraphicsView 。我们仅希望在选中某个项目时启用deleteAction ,因此将场景的selectionChanged() 信号连接到updateActions() 槽。

以下是createUndoView() 函数:

void MainWindow::createUndoView()
{
    QDockWidget *undoDockWidget = new QDockWidget;
    undoDockWidget->setWindowTitle(tr("Command List"));
    undoDockWidget->setWidget(new QUndoView(undoStack));
    addDockWidget(Qt::RightDockWidgetArea, undoDockWidget);
}

QUndoView 是一个小部件,它会将撤销堆栈中每个QUndoCommand 对应的文本(由setText() 函数设置)以列表形式显示出来。我们将它放置在一个停靠小部件中。

以下是createActions() 函数:

void MainWindow::createActions()
{
    deleteAction = new QAction(QIcon(":/icons/remove.png"), tr("&Delete Item"), this);
    deleteAction->setShortcut(tr("Del"));
    connect(deleteAction, &QAction::triggered, this, &MainWindow::deleteItem);
    ...
    undoAction = undoStack->createUndoAction(this, tr("&Undo"));
    undoAction->setIcon(QIcon(":/icons/undo.png"));
    undoAction->setShortcuts(QKeySequence::Undo);

    redoAction = undoStack->createRedoAction(this, tr("&Redo"));
    redoAction->setIcon(QIcon(":/icons/redo.png"));
    redoAction->setShortcuts(QKeySequence::Redo);

createActions() 函数按照上文所示的方式设置了所有示例操作。createUndoAction()和createRedoAction()方法帮助我们创建根据撤销堆栈状态而启用或禁用的操作。此外,操作的文本会根据撤销命令的text()自动更新。对于其他操作,我们在MainWindow 类中实现了相应插槽。

    ...
    updateActions();
}

void MainWindow::updateActions()
{
    deleteAction->setEnabled(!diagramScene->selectedItems().isEmpty());
}

创建所有操作后,我们通过调用与场景的selectionChanged 信号关联的同一函数来更新其状态。

createMenus() 和createToolBars() 函数负责将操作添加到菜单和工具栏中:

void MainWindow::createMenus()
{
    fileMenu = menuBar()->addMenu(tr("&File"));
    fileMenu->addAction(exitAction);

    editMenu = menuBar()->addMenu(tr("&Edit"));
    editMenu->addAction(undoAction);
    editMenu->addAction(redoAction);
    editMenu->addSeparator();
    editMenu->addAction(deleteAction);
    ...
    helpMenu = menuBar()->addMenu(tr("&About"));
    helpMenu->addAction(aboutAction);
}

void MainWindow::createToolBars()
{
    QToolBar *editToolBar = new QToolBar;
    editToolBar->addAction(undoAction);
    editToolBar->addAction(redoAction);
    editToolBar->addSeparator();
    editToolBar->addAction(deleteAction);
    ...
    addToolBar(editToolBar);
    addToolBar(itemToolBar);
}

以下是 `itemMoved() ` 槽:

void MainWindow::itemMoved(DiagramItem *movedItem,
                           const QPointF &oldPosition)
{
    undoStack->push(new MoveCommand(movedItem, oldPosition));
}

我们只需将一个 `MoveCommand` 压入栈中,该操作会调用其上的 `redo() ` 函数。

以下是 `deleteItem() ` 槽函数:

void MainWindow::deleteItem()
{
    if (diagramScene->selectedItems().isEmpty())
        return;

    QUndoCommand *deleteCommand = new DeleteCommand(diagramScene);
    undoStack->push(deleteCommand);
}

必须先选中一个项目才能将其删除。我们需要检查该项目是否已被选中,因为即使未选中任何项目,deleteAction 也可能被启用。这是因为当项目被选中时,我们并未捕获相应的信号或事件。

以下是addBox() 槽:

void MainWindow::addBox()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Box, diagramScene);
    undoStack->push(addCommand);
}

addBox() 函数会创建一个 AddCommand 对象,并将其压入撤销堆栈。

以下是 `addTriangle() ` 插槽:

void MainWindow::addTriangle()
{
    QUndoCommand *addCommand = new AddCommand(DiagramItem::Triangle,
                                              diagramScene);
    undoStack->push(addCommand);
}

addTriangle() 函数会创建一个AddCommand对象,并将其压入撤销堆栈。

以下是 `about()` 的实现:

void MainWindow::about()
{
    QMessageBox::about(this, tr("About Undo"),
                       tr("The <b>Undo</b> example demonstrates how to "
                          "use Qt's undo framework."));
}

“about”槽由aboutAction 触发,并为该示例显示一个“关于”对话框。

AddCommand 类定义

class AddCommand : public QUndoCommand
{
public:
    AddCommand(DiagramItem::DiagramType addType, QGraphicsScene *graphicsScene,
               QUndoCommand *parent = nullptr);
    ~AddCommand();

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
    QPointF initialPosition;
};

AddCommand 类负责将DiagramItem图形项添加到DiagramScene中。

AddCommand 类的实现

我们从构造函数开始:

AddCommand::AddCommand(DiagramItem::DiagramType addType,
                       QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    static int itemCount = 0;

    myDiagramItem = new DiagramItem(addType);
    initialPosition = QPointF((itemCount * 15) % int(scene->width()),
                              (itemCount * 15) % int(scene->height()));
    scene->update();
    ++itemCount;
    setText(QObject::tr("Add %1")
        .arg(createCommandString(myDiagramItem, initialPosition)));
}

首先创建要添加到 DiagramScene 中的 DiagramItem。setText() 函数允许我们设置描述该命令的QString 。我们利用该属性在QUndoView 以及主窗口的菜单中显示自定义消息。

void AddCommand::undo()
{
    myGraphicsScene->removeItem(myDiagramItem);
    myGraphicsScene->update();
}

undo() 将该项目从场景中移除。

void AddCommand::redo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myDiagramItem->setPos(initialPosition);
    myGraphicsScene->clearSelection();
    myGraphicsScene->update();
}

我们在此设置项的位置,因为构造函数中未进行此操作。

DeleteCommand 类定义

class DeleteCommand : public QUndoCommand
{
public:
    explicit DeleteCommand(QGraphicsScene *graphicsScene, QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;

private:
    DiagramItem *myDiagramItem;
    QGraphicsScene *myGraphicsScene;
};

DeleteCommand 类实现了从场景中移除项的功能。

DeleteCommand 类实现

DeleteCommand::DeleteCommand(QGraphicsScene *scene, QUndoCommand *parent)
    : QUndoCommand(parent), myGraphicsScene(scene)
{
    QList<QGraphicsItem *> list = myGraphicsScene->selectedItems();
    list.first()->setSelected(false);
    myDiagramItem = static_cast<DiagramItem *>(list.first());
    setText(QObject::tr("Delete %1")
        .arg(createCommandString(myDiagramItem, myDiagramItem->pos())));
}

我们知道必须有一个被选中的项,因为除非选中了要删除的项,否则无法创建 DeleteCommand,而且任何时候都只能选中一个项。如果该项被重新插入到场景中,则必须取消其选中状态。

void DeleteCommand::undo()
{
    myGraphicsScene->addItem(myDiagramItem);
    myGraphicsScene->update();
}

该项被简单地重新插入到场景中。

void DeleteCommand::redo()
{
    myGraphicsScene->removeItem(myDiagramItem);
}

该项将从场景中移除。

MoveCommand 类定义

class MoveCommand : public QUndoCommand
{
public:
    enum { Id = 1234 };

    MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                QUndoCommand *parent = nullptr);

    void undo() override;
    void redo() override;
    bool mergeWith(const QUndoCommand *command) override;
    int id() const override { return Id; }

private:
    DiagramItem *myDiagramItem;
    QPointF myOldPos;
    QPointF newPos;
};

重新实现了mergeWith()方法,以便将某项的连续移动合并为一个MoveCommand,即该项将被移动回首次移动的起始位置。

MoveCommand 类的实现

MoveCommand 的构造函数如下所示:

MoveCommand::MoveCommand(DiagramItem *diagramItem, const QPointF &oldPos,
                         QUndoCommand *parent)
    : QUndoCommand(parent), myDiagramItem(diagramItem)
    , myOldPos(oldPos), newPos(diagramItem->pos())
{
}

我们分别保存旧位置和新位置,以便进行撤销和重做操作。

void MoveCommand::undo()
{
    myDiagramItem->setPos(myOldPos);
    myDiagramItem->scene()->update();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

我们只需将项设置为旧位置并更新场景。

void MoveCommand::redo()
{
    myDiagramItem->setPos(newPos);
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));
}

我们将对象设置为新位置。

bool MoveCommand::mergeWith(const QUndoCommand *command)
{
    const MoveCommand *moveCommand = static_cast<const MoveCommand *>(command);
    DiagramItem *item = moveCommand->myDiagramItem;

    if (myDiagramItem != item)
        return false;

    newPos = item->pos();
    setText(QObject::tr("Move %1")
        .arg(createCommandString(myDiagramItem, newPos)));

    return true;
}

每当创建一个 MoveCommand 时,都会调用此函数来检查是否应将其与前一个命令合并。栈中保存的正是前一个命令对象。如果命令被合并,该函数返回 true;否则返回 false。

我们首先检查是否是同一个对象被移动了两次,如果是,则将这两个命令合并。我们更新该对象的位置,以便在撤销操作时,它将恢复到移动序列中的最后一个位置。

DiagramScene 类定义

class DiagramScene : public QGraphicsScene
{
    Q_OBJECT

public:
    DiagramScene(QObject *parent = nullptr);

signals:
    void itemMoved(DiagramItem *movedItem, const QPointF &movedFromPosition);

protected:
    void mousePressEvent(QGraphicsSceneMouseEvent *event) override;
    void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override;

private:
    QGraphicsItem *movingItem = nullptr;
    QPointF oldPos;
};

DiagramScene 实现了使用鼠标移动 DiagramItem 的功能。当移动操作完成时,它会发出一个信号。该信号由 `MainWindow` 捕获,后者负责生成 `MoveCommand`。我们不深入探讨 DiagramScene 的实现细节,因为它仅涉及图形框架相关的问题。

main() 函数

程序中的 `main() ` 函数如下所示:

int main(int argv, char *args[])
{
    QApplication app(argv, args);

    MainWindow mainWindow;
    mainWindow.show();

    return app.exec();
}

我们在 DiagramScene 的背景中绘制网格,因此使用了资源文件。该函数的其余部分用于创建MainWindow 并将其作为顶级窗口显示。

示例项目 @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.