撤消框架示例

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

撤销图示例

在 Qt undo 框架中,用户执行的所有操作都在继承QUndoCommand 的类中实现。撤消命令类知道如何redo() - 或只做第一次 - 和undo() 一个操作。用户每执行一次操作,就会在QUndoStack 上放置一条命令。由于堆栈包含文档上执行的所有命令(按时间顺序堆叠),因此可以通过撤销和重做命令来前后滚动文档的状态。有关撤销框架的高级介绍,请参阅概述文档

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

我们使用图形视图框架来实现图表。由于该框架有自己的示例(如图表场景示例),因此我们只简要介绍相关代码。

该示例由以下类组成:

  • MainWindow 是主窗口,负责安排示例中的部件。它根据用户输入创建命令,并将其保存在命令栈中。
  • AddCommand 在场景中添加项目。
  • DeleteCommand 从场景中删除一个项目。
  • MoveCommand 移动项目时,MoveCommand 会记录移动的起始和停止位置,当调用 和 时,它会根据这些位置移动项目。redo() undo()
  • DiagramScene MoveCommand 继承了 ,并在移动项目时为 发送信号。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 类负责维护撤销堆栈,即创建QUndoCommand,并在收到来自undoActionredoActiontriggered() 信号时将其从堆栈中推入或推出。

主窗口类的实现

我们先来看看构造函数:

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 是一个 widget,用于在列表中显示撤销堆栈中每个QUndoCommand 的文本,文本是通过setText() 函数设置的。我们将其放入一个停靠 widget 中。

下面是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."));
}

关于槽由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();
}

我们要设置项目的位置,因为在构造函数中没有这样做。

删除命令类定义

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(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 会捕捉到这个信号,并发出 MoveCommands(移动命令)。我们不研究 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

© 2025 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.