こんにちは。SI部の吉原です。

以前、Swingに取って代わりJAVA標準GUIとなったJavaFX 2.0について記事を書きました。
前回はSwingとJavaFXの違いを見るために、JavaFXプログラミングによる画面描画の記事でしたので
今回はJavaFXの一番の目玉とも言えるFXMLについて書こうと思います。

FXMLとは

まず始めにFXMLについて簡単に説明します。
FXMLはJavaFXに用意されているXMLベースのGUI記述言語です。
「言語」と言っても、XMLベースのマークアップ言語ですので、Javaコードより記述は非常に簡単で分かり易くなっています。

利用するコンテナやコントロールの内容をXMLベースで記述するだけでGUIがデザインでき、
FXMLで定義した情報をJava側にて読み込むコードを記述するだけで画面描画が可能となります。
要するにプログラム内でシーン(Scene)グラフを構築するのではなく、FXMLファイル内で構築できるのです。

FXMLを使用すれば、あらゆるユーザー・インタフェースを作成できますが、FXMLは、大規模で複雑なシーン・グラフ、フォーム、データ・エントリまたは複雑なアニメーションが備わったユーザー・インタフェースを作成する際に特に役立ちます。
また、FXMLにスクリプトを含めることで動的なレイアウト作成も可能となっているのです。

FXMLの使い方

それでは簡単な画面とイベントを使って、FXMLの使い方を説明しようと思います。
JavaFXプログラムとの比較をするため、作成する画面は前回と同じ親画面と子画面の2つとします。

親画面

FX_Window01

子画面(モーダルダイアログ)

FX_dialog01

まずは、比較用としてJavaFXプログラミングでの画面描画部分のコードを記載します。


JavaFX ウィンドウ

    BorderPane bordPane = new BorderPane();
    bordPane.setPrefSize(300, 100);

    /** メニューバー */
    MenuBar menuBar = new MenuBar();
    menuBar.setUseSystemMenuBar(true);
    menuBar.setPrefSize(300, 25);

    Menu menu1 = new Menu("ファイル(F)");
    MenuItem menuItem1 = new MenuItem("終了");
    menu1.getItems().add(menuItem1);

    Menu menu2 = new Menu("ヘルプ(H)");
    MenuItem menuItem2 = new MenuItem("作成情報");
    menu2.getItems().add(menuItem2);

    menuBar.getMenus().add(menu1);
    menuBar.getMenus().add(menu2);

    /** ボディ部 */
    AnchorPane anchorPane = new AnchorPane();
    anchorPane.setPrefSize(300, 100);

    // メッセージ
    Label msgLabel = new Label("好きな文字を入力してください");
    AnchorPane.setTopAnchor( msgLabel, 12.0);
    AnchorPane.setLeftAnchor(msgLabel,  8.0);
    anchorPane.getChildren().add(msgLabel);

    // inputテキスト
    Label inpLabel = new Label("Input");
    AnchorPane.setTopAnchor( inpLabel, 45.0);
    AnchorPane.setLeftAnchor(inpLabel,  8.0);
    anchorPane.getChildren().add(inpLabel);

    TextField txtField = new TextField();
    AnchorPane.setTopAnchor( txtField, 45.0);
    AnchorPane.setLeftAnchor(txtField, 40.0);
    anchorPane.getChildren().add(txtField);

    // 表示ボタン
    Button dispBtn = new Button("表示");
    AnchorPane.setTopAnchor( dispBtn, 85.0);
    AnchorPane.setLeftAnchor(dispBtn, 10.0);
    anchorPane.getChildren().add(dispBtn);

    /** アクションイベント設定 */
    menuItem1.setOnAction( e -> System.exit(0) );
    menuItem2.setOnAction( e -> dispModalDialog(primaryStage, "作成情報", "Version:1.0.0", "Date:2015-07-01"));
    dispBtn.setOnAction( e -> dispModalDialog(primaryStage, "Text", txtField.getText()));

    /** 画面表示設定 */
    bordPane.setTop(menuBar);
    bordPane.setLeft(anchorPane);

次にFXMLによる、親画面の画面描画部分を記載します。

FXML ウィンドウ

<!-- ウィンドウ全体 -->
<BorderPane xmlns:fx="http://javafx.com/fxml"
            fx:controller="fxml.view.FxmlWindowControl"
            prefWidth="300.0" prefHeight="200.0">

    <!-- メニューバー -->
    <top>
    <MenuBar>
        <menus>
            <Menu text="ファイル">
                <MenuItem text="終了" onAction="#close" />
            </Menu>
            <Menu text="ヘルプ">
                <MenuItem text="作成情報" onAction="#dispVersionInfo" />
            </Menu>
        </menus>
    </MenuBar>
    </top>

    <!-- ボディ部 -->
    <center>
    <AnchorPane prefWidth="300.0" prefHeight="100.0">
        <children>
           <Label text="好きな文字を入力してください"
                   AnchorPane.topAnchor="12.0"
                   AnchorPane.leftAnchor="8.0" />
           <Label text="Input"
                   AnchorPane.topAnchor="45.0"
                   AnchorPane.leftAnchor="8.0" />
           <TextField fx:id="txtField"
                   prefWidth="100.0"
                   AnchorPane.topAnchor="45.0"
                   AnchorPane.leftAnchor="40.0" />
           <Button onAction="#dispInputInfo"
                   text="表示" prefWidth="45.0"
                   AnchorPane.topAnchor="85.0"
                   AnchorPane.leftAnchor="10.0" />
        </children>
    </AnchorPane>
    </center>
</BorderPane>

FXMLの記述内容を簡単に説明しますと、
FXMLはXMLと同様にタグ内へ prefWidth や prefHeight など様々なプロパティを記述する事ができ、FXMLプロパティ fx:XXX を使う事でFXML独自の定義をする事ができます。

ルートコンテナは BorderPane と定義しているので、topタグやcenterタグを使い子ノードの配置場所を定義しています。
また、子ノードには AnchorPane を定義しており、中に配置しているラベル、テキスト、ボタンは children タグにて定義しています。

さて、JavaFXプログラムとFXMLを比較して感じた事ですが、やはり全体的に見てFXMLの方がコード量が少ないですね。
個人的に特に気にいったのは、JavaFXプログラムのようにコンテナを定義して配置するといった手続き的な処理が不要となり、タグ内にて一括で定義できる所ですね。
これのおかげでメンテナンスが容易になるかと思います。
また、イベント処理との紐付きもタグ内で定義できるので、アクションイベントについても分かり易くなっています。
画面描画については、FXMLにする事で非常に作成しやすくなるかと思います。


それでは、FXMLファイルの具体的な使い方を説明させていただきます。

FXML読込

まず最初にプログラム側でFXMLを読み込み、Sceneへの設定を行います。

    // FXMLファイル読込
    Parent loader = FXMLLoader.load(getClass().getResource("MainWindow.fxml"));
    // FXMLをシーンへ設定、画面表示
    Scene scene = new Scene(loader);
    stage.setScene(scene);
    stage.show();</pre>

FXMLLoaderクラスの load メソッドにて、FXMLパネルの読み込みを行っています。
load メソッドの戻り値はFXMLに記述したルートのコンテナクラスとなります。
ですので、この場合は BorderPane クラスが戻り値となります。
今回は Parent クラスに戻り値を設定していますが、それは Parent は レイアウトの親クラスであるからです。
この Parent を Scene へ設定する事で、FXMLのルートコンテナである BoaderPane が設定された事になります。

イベント処理

次にイベントの実装クラスですが、これはFXML内でクラス指定する事ができます。

<BorderPane xmlns:fx="http://javafx.com/fxml"
            fx:controller="fxml.view.FxmlWindowControl"
            prefWidth="300.0" prefHeight="200.0">
~略~
<TextField fx:id="txtField"
       prefWidth="100.0"
       AnchorPane.topAnchor="45.0"
       AnchorPane.leftAnchor="40.0" />

<Button onAction="#dispInputInfo"
       text="表示" prefWidth="45.0"
       AnchorPane.topAnchor="85.0"
       AnchorPane.leftAnchor="10.0" /></pre>

fx:controllerにてJavaのクラスと結びつけ、イベントを処理したり値を取得したりすることが出来ます。
このクラスの事をControllerクラスと呼びます。

public class FxmlWindowControl extends BorderPane {
    /** INPUTテキストフィールド */
    @FXML
    private TextField txtField;

    /** 入力情報表示 */
    @FXML
    private void dispInputInfo (ActionEvent event) {
        dispDialog("Text", txtField.getText());
    }

Controllerクラス側では、@FXMLアノテーションを付ける事でFXMLとの関連付をしています。
※属性がpublicである場合はアノテーションは不要として問題ありません

FXMLとControllerクラスは1:1の関係にあり、 fx:id にてクラス変数と、 onAction にてメソッド(イベント)と紐付けています。


ここまでで、FXMLの読込と、FXMLとControllerの関連について説明しました。

次に画面遷移について説明します。
今回は、ダイアログによる画面遷移(表示)を説明しようと思います。

ダイアログ

ダイアログ画面は Stage を使っているので、FXML側は親画面と同様の作りとなります。

<StackPane xmlns:fx="http://javafx.com/fxml"
           fx:controller="fxml.view.FxmlDialogControl"
           prefWidth="150.0" prefHeight="100.0">

    <VBox alignment="CENTER" prefWidth="140.0" prefHeight="90.0">
        <children>
           <!-- 表示メッセージ -->
           <Label text="" />
           <Label fx:id="msgLabel" />
           <Label text="" />

           <Button onAction="#close" text="close" prefWidth="45.0" />
        </children>
    </VBox>
</StackPane>

ダイアログの呼出

ダイアログの呼出処理ですが、今回はMainWindowのControllerクラスにて行います。
読込方法は、以下の通りとなります。

    FXMLLoader loader = new FXMLLoader(getClass().getResource("MainDialog.fxml"));
    try {
        loader.load();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

MainWindow.fxml読込とは異なり、FXML読込の前にFXMLLoaderクラスを定義したあとに、load() 処理を行っています。
これはレイアウトクラスには、ロードしたFXMLのControlloerを取得する機能が実装されていないからです。
FXMLLoaderを定義した事で、以下のようにControllerクラスを取得し、画面表示用メッセージを設定する事が可能となるのです。

FxmlDialogControl controller = loader.getController();
controller.setMessage(msg);

最後に親画面の設定方法ですが、これもMainWindowのControllerクラスで行います。

@FXML
private TextField txtField;
~略~
    Parent root = loader.getRoot(); // FXMLのルートコンテナ取得
    Scene scene = new Scene(root); // ルートコンテナ設定
    Stage dialog = new Stage(StageStyle.UTILITY);
    dialog.setScene(scene);
    dialog.initOwner(txtField.getScene().getWindow()); // 親画面設定
    dialog.initModality(Modality.WINDOW_MODAL);

SceneにはFXMLLoaderクラスの getRoot メソッドにてルートコンテナ(StackPane)を呼出し、Scene へ設定しています。
親画面指定はControlerクラスにあるコンポーネントより Scene を呼出し、Windowオブジェクトの設定する事で指定する事ができます。

これで、FXMLの読込とStageへの設定、Contorollerクラスによるイベント実装、画面遷移ができるようになります。
FXMLの使い方は、この他にもありますが今回はここで終わらせていただきます。


プログラム全文

最後に今回説明したプログラムの全文を載せておきます。

MainWindow.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<BorderPane xmlns:fx="http://javafx.com/fxml"
    fx:controller="fxml.view.FxmlWindowControl"
    prefWidth="300.0" prefHeight="200.0">

    <!-- メニューバー -->
    <top>
    <MenuBar>
        <menus>
            <Menu text="ファイル">
                <MenuItem text="終了" onAction="#close"/>
            </Menu>
            <Menu text="ヘルプ">
                <MenuItem text="作成情報" onAction="#dispVersionInfo"/>
            </Menu>
        </menus>
    </MenuBar>
    </top>

    <!-- ボディ部 -->
    <center>
    <AnchorPane prefWidth="300.0" prefHeight="100.0">
        <children>
           <Label text="好きな文字を入力してください"
                   AnchorPane.topAnchor="12.0"
                   AnchorPane.leftAnchor="8.0" />
           <Label text="Input"
                   AnchorPane.topAnchor="45.0"
                   AnchorPane.leftAnchor="8.0" />
           <TextField fx:id="txtField"
                   prefWidth="100.0"
                   AnchorPane.topAnchor="45.0"
                   AnchorPane.leftAnchor="40.0" />
           <Button onAction="#dispInputInfo"
                   text="表示" prefWidth="45.0"
                   AnchorPane.topAnchor="85.0"
                   AnchorPane.leftAnchor="10.0" />
        </children>
    </AnchorPane>
    </center>
</BorderPane>

FxmlWindow.java

package fxml.view;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class FxmlWindow extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        stage.setTitle("FXMLサンプル");

        // FXMLファイル読込
        BorderPane loader = FXMLLoader.load(getClass().getResource("MainWindow.fxml"));

        // FXMLを設定
        Scene scene = new Scene(loader);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String... args) {
        launch(args);
    }
}

FxmlWindowControl.java

package fxml.view;

import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class FxmlWindowControl extends BorderPane {
    /** INPUTテキストフィールド */
    @FXML
    private TextField txtField;

    /** 画面クローズ */
    @FXML
    private void close(ActionEvent event) {
        System.exit(0);
    }

    /** 作成情報表示 */
    @FXML
    private void dispVersionInfo (ActionEvent event) {
        dispDialog("作成情報", "Version:1.0.0", "Date:2015-11-04");
    }

    /** 入力情報表示 */
    @FXML
    private void dispInputInfo (ActionEvent event) {
        dispDialog("Text", txtField.getText());
    }

    /** ダイアログ表示 */
    private void dispDialog(String title, String... msg) {
        /* FXMLファイルロード */
        FXMLLoader loader = new FXMLLoader(getClass().getResource("MainDialog.fxml"));
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // コントローラを取得し、表示メッセージ設定
        FxmlDialogControl controller = loader.getController();
        controller.setMessage(msg);

        // Scene、Stageへ画面情報を設定
        Parent root = loader.getRoot();
        Scene scene  = new Scene(root);
        Stage dialog = new Stage(StageStyle.UTILITY);
        dialog.setScene(scene);
        dialog.initOwner(txtField.getScene().getWindow()); // 親画面設定
        dialog.initModality(Modality.WINDOW_MODAL);
        dialog.setResizable(false);
        dialog.setTitle("Message");
        dialog.show();
    }
}

MainDialog.fxml

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import java.lang.*?>

<StackPane xmlns:fx="http://javafx.com/fxml"
    fx:controller="fxml.view.FxmlDialogControl"
    prefWidth="150.0" prefHeight="100.0">

    <VBox alignment="CENTER" prefWidth="140.0" prefHeight="90.0">
        <children>
           <!-- 表示メッセージ -->
           <Label text="" />
           <Label fx:id="msgLabel" />
           <Label text="" />

           <Button onAction="#close"
                   text="close" prefWidth="45.0" />
        </children>
    </VBox>
</StackPane>

FxmlDialogControl.java

package fxml.view;

import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;

public class FxmlDialogControl extends StackPane {
    @FXML
    private Label msgLabel;

    @FXML
    private void close() {
        msgLabel.getScene().getWindow().hide();
    }

    /** 出力メッセージラベル生成 */
    public void setMessage(String... lstMsg) {
        String msg = "";
        for (String s : lstMsg) {
            if (msg.length() > 0) {
                msg = msg + "\n";
            }
            msg = msg + s;
        }
        msgLabel.setText(msg);
    }
}

まとめ

どうでしょう?
画面描画部分をFXMLで行う事で画面の構築処理が非常に分かり易くなったかと思います。
今回はFXMLの基本的な説明という事で、画面内容も静的なものであり、ContorollerクラスもFXML側で指定するといった手法を説明しました。
しかし、JavaFXにはカスタムコントロール式という、ContorollerクラスがFXMLファイルを読み込む方法もあります。
機会があればカスタムコントロール式と、FXMLの動的な画面構築についての手法のご紹介をさせていただければと思います。