概要
- JXUGC #9 Xamarin.Forms Mvvm 実装方法 Teachathon を開催しました - Xamarin 日本語情報
- RxJava + MVVM パターンで作るストップウォッチアプリ
この2記事に触発され、私もJavaFXの練習がてら、ストップウォッチアプリを作成してみることにしました。もっとも、2つ目の記事は1つ目の記事に触発されて書かれたものですので、私の記事は2番煎じということになりますが……。
実装の方針
- Java+JavaFXを利用してUI設計およびロジックを実装する
- JavaFXはデフォルトではMVCパターンを意識したファイル名になっているが、あえてMVVMパターンによる開発を目指す
- 普段はC#+WPF+MVVM+ReactivePropertyで開発しているので、言語仕様の違いに気をつけながら開発する
開発ステップ
ファイル名の変更
Javaでは「1ファイル1クラス」といった原則がありますので、*.java
のファイル名を決めることはクラス名を決めることに直結します。前述したようにMVVMパターンで実装するので、
-
Sample.fxml
からMainView.fxml
-
Controller.java
からMainViewModel.java
-
MainModel.java
も新規作成
するようにしました。
UI設計
C#+WPFではVisual Studio上でXAMLデザイナーが使えますので、そこで設計すればよいだけの話でした。それだけに、Xamarin.Formsでプレビューがろくすっぽ効かなかった時はなかなかに苦痛でしたが……。
一方、Java+JavaFXの場合、JavaFX Scene BuilderでUI設計を行います(EclipseもIntelliJもこのツールに対応)。ここで編集して保存すると、IDEが当該FXMLファイルを再読み込みして反映されるわけですね(逆もまた然り)。
XAMLもFXMLも、同じくXMLをベースに生み出された言語です。ただ、それぞれのコンテナ名やコントロール名は大きく異なります。一例を示しますと、
FXML | XAML | 意味 |
---|---|---|
AnchorPane | 対応なし | コンテナいっぱいまで中のコントロールを広げる |
BorderPane | (DockPanelで代用可) | 上・左・中央・右・下のどれかにコントロールを配置 |
FlowPane | WrapPanel | コントロール配置を折り返す機能が付いたコンテナ |
GridPane | Grid | 格子で区切った場所にコントロールを配置 |
HBox,VBox | StackPanel | 縦や横にコントロールを並べて配置 |
TabPane | TabControl | タブ表示の中にコントロールを配置 |
ToolBar | ToolBarPanel | ツールバーを表示する |
DatePicker | Calendar | カレンダーから日付を表示・選択する |
Hyperlink | Link | リンク付きテキストを表示する |
ImageView | Image | 画像を表示する |
MenuBar | Menu | メニューを配置 |
Menu | MenuItem | メニューの各項目(クリックすると子メニューを展開) |
MenuItem | MenuItem | メニューの各項目(クリックすると別のアクションを起こす) |
TextArea | TextBox | 複数行テキスト入力(WPFではプロパティ設定で対応) |
TextField | TextBox | 1行テキスト入力 |
WebView | WebBrowser | Webブラウザだが、FXMLだとWebKitなのでより上等 |
といった具合です。また、コントロールやコンテナのサイズ指定の意味合いがだいぶ異なっており、XAML脳だとなかなかサイズが思い通りに決まらないといったトラブルを招きます。詳しくは、次のWebページを参考にしてください。
- 2 ノードのサイズ指定と位置合せに関するヒント(リリース8)
- JavaFX 2.0でアプリケーション作成(その2) - torutkの日記
- JavaFX 2.0でアプリケーション作成(その3) - torutkの日記
- JavaFX 2.0でアプリケーション作成(その4) - torutkの日記
ちなみに、Scene Builderには、コントロールをポン付けするとなんでもかんでもGridとMarginで配置してしまうクソ仕様なんてありませんので、その辺はご安心くださいw
Modelの設計、およびViewModelによるViewとの接続
前述したように、FXMLはMVVMを意識した設計にはなっていません。そのせいか、デフォルトではコントロールに名前を付けてコードビハインドで直接アクセスしたり、Controllerのメソッドでボタンを押した際のロジックを直接記述するなどといった、WinFormsを彷彿とさせるコーディングになってしまいます。
ただ、都合がいいことに、JavaFXには標準で「StringProperty
などの通知機構付きの型」と「ChangeListener
などの通知機能」と「bindBidirectional
などのData Binding機能」が搭載されています。これらを生かすと、
- Modelに
StringProperty
などのプロパティを配置 - ViewModelでコントロールのインスタンス(つまりView側)を定義し、
bindBidirectional
やsetOnAction
などでModelと紐付け(Data Binding) - プロパティ同士は
ChangeListener
などを使えば変更を通知して連動できる
といった素敵環境が完成します。より楽したいならRxJavaを使うべきなのだろうとは思いましたが、Data Bindingだけなら別に外部ライブラリなしで全然書けるなと思いました。
ちなみにJavaFXにおけるプロパティは、C#におけるプロパティと意味合いが全然違うのでご注意ください。また、Javaの本元(Oracle)は「高レベルバインディングAPI」と「低レベルバインディングAPI」を使おうと勧めてくるのですが、下のコード例を見れば分かるように、ChangeListener一本で書いた方が楽だと思います。
// 本家が勧めてくる「高レベル・バインディングAPI」とやら
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
IntegerProperty num3 = new SimpleIntegerProperty(3);
IntegerProperty num4 = new SimpleIntegerProperty(4);
NumberBinding total =
Bindings.add(num1.multiply(num2),num3.multiply(num4));
System.out.println(total.getValue());
// こう書いた方が可読性が高いのでは??
IntegerProperty num1 = new SimpleIntegerProperty(1);
IntegerProperty num2 = new SimpleIntegerProperty(2);
IntegerProperty num3 = new SimpleIntegerProperty(3);
IntegerProperty num4 = new SimpleIntegerProperty(4);
int total;
Runnable runner = () -> {
total = num1.get() * num2.get() + num3.get() * num4.get();
};
num1.addListener((ob, oldVal, newVal) -> setTotal());
num2.addListener((ob, oldVal, newVal) -> setTotal());
num3.addListener((ob, oldVal, newVal) -> setTotal());
num4.addListener((ob, oldVal, newVal) -> setTotal());
System.out.println(total);
ロジックの実装
ここまで来ると、JavaFXみが薄れてきますね(Modelだけ考えればいいのがMVVMの強み)。先人の反省点に倣い、次の方針でロジックを実装しました。
- タイマーが起動していない状態でスタート/ストップボタンを押すと、押した瞬間の日時(A)を記録する。また、タイマーが起動し、ラップボタンが押せるようになる。この際、ラップ開始の日時Bの初期値をAと定義する(もしくは、Aを取得した直後にBも取得する)
- ラップボタンを押すと、日時Bと押した瞬間の日時(C)からラップタイムを計算し、ラップタイム一覧(D)にラップタイムを追加する。また、リストビューにそのタイムを追記する
- タイマーが起動した状態でスタート/ストップボタンを押すと、ラップタイム追記の他、一覧Dから最速ラップタイムと最遅ラップタイムを算出してダイアログ表示する
- タイマーはUIスレッドとは違うスレッドで動作しており、定期的(10ms間隔など)に計測タイムの更新を行う。ただ、UIスレッド以外が、UIやそれに関連したプロパティ(Data Bindingで紐付けされたものも含む)を弄ると実行時エラーになるので、
Platform.runLater
メソッドで更新処理をUIスレッドに移譲するのが大事
参考:JavaFXでスレッドを使って描画するときの注意 - Qiita
まとめ
Java+JavaFXで、MVVMおよびData Bindingを生かしたアプリ開発をすることができました。ReactivePropertyのようなお助け補助ライブラリがない状況だったので少し面倒でしたが、予想より楽に書き上げることができて正直驚いています。JavaFXの、Scene Builderとプロパティ機能が持つポテンシャルは侮れませんね……!
また、Javaで開発する際に忘れてはいけないのがラムダ式とStream API。前者はC#と同じような書き心地で、後者もLINQほど便利ではないにせよ役立ちました。
ところで、JavaFXのプロパティ機能は、ReactivePropertyと違ってStringProperty
だのIntegerProperty
だのと変数型が決め打ちなものばかりで、Property<T>
型がなぜかinterfaceなのは何故なのでしょう……?