元に戻す/やり直し-シンプルさの幻想

任意のテキストおよびグラフィックエディターでのこのようなシンプルで使い慣れた機能。 その実装の難しさは何でしょうか? XtraRichEditテキストエディターのUndo / Redoの開発に最初に遭遇したとき 、私たち疑問に思いましたが、どのようなアプローチを取るべきでしょうか?







変更の履歴を担当するオブジェクトが存在する必要があることは明らかでした。 このオブジェクトには、個々のアクションを表す要素のリストが含まれている必要があります。 このような各要素には、アクションをロールバック(元に戻す)するだけでなく、それを繰り返す(やり直し)にも十分な情報を格納する必要があります。



アクションは非常に異なる可能性があるため(テキストの挿入、フォントの変更、段落への赤い線の追加)、すぐに元に戻すバッファーの要素に操作のロールバック/やり直しに必要なすべてのデータを保存するだけでなく、ロールバックと繰り返す:



public abstract class HistoryItem { public abstract void Undo(Document document); public abstract void Redo(Document document); }
      
      





元に戻すバッファのクラスの最初のプロトタイプは次のとおりです。



 public class History { readonly List<HistoryItem> items = new List<HistoryItem>(); public History(Document document) { this.document = document; } public void Undo() { } public void Redo() { } public void Add(HistoryItem item) { items.Add(item); } }
      
      





手を差し伸べて、Undoメソッドを記述します。



 public void Undo() { items[items.Count - 1].Undo(document); }
      
      





簡潔に、優雅に...しかし完全に間違っています。



まず、空のリストでUndoメソッドを呼び出そうとすると、例外がスローされます。 修正します:



 public bool CanUndo { get { return items.Count > 0; } } public void Undo() { if (!CanUndo) return; items[items.Count - 1].Undo(document); }
      
      





また、間違ったコードを取得します。 結局、Undoメソッドを連続して数回呼び出すと、リスト内の最後の要素に対してUndoが何度も呼び出されますが、これは間違っています。 変更を行います。



 public void Undo() { if (!CanUndo) return; items[items.Count - 1].Undo(document); items.RemoveAt(items.Count - 1); }
      
      





これですべてが正常になりました。 さて、REDOに取り掛かる時です。 複雑なことはありません...



 public void Redo() { if (!CanRedo) return; // ??? }
      
      





おっと! 到着しました。

Undoの実装では、ロールバックプロセスで、ロールバックされた要素を回復不能に失うため、Redoを実行する必要はありません。 クリップボードの現在のアイテムを指すインデックスを取得する必要があります。 だから:



 int currentIndex = -1; public bool CanUndo { get { return currentIndex >= 0; } } public bool CanRedo { get { return items.Count > 0 && currentIndex < items.Count - 1; } } public void Undo() { if (!CanUndo) return; items[currentIndex].Undo(document); this.currentIndex--; } public void Redo() { if (!CanRedo) return; this.currentIndex++; items[currentIndex].Redo(document); } public void Add(HistoryItem item) { items.Add(item); this.currentIndex++; }
      
      





少なくとも、それは実行可能なもののように見えます。 Undoの場合ロールバック後にインデックスが変更され、 Redoの場合-ロールバック前に変更されることに注意してください。 また、履歴に要素を追加するときにインデックスをインクリメントすることも忘れませんでした。



それでは、Addメソッドを注意深く見て、履歴に5つのアクションを実行して記録し、そのうち3つをロールバックして新しいアクションを実行する場合の動作を確認してみましょう。 他の人によってそれがどのように行われたかを考えて見て(すべてがすでに私たちの前に盗まれています)、この場合現在のインデックスの後にあるアクションの履歴の一部が失われるべきであり、代わりに新しいアクションが記録され始めるという結論に達します:



 public void Add(HistoryItem item) { CutOffHistory(); items.Add(item); this.currentIndex++; } void CutOffHistory() { int index = currentIndex + 1; if (index < Count) items.RemoveRange(index, Count - index); }
      
      





最後に、Undo / Redoの最も簡単な実装が実行可能になりました。



それをどう使うか考えてみましょう。 最初に頭に浮かぶのは、次の一連のアクションです。

  1. アクションを実行します。
  2. さらにロールバック/再試行するために、このアクションに関する情報を元に戻すバッファーに入れます。


しかし、最初からアンドゥバッファの要素は必要なデータを保存できるだけでなく、必要なロールバック/やり直し操作を独立して実行できると判断しました。 そして、繰り返し操作とは何ですか? これは、現在の状態からの直接実行です。 つまり 操作を実行するには、次の操作を実行できます。

  1. 元に戻すバッファー要素を作成し、さらにロールバック/やり直しのために元に戻すバッファーに入れます。
  2. この要素に対してやり直しを実行します。


 void DoAction() { HistoryItem item = CreateActionHistoryItem(); document.History.Add(item); item.Redo(document); }
      
      





それはエレガントで美しくなりました。 アクションの最初の実行は、現在の状態からそのアクションをやり直すことです。 ロールバック-現在の状態から元に戻します。 繰り返し-その時点での現在の状態からの操作の再実行。



最後の文では、単語stateを数回使用しました。 実際には、元に戻すバッファに保存されている情報は、保存時に完全に正しいということです。 ただし、すでに次の時点で不正確になる可能性があります。 最も単純な例。 「Hello World!」というテキストがあります。







「World」という単語の前に「DevExpress」というテキストを挿入する操作を実行します。 同時に、テキスト「DevExpress」がインデックス6(0からカウント)の位置に挿入されたという情報をアンドゥバッファに入れます。







次のアクションを実行します。テキストの先頭に「We say:」という行を挿入します。 もちろん、このアクションの後、行「DevExpress」を挿入する位置に関する情報は不正確になります。







この時点で最初の操作で元に戻すを呼び出すと、ドキュメントの内容が破損します。 情報が正しくなるためには、再計算する必要があります。



場合によっては、再計算せずに実行できますか? もちろん、各操作をロールバックすることで、ドキュメントを操作時の状態に正確に戻すことができると仮定すれば、できます。 同様の要件を繰り返し操作に課す必要があります。



取り消しバッファ内の情報は、ドキュメントがこの情報が保存された初期状態になるとすぐに自動的に「正しく」なります。 状態が最初の状態と異なる場合、この情報は一般的に間違っており、使用できません。 また、操作のロールバックは実行の逆順で発生するため、ドキュメントが元の状態に戻されるまで情報を使用することはできません。



以下に、独自のテキストエディタまたはイメージエディタ用の単純な元に戻す/やり直しマネージャを実装するときに使用できる考慮事項を示します。 ただし、 次の記事で説明するように、人生ではすべてが少し異なります。



そして、同じトピックに関する極端な記事へのリンクがあります。



PS:

この記事を書く過程で、私たちは興味を持ち、Undoのような便利なものがいつ登場しましたか?



熱心にグーグルで、彼らはこれが1971年から1976年の期間のどこかで起こったという結論に達しました。 そのため、 最新の edエディターマニュアルは、元に戻すをサポートしていると主張しています。 ただし、1971年の最初のUnixのマニュアルでは、Undoについてはまだ言及されていません。 しかし、1976年に最初のバージョンがリリースされたviエディターでは、元に戻すのは元のようでした。



取り消し自体という用語は、おそらく1976年に初めて言及されました。「ユーザーが少なくとも直前のコマンドを「取り戻す」ことを許可することは非常に便利です(特別な「取り消し」コマンドを発行することによって)」(ランスA IBMのミラーとジョンC.トーマス、「インタラクティブシステムの使用における動作の問題」)(NYタイムズ誌、第5段落を参照)。



All Articles