Swingに対するJavaFXの優位点のひとつは、FXMLによってUI(ユーザ・インタフェース)とロジックの分離が可能な点だと言われる。この分離によりプログラマーと画面デザイナーとの分業を行うことができるようになるということだ。また、プログラム・ロジックとデザインとの依存性を下げることもでき、プログラムの可読性もあげることができるはず。
というわけで、今回はFXMLの基本的な利用方法について確認する。
■ サンプル・プログラム1(FXML利用方法の確認)
最初にコードを見ておくとFXMLの利用方法やイメージの理解が早いと思うので、今回はサンプルプログラムから見ていく。サンプルでは「hello world!」を表示するだけのウィンドウを出力する。
◇リソース
TestJavaFX(プロジェクト)
┗ src
┗ application
┣ TestFXML1.java
┗ TestFXML1.fxml
◇サンプルコード
TestFXML1.java
package application;
import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
/**
* FXMLファイルを利用する1
*
* @author karura
*
*/
public class TestFXML1 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
// フォント色がおかしくなることへの対処
System.setProperty( "prism.lcdtext" , "false" );
// FXMLファイルの読込
URL location = getClass().getResource( "TestFXML1.fxml" );
FXMLLoader fxmlLoader = new FXMLLoader( location );
// シーングラフの作成
Pane root = (Pane) fxmlLoader.load();
// シーンの作成
Scene scene = new Scene( root , 200 , 50 );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
}
TestFXML1.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<VBox xmlns:fx="http://javafx.com/fxml/1">
<Label text="hello world!" />
</VBox>
◇実行結果
◇解説
HTMLファイルがブラウザから呼ばれて動作するように、FXMLファイルはJavaFXアプリケーションから呼ばれて初めて動作する。FXMLをJavaFXアプリケーション上に取り込むにはFXMLLoaderクラスを用いる(30行目~34行目)。
FXMLLoaderクラスは、FXMLファイル内のタグに対応するJavaFXインスタンス(シーングラフ)を生成するためのクラスで、load関数実行時に生成したシーングラフのルート・ノードが返される。上記のFXMLファイルであれば、以下のJavaコードを記述したのと同じこととなる。
VBox root = new VBox();
Label l = new Label();
l.setText( "hello world!" );
root.getChildren().add( l );
return root;
以降の処理はFXMLを使わない場合同様で、作成されたシーングラフをシーンに追加すれば画面に表示される(37行目~41行目)。FXMLの記述方法については、次で確認していく。
■ FXMLファイルの記述方法
上記のサンプル・プログラムを見ても分けるように、FXMLファイルにはJavaFXアプリケーションで利用する画面構成(シーングラフの構成)を記述する。FXMLのファイル構造は基本的にXMLに準じており、表1のように3つの部分に分類できる。
表1:FXMLの基本構成
<?xml version="1.0" encoding="UTF-8"?> ← ①エンコーディング指定
<?import javafx.scene.layout.VBox?> ← ②使用するタグのインポート宣言
<?import javafx.scene.control.Label?>
<VBox xmlns:fx="http://javafx.com/fxml/1"> ← ③画面構成。ルートタグの内部に記述する
<Label text="hello world!" />
</VBox>
FXMLは要素(タグ)と属性で記述する。要素と属性の例を表2に示す。それぞれ分けて確認していく。
表2:要素と属性の例。要素はオレンジ色、属性は黄緑色の部分
<Label text="hello world!" ></Label>
要素(タグ)
Oracle公式サイト(
*1)を見てもFXMLタグ一覧は存在しないようだが、使えるタグは以下の2種類に分類される。ちなみに以下は筆者が勝手に命名した分類であり、公式な名前はOracle公式サイト内のFXMLの概要(
*2)を確認していただきたい。
- クラスタグ ・・・Javaのクラスを表すタグ(大文字で始まる)
- プロパティタグ・・・1のJavaクラスが持つプロパティを表すタグ(小文字で始まる)
- 制御タグ ・・・「<?○○>」や名前空間「<fx:○○>」という名前のFXML制御用タグ
1のクラスタグは、例えば「Label」「VBox」等のクラス名のタグである。利用の際には「<?import ○○>」タグによる事前インポートが必要となる。2のプロパティタグはJavaクラスのsetter/getterで設定できるクラス・プロパティ名のタグのことで、Labelクラスのプロパティ「text」等である。プロパティタグは対応するクラスタグの子として記述する。プロパティタグの名前は、基本的にプロパティ名そのものだが、例外としてstaticなsetter関数で設定するプロパティは「<(クラス名).(プロパティ名)>」という名称になる(例:<GridPane.rowIndex>)。ちなみにプロパティ・タグは、後で確認する属性としても記述できる。表1をできる限りタグで記述すると以下のようになる。
表3:表1のコードを可能な限りタグで記述した例
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<VBox xmlns:fx="http://javafx.com/fxml/1">
<Label>
<text>hello world!</text>
</Label>
</VBox>
3の制御タグとは表4のタグである。これらを利用する場合には名前空間の宣言が必要で、画面構成用のルートタグに「xmlns:fx="http://javafx.com/fxml/1"」という記述を追加する必要がある。
表4:制御タグ一覧
タグ名 |
内容 |
?xml |
FXMLファイルのエンコーディング指定(XMLのタグでもある)。FXML先頭に必須
例)<?xml version="1.0" encoding="UTF-8"?> |
?import |
使用するクラスタグのインポート(XMLのタグでもある)
例)<?import ○○?> |
?language |
FXMLでスクリプトを記述する際の言語名を指定
例)<?language ○○?> |
fx:include |
別FXMLファイルの記述を、該当部分に追加します。
例1)<fx:include fx:id="△△" source="○○.fxml"/>
例2)<fx:include source="○○.fxml" resources="リソース・バンドル" charset="utf-8"/> |
fx:constant |
Javaのクラス定数を表す
例)<Double fx:constant="NEGATIVE_INFINITY"/> |
fx:reference |
fx:id属性が記述されたタグや変数への参照を表す
例)<fx:reference source="○○"/> |
fx:copy |
fx:id属性が記述されたタグや変数の複製を作成する。利用するにはコピー用のコンストラクタを定義する必要があるが、今のところ実装されていない。つまり、カスタム・コンポーネント等でコピー・コンストラクタで定義しない限り使用できない模様
例)<fx:copy source="○○"/> |
fx:root |
type属性で基底クラスを指定し、基底クラスを継承したクラスをルートノードに設定する。FXML内に1つのみ記述可能
例)<fx:root type="○○">~</fx:root> |
fx:script |
langurageタグで指定した言語でスクリプトを記述する
例)<?language javascript?><fx:script>var i = 10;</fx:script> |
fx:define |
シーングラフには組み込まれないものを宣言するためのタグ。
例)<fx:define> <ToggleGroup fx:id="○○"/> </fx:define> |
属性
属性とはタグ内部に記述する値で、以下の2種類の記述に分類できる。
- プロパティ
- イベント・ハンドラ
1については、前述のプロパティタグをタグ内部に記述したものである。また、特別な属性として「fx:」が頭につくものがあり、主ななものを以下表5に示す。
表5:fx名前空間の主な属性
属性名 |
内容 |
fx:id |
要素にID(名前)をつける |
fx:value |
要素がvalueOf関数を持つ場合のみ利用可能 |
fx:controller |
FXMLファイルに対応するコントローラを指定する。
1つのFMMLファイルに1つだけ指定可能 |
2のイベント・ハンドラ記述は、Javaのイベント・ハンドラの仕組みをFXMLファイルで利用するための記述である。後述の方法によってFXMLとJavaFXオブジェクトを関連付けることにより、イベント・ハンドラ記述で指定した関数をイベント・ハンドラとして使用できるようになる。イベント・ハンドラ記述の一覧は表6のとおりである。実装例はサンプル・プログラム2を参照のこと。
表6:イベント・ハンドラ記述一覧
イベント・
ハンドラ記述 |
内容 |
onAction |
イベント発生時に起動する関数名やスクリプトを記述する。
例)<Button text="押してね" onAction="#○○"/>
→イベントハンドラ:public void ○○(ActionEvent e) {...} |
onChange |
コレクションの変更を監視。コレクションの種類によりイベントハンドラの引数が以下のように決まっている。
コレクション |
イベントハンドラの引数 |
ObservableList |
ListChangeListner.Change |
ObservableMap |
MapChangeListener.Change |
ObservableSet |
SetChangeListener.Change |
例)<children onChange="#○○">
→イベントハンドラ:public void ○○( ListChangeListner.Change c){...} |
on○○Change |
Javaクラスのプロパティ値の変更を監視する場合に利用する…らしい。Oracle公式サイト等を読んでもよく分からなかった。分かり次第加筆する。
例)<VBox fx:controller="××" on○○Change="#△△"/>
→イベントハンドラ:public void △△(ObservableValue value, Parent oldValue, Parent newValue) {...} |
また、一部の属性値指定では接頭辞が必要となる。接頭辞の一覧は以下の表7の通り。この接頭辞記述はプロパティ記述およびイベント・ハンドラ記述のどちらにも当てはまる。
表7:接頭辞の一覧
接頭辞 |
対象の属性値 |
@ |
URL。URLはURLエンコーディングする必要がある
例)<Image url="@my_image.png"/> |
% |
リソース・バンドル利用時のリソース名
例)<Label text="%myText"/> |
$ |
変数(fx:idの値)参照
例)<RadioButton text="選択肢1" toggleGroup="$myToggleGroup"/> |
${○○} |
式バインディング。この中では変数以外に表8の演算子が利用可能
例)
<TextField fx:id="text1" text="入力してください"/>
<Label text="${text1.text}"/> |
\ |
接頭辞のエスケープ・シーケンス
例)<Label text="\$10"/> |
# |
イベント・ハンドラ
例)<Button text="押してね" onAction="#onButtonChange"/> |
表8:式バインディングとして記述可能なリテラル・演算子
種類 |
記述方法 |
文字列定数 |
"文字です"
'文字です' |
ブール値 |
true
false |
null値 |
null |
数値 |
-42
50.0
3e5 ( = 3 * 10^5 ) |
ブール演算子 |
! :否定
&& :論理積
|| :論理和 |
四則演算子 |
+ :加算
- :減算
* :乗算
/ :除算
% :剰余(余り) |
比較演算子
(Comparable型の比較) |
> :大なり
>= :大なりイコール
< :小なり
<= :小なりイコール
== :等しい
!= :等しくない |
■ コントローラの利用
FXMLファイルにコントローラと呼ばれるJavaクラスを指定すると、fx:id属性を指定したオブジェクトをコントローラ内の同名プロパティにマッピングすることができる。マッピングすると、Javaプログラム上でインスタンス化された変数と同様の操作が行える。コントローラの指定はFXMLファイルのルートノードの「fx:controller」属性にクラス名を指定すればよい。イベント・ハンドラも同様に、onActionなどのイベント・ハンドラ記述で指定した関数がマッピングされる。このとき、マッピングされるプロパティ・関数には@FXMLアノテーションをつける必要がある。マッピングでは、FXMLとコントローラのどちらか一方にしか存在しないようなオブジェクトがあってもエラーは発生しない。
表9:FXML⇔コントローラのマッピングイメージ。赤字がコントローラの指定・定義。黄緑時がオブジェクトのマッピング。オレンジ色がイベントハンドラのマッピング。
【sample.fxml】
~略
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.SampleFXMLController">
<Button fx:id="button1" text="push!" onAction="#onPushButton1" />
</VBox>
~略
↓ マッピング
【SampleFXMLController.java】
public class SampleFXMLController
{
@FXML
private Button button1;
@FXML
public void onPushButton1( ActionEvent e )
{...}
}
また、コントローラ内にinitialize()メソッドを定義すると、FXMLファイルのロードが完了した時に1度だけ呼び出されるようになる。
FXMLファイル内でfx:includeを利用し別のFXMLを読み込んだ場合でも、同時にfx:id属性を指定するとマッピングが行われる。この場合、指定したfx:idと同名のプロパティに読み込み先のルートノードがマッピングされ、読み込み先にコントローラが設定されている場合は「(fx:id)Controller」という名前のオブジェクトに読み込み先のコントローラがマッピングされる。実装例はサンプル・プログラム2を参照のこと。
■ サンプル・プログラム2(FXMLの要素技術確認)
FXML要素技術の動作を確認するサンプルプログラムを以下に示す。少し長くなってしまったが、サンプルの動作概要は以下の通りである。
- ウィンドウ表示をするためのクラス(TestFXML2.java)にてFXMLLoader::load関数を使ってTestFXML2.fxmlを読み込み画面表示している
- TestFXML2.fxml内部ではfx:includeタグでTestFXML3.fxmlを読み込んでいるため、TestFXML3.fxmlの内容も画面表示される
- TestFXML2.fxml・TestFXML3.fxmlのコントローラとして、それぞれTestFXML2ControllerがTestFXML3Controllerが設定されているため、FXML読み込みと同時にコントローラへのマッピングが行われることになる
◇リソース
TestJavaFX(プロジェクト)
┣ src
┃ ┗ application
┃ ┣ TestFXML2.java
┃ ┣ TestFXML2Controller.java
┃ ┣ TestFXML3Controller.java
┃ ┣ TestFXML2.fxml
┃ ┗ TestFXML3.fxml
┗ img
┗ chara_one.png
◇サンプル・コード
【TestFXML2.java】
package application;
import java.net.URL;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
* FXMLファイルを利用する2
*
* @author karura
*
*/
public class TestFXML2 extends Application
{
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception
{
// フォント色がおかしくなることへの対処
System.setProperty( "prism.lcdtext" , "false" );
// FXMLファイルの読込
URL location = getClass().getResource( "TestFXML2.fxml" );
FXMLLoader fxmlLoader = new FXMLLoader( location );
// シーングラフの作成
fxmlLoader.setRoot( new VBox() );
Pane root = (Pane) fxmlLoader.load();
// シーンの作成
Scene scene = new Scene( root , 500 , 300 );
// ウィンドウ表示
primaryStage.setScene( scene );
primaryStage.show();
}
}
【TestFXML2.fxml】
<?xml version="1.0" encoding="UTF-8"?>
<?language javascript?>
<?import java.lang.Integer?>
<?import java.lang.String?>
<?import javafx.scene.image.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.beans.property.SimpleStringProperty?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.TestFXML2Controller">
<!-- 変数宣言 -->
<fx:define>
<Integer fx:id="maxint" fx:constant="MAX_VALUE" />
</fx:define>
<children>
<!-- 外部ファイルの取込 -->
<fx:include fx:id="fxml3" source="TestFXML3.fxml" />
<Button fx:id="button0" onAction="#onPushButton0" text="confirm FXML1 info!" />
<!-- 変数参照の利用例 -->
<Label text="$button0.text"/>
<!-- カウンタ表示(イベント・ハンドラの利用例) -->
<HBox>
<!-- fx:referenceの利用例 -->
<Label fx:id="label1" text="First count is :" />
<Label fx:id="label2">
<text><fx:reference source="maxint" /></text>
</Label>
</HBox>
<HBox>
<!-- バインディングの利用例 -->
<Label fx:id="label3" text="Current count is :" />
<Label fx:id="label4" text="${controller.cnt}"/>
</HBox>
<Button fx:id="button1" onAction="#onPushButton1" text="count down!" />
<!-- バインディング式・演算子の利用例 -->
<HBox>
<Label text="( 1 + 1 ) * 3 / 2 ="/>
<Label text="${( 1 + 1 ) * 3 / 2 }"/>
</HBox>
<!-- 画像表示 -->
<ImageView>
<image>
<!-- FXMLファイルからの相対パスを指定することに注意 -->
<Image url="@../../img/chara_one.png"/>
</image>
</ImageView>
<ImageView>
<image>
<!-- もちろんweb上のファイルも指定可能 -->
<Image fx:id="img2" url="@http://blog.cnobi.jp/v1/blog/user/81fa4a2366161d008bd6cd49023192f1/1443964424"/>
</image>
</ImageView>
<!-- onChangeの使い方 -->
<HBox fx:id="changeBox">
<children onChange="#onChildChange"/>
</HBox>
<Button fx:id="button2" onAction="#onPushButton2" text="invoke onChange function!" />
</children>
</fx:root>
【TestFXML3.fxml】
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Label?>
<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.TestFXML3Controller">
<Label text="hello world!" />
</VBox>
【TestFXML2Controller.java】
package application;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
/**
* TestFXML2.fxmlのコントローラ
*
* @author karura
*
*/
public class TestFXML2Controller implements Initializable
{
/*
* FXML内で宣言されるオブジェクト
* コントローラ側にはJavaから操作するオブジェクトだけが
* 宣言されていればよい
*/
@FXML
private Label label4;
@FXML
private Button button1;
@FXML
private Integer maxint;
@FXML
private HBox changeBox;
/*
* バインディング変数と
* バインディング利用時に必要となる関数
*/
@FXML
private IntegerProperty cnt = new SimpleIntegerProperty();
public int getCnt(){ return cnt.get(); }
public void setCnt( int cnt ){ this.cnt.set( cnt ); }
public IntegerProperty cntProperty(){ return cnt; }
/*
* TestFXML1.fxmlのインポートに対応するオブジェクト宣言
*/
@FXML
private VBox fxml3;
@FXML
private TestFXML3Controller fxml3Controller;
/**
* 初期化関数
*/
@Override
public void initialize(URL url, ResourceBundle rb)
{
// 初期化開始メッセージ
System.out.println("initialise TestFXML2Controller.");
// カウンタの値を初期化
cnt.set( maxint );
}
/**
* Button0を押した場合のイベントハンドラ
* (fx:includeしたFXMLオブジェクトの操作)
* @param e
*/
@FXML
public void onPushButton0( ActionEvent e )
{
// includeしたFXMLファイルの情報を出力
Label l = (Label)(fxml3.getChildren().get(0));
System.out.println( l.getText() );
// includeしたFXML付属のコントローラの関数を呼出
fxml3Controller.initialize( null , null );
}
/**
* Button1を押した場合のイベントハンドラ
* (バインディングの利用方法確認)
* @param e
*/
@FXML
public void onPushButton1( ActionEvent e )
{
// カウントダウンしてラベル表示
cnt.set( cnt.get() - 1 );
// 標準出力
System.out.println( String.format( "button is pushed!(%d)" , cnt.get() ));
}
/**
* Button2を押した場合のイベントハンドラ
* (onChangeイベントを発生させる)
* @param e
*/
@FXML
public void onPushButton2( ActionEvent e )
{
changeBox.getChildren().add( new Label("0") );
}
/**
* changeBoxのonChangeイベントハンドラ
* @param c
*/
@FXML
public void onChildChange( ListChangeListener.Change c )
{
System.out.println( "children change!" );
}
/**
* StringのonChangeイベントハンドラ
* @param c
*/
public void onStringChange( ObservableValue value, Parent oldValue, Parent newValue )
{
System.out.println( "string change!" );
}
}
【TestFXML3Controller.java】
package application;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.Initializable;
/**
* TestFXML3.fxmlのコントローラ
*
* @author karura
*
*/
public class TestFXML3Controller implements Initializable
{
/**
* 初期化関数
*/
@Override
public void initialize(URL url, ResourceBundle rb)
{
System.out.println("initialise TestFXML3Controller.");
}
}
◇実行結果
initialise TestFXML3Controller.
initialise TestFXML2Controller.
◇解説
細かい箇所はコードを読んでいただきたいが、サンプルの動作概要として記述した内容を解説する。まずはfx:includeタグから。
fx:includeタグ(TestFXML2.fxml:20行目)により、TestFXML3.fxmlがfx:id=fxml3として読み込まれている。これにより該当箇所に「hello world!」というTestFXML3.fxmlの内容が画面出力されているのが確認できる。また、読み込み完了と同時にTestFXML3.fxmlのコントローラ(TestFXML3Controller)がインスタンス化され、マッピングが実行され、その後initialize関数(TestFXML3Controller.java:20行目)が呼び出されたため、標準出力に「initialise TestFXML3Controller.」が出力されているのが確認できる
次にコントローラのネストに関して確認する。TestFXML2.fxmlの読み込みが完了した時点で「initialise TestFXML2Controller.」という標準出力が行われるため、コントローラはTestFXML3Controller→TestFXML2Controllerの順番で初期化されたことが確認できる。TestFXML3の内容がTestFXML2Controllerにマッピングされているかは、「confirm FXML1 info!」ボタン(TestFXML2.fxml)を押すことで確認できる。このボタンを押下すると、イベント・ハンドラTestFXML2Controller::onPushButton0が起動し、マッピングされたTestFXML3の情報を標準出力に出力していることが確認できる(TestFXML2Controller.java:81行目~90行目)。
表10:「confirm FXML1 info!」ボタン押下時の標準出力の内容
hello world!
initialise TestFXML3Controller.
■ 参照
- Oracle Java SE 公式サイト
- Oracle Java SE 公式サイト「FXMLの概要」
- Stack OverFlow「Binding a Label's text property (in an FXML file) to an IntegerProperty (in a controller)」