Swingのなかにあるjavax.swing.undoというパッケージを使ってUndo/Redoを実装するということなのだが、使い方については"The Java Tutorial"に記載されてはいるものの実態がよくわからなかった。いつものごとく、やっつけでサンプルを作成して、どうなっているかを調べてみた。ちなみにサンプルソースは、どうにもならないほど煩雑になってしまったため、公開はしません。
念のためですが、Undo/Redoというのは、なんだかの操作によって変更された履歴を保存して、一つ前の状態に戻すことがUndo、Undoで戻したものを元の戻すのがRedoということである。
The Java Tutorialでも書かれているが、一般的な実装方法は、以下のような感じになっている。
- UndoManagerを生成
- これは単純に、UndoManagerをクラスの変数にして、
new UndoMnager();
を代入すればよい。 - UIの生成
- これは、Undo/Redoをさせるためのメニューアイテムなどを作っておくということであるが、ドキュメントの例ではAbstractActionをサブクラス化して、メニュに追加しているが、普通にMenuItemを作ってもかまわない。
メニューアイテムが選択されたときの動作(すなわち、ActionListenerのpublic void actionPerformed(ActionEvent e)の実装)は、Undoのときは、UndoManager.undo();でRedoのときは、UndoManager.redo();を呼び出せばよい。
あとは、メニューアイテムは、UndoManager.canUndo();やUndoManager.canRedo();でUndo/Redoが可能かどうか判断して制御する。 - Undo/Redo対象となるオブジェクトの監視
- これは、Undo/Redoの状態に変化したときのイベントを拾ってくるためのものである。
ここで、変更された履歴のデーターをUndoManagerに登録するのものであるが、同時にUIの制御のためにUndo/Redoが可能かどうかを調べて、UIに反映させる(enable/disableする)。
UndoableEditEventというイベントを受けて取るためのUndoableEditListenerをどこかに実装する。具体的には、
といった感じになる。void undoableEditHappened(UndoableEditEvent e) { //このundoManagerは、クラスメンバー undoManager.addEdit(e.getEdit()); //このあとUIの制御をする undoAction,redoActionもクラスメンバー undoAction.setEnabled(undoManager.canUndo()); redoAction.setEnabled(undoManager.canRedo()); }
といったところが、ドキュメントから読み取れることなのだが、さらにDocumentというものがあるといっている。これらの内容は、どうやらjavax.swing.text.JTextComponentに特化した話のように見える。
ドキュメント内のサンプルコードでは、Undo/Redoの対象となるオブジェクトに対して、doc.addUndoableEditListener(new MyUndoableEditListener());
という記述がされているが、"doc"にあたるものは、例えば、JTextAreaの場合、JTextArea.getDocument()から得られるjavax.swing.text.Documentとなっている。getDocument()は、JTextComponentで定義されている。
しかしながら、JTextComponent以外のUIコンポーネントでUndo/Redoをするにはどうすればいいのかというのが疑問となる。おそらく、javax.swing.text.Documentに実装されているUndo/Redo関連の処理を自前でやらないといけないのであろうと考えてみた。
そうすると、javax.swing.undoのパッケージにあるクラスやインタフェースをどう使うのか?ということになる。
それをいろいろやってみた結果、「履歴を吐き出し、読み込む仕組み」「Undo/Redoの状態の変化を通知する仕組み」の2つが必要であるということにたどり着いた。
何か変化があったときにモデルの状態(履歴)をUndoMangerに記憶させておき、Undo/Redoの操作により、UndoManagerから履歴を受け取って、状態を戻すというのが、基本的な動作となる。
- 履歴を吐き出し、読み込む仕組み
- ストレートにいってしまうと、StateEditableインターフェースを実装しているオブジェクトを用意することである。ここでいうオブジェクトは、いわゆるモデルと思ってよい。
void storeState(Hashtable<Object,Object> state)は、そのオブジェクトの状態を送り出すときに呼び出される。パラメタのHashtableに適当なKeyとデータを追加する(putする)。
void restoreState(Hashtable<?,?> state)は、Undo/Redoにより、状態を受け取るときに呼び出される。パラメタのHashtableは、storeStateで渡したものと同じものである(はず)。
Hashtableになっているのは、おそらくオブジェクト内部に複数のデータが存在しているときに、Keyとデータをセットで管理するためであると考えられる。例えば、オブジェクトの中に文字列しかなければ、Keyは"Text"で値はその文字列のStringにすればよいということだろう。Keyはなんでもいいので、"Text"でなくても"String"でもいいでしょう。 - Undo/Redoの状態の変化を通知する仕組み
- これは、javax.swing.text.Documentに相当するものだが、基本的には、UndoableEditListenerを登録、削除とリスナーへの通知をするオブジェクトである。UndoableEditSupportというクラスを使うと、リスナーの管理などをしてくれるので、これを使ったほうが便利である。
具体的には、モデルの状態を変更してリスナーに通知する方法は、ドキュメントにも記述されている通り、以下のようにすればよい。
ちなみにこの操作により、前記のvoid storeState(Hashtable<Object,Object> state)が呼び出される。//dataはStateEditableを実装しているオブジェクト StateEdit stateEdit = new StateEdit(data); //ここでコンポーネントを操作するメソッドを呼ぶ data.doAnything(); stateEdit.end(); //undoEditSupportは、イベントリスナーを保存している undoEditSupport.postEdit(stateEdit);
というわけで、実際の実装では、UIコンポーネントと内部にあるデータ(すなわちモデル)を分離する。そのモデルにStateEditableインターフェースを実装する。UIコンポーネントにUndoableEditListenerを登録する方法を追加する。モデルが変更されたときにリスナーに通知する処理の追加をする(上記のStateEditを使ったコード)。ということになる。