undo/redo はとても魅力的な機能です。初めて undo/redo が動作するするシステムを触った時は、
いったいどうやって実装しているのだろうと不思議に思いました。だって、ユーザが行った操作をずっと覚えていて、
それを取り消したり再実行できるわけですから、かなり複雑な機構が必要ではないかと思いませんか?
ViViにも undo/redo を実装しようと数時間考えて以下に説明する方法を採用することにしました。
自分では直感的で素直な実装と思っています。
undo/redo を実現するにはユーザが行った操作を覚える必要があります。ViViではそのためのクラス CUndoItem と、その派生クラスオブジェクトの順序付き集合を管理する CUndoMgr を導入しました。
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 は前節で示した 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 アイテムはブロックオブジェクトがまとめて管理します。
Copyright (c) 1997 by Nobuhide Tsuda, All Right Reserved.
このホームページに関するご質問、ご要望、バグレポート等は
ntsuda@beam.or.jp
までメールをいただければ幸いです。