LoginSignup
5
4

More than 5 years have passed since last update.

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

Posted at

はじめに

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

実装の流れ

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

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

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

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

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

実装方法

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

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

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

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

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から呼び出して使っています。
使い方は以下のような感じです。

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
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を示します。

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
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から呼んでいます。

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を行き来できて便利です。

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

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

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

おわりに

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

GitHub - QtUndoFrameworkSample

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

参考

Qt Documentation - Undo Framework Example
Qt Documentation - Overview of Qt's Undo Framework

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4