专业的编程技术博客社区

网站首页 > 博客文章 正文

Qt编程进阶(105):通过Qt撤销框架来执行撤销/重做的功能

baijin 2024-09-08 01:53:54 博客文章 13 ℃ 0 评论

在Qt的撤销框架中,所有的用户动作的运行是从QUndoCommand类继承而来,通俗地说,撤销的类知道程序当中上一步与下一步的动作,用户希望的所有的表现形式都是通过一个QUndoStack来支配。Stack承载了所有的事件的执行,它根据事件依次地排列进来。在堆中包含了文档状态的前进与后退的命令,它直观地表现为撤销与重做。

在本例子中我们将通过Qt撤销框架来执行撤销/重做的功能。如下图所示为重新制作了一个图片的绘画过程,类似于排列积木。如果感觉排列的方式不好,可以在历史记录窗口中选择。

退回的节点,是为了可以删除其中的一条项目,这样,无论是箱体还是矩形,都可根据鼠标的移动放置在窗口上。撤销堆的信息在QUndoView显示出来。显示的信息包含了每一步移动的信息,通过选择QUndoView上的项目,做到用户所期望的显示。

一、类组成概述

例子是由下面的类组成。

  • (1) MainWindow:主窗口类,用来管理主窗口将例子的部件排列,由用户或QUndoStack来建立命令,移动图形。
  • (2) AddCommand:添加一个项目到场景当中。
  • (3) DeleteCommand:从场景当中删除一个项。
  • (4) MoveCommand:用来记录移动、开始与结束的位置,同时为redo()与undo()函数的调用提供依据。
  • (5) DiagramScene:从QGraphicsScene继承,当一项移动时,发射信号给MoveCommands。
  • (6) DiagramItem:从QGraphicsPolygonltem继承而来,功能是让绘图显示出来。

二、主窗口类

class MainWindow : public QMainWindow
{
	Q_OBJECT
public:
  MainWindow(QWidget *parent = nullptr);
  ~MainWindow();
public slots:
  void itemMoved(DiagramItem *movedDiagram,
  const QPointF &moveStartPosition);
private slots:
  void deleteItem();
  void addBox();
  void addTriangle();
  void about();
  void itemMenuAboutToShow();
  void itemMenuAboutToHide();
private:
  void createActions();
  void createMenus();
  void createUndoView();
  QAction *deleteAction;
  QAction *addBoxAction;
  QAction *addTriangleAction;
  QAction *undoAction;
  QAction *redoAction;
  QAction *exitAction;
  QAction *aboutAction;
  QMenu *fileMenu;
  QMenu *editMenu;
  QMenu *iteraMenu;
  QMenu *helpMenu;
  DiagramScene *diagramScene;
  QUndoStack *undoStack;
  QUndoView *undoView;
};

MainWindow类维持一个undo的堆,它建立一个QUndoCommands并在堆中弹出或者压入,弹出压入的命令需要获得撤销事件与重做事件发出triggered()信号才能进行。

下面是从一个构造函数开始的。

MainWindow::MainWindow(QWidget *parent):QMainWindow(parent)
{
  undoStack = new QUndoStack(this);
  createActions();
  createMenus();
  connect(undoStack, SIGNAL(canRedoChanged(bool)),
  	redoAction, SLOT(setEnabled(bool)));
  connect(undoStack, SIGNAL(canUndoChanged(bool)),
  	undoAction, SLOT(setEnabled(bool)));
  createUndoView();
  diagramScene = new DiagramScene();
  diagramScene->setSceneRect(QRect(0, 0, 500, 500));
  connect(diagramScene, SIGNAL(itemMoved(DiagramItem *, const QPointF &)),
  	this, SLOT(itemMoved(DiagramItem *, const QPointF &)));
  QGraphicsView *view = new QGraphicsView(diagramScene);
  setCentralWidget(view);
  setWindowTitle("UnRedo");
  resize(700,500);
}

通过连接canRedoChanged()与canUndoChanged()信号到撤销与重做的动作槽,这样就实现了撤销与重做的命令,剩下的是在构造函数当中建立一个图表,下面是用来建设撤销的图表方法。

void MainWindow::createUndoView()
{
  undoView = new QUndoView(undoStack);
  undoView->setWindowTitle(tr("Command List"));
  undoView->show();
  undoView->setAttribute(Qt::WA_QuitOnClose, false);
}

QUndoView是用来显示文本的部件,用setText()函数来完成每个QUndoCommand,并在撤销堆当中提供列表。

下面介绍的是建立动作的函数。

void MainWindow::createActions()
{
  deleteAction = new QAction(tr("删除图形"), this);
  deleteAction->setShortcut(tr("Del"));
  connect(deleteAction, SIGNAL(triggered()), this, SLOT(deleteItem()));
  undoAction = new QAction(tr("恢复"), this);
  undoAction->setShortcut(tr("Ctrl+Z"));
  undoAction->setEnabled(false);
  connect(undoAction, SIGNAL(triggered()), undoStack, SLOT(undo()));
  redoAction = new QAction(tr("重做"), this);
  QList<QKeySequence> redoShortcuts;
  redoShortcuts << tr("Ctrl+Y") << tr("Shift+Ctrl+Z");
  redoAction->setShortcuts(redoShortcuts);
  redoAction->setEnabled(false);
  connect(redoAction, SIGNAL(triggered()), undoStack, SLOT(redo()));
  addBoxAction = new QAction(tr("添加矩形"),this);
  connect(addBoxAction, SIGNAL(triggered()),this,SLOT(addBox()));
  addTriangleAction = new QAction(tr("添加三角形"),this);
  connect(addTriangleAction,SIGNAL(triggered()),this,SLOT(addTriangle()));
  exitAction = new QAction(tr("退出"),this);
  connect(exitAction,SIGNAL(triggered()),this,SLOT(close()));
  aboutAction = new QAction(tr("关于"),this);
  connect(aboutAction,SIGNAL(triggered()),this,SLOT(about()));
}

管理例子中所有的动作,这里使用了两种动作来做到对堆的直接访问,它们分别是undo()和redo()槽。当没有可以重做或撤销的堆时,它们就自动停止。

void MainWindow::createMenus()
{
  fileMenu= menuBar()->addMenu(tr("文件"));
  fileMenu->addAction(exitAction);
  editMenu = menuBar()->addMenu(tr("编辑"));
  editMenu->addAction(undoAction);
  editMenu->addAction(redoAction);
  editMenu->addSeparator();
  editMenu->addAction(deleteAction);
  connect(editMenu, SIGNAL(aboutToShow()), this, SLOT(itemMenuAboutToShow()));
  connect(editMenu, SIGNAL(aboutToHide()), this, SLOT(itemMenuAboutToHide()));
  itemMenu = menuBar()->addMenu(tr("图形"));
  itemMenu->addAction(addBoxAction);
  itemMenu->addAction(addTriangleAction);
  helpMenu=menuBar()->addMenu(tr("帮助"));
  helpMenu->addAction(aboutAction);
}

在这里通过两个槽来做到图形动作的撤销与重做,它们是通过选择小窗口中的列表项目实现的,这两个槽函数分别是aboutToShow()与aboutToHide()。

下面是itemMoved槽的创建方法。

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

当一个选中的项目需要被删除掉,就需要判断这个删除动作是不是可以进行操作。它在完成删除动作的同时,不捕捉任何的信号或者事件。

下面包含了itemMenuAboutToShow()与itemMenuAboutToHide()槽。

void MainWindow::itemMenuAboutToHide()
{
	deleteAction->setEnabled(true);
}
void MainWindow::itemMenuAboutToShow()
{
  undoAction->setText(tr("Undo ") + undoStack->undoText());
  redoAction->setText(tr("Redo ") + undoStack->redoText());
  deleteAction->setEnabled(!diagramScene->selectedItems().isEmpty());
}

以上的两个函数是用来获得动态的项目菜单,两个槽分别被aboutToShow()与aboutToHide()信号连接。这里值得注意的是,需要判断删除的动作是不是已经没有任何的项目可以撤销或者重做,这样做的目的是给动作事件作为参考。

下面是addBox()槽。

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

addBox()函数用来建立addCommand命令,并将它们推入到undo堆当中。

这里定义了addTriangle()槽。

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

addTriangle()函数是用来建立一个命令并把它推入到undo堆当中。

三、AddCommand类定义

class AddCommand : public QUndoCommand
{
public:
  AddCommand(DiagramItem::DiagramType addType, QGraphicsScene *graphicsScene,
  QUndoCommand *parent = 0);
  ~AddCommand();
  void undo();
  void redo();
private:
  DiagramItem *myDiagramItem;
  QGraphicsScene *myGraphicsScene;
  QPointF initialPosition;
};

在DiagramScene当中提供了添加图表的功能。

AddCommand类的执行首先从构造函数开始。

AddCommand::AddCommand(DiagramItem::DiagramType addType,
QGraphicsScene *scene, QUndoCommand *parent)
:QUndoCommand(parent)
{
  static int itemCount = 0;
  myGraphicsScene = scene;
  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)));
}

在这里先要建立一个图表到一个图形项目当中。setText()函数作为QString类型来使用,它用于描述一个命令。

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

undo()删除掉了项目中的图形。

四、DeleteCommand类定义

class DeleteCommand : public QUndoCommand
{
public:
  DeleteCommand(QGraphicsScene *graphicsScene,
  QUndoCommand *parent = 0);
  ~DeleteCommand();
  void undo();
  void redo();
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())));
}

通过判断来决定是否已经提供了向前或向后的显示,同时为用户定义一个列表,这个列表是用来选择向前或向后的项目,通过文本的设置,来获得当前项目的场景并显示出来。

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 = 0);
  void undo();
  void redo();
  bool mergeWith(const QUndoCommand *command);
  int id() const { return Id; }
private:
  DiagramItem *myDiagramItem;
  QPointF myOldPoa;
  QPointF newPos;
};

在mergeWith()中,提供了连续项目的移动命令,它通过项目中提示信息,返回到第一个移动位置。

MoveCommand的构造函数如下。

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

在这里要设置一个新的与旧的位置,它为以后的撤销与重做提供了依据,重做与撤销的动作是分别保存的。

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被建立,就需要函数来检测前一个命令与当前命令的联合,前一个命令被压入到堆当中,函数将通过返回真的方法表明当前的命令已经被合并到堆当中,出现错误将返回false。

六、DiagramScene类定义

class DiagramScene : public QGraphicsScene
{
	Q_OBJECT
public:
	DiagramScene(QObject *parent = 0);
signals:
  void itemMoved(DiagramItem *movedItem,
  	const QPointF &movedFromPosition);
protected:
  void mousePressEvent(QGraphicsSceneMouseEvent *event);
  void mouseReleaseEvent(QGraphicsSceneMouseEvent *event);
private:
  QGraphicsItem *movingItem;
  QPointF oldPos;
};

DiagramScene用来执行了一个通过图表的移动来更改场景的方法,当一个鼠标移动结束,将会发送一个信号,它通过MainWindow来捕捉这个信号。同时还建立了一个MoveCommands。

七、主函数

程序的主函数方法如下。

int main(int argv, char *args[])
{
  Q_INIT_RESOURCE(undoframework);
  QApplication app(argv, args);
  MainWindow mainWindow;
  mainWindow.show();
  return app.exec();
}

在这里使用了资源文件,同时将MainWindow作为一个顶级的窗口来使用。

——————————————————

对于本文实例完整代码有需要的朋友,可关注并在评论区留言!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表