2008年3月28日金曜日

Java SwingでUndo/Redoを実装する

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というクラスを使うと、リスナーの管理などをしてくれるので、これを使ったほうが便利である。
具体的には、モデルの状態を変更してリスナーに通知する方法は、ドキュメントにも記述されている通り、以下のようにすればよい。
//dataはStateEditableを実装しているオブジェクト
StateEdit stateEdit = new StateEdit(data);
//ここでコンポーネントを操作するメソッドを呼ぶ
data.doAnything();
stateEdit.end();
//undoEditSupportは、イベントリスナーを保存している
undoEditSupport.postEdit(stateEdit);
ちなみにこの操作により、前記のvoid storeState(Hashtable<Object,Object> state)が呼び出される。

というわけで、実際の実装では、UIコンポーネントと内部にあるデータ(すなわちモデル)を分離する。そのモデルにStateEditableインターフェースを実装する。UIコンポーネントにUndoableEditListenerを登録する方法を追加する。モデルが変更されたときにリスナーに通知する処理の追加をする(上記のStateEditを使ったコード)。ということになる。

2008年3月23日日曜日

JavaのRegexのパターンマッチがうまくいかない?

明らかに小ネタな話であるが、PHPとかのつもりでJavaのRegexでパターンマッチさせると、マッチしないと勘違いするかもしれないという話。
Javaで単純にRegexに文字列がマッチしているか否かを知るには、以下のようなコードを使う。

  • java.lang.String str = "…";
    str.matches(regex);
  • java.util.regex.Pattern.matches(regex, str);
  • java.util.regex.Pattern pattern = java.util.regex.Pattern.complie(regex);
    java.util.regex.Matcher matcher = pattern.matcher(str);
    matcher.matches();

いずれにしろ、java.util.regex.Patternがregexで記述されたパターンを処理している。

ところで、PHPでパターンマッチさせるときには、だいたい、PCRE(Perl互換正規表現)を使うわけだが、そこで単純なパターンマッチは、pcre_match(pattern, str);となる。
例えば、文字列の先頭が"prefx"という文字列で始まっているかどうかを知るには、PHPではpcre_match('/^prefix/', str) > 0と書く。

そこで、同じ感じでJavaで書くと、java.util.regex.Pattern.matches("^prefix", str);と書きたくなる。しかし、こう書いてしまうとうまくマッチしてくれないのである。
それは、Regexでは、パターンに完全にマッチしてないとだめなのである。

すなわち、Javaではjava.util.regex.Pattern.matches("^prefix.*", str);と書いてあげないとマッチしないということになる。

2008年3月22日土曜日

投稿のテスト

MicrosoftのWindows LiveにWriterという、ブログの編集ツールがあるのだが、それを使ってためしに投稿してみるテスト。
しかし、結局のところWYSIWYGなエディタを使わずに、テキストモードでHTMLタグを打ち込んでいる始末。

ちゃんと、ブログを読み込んで、レイアウトなどを保存して、プレビューしてくれるので、Bloggerのプレビューよりはよいかもしれない。
このツールの良し悪しは、しばらく使ってみてから、レビューしてみることにする。