テキストエディタのように、元に戻す/やり直し機能を実装する方法について考えてみてください。どのアルゴリズムを使用する必要があり、何を読むことができますか。ありがとう。
テキストエディタのように、元に戻す/やり直し機能を実装する方法について考えてみてください。どのアルゴリズムを使用する必要があり、何を読むことができますか。ありがとう。
回答:
元に戻すの種類の2つの主要な区分について知っています
テキストエディタの場合、この方法で状態を生成することは、計算量が多すぎることはありませんが、Adobe Photoshopのようなプログラムの場合、計算量が多すぎるか、まったく不可能な場合があります。たとえば、-ぼかしアクションの場合、ぼかし解除アクションを指定しますが、データがすでに失われているため、元の状態に戻すことはできません。したがって、状況(論理的な逆アクションの可能性とその実現可能性)に応じて、これら2つの広いカテゴリから選択し、必要な方法で実装する必要があります。もちろん、あなたのために働くハイブリッド戦略を持つことは可能です。
また、Gmailのように、アクション(メールの送信)が最初から行われないため、時間制限のある取り消しが可能な場合もあります。つまり、そこで「元に戻す」のではなく、アクション自体を「実行しない」だけです。
私は2つのテキストエディタを最初から作成しましたが、どちらも非常に原始的な形式の元に戻す/やり直し機能を採用しています。「プリミティブ」とは、機能の実装が非常に簡単だったが、非常に大きなファイル(たとえば、>> 10 MB)では不経済であることを意味します。ただし、システムは非常に柔軟です。たとえば、無制限のレベルの取り消しをサポートします。
基本的に、私は次のような構造を定義します
type
TUndoDataItem = record
text: /array of/ string;
selBegin: integer;
selEnd: integer;
scrollPos: TPoint;
end;
次に、配列を定義します
var
UndoData: array of TUndoDataItem;
次に、この配列の各メンバーは、テキストの保存された状態を指定します。ここで、テキストの編集(文字キーダウン、バックスペースダウン、削除キーダウン、切り取り/貼り付け、マウスによる選択の移動など)ごとに、(たとえば)1秒のタイマーを(再)開始します。トリガーされると、タイマーは現在の状態をUndoData
配列の新しいメンバーとして保存します。
元に戻す(Ctrl + Z)で、エディターをその状態に戻しUndoData[UndoLevel - 1]
、UndoLevel
を1つ減らします。デフォルトでUndoLevel
は、はUndoData
配列の最後のメンバーのインデックスと同じです。やり直し(Ctrl + YまたはShift + Ctrl + Z)で、エディターを状態に戻しUndoData[UndoLevel + 1]
、UndoLevel
を1つ増やします。もちろん、配列UndoLevel
の長さ(マイナス1)と等しくないときに編集タイマーがトリガーされた場合、Microsoft Windowsプラットフォームで一般的であるようUndoData
にUndoLevel
、この配列のすべての項目を後にクリアします(ただし、Emacsの方が優れています正しく-MicrosoftWindowsアプローチの欠点は、多くの変更を元に戻した後、誤ってバッファーを編集した場合、以前のコンテンツ(元に戻されなかった)が完全に失われることです)。この配列の縮小をスキップすることをお勧めします。
画像エディタなどの別の種類のプログラムでは、同じ手法を適用できますが、もちろん、UndoDataItem
構造がまったく異なります。それほど多くのメモリを必要としないより高度なアプローチは、元に戻すレベル間の変更のみを保存することです(つまり、「alpha \ nbeta \ gamma」と「alpha \ nbeta \ ngamma \ ndelta」を保存する代わりに、次のことができます。 「alpha \ nbeta \ ngamma」と「ADD \ ndelta」を保存します(意味がわかります)。各変更がファイルサイズと比較して小さい非常に大きなファイルでは、これにより元に戻すデータのメモリ使用量が大幅に減少しますが、実装が難しく、エラーが発生しやすくなります。
少し遅れますが、ここに行きます:あなたは特にテキストエディタを参照します、以下はあなたが編集しているものに適応できるアルゴリズムを説明します。関連する原則は、行った各変更を再作成するために自動化できるアクション/指示のリストを保持することです。元のファイルに変更を加えないで(空でない場合)、バックアップとして保持します。
元のファイルに加えた変更の前後リンクリストを保持します。このリストは、ユーザーが実際に変更を保存するまで、一時ファイルに断続的に保存されます。これが発生すると、変更を新しいファイルに適用し、古いファイルをコピーして、同時に変更を適用します。次に、元のファイルの名前をバックアップに変更し、新しいファイルの名前を正しい名前に変更します。(保存された変更リストを保持するか、削除して後続の変更リストに置き換えることができます。)
リンクリストの各ノードには、次の情報が含まれています。
delete
その後にinsert
insert
挿入されたのがデータの場合。の場合delete
、削除されたデータ。を実装するUndo
には、「current-node」ポインタまたはインデックスを使用して、リンクリストの末尾から逆方向に作業します。変更があったinsert
場合は、リンクリストを更新せずに削除を実行します。そして、それがあった場所ではdelete
、リンクリストバッファのデータからデータを挿入します。これは、ユーザーからの「元に戻す」コマンドごとに実行します。Redo
'current-node'ポインターを前方に移動し、ノードごとに変更を実行します。ユーザーが元に戻した後にコードに変更を加えた場合は、「current-node」インジケーターの後のすべてのノードをテールに削除し、テールを「current-node」インジケーターに等しく設定します。次に、ユーザーの新しい変更がテールの後に挿入されます。そしてそれはそれについてです。
私のたった2セントは、操作を追跡するために2つのスタックを使用したいということです。ユーザーがいくつかの操作を実行するたびに、プログラムはそれらの操作を「実行された」スタックに配置する必要があります。ユーザーがこれらの操作を元に戻したい場合は、「実行済み」スタックから「リコール」スタックに操作をポップするだけです。ユーザーがこれらの操作をやり直したい場合は、「リコール」スタックからアイテムをポップして、「実行済み」スタックにプッシュバックします。
それが役に立てば幸い。
アクションが可逆的である場合。たとえば、1を追加して、プレーヤーを移動させるなど、コマンドパターンを使用して元に戻す/やり直しを実装する方法を確認し ます。リンクをたどると、その方法の詳細な例が見つかります。
そうでない場合は、@ Lazerの説明に従って保存状態を使用してください。
既存の元に戻す/やり直しフレームワークの例を研究するかもしれません。最初のGoogleヒットはcodeplex(.NET用)です。それが他のどのフレームワークよりも良いのか悪いのかはわかりませんが、たくさんあります。
アプリケーションに元に戻す/やり直し機能を持たせることが目標である場合は、アプリケーションの種類に適していると思われる既存のフレームワークを選択することもできます。
独自の元に戻す/やり直しを作成する方法を学びたい場合は、ソースコードをダウンロードして、パターンと接続方法の詳細の両方を確認できます。
Mementoパターンは、このために作られました。
これを自分で実装する前に、これは非常に一般的であり、コードはすでに存在することに注意してください。たとえば、.Netでコーディングしている場合は、IEditableObjectを使用できます。
議論に加えて、私は直感的なものについての考えに基づいてUNDOとREDOを実装する方法についてのブログ投稿を書きました:http://adamkulidjian.com/undo-and-redo.html
基本的な元に戻す/やり直し機能を実装する1つの方法は、mementoとコマンドの両方のデザインパターンを使用することです。
Mementoは、たとえば、後で復元するオブジェクトの状態を維持することを目的としています。この記念碑は、最適化の目的で可能な限り小さくする必要があります。
コマンド・パターンは、必要に応じて、いくつかの命令を実行するオブジェクト(コマンド)にカプセル化します。
これらの2つの概念に基づいて、TypeScriptでコーディングされた次のような基本的な元に戻す/やり直しの履歴を記述できます(フロントエンドライブラリInteractoから抽出および適合)。
このような履歴は、次の2つのスタックに依存しています。
コメントはアルゴリズム内で提供されます。元に戻す操作では、REDOスタックをクリアする必要があることに注意してください。その理由は、アプリケーションを安定した状態にするためです。過去に戻って実行したアクションをやり直すと、将来を変更すると、以前のアクションは存在しなくなります。
export class UndoHistory {
/** The undoable objects. */
private readonly undos: Array<Undoable>;
/** The redoable objects. */
private readonly redos: Array<Undoable>;
/** The maximal number of undo. */
private sizeMax: number;
public constructor() {
this.sizeMax = 0;
this.undos = [];
this.redos = [];
this.sizeMax = 30;
}
/** Adds an undoable object to the collector. */
public add(undoable: Undoable): void {
if (this.sizeMax > 0) {
// Cleaning the oldest undoable object
if (this.undos.length === this.sizeMax) {
this.undos.pop();
}
this.undos.push(undoable);
// You must clear the redo stack!
this.clearRedo();
}
}
private clearRedo(): void {
if (this.redos.length > 0) {
this.redos.length = 0;
}
}
/** Undoes the last undoable object. */
public undo(): void {
const undoable = this.undos.pop();
if (undoable !== undefined) {
undoable.undo();
this.redos.push(undoable);
}
}
/** Redoes the last undoable object. */
public redo(): void {
const undoable = this.redos.pop();
if (undoable !== undefined) {
undoable.redo();
this.undos.push(undoable);
}
}
}
Undoable
インターフェイスは非常に簡単です:
export interface Undoable {
/** Undoes the command */
undo(): void;
/** Redoes the undone command */
redo(): void;
}
これで、アプリケーションで動作する取り消し可能なコマンドを作成できます。
たとえば(まだInteractoの例に基づいています)、次のようなコマンドを作成できます。
export class ClearTextCmd implements Undoable {
// The memento that saves the previous state of the text data
private memento: string;
public constructor(private text: TextData) {}
// Executes the command
public execute() void {
// Creating the memento
this.memento = this.text.text;
// Applying the changes (in many
// cases do and redo are similar, but the memento creation)
redo();
}
public undo(): void {
this.text.text = this.memento;
}
public redo(): void {
this.text.text = '';
}
}
これで、コマンドを実行してUndoHistoryインスタンスに追加できます。
const cmd = new ClearTextCmd(...);
//...
undoHistory.add(cmd);
最後に、元に戻すボタン(またはショートカット)をこの履歴にバインドできます(やり直しの場合も同じです)。
このような例については、Interactoのドキュメントページで詳しく説明されています。