JavaFX ComposerとState
昨年の末の事ですが、NetBeansのプラグインとして、JavaFX Composerのプレビュー版が公開されました。
現在、2月に入ってPreview2がリリースされています。それでも、あくまでプレビュー版であり、ベータ版ですらありませんが、一言で言うならばNetBeansのSwingエディタと似たフィーチャのJavaFX版といった所です。
また、JavaFX Composerでは主にビジネスアプリケーションの構築を目標としている為、位置付けとしてはFlex Builder(Flash Builder)に近いプロダクトになります。
JavaFX Composerとは?
JavaFX Composerのフィーチャとしては以下のものがあげられています(http://wiki.netbeans.org/JavaFXComposer)。
- ビジュアルエディタ
- アニメーションエディタ
- ステートのサポート
- Webサービス・データベース・ローカルストレージ等へのデータアクセスサポート
- バインディングのサポート
- マルチスクリーンサイズのサポート
今回は、あまり聞き覚えのないと思われるステートについて解説してみます。
ステートと画面遷移
JavaFX Composerにおいて「ステート」とは、画面遷移での1つの状態を表します。ですが、画面の単位と考えてしまうのは早計です。
例えば、あるビジネスアプリケーションで、あるデータの更新画面、更新確認画面、更新完了画面があるとします。Webアプリケーションであれば、これらは個別の画面として設計され、個別のHTMLとして実装される事になるでしょう。しかし、各画面には共通して表示される項目は多く、ベタに実装すると非常に効率の悪い重複したコードになると思います。従来の考え方では、必要に応じて共通コンポーネントを抽出し・・・というアプローチで画面設計から実装コードを作成していったわけです。
一方、画面の状態を意識して設計するならば、更新画面の「入力状態」「確認状態」「完了状態」という考え方になります。最初に更新画面という大枠(ベース)を設計し、各状態でどこがどのように変化するかを考えていきます。例えば、入力状態の時、この項目が入力コンポーネントに変化し、メッセージがどう変化し・・・といったイメージです。考え方としては状態を意識する方が自然なのですが、実装がHTMLである場合に、どうしても画面単位と考えてしまうわけです。JavaFX Composerを使う事で、自然に状態を考慮して設計された画面の遷移を実装することができます。
尚、従来のWebアプリケーションであっても状態に着目して画面を設計していくことはできます。しかし、実装するとなるとどうしてもAjaxを過剰に使ったJavaScript地獄が待っているでしょう。
ところで、プログラミングの世界でステートと言えば、デザインパターンでいうところの「ステートパターン」が思いつくかと思います。JavaFX ComposerのステートはJavaFX Scriptを使ったステートパターンで実装されています。
ちなみに、Flexでも同じような機能としてステートと呼ばれる機能があります。
ステートを試してみよう
チュートリアルを元にJaaFX Composerでステートを試してみます。目標としては2つのステートを定義し、ステートの変更によりメッセージ内容を変化させていきます。
参考:http://wiki.netbeans.org/JavaFXComposerStatesTutorial2
JavaFX Composerを導入した上で、プロジェクト(JavaFX Desktop Business Application)を作成してください。
すると、幾つかのファイルが生成されますが、Main.fxがアプリケーションのソースファイルになります。また、NetBeansのSwingエディタと同じように、デザインビューとソースビューを切り替えることが可能です。デザインビューを開くとStatesという項目があり、初期状態では
Statesの横にある+をクリックしてステートを追加します。
Red State と Blue Stateの2つのステートを追加しました。ここではRed StateをStart Stateとして設定しています。Blue StateはRed Stateを継承して作成していますが、現時点ではRedでなにも定義していないので意識しなくて良いと思います。これでRedとBlueの2つのステートが定義されました。
次にパレットからLabelをドラック&ドロップして画面に配置します。この時に注意して欲しい事は、Statesで
続けてプロパティのTextを「Master State なう」に変更します。ここでStatesをRed StateとBlue State等に切り替えても、「Master State なう」のままです。
次にRed Stateに切り替えてからLabelを選択し、Textを「Red State なう」に変更します。続けてBlue Stateに切り替えると、Labelは「Master State なう」に戻りますが、同じように「Blue State なう」に変更します。
この時点で、Statesを切り替えるとメッセージが変更されることを確認してください。尚、実行すると最初のステートとして設定したRed Stateとなり「Red State なう」が表示されます。
ステートの変更イベントを作る
続けてステートを変更するためのイベントを作りましょう。
続けてActionプロパティを編集します。「Generate:Go to next state」という項目があるのでこれを選択します。
すると自動的に関数が埋め込まれ、以下のようなコードが生成されるはずです。
function buttonAction(): Void { currentState.next(); }
Swingなどで自前でStateを管理したことがある人であれば、なんとなく裏側でなにが行われようとしているのかが解るでしょう。
では、実行してからボタンをクリックしてみてください。「Red State なう」から「Blue State なう」にメッセージが変化します。
なにが行われているのか?
ソースコードを読み解いてみます。まず、NetBeansで自動生成されているMain.fxのコードを見ていくと、initブロックに以下のような記述が見えてきます。
// <editor-fold defaultstate="collapsed" desc="Generated Init Block"> init { label = javafx.scene.control.Label { layoutX: 89.0 layoutY: 73.0 text: "Master State \u306A\u3046" }; button = javafx.scene.control.Button { layoutX: 89.0 layoutY: 107.0 text: "Next" action: buttonAction };
ここはコンポーネントの初期化を行っている箇所です。TextにはMaster Stateとなっていることを考えれば、masterステートで初期化されていることが伺えます。
続けて着目するのは、currentStateの生成です。
currentState = org.netbeans.javafx.design.DesignState { names: [ "Red State", "Blue State", ] stateChangeType: org.netbeans.javafx.design.DesignStateChangeType.PAUSE_AND_PLAY_FROM_START actual: 0 createTimeline: function (actual) { if (actual == 0) { javafx.animation.Timeline { keyFrames: [ javafx.animation.KeyFrame { time: 0.001ms action: function() { label.text = "Red State \u306A\u3046"; } } ] } } else if (actual == 1) { javafx.animation.Timeline { keyFrames: [ javafx.animation.KeyFrame { time: 0.001ms action: function() { label.text = "Blue State \u306A\u3046"; } } ] } } else { null } }
ここがステートの胆となっている部分です。DesignStateはプロジェクトを作成した時に生成されるクラスです。将来的にはJavaFXのAPIに組み込まれるか、もしくはライブラリとして提供されると思われます。
ここではそのDesignStateのインスタンスを生成しているのですが、createTimeline: で各状態を判断し、Timelineを生成している所がポイントです。ここで定義されているのはfunctionで、戻り値はTimelineのインスタンスです(JavaFXは最後に評価した値が暗黙に戻り値となる)。
actualは遷移するステートのインデックスです。0はRed, 1はBlueに相当します。なので、各Timelineでは、そのステートで変更されるプロパティを更新するイベント(Timeline)オブジェクトを生成しているわけです。インデックスの対応に関しては、
names: [ "Red State", "Blue State", ]
この行が自動生成されているところからも読み取れます。
尚、JavaFXでは(Javaを使わない限り)ハッシュが利用できません(恐らくはパフォーマンスとマルチスレッドとのトレードオフ)。したがって、なるべくシーケンス(これはスレッドセーフな配列)を利用します。なのでインデックスを使用していると思われます。
ここだけを読んでみると、どうもstatesを定義していて、変更されるタイミングで実行されるTimelineがあり、その中でコンポーネントのプロパティが変更されるようです。では、どのようにこのTimelineが利用されているかを調べるため、org.netbeans.javafx.design.DesignStateを覗いてみます。
まず確認するのは、next関数。これはボタンクリック時に実行される関数です。
public function next () { if (actual < sizeof names - 1) { actual = actual + 1; } }
この関数ではactual の値を増加させているだけです。actualはが現在のステートのインデックスです。
ところが、actualを変えただけで、変更を通知するようなコードがありません。次に見るべき場所は、actual の on replaceだと直ぐに気づいた人は、JavaFXに慣れている人でしょう(たぶん、変態です:p)。JavaFX Scriptの良さは、変更するという処理と変更されたらという処理が綺麗に分離されており、簡単に結びつけられることだと思います。
案の定、こんなコードを見つけられます。
public var actual = -1 on replace old { if (stateChangeType == DesignStateChangeType.PAUSE_AND_PLAY_FROM_START) { actualTimeline.stop (); actualTimeline = createTimeline (actual); actualTimeline.playFromStart (); } else if (stateChangeType == DesignStateChangeType.FINISH_AND_PLAY_FROM_START) { actualTimeline.time = actualTimeline.totalDuration; actualTimeline = createTimeline (actual); actualTimeline.playFromStart (); } else if (stateChangeType == DesignStateChangeType.CONTINUE_AND_PLAY) { actualTimeline.play (); } else if (stateChangeType == DesignStateChangeType.DO_NOTHING) { } onActualStateChanged (old, actual); }
stateChangeTypeはどのように状態を変えるかという話です。デフォルトではPAUSE_AND_PLAY_FROM_STARTに設定されているので、既存のTimelineは止めて新しいTimleneを開始します。コードを見ると、既存のtimelineは止め、新しくTimelineを生成し、playするという流れになっているのが解ります。そして最後に、onActualStateChangedという状態が変更された事を通知するイベントが発火されています。
public var onActualStateChanged: function (oldState: Integer,newState: Integer): Void;
こちらは未定義です。つまりDesignStateのインスタンスに設定すれば、簡単にイベントのフックが可能ということですね!
ところで、なんで actual の on replaceで各コンポーネントの状態を即時に反映せずに、わざわざTimelineを使っているのでしょう?それは、アニメーションを簡単にさせるためなのですが、それに関してはまた後日。
ステートをループさせるには?
nextはステートが次に進みますが、次のステートがない場合にはステートが変化しません。ステートをループさせ、Red - Blue - Red - Blueという形にしたい場合には、nextではなくnextWrappedを使います。
function buttonAction(): Void { currentState.nextWrapped(); }