元に戻す/やり直しの実装


84

テキストエディタのように、元に戻す/やり直し機能を実装する方法について考えてみてください。どのアルゴリズムを使用する必要があり、何を読むことができますか。ありがとう。


3
たぶん、あなたのソフトウェアが働いている領域(テキスト処理?グラフィックス?データベース?)と、おそらくプラットフォーム/プログラミング言語についての詳細を追加してください。
ペッカ

回答:


94

元に戻すの種類の2つの主要な区分について知っています

  • 状態の保存: 元に戻すの1つのカテゴリは、実際に履歴の状態を保存する場所です。この場合、発生するのは、すべての時点で、メモリのある場所に状態を保存し続けることです。元に戻す場合は、現在の状態をスワップアウトし、メモリにすでに存在していた状態にスワップします。これは、たとえば、AdobePhotoshopの履歴やGoogleChromeの閉じたタブを再度開く方法です。

代替テキスト

  • 状態の生成: もう1つのカテゴリは、状態自体を維持する代わりに、アクションが何であったかを覚えているだけです。元に戻す必要がある場合は、その特定のアクションを論理的に逆にする必要があります。簡単な例として、元に戻すをサポートするテキストエディタでCtrl+を実行するBと、太字のアクションとして記憶されます。今、各アクションには、その論理的な逆のマッピングがあります。あなたが行うときに、Ctrl+をZ、それが逆アクションテーブルとアンドゥアクションがあることを発見から見上げるCtrl+B再び。それが実行され、以前の状態になります。したがって、ここでは、以前の状態はメモリに保存されませんでしたが、必要なときに生成されました。

テキストエディタの場合、この方法で状態を生成することは、計算量が多すぎることはありませんが、Adobe Photoshopのようなプログラムの場合、計算量が多すぎるか、まったく不可能な場合があります。たとえば、-ぼかしアクションの場合、ぼかし解除アクションを指定しますが、データがすでに失われているため、元の状態に戻すことはできません。したがって、状況(論理的な逆アクションの可能性とその実現可能性)に応じて、これら2つの広いカテゴリから選択し、必要な方法で実装する必要があります。もちろん、あなたのために働くハイブリッド戦略を持つことは可能です。

また、Gmailのように、アクション(メールの送信)が最初から行われないため、時間制限のある取り消しが可能な場合もあります。つまり、そこで「元に戻す」のではなく、アクション自体を「実行しない」だけです。


9
保存状態と「転送」アクションを組み合わせておくと役立つ場合があります。簡単なアプローチとして、5つのアクションごとに「メジャー保存状態」を維持し、最後の「メジャー保存状態」の後に保存状態を維持する場合、保存状態を元に戻すことで最初の数回の元に戻す操作を実行できます。次に、前のメジャーセーブから4つのアクションを繰り返して元に戻します。もう少し一般的なアプローチは、異なるレベルの保存状態に対して2の累乗の進行を使用することです。したがって、O(1)の順方向反復を使用して、Nレベルの取り消しのためにlg(N)の保存状態を保存する必要があります。
スーパーキャット2010年

答えは、このハイブリッドアプローチも追加する必要があります。これは、前の状態を生成して元に戻すのが複雑すぎて、編集中のデータが大きすぎる場合に非常に実行可能です。2つの間のバランス。固定された4の長さまたは2の累乗の長さの進行の代わりに多くの戦略を採用することができます。前の状態の生成が複雑すぎる場合は常に状態を保存するように。
zookastos

20

私は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プラットフォームで一般的であるようUndoDataUndoLevel、この配列のすべての項目を後にクリアします(ただし、Emacsの方が優れています正しく-MicrosoftWindowsアプローチの欠点は、多くの変更を元に戻した後、誤ってバッファーを編集した場合、以前のコンテンツ(元に戻されなかった)が完全に失われることです)。この配列の縮小をスキップすることをお勧めします。

画像エディタなどの別の種類のプログラムでは、同じ手法を適用できますが、もちろん、UndoDataItem構造がまったく異なります。それほど多くのメモリを必要としないより高度なアプローチは、元に戻すレベル間の変更のみを保存することです(つまり、「alpha \ nbeta \ gamma」と「alpha \ nbeta \ ngamma \ ndelta」を保存する代わりに、次のことができます。 「alpha \ nbeta \ ngamma」と「ADD \ ndelta」を保存します(意味がわかります)。各変更がファイルサイズと比較して小さい非常に大きなファイルでは、これにより元に戻すデータのメモリ使用量が大幅に減少しますが、実装が難しく、エラーが発生しやすくなります。


@AlexanderSuraphel:彼らは「より高度な」アプローチを使用していると思います。
Andreas Rejbrand 2015年


8

少し遅れますが、ここに行きます:あなたは特にテキストエディタを参照します、以下はあなたが編集しているものに適応できるアルゴリズムを説明します。関連する原則は、行った各変更を再作成するために自動化できるアクション/指示のリストを保持することです。元のファイルに変更を加えないで(空でない場合)、バックアップとして保持します。

元のファイルに加えた変更の前後リンクリストを保持します。このリストは、ユーザーが実際に変更を保存するまで、一時ファイルに断続的に保存されます。これが発生すると、変更を新しいファイルに適用し、古いファイルをコピーして、同時に変更を適用します。次に、元のファイルの名前をバックアップに変更し、新しいファイルの名前を正しい名前に変更します。(保存された変更リストを保持するか、削除して後続の変更リストに置き換えることができます。)

リンクリストの各ノードには、次の情報が含まれています。

  • 変更の種類:データを挿入するか、データを削除します。データを「変更」するということは、deleteその後にinsert
  • ファイル内の位置:オフセットまたは行/列のペアにすることができます
  • データバッファ:これはアクションに関連するデータです。insert挿入されたのがデータの場合。の場合delete、削除されたデータ。

を実装するUndoには、「current-node」ポインタまたはインデックスを使用して、リンクリストの末尾から逆方向に作業します。変更があったinsert場合は、リンクリストを更新せずに削除を実行します。そして、それがあった場所ではdelete、リンクリストバッファのデータからデータを挿入します。これは、ユーザーからの「元に戻す」コマンドごとに実行します。Redo'current-node'ポインターを前方に移動し、ノードごとに変更を実行します。ユーザーが元に戻した後にコードに変更を加えた場合は、「current-node」インジケーターの後のすべてのノードをテールに削除し、テールを「current-node」インジケーターに等しく設定します。次に、ユーザーの新しい変更がテールの後に挿入されます。そしてそれはそれについてです。


8

私のたった2セントは、操作を追跡するために2つのスタックを使用したいということです。ユーザーがいくつかの操作を実行するたびに、プログラムはそれらの操作を「実行された」スタックに配置する必要があります。ユーザーがこれらの操作を元に戻したい場合は、「実行済み」スタックから「リコール」スタックに操作をポップするだけです。ユーザーがこれらの操作をやり直したい場合は、「リコール」スタックからアイテムをポップして、「実行済み」スタックにプッシュバックします。

それが役に立てば幸い。



2

既存の元に戻す/やり直しフレームワークの例を研究するかもしれません。最初のGoogleヒットはcodeplex(.NET用)です。それが他のどのフレームワークよりも良いのか悪いのかはわかりませんが、たくさんあります。

アプリケーションに元に戻す/やり直し機能を持たせることが目標である場合は、アプリケーションの種類に適していると思われる既存のフレームワークを選択することもできます。
独自の元に戻す/やり直しを作成する方法を学びたい場合は、ソースコードをダウンロードして、パターンと接続方法の詳細の両方を確認できます。


2

Mementoパターンは、このために作られました。

これを自分で実装する前に、これは非常に一般的であり、コードはすでに存在することに注意してください。たとえば、.Netでコーディングしている場合は、IEditableObjectを使用できます。


1

議論に加えて、私は直感的なものについての考えに基づいてUNDOとREDOを実装する方法についてのブログ投稿を書きました:http//adamkulidjian.com/undo-and-redo.html


1
上記にリンクしたブログ投稿での「InjectionintotheMiddle」についての議論に感謝します。私の脳は、誰かが1つ以上のアクションを元に戻し、新しいことをした後、元に戻したりやり直したりした場合にどうすればよいかを理解しようとして、すべて結び目で縛られていました。あなたのイラストは、ユーザー側の新しいアクションの後にキューの「やり直し」部分をクリアすることが、正気を維持するための賢明な方法である理由を理解するのに役立ちました。ありがとう!
ダンロビンソン

@DanRobinsonお返事ありがとうございます。きれいでわかりやすいブログ投稿を写真で書くことに多くの時間を費やしたので、それが違いを生んだことを嬉しく思います。:)
アダム

0

基本的な元に戻す/やり直し機能を実装する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のドキュメントページで詳しく説明されています

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.