Undo / Redo on JavaFX
今回は、JavaFX で Undo / Redo してみようと思います。
Undo / Redo は ちょっとまともなアプリケーションを作ろうとすると どうしても避けられない
機能です。
JavaFX で Undo / Redo する方法としては
- swing の UndoManager を使う
- 自分でがんばる
自分で Undo / Redo を実装する場合、まずはどの様に現在の情報を保存しておくかを決めないと...
保存する情報は次の2つ...
- 状態
- 手続 (手順)
これらは DB に例えるなら...
前者は スナップショットをテーブルやファイルに保存しておくようなイメージ。
状態そのものをデータとして保存しておきます。
後者は Redo ログ の様な感じ。
データそのものではなく、元に戻すための手順を保存しておきます。
どちらを使うかは状況次第?
もちろん、組み合わせて使うのもありですが...
今回は後者で実装することに...
なぜ、後者なのかというと JavaFX には
- 関数型の変数
- クロージャ
これらを利用すれば、簡単に元に戻すための手順を保存しておくことができます。
まずは 今回紹介する Undo / Redo を実際に試してみましょう。
では、早速 実装ですが...
今回は undo, redo が可能な Drag and Drop を実装してみましょう。
まずは
- undo, redo の手順を保持するためのクラス Undoable
- Undoable を管理するためのクラス UndoManager
本来であれば Command パターンの方がよいのかも...
でも 今回は undo, redo がメインと言うことで...
次は、以下のように undo, redo の手順を示す関数を保存します。
var umgr = UndoManager { limit: 100 } Draggable { node: node var translateX: Number; var translateY: Number; // Drag 前の位置を保存 onStart: function(e) { translateX = node.translateX; translateY = node.translateY; } // undo / redo の手順を保存 onDone: function(e) { undoables.add(Undoable { // undo の手順を示す関数を生成するブロック undo: { def _translateX = translateX; def _translateY = translateY; function(): Void { node.translateX = _translateX; node.translateY = _translateY; } } // redo の手順を示す関数を生成するブロック redo: { def _translateX = node.translateX; def _translateY = node.translateY; function(): Void { node.translateX = _translateX; node.translateY = _translateY; } } }); } }
Draggable は 『Drag and Drop on JavaFX Part 1』 で紹介したものに onStart と onDone を追加しています。
onStart は Drag が開始される前に実行されるコールバック関数で Drag する前の位置を保存しています。
onDone は Drag が終了した後に実行されるコールバック関数で undo / redo の手順を保存しています。
undo の手順を保存するコードは
undo: { def _translateX = translateX; def _translateY = translateY; function(): Void { node.translateX = _translateX; node.translateY = _translateY; } }たったこれだけですが
- ブロック
- 関数型の変数
- クロージャ
ちなみに、ブロックとは "{" と "}" で囲んだ式の固まりで 全てブロックのスコープ内で評価され、最後の式の結果のみが戻り値として返されます。
これは 以下のように無名関数を評価するのとほぼ等価です。
undo: (function() { def _translateX = translateX; def _translateY = translateY; function(): Void { node.translateX = _translateX; node.translateY = _translateY; } })();今回の場合、関数オブジェクトがブロックから返され、関数型の変数 undo にセットされます。
ここで大事なポイントですが、関数内で利用したい値は ブロック内の変数に格納しておくこと。
と言うのも、関数が評価されるタイミングは 関数の宣言時ではなく UndoManager#undo() がコールされたときだからです。 ちなみにこういうヤツを巷ではクロージャと呼んでいます。
もし、以下のように関数の中で 外部からアクセス可能な変数を直接参照してしまったら、関数が評価されるころには その変数の内容は すでに期待しているものではないでしょう。
undo: function(): Void { node.translateX = translateX; node.translateY = translateY; }
後は 実際に undo, redo するためのボタンを用意するだけです。
以下のようにとっても簡単です。
undoable, redoable はそれぞれ undo, redo できるかどうかを表す変数で、これを Button#disable に bind しておけば
実際に undo, redo できる時だけボタンを有効にできるのです。なんて楽なんだろう。
undo(), redo() 関数は UndoManager に保存してある 手順を元に undo, redo します。
Button { text: "Undo" disable: bind not umgr.undoable action: umgr.undo } Button { text: "Redo" disable: bind not umgr.redoable action: umgr.redo }
どうでしょうか?
JavaFX なら とっても簡単に Undo / Redo が実現できることがわかっていただけたのではないでしょうか?