Doc3 undo/redo

前のドキュメント 次のドキュメント


■ undo/redo

 undo/redo はとても魅力的な機能です。初めて undo/redo が動作するするシステムを触った時は、 いったいどうやって実装しているのだろうと不思議に思いました。だって、ユーザが行った操作をずっと覚えていて、 それを取り消したり再実行できるわけですから、かなり複雑な機構が必要ではないかと思いませんか?
 ViViにも undo/redo を実装しようと数時間考えて以下に説明する方法を採用することにしました。 自分では直感的で素直な実装と思っています。

 undo/redo を実現するにはユーザが行った操作を覚える必要があります。ViViではそのためのクラス CUndoItem と、その派生クラスオブジェクトの順序付き集合を管理する CUndoMgr を導入しました。

■ CUndoItem

 ViViでの操作はすべて CUndoItem クラスの派生クラスオブジェクトにより記憶されます。CUndoItem は undo/redo を行う仮想関数を持ちます。
 doUndo, doRedo は名前どおりに undo/redo を行うメソッドです。最初の引数で指定されるドキュメントマネージャ が保持するテキストに対し処理を(CDocLineMgr のメソッドを使って)行います。 2番目以降の引数はカーソル位置や画面更新のためのヒント情報です。詳細は別のドキュメントで後述します。

    class CUndoItem {
    protected:
        short   m_flags;
    public:
        CUndoItem(short flags = 0)          { m_flags = flags; };
        virtual ~CUndoItem(void)            {};

        virtual void doUndo(CDocLineMgr *, STextPos *, STextPos *, STextPos *) = 0;
        virtual void doRedo(CDocLineMgr *, STextPos *, STextPos *, STextPos *) = 0;
        .....
    };

 各派生クラスは undo/redo を行うために必要な情報を保持します。挿入操作を記憶する CUndoItemInsertText では挿入位置とその文字列をメンバ変数に持ちます。
 ただし、挿入操作を行った後に挿入したテキストを保持していてもメモリのムダなので、 undo を行った場合にのみテキストを保持するようにしています。

    class CUndoItemInsertText : public CUndoItem {
        STextPos    m_tPos1;            //  文字列を挿入した位置
        STextPos    m_tPos2;            //  挿入直後の位置
        CString     m_text;             //  挿入したテキスト
    public:
        CUndoItemInsertText(short, STextPos *, STextPos *);
        ~CUndoItemInsertText(void)          {};

        void doUndo(CDocLineMgr *, STextPos *, STextPos *, STextPos *);
        void doRedo(CDocLineMgr *, STextPos *, STextPos *, STextPos *);
        .....
    };

 この他の派生クラスには以下のものがあります。

    CUndoItemDeleteText         文字列削除
    CUndoItemReplaceText        文字列置換
    CUndoItemMoveText           文字列移動
    CUndoItemShift              シフト(インデント)
    CUndoItemLineorder          行順序(ソート、反転)
    CUndoItemLineorderRevers    行順序反転
    CUndoItemBlockorder         ブロック単位での行順序変更
    CUndoItemTranslateCode      コード変換

■ CUndoMgr

 CUndoMgr は前節で示した CUndoItem の派生クラスオブジェクトを管理するクラスです。
 オブジェクトは双方向リンクにより管理され、undo マネージャは次に undo アイテムを入れる場所 (これをカレント位置と呼びます)を覚えています。 ユーザが編集を行うとそれに対応した undo アイテムオブジェクトが作成され、undo マネージャはそのオブジェクトを適切な位置に挿入し、不要になったアイテムをデリートします。
 undo を行う場合はカレント位置の undo オブジェクトの doUndo 仮想関数を使って undo を行い、 カレント位置をひとつもどします。redo の場合は undo オブジェクトの doRedo 仮想関数を使い、 カレント位置をひとつ進めます。
 図示すると下図のようになります。

┌─────────┐
│ undo オブジェクト│
├─────────┤
│ undo オブジェクト│
├─────────┤     ↑redo 時に移動
│ undo オブジェクト│     │
├─────────┤
│ undo オブジェクト│←─ カレント位置
├─────────┤
│ undo オブジェクト│     │
├─────────┤     ↓undo 時に移動
│    :    │
│    :    │
│    :    │
├─────────┤
│ undo オブジェクト│
└─────────┘

編集操作が行われた場合:

              ┌─────────┐
              │ undo オブジェクト│
              ├─────────┤
              │ undo オブジェクト│
┌─────────┐   ├─────────┤
│ 新オブジェクト │──→│ undo オブジェクト│
└─────────┘   ├─────────┤
              │ undo オブジェクト│←─ カレント位置
              ├─────────┤
              │ undo オブジェクト│
              ├─────────┤
              │    :    │
              │    :    │
              │    :    │
              ├─────────┤
              │ undo オブジェクト│
              └─────────┘



               ┌─────────┐
               │ undo オブジェクト│
               ├─────────┤
               │ undo オブジェクト│ *デリートされる
               ├─────────┤
┌─────────┐    │ undo オブジェクト│
│ 新オブジェクト │←┐  └─────────┘
├─────────┤ │
│ undo オブジェクト│ └─ カレント位置
├─────────┤
│ undo オブジェクト│
├─────────┤
│    :    │
│    :    │
│    :    │
├─────────┤
│ undo オブジェクト│
└─────────┘

 CUndoMgr の宣言は以下のような感じです。

    class CUndoMgr {
        int             m_current;      //  オブジェクトを次に入れる位置
        POSITION        m_crntPos;      //  m_current < GetCount() の時有効
        CUndoItemBlock  *m_block;       //  オープンされているブロックへのポインタ
        CUndoItemList   m_itemList;
    public:
        CUndoMgr(void)                  { m_current = 0; m_block = NULL; };
        ~CUndoMgr(void)                 { doDelete(0); };

        void    resetModifiedFlags(void);   //  ドキュメントが保存された時にコールされる

        void    doDelete(int from=0);       //  from 以降をデリートする
        void    addUndoItem(CUndoItem *, CViviDoc *);   //  undoItem オブジェクトをリストに加える
        BOOL    canUndo(void);
        BOOL    canRedo(void)               { return m_current < m_itemList.GetCount(); };

        BOOL    openBlock(CViviDoc *, short redraw=0);
        void    closeBlock(void);

        BOOL    doUndo(CDocLineMgr *, STextPos *, STextPos *, STextPos *, int &, int &);
        BOOL    doRedo(CDocLineMgr *, STextPos *, STextPos *, STextPos *, int &, int &, int&);
    };

 ViViでは複数の動作をひとつの undo 単位とすることができます。これを実現するためのメソッドが openBlock() と closeBlock() です。ブロックをオープンしている状態で追加した undo アイテムはブロックオブジェクトがまとめて管理します。


前のドキュメント 次のドキュメント ViViのページに戻る。 

Copyright (c) 1997 by Nobuhide Tsuda, All Right Reserved.
このホームページに関するご質問、ご要望、バグレポート等は  ntsuda@beam.or.jp  までメールをいただければ幸いです。