Undo/Redo を実装する

2009-04-04 公開
2009-04-04 更新

ホーム » プログラミング » この記事

はじめに

 何らかの「編集」を行うツールには、直前の操作を取り消したり戻したりする Undo/Redo の機能が欲しいものです。(Undo 機能があることに慣れ過ぎて、紙に手書きで文字や絵を書いているときにも Ctrl+Z が欲しくなります!)

 非常に便利な Undo/Redo 機構ですが、プログラムとしては、一体、どのように実装するのでしょうか。.NET Framework の標準機能自体には、Undo/Redo を実現するための汎用的な仕組み、といったものは提供されていません。自前で Undo/Redo システムを作り込む必要があります。

※この記事は未完成です。説明が不十分な所が多々あります。あとで追記します。

システムの概要

 Undo/Redo 機構とは、ソフトウェアの要求にもよりますが、基本的には「操作のスタック」であると捉えることができます。

操作と逆操作

 編集の対象となる「項目」に、何らかの「操作」を実行すると、結果として項目の状態が変化します。このとき、変化した状態を元に戻す「逆操作」があるのなら、その実行は Undo となります。(Redo は、Undo で元に戻した状態から、再び前の操作を正方向で実行することを意味する。)

 たとえば、Content オブジェクト(「項目」)の Body プロパティ(String 型)の値を変更する操作を考えます。この逆操作とは、Body の値を変更前の値に設定しなおすことです。

操作の履歴

 ユーザーが実行した過去の操作を記録する「操作の履歴」を用意します。ユーザーが「操作」を実行する度に、その操作を履歴に追加します。

 通常、Undo したいのは直前に実行した操作です。また、Undo を連続すると、過去の操作を逆順に一つずつ元に戻すことができます。――これはつまりスタックです。Undo とは、操作履歴スタックの一番上にある操作を取り出して、その逆操作を実行することです。

*

 また、Redo を実現するためには、Undo 用のスタックの隣に、Redo 用のスタックを置きましょう。Undo スタックから取り出して Undo した操作は、Redo スタックの一番上に載せます。逆も同様です。

IUndoableOperation

 “操作をスタックに積み上げる”ためには、「操作」はオブジェクトの形をしていることが望ましいです。スタックは、そのまま System.Collections.Generic.Stack<T> クラスを使えます。

 という訳で、まずは IUndoableOperation インターフェースを定義します。「Undo できる操作」です。

public interface IUndoableOperation
{
    #region Methods

    void Execute();

    void Undo();

    #endregion
}

 要求によって最適な形は異なるとは思いますが、ここでは、操作と逆操作を一つのオブジェクトにまとめています。最初の実行と Redo のときは Execute メソッドを、Undo のときは Undo メソッドを呼び出します。IUndoableOperation の実装者は、それぞれの「操作」について、これらのメソッドを実装します。

UndoableOperationHistory

 操作履歴の管理もクラスとしてまとめておきましょう。UndoableOperationHistory オブジェクトは、実行された IUndoableOperation オブジェクトを受け取って、内部に保持する Undo スタックに追加します。

public class UndoableOperationHistory
{
    #region Fields

    private Stack<IUndoableOperation> undoStack;

    private Stack<IUndoableOperation> redoStack;

    #endregion

    #region Methods

    public void Append(IUndoableOperation operation)
    {
        if (operation == null)
        {
            throw new ArgumentNullException("operation");
        }

        // 操作履歴を更新する。
        this.redoStack.Clear();

        // 操作を履歴に追加する。
        this.undoStack.Push(operation);
    }

    public void Undo()
    {
        if (this.undoStack.Count > 0)
        {
            // 操作を取り出す。
            var operation = this.undoStack.Pop();

            // 操作を実行する。
            operation.Undo();

            // 操作履歴を更新する。
            this.redoStack.Push(operation);
        }
    }

    public void Redo()
    {
        if (this.redoStack.Count > 0)
        {
            // 操作を取り出す。
            var operation = this.redoStack.Pop();

            // 操作を実行する。
            operation.Execute();

            // 操作履歴を更新する。
            this.undoStack.Push(operation);
        }
    }

    #endregion
}

 操作の実行と逆操作の実行を IUndoableOperation で定義したので、操作履歴自体に Undo/Redo メソッドを設けることができます。

 なお、操作を履歴に追加する際には、現在の Redo スタックを破棄しています。Undo/Redo の整合性を維持するために、このような実装になります。

実装の詳細

 基本的な履歴管理はできるようになりました。具体的な操作の実装と、その他の詳細な機能を詰めてゆきます。

操作の情報

 IUndoableOperation の Execute および Undo メソッドには、当然、編集の対象となる「項目」や、操作のパラメーター、変更前の値、などといった情報が別途必要になります。それらは IUndoableOperation を実装するクラスにプロパティとして設けることになるでしょう。

 たとえば、ContentSetBodyOperation は、Content プロパティ(編集操作の対象)、OldBody プロパティ(変更前の値)、NewBody プロパティ(変更後の値)を持ちます。

IsExecuted

 操作の Undo/Redo は、通常は一対一で対応するものです。同じ IUndoableOperation について、Execute あるいは Undo メソッドを連続して実行することは禁止します。(予期しないメソッドの呼び出しには System.InvalidOperationException を送出する。)IUndoableOperation には IsExecuted プロパティを設けます。

Undo/Redo 情報の取り出し

 画面には Undo/Redo ボタンを配置します。操作履歴に何も入っていないときは、これらのボタンは無効になります。また、「次に何の操作を Undo できるのか」は、ツールチップテキストなどでユーザーに提示することが望ましいでしょう。

 IUndoableOperation には「操作の名前」を示す Name プロパティを設けます。ボタンの状態を更新するときに、この値を参照します。

 UndoableOperationHistory には、CanUndo および CanRedo プロパティを設けることができます。その他、次の Undo/Redo の操作を取り出すための NextUndoOperation、NextRedoOperation プロパティが必要になるでしょう。

MacroOperation

 ユーザーが画面上で行う操作は、実際には、いくつかの小さな操作の集まりであるとも見なせます。たとえば、Content オブジェクトの Body プロパティ(項目の本文)を編集する際には、同時に LastModificationTime プロパティ(最終更新日時)も更新します。

「本文を編集する」操作について、その実装は Body と LastModificationTime を一緒に扱うのでしょうか。それとも、Body の変更と LastModificationTime の変更は、それぞれクラスを分割すべきでしょうか。

 クラスを分割する場合、その二つの IUndoableOperation は、画面上では一つの操作であるように見せなければなりません。Undo ボタンを押して、LastModificationTime だけしか戻らない(Body を戻すには、もう一度 Undo する必要がある)というのでは、ちょっと困ります。

 複数の子操作をまとめて扱う仕組みとして、MacroOperation オブジェクトを考えます。それ自体も IUndoableOperation の実装の一つで、内部に IUndoableOperation の集合(子操作の集合)を持ちます。Append メソッドで追加された子操作を、Execute メソッドでは昇順に、Undo メソッドでは降順に、それぞれ実行します。この例では、SetBodyOperation と SetLastModificationTimeOperation を MacroOperation に包んで実行、履歴に登録します。

ダウンロード

 ここで考えた IUndoableOperation 関連のクラス群は、ライブラリにまとめています。名前空間は SunnyGrove.Entities としました。

更新履歴

2009-04-04

メッセージを送る



e-mail: webmaster(at)sunny-grove.net
Twitter: @toru_t
ソフトウェアの不具合報告について

※コメントは非公開です。ここには表示されません。


*

Copyright © 2008-2010 Toru TAKAGI. All rights reserved.

*