QtのUndo Frameworkで、Undo/Redoを実装する方法

# はじめに

テキストエディタやペイントソフトでよくあるUndo/Redoを、自前で実装するのは大変そうですが、QtのUndo Frameworkを使って簡単に実装する方法を調べたのでまとめます。

# 実装の流れ

ざっくり示すと、以下のような感じです。

1. ドキュメントに変更を加える操作をコマンド化する（QUndoCommand）
1. コマンドをスタックに積む（QUndoStack::push()）
1. スタックからコマンドを取り出して、Undo/Redoを行う

例えば、「キャンバスに頂点を打つ」という操作によって、ドキュメントに頂点情報を追加されるわけですが、この「頂点情報を追加する」というのをQUndoCommand::redo()に書きます。

しかし、これだけではどうすれば元の状態に戻せるかわかりません。なので、戻す方法も書いておく必要があります。
上記の場合、「頂点を削除」することで、前の状態に戻せるので、これをQUndoCommand::undo()に書きます。

コマンドを使う時は、生成したコマンドをQUndoStackにプッシュしてあげるだけです。

# 実装方法

操作をコマンド化するときはQUndoCommandを継承したクラスを用意します。先の例だと、以下のような感じです。

まずは、ヘッダーファイルです。

```cpp:AddPointCommand.h
#ifndef ADDPOINTCOMMAND_H
#define ADDPOINTCOMMAND_H

#include <QUndoCommand>
#include <QPoint>

class AddPointCommand : public QUndoCommand
{
	public:
		AddPointCommand(const QPoint &point, QList<QPoint> *pointList, QUndoCommand *parent = 0);

		// QUndoCommand interface
	public:
		void undo() Q_DECL_OVERRIDE;
		void redo() Q_DECL_OVERRIDE;

	private:
		QPoint m_point;
		QList<QPoint> *m_pointList;
};

#endif // ADDPOINTCOMMAND_H
```

続いて、実装ファイルです。

```cpp:AddPointCommand.cpp

#include "AddPointCommand.h"

AddPointCommand::AddPointCommand(const QPoint &point, QList<QPoint> *pointList, QUndoCommand *parent):
	QUndoCommand(parent),
	m_point(point),
	m_pointList(pointList)
{

}

void AddPointCommand::undo()
{
	if (m_pointList->isEmpty())
	{
		return;
	}

	// 頂点リストの最後に点が最後に追加した点なので、それを取り除く
	auto point = m_pointList->takeLast();

	// 実行した操作に説明をつける
	setText(QString("Remove : (%1, %2)").arg(point.x()).arg(point.y()));
}

void AddPointCommand::redo()
{
	// 頂点リストの最後に点を追加
	m_pointList->push_back(m_point);

	// 実行した操作に説明をつける
	setText(QString("Add : (%1, %2)").arg(m_point.x()).arg(m_point.y()));
}

```

AddPointCommandのコンストラクで、追加したい点と変更を加えたい頂点リストを受け取とります。
そして、redo()に頂点を追加する処理、undo()に頂点を削除する処理を記述しています。

このAddPointCommandは、MainWindowのmouseReleaseEventから呼び出して使っています。
使い方は以下のような感じです。

```cpp:MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QUndoStack>

namespace Ui {
	class MainWindow;
}

class MainWindow : public QMainWindow
{
		Q_OBJECT

	public:
		explicit MainWindow(QWidget *parent = 0);
		~MainWindow();

		// QWidget interface
	protected:
		void mouseReleaseEvent(QMouseEvent *e) Q_DECL_OVERRIDE;

	private:
		Ui::MainWindow *ui;
		QList<QPoint> m_pointList;
		QUndoStack *m_undoStack;

};

#endif // MAINWINDOW_H
```

```cpp:MainWindow.cpp
#include "MainWindow.h"
#include "ui_MainWindow.h"

#include <QMouseEvent>

#include "AddPointCommand.h"

MainWindow::MainWindow(QWidget *parent) :
	QMainWindow(parent),
	ui(new Ui::MainWindow),
	m_undoStack(new QUndoStack)
{
	ui->setupUi(this);

	connect(ui->btnUndo, &QPushButton::clicked, m_undoStack, &QUndoStack::undo);
	connect(ui->btnRedo, &QPushButton::clicked, m_undoStack, &QUndoStack::redo);
}

MainWindow::~MainWindow()
{
	delete ui;
}

void MainWindow::mouseReleaseEvent(QMouseEvent *e)
{
	AddPointCommand *addPointCommand = new AddPointCommand(e->pos(), &m_pointList);
	m_undoStack->push(addPointCommand);
}
```

QUndoStackのインスタンスm_undoStackを生成し、push()を使って、addPointCommandをプッシュしています。
プッシュすると、自動的にaddPointCommandのredo()が呼ばれて、頂点が追加されます。

「元に戻す」と「やり直し」は、QUndoStackのundo()、redo()で行えます。上記の例では、QPushButtonのclickedシグナルと接続することで、実現しています。

# 同じコマンドをまとめるmergeWith

上記の例と同じノリで、「頂点を動かす」という操作をコマンド化すると、少し面倒なことが起こります。

例えば、MovePointCommandを作り、ある点を(0, 0)から(0, 300)まで動かすと、数ピクセル動くたびにMovePointCommandが積まれてしまいます。そして、この状態で操作を戻そうとすると、ユーザは何回も「元に戻す」ボタンを押さないといけなくなります。

このように、同じコマンドが続く時は、mergeWithを使ってコマンドをまとめます。

コマンドが同じかどうか判定するにはid()を使うので、他のコマンドとかぶらないような整数を返すように、オーバーライドする必要があります。
mergeWithの中で、直前のコマンドと現在のコマンドのidを評価し、-1以外で、かつ等しい場合にtrueを返してやれば、直前のコマンドに現在のコマンドがまとめられます。

以下に、頂点を動かすMovePointCommandのidとmergeWithを示します。

```cpp:MovePointCommand.h
#ifndef MOVEPOINTCOMMAND_H
#define MOVEPOINTCOMMAND_H

#include <QUndoCommand>
#include <QPoint>

class MovePointCommand : public QUndoCommand
{
	public:
		MovePointCommand(const QPoint &newPos, int index, QList<QPoint> *pointList, QUndoCommand *parent = 0);

		QPoint m_newPosition;

		// QUndoCommand interface
	public:
		enum { ID = 1234 };

		void undo() Q_DECL_OVERRIDE;
		void redo() Q_DECL_OVERRIDE;

		int id() const Q_DECL_OVERRIDE;
		bool mergeWith(const QUndoCommand *other) Q_DECL_OVERRIDE;

	private:
		int m_index;
		QList<QPoint> *m_pointList;
};

#endif // MOVEPOINTCOMMAND_H
```


```cpp:MovePointCommand.cpp
#include "MovePointCommand.h"

MovePointCommand::MovePointCommand(const QPoint &newPos, int index, QList<QPoint> *pointList, QUndoCommand *parent):
	QUndoCommand(parent),
	m_newPosition(newPos),
	m_index(index),
	m_pointList(pointList)
{

}

void MovePointCommand::undo()
{
	...
}

void MovePointCommand::redo()
{
	...
}

int MovePointCommand::id() const
{
	return ID;
}

bool MovePointCommand::mergeWith(const QUndoCommand *other)
{
	// 最後のコマンドがMovePointCommand以外ならマージしない
	if (other->id() != id())
	{
		return false;
	}

	m_newPosition = static_cast<const MovePointCommand*>(other)->m_newPosition;

	return true;
}
```

上記のMovePointCommandは、コンストラクで動かしたい頂点のインデックスと、新しい位置と頂点リストを受け取ります。
そして、redo()で指定された頂点を、指定された位置に動かしています。

MovePointCommandはMainWindowのmouseMoveEventから呼んでいます。

```cpp:MainWindow.cpp
void MainWindow::mouseMoveEvent(QMouseEvent *e)
{
	MovePointCommand *movePointCommand = new MovePointCommand(e->pos(), m_targetIndex, &m_pointList);
	m_undoStack->push(movePointCommand);
}
```

マウスの移動に合わせて、連続で発生するMovePointCommandが一つにまとまるので、「元に戻す」を行えば、移動開始直前の位置に戻ってくれます。

# QUndoViewで、QUndoStackに積まれたコマンドをみる

最後に、QUndoViewを紹介します。

これは、QListView派生のウィジェットで、QUndoStackのインスタンスを渡しておくと、そこに積まれるコマンドをリストで表示してくれます。
さらに、マウス操作でUndo/Redoを行き来できて便利です。

使い方は簡単で、以下のような感じです。

```cpp
	QUndoView *undoView = new QUndoView(m_undoStack);
	undoView->show();
```

リストには、各コマンドのUndo/redo時に、setTextで設定したテキストが表示されます。

# おわりに

例によって、サンプルをGitHubにあげておきます。
クリックで点を追加、ドラッグで点を移動するプログラムです。

[GitHub - QtUndoFrameworkSample](https://github.com/tatsuteb/QtUndoFrameworkSample)

今回は、無いと困るけど、自分で仕組みを作るには面倒臭いUndo/Redoについてまとめてみました。
ただ、もっと複雑なデータを扱う大規模なプログラムでは、さらに工夫が必要かもしれません。

# 参考

[Qt Documentation - Undo Framework Example](http://doc.qt.io/qt-5/qtwidgets-tools-undoframework-example.html)
[Qt Documentation - Overview of Qt's Undo Framework](http://doc.qt.io/qt-5/qundo.html)