撤销框架示例
此示例展示了如何使用 Qt 撤销框架实现撤销/重做功能。
在 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,在收到 undoAction
和 redoAction
的 triggered()
信号时从栈中推入和弹出。
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
并将其作为一个顶级窗口显示。
© 2024 The Qt Company Ltd. 本文档中的内容贡献均为各自所有者的版权。本提供_here_的文档根据 Free Software Foundation 发布的 GNU 自由文档许可版本 1.3 的条款许可。Qt 及相关标志是 The Qt Company Ltd. 在芬兰和其他国家/地区的商标。所有其他商标均为其各自所有者的财产。