元に戻す/やり直し-尾が犬を振る



この記事では、 XtraRichEditテキストエディターで元に戻す/やり直した方法について引き続き説明します。



それはすべて、私たちのチームがテキストエディターのコンセプトのソースを与えられたという事実から始まりました。このコンセプトを思い浮かべたいという願望がありました。 コンセプトの開発に1か月しかかからなかったという事実にもかかわらず、彼は多くのことを行うことができました。テキストの入力と削除、カーソルの水平および垂直移動が可能です。



テキストのセクション(フォント、そのサイズなど)と、すべてのプロパティ、スタイルなどを備えた段落のような他のすべての「ささいなこと」をフォーマットする機能を追加する必要がありました。 そして、コンセプトは元に戻す/やり直す方法を知りませんでした。



概念のソースを掘り下げなければならなかった2週間、Undo / Redoをゼロから書き直さずにねじ込む唯一の方法は、すべてのアクション(実際にドキュメントのコピーを作成する)のためにドキュメントの状態全体を保存することであることが完全に明らかになりました:



public class DocumentHistoryItem : HistoryItem { readonly DocumentState previousState; readonly DocumentState nextState; public DocumentHistoryItem(DocumentState previousState, DocumentState nextState) { this.previousState = previousState; this.nextState = nextState; } public override void Undo(Document document) { document.ApplyState(previousState); } public override void Redo(Document document) { document.ApplyState(nextState); } }
      
      





もちろん、これは受け入れられませんでした。なぜなら、 このアプローチでは、使用されるメモリが多すぎます。 少なくとも1メガバイトのサイズのドキュメントを想像してください。 2年生のタスク:各キーストロークがドキュメントの変更とアンドゥバッファーへの書き込みにつながる場合、メモリがなくなる前にいくつのキーを押す必要がありますか?



元に戻す/やり直しをより受け入れやすい方法で固定できないのはなぜですか? 実際、彼らがコンセプトを開発したとき、彼らは最初にUndo / Redoの将来の実装を考慮しなかったということです。 テキストに対するすべての操作は、逆の操作の実行が非常に問題となるような方法で実装されました。 さらに、テキストとその個々のセクションのプロパティ(フォント、テキストサイズなど)を保存するという非常に編成が、元に戻す/やり直しの実装には非常に不便であることが判明しました。 操作ごとに、かなり大量のデータを保存する必要がありました。



たとえば、フォント名の変更に関するデータを保存するのは簡単でした。 これを行うには、アクションを元に戻したりやり直したりできるように、古いフォント名と新しいフォント名を保持する必要がありました。 同様に、フォントサイズについては、古いサイズと新しいサイズを保存する必要がありました。 ただし、1つのアクション(たとえば、[フォント]ダイアログ)でフォント名とそのサイズの両方が変更された場合、既に2つの要素を元に戻すバッファーに保存する必要がありました。 しかし、フォントの実際の名前とサイズに加えて、これらの値を適用するテキストのセクションに情報を保存する必要がありました。



最後に、既存のコードに元に戻す/やり直しを埋め込む試みがいくつか失敗した後、結論に達しました:元に戻す/やり直しの問題を回避するには、データ構造とアルゴリズムを特別な方法で再設計する必要があります。



文書に対する各ユーザーアクションは、文書に対する一連の基本操作に変換することが決定されました。 このような各操作は非常に基本的なものである必要があるため、逆操作を簡単に実装できます。 このような操作には、直接実行および逆実行に必要な最小限のデータも保存されます。 基本操作は単一の複合要素にパックされ、取り消しバッファーに配置されます。



 public class CompositeHistoryItem : HistoryItem { readonly List<HistoryItem> items = new List<HistoryItem>(); public void AddItem(HistoryItem item) { items.Add(item); } protected override void UndoCore() { for (int i = items.Count - 1; i >= 0; i--) items[i].Undo(); } protected override void RedoCore() { int count = items.Count; for (int i = 0; i < count; i++) items[i].Redo(); } }
      
      





複合操作の要素のロールバックは逆の順序で実行され、ロールバックは直接の順序で実行されることに注意してください。



アクションを基本操作に分解するには、データ構造を特別な方法で整理する必要がありました。 各間隔内のテキストの書式設定が同じになるように、段落内のテキストをランに分割しました。







任意の選択されたテキストの書式設定を変更する場合、テキストは最初に選択に従って間隔に分割され、次に選択範囲内にあるそれらの間隔の書式設定が変更されました。 したがって、3つの基本操作「間隔を分割する」、「2つの隣接する間隔を結合する」、および「間隔のフォーマットを変更する」が区別されました。



最後の基本操作は、個別に考慮する必要があります。 元に戻すバッファに保存されるデータ量を最小限に抑えるために、間隔にテキストプロパティを含むオブジェクトは、mutable(CharacterFormattingBase)とimmutable(CharacterFormattingInfo)の2つのサブオブジェクトに分割されました。 不変オブジェクトはキャッシュリスト(CharacterFormattingInfoCache)に格納され、リストの要素は一意であり、リストから削除されることはありませんでした。 可変オブジェクトは、リスト内の不変オブジェクトのインデックスのみを保存しました。







これにより、変数オブジェクトのプロパティ(または複数のプロパティを一度に変更できます。これはすでに技術的な問題です)を、インデックスキャッシュで適切な不変オブジェクトを検索し、変数オブジェクトに格納することまで減らすことができました。 適切なオブジェクトが見つからない場合は、作成されてキャッシュに追加されました。



このアプローチを使用して、(もちろん、単一のトランザクション内で)任意の数の間隔プロパティを変更するために、古いバッファと新しいバッファの2つのインデックスのみをアンドゥバッファに保存することができました。



一見、増え続けるキャッシュがメモリの割り当てを誤っているようです。 しかし、実際にはそうではありません。 実際、実際のドキュメントにはそれほど多くの異なるテキストプロパティの組み合わせはありません。 さらに、ほとんどのテキストは通常​​同じ書式設定です。 また、各間隔で書式設定を行うプロパティのすべての値を複製する代わりに、キャッシュ内の唯一のオブジェクトのインデックスのみを複製します。



最後に、アクションとその基本操作への分割の例を検討します。



最初は、連続した単一のテキストがあります。







テキスト「Wor」の一部を太字で強調表示します。



このアクションは、3つの基本操作に分けることができます。



テキストを2つの隣接するセクションに分割します。







テキストの右側のセクションをさらに2つのセクションに分割します。







インデックス1のプロットには、太字フォントを適用します。







各操作は非常にシンプルで、簡単に元に戻せ、最小限のデータを保存します。 したがって、テキストのセクションを切り取る操作では、切り取るセクションのインデックスと、セクションが発生する前のセクションの文字のインデックスを保存するだけで十分です。 フォントスタイルを適用する操作により、テキストセクションの数(実行インデックス)、およびCharacterFormattingInfoオブジェクトの古いインデックスと新しいインデックスがキャッシュに保存されます。



元に戻す/やり直しによって、データをより適切に整理する方法が決まります。 つまり、1つのセクションのテキスト全体がまったく同じ書式を持つように、テキストをセクションに分割する必要があります。 区画には番号を付ける必要があります。 このようなデータ編成により、アクションを基本操作に自然かつ簡単に分割できます。 また、これにより、Undo / Redoの実装が簡素化され、保存する必要があるデータの量が最小限に抑えられます。



XtraRichEditを開発するとき、最初に「元に戻す/やり直し」アプローチを使用し始めました。 各アクションを設計するとき、最初にこのアクションを元に戻す/やり直しの概念に組み込む方法、どの基本操作に割り込むか、メモリに保存されている最小量のデータでどのように処理するかについて考えました。 これにより、実装が簡単なドキュメントモデルを作成できました。これは、メモリをかなり効率的に使用し、ドキュメントの書式設定中にモデル内を歩き回ったり、さまざまな変更を加えたりする際に、大量の計算リソースを必要としません。



このトピックに関する以前の記事 | このトピックに関する次の記事



All Articles