動的なUIのJavaFXアプリケーションでノードとコントロールを結びつける

  • 5
    Like
  • 0
    Comment

この記事はJavaFX Advent Calendar 2016の3日目です。昨日は@y_q1mさんの「JavaFX アプリケーションを素敵に着飾ってみる」です。明日は@arachan@githubさんです。

概要

JavaFXアプリケーションを作る上で、GridPaneとかTabPaneとかに追加された子要素をコントローラ付きで扱いたい(それぞれのNodeインスタンスに処理を持たせたい)場合にどうしようかという話です。
※当方野良JavaFX書きなのでもっといい方法があればアドバイスください。

カスタム・コントロールでNodeとCntrolを兼用する

通常、FXMLLoaderクラスを使用してUIを構成すると、Initializableを実装したコントローラクラスとFXMLLoader::loadから提供されるroot要素は分離されます。これはデザインと実装の分離という観点から言えば正しい構造です。しかし、このままではGridPaneの子要素のNodeを取得して逐次処理をしたい場合やTabPaneでセレクトされたTabを取得して処理する場合に不便になってしまいます。
そこで、NodeとControlを兼ねたカスタム・コントロールを使うことで、コントローラの処理を持ちつつレイアウトに追加可能なNode要素を用意することで対処できます。

カスタム・コントロール作成の大まかなフロー

公式ドキュメントで解説されているやり方は大体こんな感じです。

1 fx:rootでルート・コンテナを定義したレイアウトファイルを作成する

<?xml version="1.0" encoding="UTF-8"?>
<fx:root type="(ルート・コンテナ)" xmlns:fx="http://javafx.com/fxml">
    (レイアウトコード)
</fx:root>

ルート・コンテナはPaneでもButtonでもTextFieldでも好きな単位で指定できます。

2 設定したルートコンテナを継承したクラスを作成する

import java.io.IOException;
import javafx.scene.layout.VBox;

public class CustomControl extends VBox { // ルート・コンテナがVBoxだとした場合
    // fx:idを指定した場合はインジェクションできる

    public CustomControl() {
        FXMLLoader loader = new FXMLLoader(getClass().getResource(
"(レイアウトファイル).fxml"));
        loader.setRoot(this); // fx:rootをCustomControlとして扱う
        loader.setController(this); // fx:controllerもCustomControlとして扱う

        try {
            fxmlLoader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        // ここからインジェクションした変数を
    }
    // onActionなどを定義した場合はここで宣言できる
}

3 インスタンス化して使用する

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class App extends Application {

    @Override
    public void start(Stage primaryStage) {
        try {
            CustomControl root = new CustomControl();
            primaryStage.setScene(new Scene(root));
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

ちなみに、デフォルトコンストラクタを作成していればFXML上でもそのまま使えます。

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<HBox xmlns:fx="http://javafx.com/fxml">
    <CustomControl />
</HBox>

カスタム・コントロールをどう使うか

本題です。
TabPaneを使ったエディタを考えましょう。例のために新しく何かを作るのは面倒なので拙作の作りかけMarkdownエディタを引用していきます。

スクリーンショット 2016-12-03 19.46.16.png

選ばれているTabの内容を保存する場合を考えていきます。
TabPaneでは現在選択されているタブがSingleSelectionModel<Tab>クラスを通じて取得できます。これはUI要素のTabが返却されるので、カスタムコントロールにする必要があります。

MainFrame.java
// 一部抜粋
public class MainFrame extends VBox { // TabPaneを置いたコントロール
    @FXML
    private TabPane tabPane;

    @FXML
    public void save() { // 保存
        EditorTab selected = (EditorTab) tabPane.getSelectionModel().getSelectedItem();
        if (selected.hasFile()) selected.save();
        else saveWithName();
    }

    @FXML
    public void saveWithName() { // 名前を付けて保存
        ((EditorTab) tabPane.getSelectionModel().getSelectedItem()).saveWithName(getScene().getWindow());
    }
}
EditorTab.java
// 一部抜粋
public class EditorTab extends DraggableTab { // DraggableTabはTabを継承して機能追加したもの
    private EditorTab(String name) { // 名前付きタブを生成する
        super(name);
        FXMLLoader loader = new FXMLLoader(getClass().getResource("tab.fxml"));
        loader.setController(this);
        loader.setRoot(this);
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean hasFile() {
        // 新規作成した場合とファイルを開いた場合の判別
    }

    public void saveWithName(Window parent) {
        // ダイアログ+保存処理
    }

    public void save() {
        // 保存処理
    }
}
tab.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<fx:root type="Tab" xmlns="http://javafx.com/javafx/8.0.76-ea" xmlns:fx="http://javafx.com/fxml/1">
    <!-- 省略 -->
</fx:root>

キャストする必要はありますが、こうすることでタブごとの処理を委譲することができます。

おまけ

こんな感じでコマンドライン引数を取得することもできます。

MainFrame.java
// 一部抜粋
public class MainFrame extends VBox {
    public MainFrame(List<String> args) {
        // 初期化処理
    }
}
primaryStage.setScene(new Scene(new MainFrame(getParameters().getUnnamed())));

参考

JavaFX: JavaFXスタート・ガイド
JavaFX: FXMLの習得
※今回使ったのは4 FXMLを使用したカスタム・コントロールの作成です。

今回使った拙作Markdownエディタに関して

git clone https://github.com/skht777/javafx-markdown.git

特に書くことないんですが、元々JavaFXでJSを使ったフロントエンドを利用するために作っていたのでbowerが必要です。