mike-neckのブログ

Java or Groovy or Swift or Golang

JavaFXで画面を作るときにFXMLを小さく作る #javafx #javafx_ja

これはJavaFX Advent Calendar 2014の4日目の記事です。

昨日は@boochnichさんScene Builder 2.0.1をビルドするです。

明日はsoutokuさんJavaFXでWYSIWYGエディタを作るです。


JavaFXアプリケーションを作るときに、FXMLを用いると比較的見通しがよく、メンテナンスしやすいアプリケーションが作れると思います。

これはSwingなどでアプリケーションを作ったことがある方なら、だいたいわかると思います。

ただ、アプリケーションが大きくなり始めると、FXMLファイルが複雑になって、メンテナンスが難しいものになってしまうのではないかと思います。

というわけで、僕がJavaFXアプリケーションを作るとき(めったに作らないけど…)は、FXMLを極力小さく作るようにしています。

ここでは例としてドラクエXでくじ引きした時にカウントを取るのが面倒でつくったアプリを取り上げます。

これは、まあ、見たまんまのアプリで、あたった賞の等級にある「当」ボタンを押すと、テキストフィールドの数値と合計の部分の数値が1増えて、「×」ボタンを押すと数値が減るという単純なものです。

これは各賞の部分が同じ形になっているため、小さいアプリケーションながらも、ボタンの位置をずらすといった変更を行うときに、同じ作業を何度もする必要があるなど、メンテナンスが煩わしいアプリケーションです。

最初作ったときはScene Builderで「×」ボタンなしのものを作ったわけですが、「×」ボタンを追加するときにあまりの煩わしさに、FXMLを小分けするような造りにしました。

コード

このアプリケーションのFXMLは次のような感じになっています。

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.text.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>


<fx:root type="javafx.scene.layout.HBox" prefHeight="39.0" prefWidth="280.0" style="-fx-background-color: #cff;" xmlns:fx="http://javafx.com/fxml">
    <children>
        <Label fx:id="label" contentDisplay="CENTER" prefHeight="25.0" prefWidth="80.0" textFill="#003399" textOverrun="CLIP">
            <HBox.margin>
                <Insets bottom="7.0" left="10.0" right="7.0" top="7.0" />
            </HBox.margin>
        </Label>
        <Label fx:id="amount" alignment="CENTER_RIGHT" prefHeight="25.0" prefWidth="50.0" style="-fx-border-color: #039; -fx-border-radius: 3px;" text="0">
            <HBox.margin>
                <Insets bottom="7.0" left="10.0" right="7.0" top="7.0" />
            </HBox.margin>
            <padding>
                <Insets right="7.0" />
            </padding>
        </Label>
        <Button fx:id="plus" onAction="#increment" alignment="CENTER" mnemonicParsing="false" prefHeight="25.0" prefWidth="40.0" style="-fx-background-color: #34f; -fx-font-weight: bolder;" text="当" textFill="WHITE">
            <HBox.margin>
                <Insets bottom="7.0" left="10.0" right="7.0" top="7.0" />
            </HBox.margin>
            <font>
                <Font name="System Bold" size="13.0" />
            </font>
        </Button>
        <Button fx:id="minus" onAction="#decrement" alignment="CENTER" mnemonicParsing="false" prefHeight="25.0" prefWidth="40.0" style="-fx-background-color: #f44; -fx-font-weight: bold;" text="×" textFill="WHITE">
            <HBox.margin>
                <Insets bottom="7.0" left="10.0" right="7.0" top="7.0" />
            </HBox.margin>
        </Button>
    </children>
    <padding>
        <Insets bottom="7.0" left="7.0" right="7.0" top="7.0" />
    </padding>
</fx:root>

これに対するコントローラーは以下のとおりです。

package org.mikeneck.dqx.lottery.control;

import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import org.mikeneck.dqx.lottery.LotteryException;
import org.mikeneck.dqx.lottery.intercept.CountManager;
import org.mikeneck.dqx.lottery.model.Rank;

import java.io.IOException;

public class LotteryResultRow extends HBox {

    @FXML
    private Label label;

    @FXML
    private Label amount;

    @FXML
    private Button plus;

    @FXML
    private Button minus;

    private final CountManager manager;

    private final Rank rank;

    public LotteryResultRow(Rank rank, CountManager manager) {
        this.rank = rank;
        this.manager = manager;

        FXMLLoader loader = new FXMLLoader(getClass()
                .getClassLoader()
                .getResource(rank.getFxmlFile()));
        loader.setRoot(this);
        loader.setController(this);
        try {
            loader.load();
        } catch (IOException e) {
            String message = String.format("fxmlファイルのロードに失敗しました。 %s", rank.toString());
            throw new LotteryException(message, e);
        }
    }

    public void setLabelText (String text) {
        label.setText(text);
    }

    @FXML
    public void increment () {
        int now = manager.increment(rank);
        updateView(now);
        manager.updateTotal();
    }

    @FXML
    public void decrement () {
        int now = manager.decrement(rank);
        updateView(now);
        manager.updateTotal();
    }

    public void updateView () {
        int now = manager.get(rank);
        updateView(now);
    }

    private void updateView(int now) {
        amount.setText(String.format("%d", now));
    }
}

また、この小さな部品を画面の中に組み込むコードは次のように親要素のchildNodesに追加していきます。

        final ObservableList<Node> nodes = vBox.getChildren();
        Stream.of(Rank.values()).forEach(rank -> {
            LotteryResultRow row = new LotteryResultRow(rank, countManager);
            row.setLabelText(rank.getLabelText());
            countManager.setRow(rank, row);
            nodes.add(row);
        });

これで、FXMLを小さくして柔軟な変更が可能なアプリケーションを作ることができます。

ポイント

部品ごとにFXMLを小分けにするときは、だいたい次のようにやっています。

メリット

FXMLを小分けにするメリットは

  • 柔軟な変更が可能になる
  • いらなくなったら捨てれば良い

といったところです。

デメリット

当然デメリットも有ります。

  • 必要なオブジェクトの依存関係の把握が結構面倒臭い

必要なオブジェクトの云々についてはDIライブラリー(Guiceなど)を使えば解決できると思うので、僕としてはそれほどデメリットに感じていません。


以上、あまりガチな内容ではありませんでしたが、JavaFXアプリケーションを作るときにFXMLを小さくする方法について書いてみました。

あと、この方法ググった時に日本語情報なかったけど、検索キーワード悪かったのかな…(´・ω・`)