撤销框架示例

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

The Undo Diagram Example

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

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

我们使用 graphics view framework 来实现图表。我们只简要介绍相关的代码,因为框架本身就有示例(例如, 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 类维护撤销栈,即创建 QUndoCommand,在收到 undoActionredoActiontriggered() 信号时从栈中推入和弹出。

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

这是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 捕获的,它创建 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

© 2024 The Qt Company Ltd. 本文档中的内容贡献均为各自所有者的版权。本提供_here_的文档根据 Free Software Foundation 发布的 GNU 自由文档许可版本 1.3 的条款许可。Qt 及相关标志是 The Qt Company Ltd. 在芬兰和其他国家/地区的商标。所有其他商标均为其各自所有者的财产。