Help us understand the problem. What is going on with this article?

Undo/Redo実装の考え方と実例

More than 1 year has passed since last update.

概要

Undo/Redoのあるなしで作業効率が全然違う

GUIツールを使って作業する人からUndo/Redo実装を欲しがられる確率は100%です。
人間なので誰でも操作ミスはしますし、「あっ間違えちゃったUndo!」ができないとメチャクチャ不便なんです。Undoすら間違えるのでRedoも当然欲しい。もはやこれが無いとGUIツールは使ってもらえないレベル、存在価値がゼロに等しいです。

言語によらない考え方

C++、C#、JavaScript、その他どんな言語であっても実装するときの考え方は変わりません。今回はWeb動作させやすいJavaScriptで記載します。

実装前のサンプル

以下3つの操作とグループ化の概念のある状態をサンプルとします。

サンプル

  • ボタンから要素の「追加」ができる
  • ボタンから要素の「削除」ができる(矩形選択で一括削除)
  • 要素をドラッグ操作で「移動」することができる(矩形選択で一括移動)

この3つ全てにUndo/Redoを追加してみます。

実装

Undo/Redoを構成する要素

  • 1操作を「コマンド」という概念で扱い、線形コンテナ(配列など)に積んで記録していく。次にコマンドを積む予定のコンテナ内の位置を「インデックス」として保持する。
  • 新規コマンドを記録する際は、インデックスから後ろのコンテナ内容をクリアしてから新規コマンドをコンテナに積み、インデックスを1つ進める。
  • Undoでインデックスを1つ戻す、Redoで1つ進める。
  • インデックスが動くたびに指していたコマンド内容を実行する。進める時(Do/Redo)と戻す時(Undo)の2実装が必要。

これらを満たすことでUndo/Redoの実装となります。
今回は「追加」「削除」「移動」の3種類の実装と、グループ化を実装します。

実装するコマンド一覧

  • 追加コマンド
    • Do: 所定のパラメータをバックアップし、そのパラメータで要素を追加する
    • Undo: 要素を削除する
  • 削除コマンド
    • Do: 現在のパラメータをバックアップし、要素を削除する
    • Undo: バックアップしたパラメータで要素を追加する
  • 移動コマンド
    • Do: 要素の持つ現在のパラメータをバックアップ保存した後、所定のパラメータで書き換える
    • Undo: 要素の持つパラメータをバックアップ保存されたパラメータで書き換える
  • コマンドグループ
    • コマンドをコンテナで持つ
    • コマンドグループ自体も1コマンドとして扱える

結果

Undo/Redoができるようになりました

実装結果

上部に「Undo」「Redo」ボタンが増えました。
押すことでUndo/Redoが実行されます。

説明

コマンドを記録する、という内容を RecordCommand メソッドとしてまとめました。
引数は「コマンド名」「パラメータ」「Do実装」「Undo実装」です。
Do/Undo実装では渡されたパラメータに応じた操作しか行いません。

  • 追加/削除コマンド
    • 追加/削除されるときに位置情報もパラメータに渡すことで、Undo/Redo実行時に位置が復元できるようになっています。
  • 移動コマンド
    • 移動開始処理(start)に移動前の位置情報を保存し、移動終了処理(stop)に移動前と移動後の位置情報両方をパラメータに渡しました。
    • 移動中処理(drag)は呼ばれる回数が非常に多いので、この中で RecordCommand をするのはオススメできません。
  • コマンドグループ
    • RecordCommand を StartGroupRecording/FinishGroupRecording メソッドで囲むことで、囲まれた RecordCommand はグループ化されます。
    • このサンプルでは、選択中のものを一括操作できる削除コマンドと移動コマンドを囲みました。

応用する

今回は実例はありませんが、Undo/Redo実装から次の機能も実装できるようになります。

ヒストリー機能

コンテナに積まれているコマンド名を一覧表示すると、そのままヒストリー機能になります。
アーティストの方は特に欲しがります。

編集内容に変更があるかどうか(変更があったら閉じるときに確認ダイアログなど)

コンテナに何か積まれていたら変更あり、そうでなければ変更なしです。

今回実装しなかったが必要そうなもの

グループのグループ

StartGroupRecording でグループ情報をスタックにpush、FinishGroupRecording でスタックからpopするような実装をすると、StartGroupRecording/FinishGroupRecordingを入れ子で呼べるようになります。

コマンド記録の無効化

ファイル保存された編集内容を読み込む際など、一括で大量のコマンド操作を行う間はコマンド記録を無効化するべきです。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away