seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

JavaFX8で簡単なアプリケーションを簡単に作る方法

動機、なぜJavaFXでアプリを作るのか?

Javaには、Swingという、十分に成熟しておりパフォーマンスも悪くないUIライブラリがある。

現状、普通に使っている分には何の不自由もない。

また、Swingは今後発展することはないとはいえ、(すぐに)廃止されるわけではない。*1

※ 2018/9以降、JavaFXはOpenJDK, OracleJDKともに同梱されないことになった。すなわち、JavaFXJavaの標準のツールキットではなくなる。なので、無理にJavaFXを勉強しなくてもいいかもね。(というか、Oracleの業態からいってクライアント向けの機能は必要としていないから、今後はGuiまわりは全部切られるかも?) 2018/6/2追記 Java11でJavaFXを使う方法について記事を書きました。(2018/7/27)

ならば、どうしてJavaFXなどという、新しいUIライブラリを覚えなければならないのか。

見るからにめんどくさそうなFXMLというマークアップ言語まで付いている。

....。

と、思っていたのだけれど、

実際に使ってみたらSwingよりも思いのほか簡単だった。


案ずるより産むが易し、というやつである。

JavaFXを使うメリット

実際にJavaFXを使ってみて、Swingに対して以下のようなメリットを感じることができた。

  • FXMLを使うことによりUIのレイアウト構造をコードから分離することができる
    • Swingではロジックとレイアウトがごちゃまぜになっていて、作るのは良いが、修正する場合に困難が伴わなかったことがない。
    • その点、JavaFXはコードではレイアウトしないので保守性が高そうである。
  • FXMLはXML構造であるため、GUIのようなオブジェクトツリー構造を記述するのに適している
    • XMLであるためツールとの親和性が高い。(Scene Builderという外部ツールでレイアウトを編集できる。)
    • やってることも、実は単純だったりする。(手で作るのも難しくないと思われる。やりたくはないけど。)
  • バインディングはめんどくさい反面、とても便利で強力だったりする。
  • CSSによる細かなレイアウトの調整やデコレーションが、プログラムコードとは分離して表現できる。
    • (ただしウェブ的なcssとは、ちょっと違う。)

上記のようなメリットを実感したため、今後はJavaFX8を優先的に使ってゆこうと思い始めている。


ここでは、これまで経験した、JavaFXで簡単にアプリを作るための簡単な方法について、軽くまとめておきたいと思う。

(また、先日のJava Day Tokyo 2015の櫻庭さんのセッションで得られた新たな知見も追記しておく。)

用意するもの

  • JDK8u40以降
  • Scene Builder 2.0
  • IDE
    • Eclipse 4.4 Luna + e(fx)clipse
    • NetBeans8
JDK8u40を使う理由

JavaFX2はJava7より利用可能になっているが、ここではJava8u40以降を使う。


Java7からJavaFX2が同梱され利用可能だったが、JavaFXのランタイムがクラスパスに通っていないため、通常の方法ではアプリケーションを配布することも実行することもめんどくさかった。

しかし、Java8からはJavaFX8は標準のクラスパスに入っており、単純に実行可能jarに丸めて配布しても動くようになっている。

JavaFX2(Java7)時代のJavaFXのビルドと配備方法は大変複雑だったが、JavaFX8では、ただのアプリと同じである。


また、JavaFX2にはメッセージボックスのような、よく使われるダイアログが(何故か)存在しない。


これも、Java8u40から、JavaFX Dialogsの成果物Alertクラスが標準に取り込まれたので、メッセージボックスの表示も標準でできるようになっている。

http://code.makery.ch/blog/javafx-dialogs-official/

※ Java8u40未満では上記のJavaFXDialogsのライブラリを使うと良いだろう。

Scene Builder 2.0

Scene Builderは、JavaFXGUIをレイアウトするためのツールである。

このツールを使うことにより、コンポーネントの配置状態を表すFXMLというXMLファイルをグラフィカルに編集、保存することができる。


これはJDKには付属していないので、別途ダウンロードする必要がある。

http://www.oracle.com/technetwork/java/javase/downloads/javafxscenebuilder-info-2157684.html

ただし、現行のSceneBuilder2はFXMLのすべての機能をサポートしているわけではなく、
手作業で手の込んだFXMLを作るとScene Builderでは開けなくなったりするので注意が必要である。


簡単なアプリケーションを作るうえでは、Scene Builderだけで十分ではないか、と思う。


なお、最新のScene BuilderのバイナリはOracleからは配布されておらず、OpenJDK側でソースとして提供されている。

これを使うには、自分でビルドするか、だれかがビルドしてくれたバイナリをもらってくる必要がある。*2

Scene Builder自身もJavaFXで書かれているので、jdk8環境があればビルドできるようである。*3

IDE

JavaFX8は、ふつうのJavaコード + FXMLというXMLファイルで作れるので、原理的には何のIDEでも良い。


Eclipseの場合は、Java8をサポートするLuna以降が必要であり、且つ、FXMLのサポートがつく"e(fx)clipse"プラグインを入れる。

Eclipsejavafxで始まるパッケージを非公開クラスの利用だとしてエラー表示するが、このプラグインを入れると解消される。

設定から、Scene Builderの実行ファイルの位置などを指定する必要がある。


NetBeans8の場合でも、FXMLの編集にはScene Builderを使うので、ツールメニューからオプションを開き、JavaのタブからScene Builderのホームディレクトリを指定する必要がある。

これでIDEとScene Builderとの連携ができるようになる。

(なお、NetBeans8でJavaFXプロジェクトをMavenで作ると、Java7時代のめんどくさいビルドスクリプトが出来上がるの注意。JavaFX8では、普通のアプリのビルド方法と同じで良い。)

JavaFXのお約束

エントリポイント

従来、SwingのJFrameを使った簡単なアプリケーションは以下のような形で作っていたと思う。

class MyFrame extends JFrame {

    public MyFrame() {
        // 初期化処理
    }

    public static void main(String... args) throws Exception {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                MyFrame inst = new MyFrame();
                inst.setLocationByPlatform(true);
                inst.setVisible(true);
            }
        });
    }
}

mainメソッドでイベントディスパッチスレッド(EDT)に入ってJFrameを作成する、という流れである。

(手抜きなコード例だと、EDTに入らずに、いきなりnew JFrameしていたりする例も散見されるが、これは禁止事項である。)


これに対して、JavaFXでは、アプリケーションのエントリポイントは、かならず以下のような形式になる。

package jp.seraphyware.simpleapp0;

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

public class SimpleApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {
        // ここからスタートする
    }

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

まず、アプリケーションは、必ずApplicationを継承したクラスになる。

JavaFXに対応したランチャは、Javaアプレットのようにmainは経由せず、startから開始する。*4


mainから開始する古典的な実行形式の場合には上記のように単にlaunchを呼ぶ形にしておく。
これにより、JavaFXアプリケーションスレッドに入ったあとでstartメソッドが呼び出されるようになる。

(この場合でSwingと異なる点は、launchメソッドを呼んだ後、JavaFXアプリケーションスレッドが終了するまで制御が返らない点であろう。)


startメソッドは呼び出された時点でJavaFXアプリケーションスレッド上にいる。


引数にはStageが1つ受け取られているが、これがSwingでいうところのJFrameに相当する、メインウィンドウとなる。

このstartメソッド内でJavaFXコンポーネントを作成して、Stageに上に乗せることで、ウィンドウを作成する。

(アプリケーションは、必ずしも、このprimaryStageを使う必要はなく、任意にStageを作成して使うこともできるが、アプレット等でブラウザに埋め込まれている場合は、このprimaryStageが、その埋め込み領域を表している。)


コンポーネントを作成する方法としては、

のいずれの方法でもよい。

必ずFXMLを使わなければならない、というものではない。


JavaFXアプリケーションスレッドが終了する場合にはstopメソッドが呼び出されるので、ここで後始末ができる。

UIスレッド/JavaFXアプリケーションスレッド

JavaFX8では、JavaFXアプリケーションスレッド以外からUIを操作したい場合には、Swingの場合と同様にJavaFXアプリケーションスレッド上で実行するRunnableを引き渡す必要がある。

Platform#runLater(Runnable r)


JavaFXアプリケーションスレッドは、Swing/AWTのEDT(UIスレッド)とは別のスレッドである。

JavaFX8では、SwingNodeによって、JavaFXの中でSwingを利用することもできるが、
SwingとJavaFXのスレッドは別物なので、SwingNode上のSwingコンポーネントを作成、操作する場合には、必ず、SwingのUIスレッドで行う必要がある。


この場合、Swing用にはSwingUtilities#invokeLaterを引き続き使用する必要がある。


全部がObservableである。

JavaFXでは従来のJava Beansと異なり、"なんとかProperty"という形式のプロパティを持つ。

たとえば、textというプロパティを持つJavaFX Beansであれば、だいたい以下のようになる。

package jp.seraphyware.simpleapp0;

import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class JavaFXSimpleModel {

    private SimpleStringProperty textProperty;

    public StringProperty textProperty() {
        if (textProperty == null) {
            textProperty  = new SimpleStringProperty(this, "text");
        }
        return textProperty;
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

javafx.beans.***Propertyクラスは、値の変更を検知可能な(Observable)値のホルダーであり、
JavaFXでは、あらゆるプロパティがObservableである。


また、これはバインド可能(一方向、双方向)であり、ある値が変更されると連結されている別のコンポーネントのプロパティも自動的に変更される、という動作ができる。

バインドは、かなり強力で、複数のプロパティの演算結果をバインドする、というようなこともできるようになっている。


JavaFXでは、とにかく全部がObservableである。

JavaFXによる簡単なGUIアプリの作成

まず、以下のような簡単なGUIを作りたいと思う。

まずはFXMLによるGUIのレイアウトを行う

画面のレイアウトのデザインにはScene Builderを使う。


EclipseNetBeans上で適当に空のFXMLファイルを作成し、そのファイルを右クリックして「Scene Builderで開く」みたいなメニューがあれば、それでScene Builderが開かれる。*5

Swingの場合はJPanelの中にレイアウトマネージャを設定してレイアウトを組んでいたが、JavaFXの場合では、はじめからGridPane, BorderPane, FlowPane, VBox, HBoxのようなレイアウトペインになっている。

この上に必要なコンポーネントを乗せて行くことで画面をデザインしてゆくことになる。


実際に画面のレイアウトするうえでの注意は「アンカーペインには頼りすぎない」ことが使ってみたところの教訓といえようか。


アンカーペイン(AnchorPane)は、Visual Studioのフォームエディタのような感覚でコンポーネントを並べてゆけるうえに、アンカーを指定することで画面サイズにあわせて拡縮するようなレイアウトも容易に作れる。

しかし、コンポーネント自身の適切なサイズは使用するフォントや文字数により変わるため、ローカライズした場合やWindows/Linux/Mac間でフォントが変わった場合などでサイズが変わるため、なかなか調整が難しい。


なので、今のところの感触では、Swingを手動でレイアウトを組んでいたころのように、GridPane, BorderPane, FlowPane, VBox, HBoxなどのレイアウトを使ってコンポーネントのサイズを自動調整させつつ、それらを組み合わせて配置してゆくのが良いだろう、と思う。

fx:idの割り当て

プログラム側からコンポーネントを制御できるようにするため「fx:id」属性で名前をつけておく必要がある。


これによりFXMLとコントローラクラス上のフィールドとが結び付けられるようになる。

  • txtDir
  • txtNamePattern
  • btnBrowse
  • chkSubdir
  • btnOK
アクション名の割り当て

ボタンのアクションに対しては、以下のようにメソッド名を割り当てておく。


これによりFXMLとコントローラクラス上のメソッドと連結されることになる。

  • #onBrowse
  • #onOK
  • #onCancel

※ ここにスクリプトを直接書くことも可能。


SceneBuilderの画面を示す。

出来上がったFXMLの例

できあがったFXMLは以下のようなものである。

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="ディレクトリ" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="ファイル名パターン" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="参照" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="サブディレクトリを含む" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="実行" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="キャンセル" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

FXMLはとても汎用的にできていて、fxで始まる名前空間のもの以外は、特殊なものはない。

これはオブジェクトツリーを作成するための表記そのものといってよい。

http://docs.oracle.com/javafx/2/api/javafx/fxml/doc-files/introduction_to_fxml.html


FXMLのルールは、ざっくりといえば、

  • 大文字のタグはクラス名であり、そのクラスをnewすることを意味する
  • 小文字のタグまたは属性名がプロパティを意味する
    • 属性名を使う場合は文字列または数値のリテラルのみ指定可能
    • 小文字のタグを使う場合には、リスト表現や、fxで始まる名前空間を利用してJavaの定数を参照することも可能。

これらをつなげてツリーを作ることでUI要素の親子関係を表現しているのである。


また、要素の出現順で、タブキーによるフォーカス移動順(フォーカストラバーサル)が決まるなどの性質もある。


上記例では、このFXMLをロードすると、ルート要素として各種UI要素を配置済みのVBoxが返されることになる。

FXMLに対応するコントローラクラスの作成

前述のように、コンポーネントに名前を割り当てたり、ボタンのアクションのメソッド名を割り当てた場合、それらを受ける「コントローラ」クラスの定義を行う必要がある。

前述の例であれば、以下のようなコントローラが必要となる。

public class SimpleAppController implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        // ここにBrowseボタンのアクションを定義する
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        // ここにOKボタンのアクションを定義する
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }
}

コントローラにするクラスには、フィールドまたはメソッドに対して@FXMLアノテーションをつけたものが、FXMLLoaderによってJavaFXコンポーネントと結び付けられる。

また、Initializableインターフェイスを実装する場合、
各種フィールドやメソッドがコンポーネントと関連づけられたあとに、initializeメソッドが呼び出されるので、ここで追加の初期化処理を行うことができる。

ここでは、2つのテキストフィールドに何らかの文字が入力されていないかぎりOKボタンを有効としないような活性制御のバインドを設定している。
(txtDirが空か、txtNamePatternが空の場合に、btnOKのdisableプロパティをTrueに設定するバインドを設定している。)


なお、onCancelメソッドでは、ウィンドウを閉じるために、やや回りくどいことをしているが、
コントローラはどこからも継承していない独立しているクラスなので、あらかじめフィールドにステージの情報を入れておくか、何かしらのUI要素を遡って、現在表示しているウィンドウを取得するなどの方法を取る必要がある。

コントローラクラスの指定方法

コントローラクラスの指定方法には、いくつかあり

  • FXMLLoaderで直接、コントローラのインスタンスを指定する (fx:controller属性は書かない)
  • FXMLコード中のルート要素のfx:controller属性にクラス名を指定する
    • FXMLLoaderでコントローラのファクトリを指定することもできる.
      • SpringなどDIコンテナ管理のコントローラを作りたい場合などは、ここでインジェクションしたあとのインスタンスを渡すなどのファクトリクラスの定義ができる。


今回は、FXMLにはコントローラクラスは記述せず、FXMLLoaderで直接コントローラのインスタンスを指定する方法を用いる。

FXMLのロード

FXMLは以下のようにしてロードすることができる。

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {
....(前略)....

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

....(後略)....
}

FXMLLoaderクラスによってFXMLをロードできる。

FXMLファイルの指定方法にはURLによる方法とストリームによる方法などがあるが、基本はURLによる指定を使うほうが良い。


これはFXML表記で"@picture1.png"のような形式でFXMLファイルからの相対位置でリソースのありかを指定する表記方法があるが、
ストリームでFXMLを読み込ませる場合には、その位置の指定が取れなくなるためである。


FXMLをロードしフィールドやメソッドは、setControllerメソッドに対してthisを渡すことで明示している。

これにより、このクラス自身がコントローラを兼ねることになる。


あとは、これをloadすると、FXMLの最初の要素である"VBox"のインスタンスが得られるので、
これをSceneオブジェクトに入れて、それをStageにセットすることで、画面上に表示できるようになる。


※なお、FXMLLoaderはオブジェクトツリーの汎用ファクトリであり、任意の独自のクラスや、あるいはStageそのものをルートとしてオブジェクトツリーを作成することも可能である。
(ただし、その場合はSceneBuilderでは編集できなくなるので、実用性は少ないかもしれない。)

完全なソースコード
package jp.seraphyware.simpleapp1;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import jp.seraphyware.utils.ErrorDialogUtils;

/**
 * 簡単なFXMLアプリケーション作成例.
 *
 * @author seraphy
 */
public class SimpleApp extends Application implements Initializable {

    @FXML
    private TextField txtDir;

    @FXML
    private TextField txtNamePattern;

    @FXML
    private CheckBox chkSubdir;

    @FXML
    private Button btnOK;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * アプリケーション開始
     */
    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // FXMLをロードする.
        // (ローカライズしない場合はリソースバンドルを指定しなくて良い)
        FXMLLoader ldr = new FXMLLoader(fxml, null);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle("SimpleApp Sample");

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトルの設定
        dirChooser.setTitle("フォルダの選択");

        // OKボタンの活性制御
        btnOK.disableProperty().bind(
                Bindings.or(
                        txtDir.textProperty().isEmpty(),
                        txtNamePattern.textProperty().isEmpty()
                        ));
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    @FXML
    protected void onOK(ActionEvent evt) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setHeaderText("実行!");

        StringBuilder buf = new StringBuilder();
        buf.append("dir=").append(txtDir.getText()).append("\r\n")
                .append("namePattern=").append(txtNamePattern.getText())
                .append("\r\n").append("subdir=")
                .append(chkSubdir.isSelected() ? "Yes" : "No");
        alert.setContentText(buf.toString());

        alert.showAndWait();
    }

    @FXML
    protected void onCancel(ActionEvent evt) {
        // 現在ボタンを表示しているシーンを表示しているステージに対して
        // クローズを要求する.
        ((Stage) ((Button) evt.getSource()).getScene().getWindow()).close();
    }

    /**
     * エントリポイント
     * @param args
     * @throws Exception
     */
    public static void main(String... args) throws Exception {
        launch(args);
    }
}

このように、単純なアプリであればコントローラ兼メインクラスとなるクラス1つと、レイアウトを定めているFXMLリソースが1つあれば簡単に作ることができる。

FXMLファイルの中身もわかってしまえば簡単である。


JavaFXでやってみると、同等な画面をSwingで手作業で作るよりも簡単であることが分かった。

リソースバンドルを使った簡単なアプリの作成例

JavaFXアプリケーションをローカライズする場合にはリソースバンドルを指定することで簡単に対応できるようになっている。

FXML上に文字列リテラルを書く代わりに、"%resource.key"のような形式とすることで、リソースバンドルより自動的に文字列を引き当てることができるのである。

文字列リテラルからリソースキーに変更したFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <TextField fx:id="txtDir" GridPane.columnIndex="1" />
            <Button fx:id="btnBrowse" mnemonicParsing="false" onAction="#onBrowse" text="%browse" GridPane.columnIndex="2" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>


これは前述のFXMLから文字列リテラルを全て%resource.keyの形式に置き換えただけである。

リソースバンドルの定義

リソースバンドルには、ここではXML形式のリソースバンドルを使用するものとする。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>SimpleApp Resource</comment>
<entry key="title">SimpleApp サンプル#2</entry>
<entry key="dirChooser.title">フォルダの選択</entry>

<entry key="directory">ディレクトリ</entry>
<entry key="browse">参照</entry>
<entry key="namePattern">ファイル名パターン</entry>
<entry key="includeSubDirs">サブディレクトリを含む</entry>

<entry key="ok">実行</entry>
<entry key="cancel">キャンセル</entry>
</properties>

※ 本サンプルで使っているXMLResourceBundleControlは、XMLプロパティファイル形式をリソースバンドルとして読み込めるようにリソースバンドルコントローラを拡張した実装であるが、本質的に、ただのリソースバンドルでも同じである。
(実装例: https://gist.github.com/seraphy/b421ac03d64d541667b2)

FXMLLoader側の修正

FXMLLoader側は、リソースバンドルを指定すれば良いだけである。

    @Override
    public void start(Stage primaryStage) throws Exception {
        // FXMLをリソースから取得する.
        // (タブオーダーもFXMLの定義順になる.)
        // (FXML中に「@参照」による相対位置指定がある場合は、このURL相対位置となる.)
        URL fxml = getClass().getResource(getClass().getSimpleName() + ".fxml");

        // XMLリソースバンドルを読み込む
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());

        // FXMLをロードする.
        // (ローカライズするのでリソースバンドルも指定する)
        FXMLLoader ldr = new FXMLLoader(fxml, rb);

        // このインスタンス自身をコントローラとする.
        // @FXMLアノテーションによりFXMLと結び付けられる.
        ldr.setController(this);

        // FXMLをロードする.
        Parent root = ldr.load();

        // ステージのタイトル
        primaryStage.setTitle(rb.getString("title"));

        // ステージの表示
        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        // ディレクトリ選択テキストにフォーカスを当てる
        txtDir.requestFocus();
    }

見ての通り、コード側は、ほとんど修正はない。


ただリソースバンドルを読み込むだけで、%resource.keyを取得する部分はFXMLLoaderが全部やってくれている。


JavaFXでやればFXMLのリテラル修正とリソースバンドルの読み込み、これだけでローカライズに対応できるので、
SwingでリソースバンドルのgetStringを延々と手作業で記述するよりもはるかに簡単にできることが分かる。

カスタムコンポーネントの作成

JavaFXではFXMLを使ったカスタムコンポーネントの定義も考慮されている。

ここでは、テキストフィールドと参照ボタンの2つのコンポーネントを組み合わせた「DirTextField」というカスタムコンポーネントを定義してみる。


2つのコンポーネントをもつカスタムコンポーネント用のFXML
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<fx:root type="javafx.scene.layout.BorderPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</fx:root>

こちらも、Scene Builder2で編集可能である。


サイズ可変でも対応できるように、BorderPaneでセンターにテキスト、Rightにボタンを配置している。


今までと違うのは、ルート要素が「fx:root」というタグになっており、type属性にBorderPaneのクラスを指定している。

これにより、Scene Builder上では、type属性に示されたBorderPaneであるかのようにふるまう。


この意味するところは、FXMLLoaderによってロードされるときルート自身はnewされずに、親として指定された作成済みインスタンスに対して属性や子アイテムを追加してゆく、ということである。

カスタムコンポーネント用FXMLのロード

このFXMLはルート、すなわち自分自身がBorderPaneを継承するカスタムクラスであることを示しており、以下のようにBorderPaneを継承するカスタムクラスのコンストラクタでロードする形となる。

package jp.seraphyware.simpleapp3;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;
import jp.seraphyware.utils.XMLResourceBundleControl;

public class DirTextField extends BorderPane {

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    /**
     * コンストラクタ
     */
    public DirTextField() {
        URL url = getClass().getResource(getClass().getSimpleName() + ".fxml");
        ResourceBundle rb = ResourceBundle.getBundle(getClass().getName(),
                new XMLResourceBundleControl());
        FXMLLoader ldr = new FXMLLoader(url, rb);

        // このインスタンス自身がルートオブジェクト
        ldr.setRoot(this);

        // このインスタンス自身がコントローラ
        ldr.setController(this);

        try {
            // ルートを指定済みなので、このインスタンスにFXMLがロードされる.
            ldr.load();

        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }

        // ダイアログのタイトル設定
        dirChooser.setTitle(rb.getString("dirChooser.title"));

        // フォーカスリスナ
        focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

自分自身がコントローラであることは先の例と同様である。


今回は、それに加えてルートが自分自身であることを示すためにFXMLLoader#setRoot()で自分自身をルートであるように指定する。


また、このクラス専用のリソースバンドルを読み込んでいる。


rootがthisとなっていることにより、loadメソッドによって自分自身に設定することになるため、loadメソッドの戻り値は無視して構わない。

textPropertyなどのプロパティは、このカスタムコンポーネントの持つ値を外部からアクセス可能にするためのものである。

カスタムコンポーネントの利用

カスタムコンポーネントの利用は、FXML上で、カスタムクラス名を指定すれば良いだけである。

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import jp.seraphyware.simpleapp3.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <DirTextField fx:id="dirTextField" text="C:\" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中にある「DirTextField」というタグがカスタムコンポーネントのクラス名になる。

★ちなみに、Scene Builderでは、カスタムコンポーネントは、コンポーネントペインの歯車アイコンから「Import JAR/FXML File...」を選択し、そのFXMLを内包するjarクラスをインポートすることにより、左ペインに「カスタム」の一覧にカスタムコンポーネントが使えるようになり、グラフィカルエディタ上でもコンポーネントを認識するようになる。

しかし、とくにカスタムコンポーネントをインポートしない場合でも、SceneBuilderでは得体の知らないクラスとして画面上では穴が開いた状態になり、グラフィカルな画面からの操作は不可能ではあるが、とくにエラーになるわけではない。

ここでは手抜きのため、SceneBuilderにはインポートしないものとする。

※ なお、SceneBuilder2でインポートしたカスタムコンポーネントを消すには、現状、消すコマンドがないので、%APPDATA%\Scene Builder\Library上にあるFXMLファイルかJARファイルを手動で削除する必要がある。


「fx:id」で名前を「dirTextField」と指定しているので、コントローラ側では以下のように、カスタムクラスのフィールド名で受けることができる。

    @FXML
    private DirTextField dirTextField;
実行例

カスタムコンポーネントのFXMLによる定義方法も利用方法も簡単である。


また、このカスタムコントロールの手法は、FXMLにカスタムコントロールを直接埋め込む場合だけでなく、VBoxのようなペインに対して、プログラム的に2個以上連続して作成させるような場合にも、とても有効な手法ではないかと考えられる。

ネストしたコントローラの利用

カスタムコンポーネントに似た別の方法として、ネストしたFXMLの利用がある。

ネストしたFXMLとは、FXMLの中で別のFXMLをincludeするものである。


includeといってもC言語プリプロセッサ的な外部ファイルを流し込むだけの単純なincludeではなく、外部の独立したコントローラを生成して外部のFXMLを親のFXMLに埋め込むものである。


親子関係をもった状態で作成されるため、実質的に、カスタムコントロールとほとんど同じような感じとなる。

FXMLの定義

FXMLの定義は前述のカスタムコントロールの例をモデルとするが、異なるのはルート要素が「fx:root」ではなく、普通のFXMLと同様に明示的に「BorderPane」のようなクラス指定になっている、ということである。


これの意味するところは、FXMLLoaderによって実際にBorderPaneがnewされる、ということである。

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<BorderPane fx:id="root" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	fx:controller="jp.seraphyware.simpleapp4.DirTextFieldController">
   <center>
      <TextField fx:id="txtDir" BorderPane.alignment="CENTER">
         <opaqueInsets>
            <Insets />
         </opaqueInsets>
         <BorderPane.margin>
            <Insets right="5.0" />
         </BorderPane.margin></TextField>
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#onBrowse" text="%browse" BorderPane.alignment="CENTER" />
   </right>
</BorderPane>

もう一つ、重要な点は、ルート要素に「fx:controller」による、コントローラクラスの指定がある点である。


これにより、このFXMLを構築するときに自動的に"jp.seraphyware.simpleapp4.DirTextFieldController"クラスがデフォルトコンストラクタでインスタンス化され、コントローラとして使用される。

(※ FXMLLoaderでコントローラのファクトリを指定している場合は、インスタンス取得のためにファクトリによってコントローラのインスタンスを取得する。)

コントローラの定義

FXMLの受けるコントローラは以下のように定義する。

package jp.seraphyware.simpleapp4;

import java.io.File;
import java.net.URL;
import java.util.ResourceBundle;

import javafx.beans.property.StringProperty;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.DirectoryChooser;
import jp.seraphyware.utils.ErrorDialogUtils;

public class DirTextFieldController implements Initializable {

    @FXML
    private BorderPane root;

    @FXML
    private TextField txtDir;

    /**
     * ディレクトリ選択ダイアログ
     */
    private DirectoryChooser dirChooser = new DirectoryChooser();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // ダイアログのタイトル設定
        dirChooser.setTitle(resources.getString("dirChooser.title"));

        // フォーカスリスナ
        root.focusedProperty().addListener((o, old, newval) -> {
            if (newval) {
                txtDir.requestFocus();
            }
        });
    }

    @FXML
    protected void onBrowse(ActionEvent evt) {
        try {
            String srcDir = txtDir.textProperty().get();
            if (srcDir != null && !srcDir.isEmpty()) {
                File initDir = new File(srcDir);
                if (initDir.isDirectory()) {
                    dirChooser.setInitialDirectory(initDir);
                }
            }
            File selectedDir = dirChooser.showDialog(((Button) evt.getSource())
                    .getScene().getWindow());
            if (selectedDir != null) {
                txtDir.setText(selectedDir.getAbsolutePath());
                txtDir.requestFocus();
            }

        } catch (Exception ex) {
            ErrorDialogUtils.showException(ex);
        }
    }

    public StringProperty textProperty() {
        return txtDir.textProperty();
    }

    public String getText() {
        return textProperty().get();
    }

    public void setText(String text) {
        textProperty().set(text);
    }
}

実質的に、カスタムコントローラの場合と、ほとんど同じである。


なお、"fx:id"属性を使って、BorderPane自身を"root"という名前にしてコントローラのフィールドに割り当てているので、initializeによる初期化時にルートに対する操作も可能としている。

(この方法はネストしたコントローラのケースに限らず、FXML全般で使える。)


なお、initializeメソッドで受け取るリソースバンドルは、fx:includeタグのresources属性で指定可能であり、省略した場合は親のリソースバンドルが、そのまま子にも渡ってくる。

ただし、includeタグで指定したリソースの場合は内部的には

resources = ResourceBundle.getBundle(childResourceName, Locale.getDefault(),
    parentResources.getClass().getClassLoader());

と同等な処理で取得されるため、取得方法が柔軟ではないことに注意が必要である。

親のFXML

ネストしたコントローラを使うFXMLは、単に「fx:include」を使うだけである。

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <GridPane hgap="3.0" vgap="3.0">
        <columnConstraints>
          <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
          <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
            <ColumnConstraints hgrow="SOMETIMES" percentWidth="0.0" />
        </columnConstraints>
        <rowConstraints>
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
          <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        </rowConstraints>
         <children>
            <Label text="%directory" GridPane.halignment="RIGHT">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <Label text="%namePattern" GridPane.halignment="RIGHT" GridPane.rowIndex="1">
               <GridPane.margin>
                  <Insets right="5.0" />
               </GridPane.margin>
            </Label>
            <fx:include fx:id="dirTextField" source="DirTextField.fxml" GridPane.columnIndex="1" />
            <TextField fx:id="txtNamePattern" text="*.*" GridPane.columnIndex="1" GridPane.rowIndex="1" />
            <CheckBox fx:id="chkSubdir" mnemonicParsing="false" text="%includeSubDirs" GridPane.columnIndex="1" GridPane.rowIndex="2" />
         </children>
      </GridPane>
      <FlowPane alignment="TOP_RIGHT" hgap="5.0" vgap="5.0">
         <children>
            <Button fx:id="btnOK" defaultButton="true" mnemonicParsing="false" onAction="#onOK" text="%ok" />
            <Button cancelButton="true" mnemonicParsing="false" onAction="#onCancel" text="%cancel" />
         </children>
         <padding>
            <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
         </padding>
      </FlowPane>
   </children>
   <padding>
      <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
   </padding>
</VBox>

この中の「」が、ネストしたコントローラである。

カスタムコントロールとほとんど同じであるが、SceneBuilder2で開くと、インポートなどの事前処理をせずとも普通にFXMLを読み込めるので、それがBorderPaneであることがわかるようになっている。


(なお、現状ではSceneBuilder2からはfx:includeを扱うのはめんどくさいので、FXMLを手作業で直したほうが簡単である。)

親コントローラ

親コントローラでは、子コントローラを受け取るために以下のような記述ができる。

    /**
     * 入れ子になったFXMLのルートがパイントされる.
     * フィールド名は子FXMLのidと同じでなければならない.
     */
    @FXML
    private BorderPane dirTextField;

    /**
     * 入れ子になったFXMLでインスタンス化されたコントローラがバインドされる
     * フィールド名は"子FXMLのid + Controller"でなければならない.
     */
    @FXML
    private DirTextFieldController dirTextFieldController;

"ネストしたコントローラのid"と同じフィールド名によって、そのコンポーネントを得ることができ、
"ネストしたコントローラのid + Controller"というフィールド名によって、その子コントローラを得ることができる。


ここでは親FXMLのロード方法は変更する必要はない。

実行結果

実行結果としては、カスタムコンポーネントもネストしたコントローラも同じである。


なので、画面キャプチャは省略する。


カスタムコントロールとネストしたコントローラの使い分けとしては、

ということではないか、と考えられる。

CSSの適用

FXMLはUIコンポーネントの構造を表現しており、スタイルはcssで表現することができるようになっている。

http://docs.oracle.com/javafx/2/api/javafx/scene/doc-files/cssref.html


CSSといっても、FXML自体にレイアウトペイン各種が存在しているため、CSSでレイアウトを決めるということはない。

CSSではフォントや前景色、背景色などの、こまかな調整を行うために使うことができる。


同様なことはFXML上のインラインスタイルでも指定可能であるが、パフォーマンス的にはcssのほうが良いようである。*6



CSSではあるが、記述ルールがCSSというだけであり、HTMLなどで使われるウェブのCSSとは何の関係もない。


すべての属性は「-fx-」のベンダプレフィックスにより始まっており、使われているプロパティ名も、基本的にはJavaFXコンポーネントのプロパティ名を元にしている、とのことである。*7

たとえば、ボタンテキストの前景色(いわゆるフォントの色)を指定するのには「-fx-text-fill」という属性になる。

これはHTMLとは何の関係もなく、textFillプロパティ側からの命名である。


また、FXMLのCSSは、使えるクラス名としては、

  • .root → ルート(すべてに適用)
  • .クラス名を小文字化・ハイフン区切りにしたもの → そのクラスのコンポーネントに適用
    • Buttonクラスならば、.button
    • ToggleButtonクラスならば、.toggle-button
  • FXML側でUI要素ごとに任意のクラス名を追加指定することも可能
  • ボタンなどはhoverなどの擬似セレクタも使用可能

などの既定のルールがある。


使用例

以下のCSSをFXMLと同じディレクトリ上の「SimpleApp5.css」という名前で保存しておく。


また、同じディレクトリ上に背景画像用の「background-image.jpg」を置いておく。*8

.root {
    -fx-background-color: #ffff80;
    -fx-font-size: 12pt;
    -fx-background-image: url("background-image.jpg");
    -fx-background-repeat: stretch;
    -fx-background-size: 800 400;
    -fx-background-position: right bottom;
}

.button {
    -fx-background-color: #8888ff;
    -fx-text-fill: #ffff88;
    -fx-background-insets: 2;
    -fx-background-radius: 10;
    -fx-padding: 3 20 3 20;
}

.text-field {
    -fx-background-insets: 3;
    -fx-background-color: #eeeeee;
}

.label {
    -fx-text-fill: black;
}

.button:hover {
    -fx-background-color: #80f0ff;
}


FXML側では"@"による相対位置指定で、このCSSリソースを相対参照するように設定する。

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

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity"
	xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
	stylesheets="@SimpleApp5.css">
...(中略)...
</VBox>

内部でfx:includeされているネストされたコントローラは暗黙で親のCSSを引き継ぐので、同じCSSが適用される。


CSSはプログラム的に切り替えることも可能である。

実行結果

このようにCSSによってレイアウトや見た目の細かな調整ができるようになっている。


これらについてはコードは不要であり、デザインとロジックが分離されているため、コードで書くよりも何をしているのか、あとから見てもわかりやすいといえる。


※ 問題は、cssで使える属性が何か?を把握することであるが、Java Day Tokyo 2015で聞いてきたテクニックとして、
Scene BuilderのViewメニューから「Show CSS Analyzer」を選択すると、適用可能な属性一覧と、現在選択している要素の属性のソースがどこからきているか、などをチェックできるので、これを使うと便利とのことである。
(ただし、Scene Builder側からはcssを編集することはできない。確認には、そこそこ便利に使える。)


Antによる実行可能jarの作成と、JavaFXのランタイムを含む実行イメージの作成方法

Java8には"ant-javafx.jar"というant用のプラグインが同梱されており、
JavaFXをjar化するためのタスクを使えるようになっている。

実行可能jarの作成

JavaFX8は通常の実行可能jarとしてビルドできるが、せっかくなので、ant-javafxプラグインを使ってビルドしてみる。


以下、build.xmlを示す。

<?xml version="1.0" encoding="UTF-8"?>
<project name="project" default="default" basedir="."
    xmlns:fx="javafx:com.sun.javafx.tools.ant">

    <!-- ビルド情報 -->
    <description>SimpleApp5のビルド</description>
    <property name="appName" value="SimpleApp5"/>
    <property name="mainClass" value="jp.seraphyware.simpleapp5.SimpleApp5"/>
    <property name="vendorName" value="seraphyware.jp"/>

    <!-- Jdk8のjavapackagerのantタスクを定義する -->
    <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
        uri="javafx:com.sun.javafx.tools.ant"
        classpath=".:${java.home}/../lib/ant-javafx.jar" />

    <!-- アプリケーション情報 -->
    <fx:application
        id="appid"
        name="${appName}"
        mainClass="${mainClass}"/>

    <!-- 実行可能jarとしてビルドする -->
    <target name="default" description="ビルドと実行可能jarの作成">
        <delete dir="work"/>
        <mkdir dir="work"/>
        <javac srcdir="src" destdir="work"
            includeantruntime="false">
            <classpath>
                <pathelement path="work"/>
            </classpath>
        </javac>

        <fx:jar destfile="${appName}">
            <fx:application refid="appid"/>

            <fileset dir="work"/>
            <fileset dir="src" excludes="**/*.java"/>

            <manifest>
                <attribute name="Implementation-Vendor" value="${vendorName}"/>
                <attribute name="Implementation-Version" value="1.0"/>
            </manifest>
        </fx:jar>
        <delete dir="work"/>
    </target>
</project>

このant-javafx.jarは、JDK8の中に含まれているが、Antから暗黙では参照できないので「${java.home}/../lib/ant-javafx.jar」のように指定している。

JREを含む実行可能イメージの作成

このant-javafxプラグインには他にも様々な機能があり、JREを含む"自己完結型アプリケーションのパッケージ化"が可能になっている。

たとえば、fx:deployタスクを使い、nativeBundler="all"と設定すると、

が作成される。


※ それぞれの環境でビルドする必要がある。(dmg,exe,debが欲しいならば3つのosでビルドしなければならない。)


以下はJREを含む実行可能イメージの作成用スクリプトである。

    <!-- バンドルの作成 -->
    <target name="makeBundles" depends="default"
        description="自己完結型アプリケーションのパッケージ化">
        <delete dir="dist"/>
        <mkdir dir="dist"/>

        <!-- JREを含む実行イメージの作成.(imageタイプ)

         nativeBundles="all"にすると、現在のビルド時の環境で作成できる、
         すべてのタイプのインストーラを含む配布パッケージを作成する.

         * WindowsならInnoSetupがあればexe形式、WiXがあればmsi形式を作成する.
         * Macの場合はdmgとpkgを作成する.
         * Linuxの場合はdebとrpmを作成する.
         -->
        <fx:deploy
            width="800"
            height="600"
            nativeBundles="image"
            outdir="dist"
            outfile="${appName}">

            <fx:application refid="appid"/>

            <fx:resources>
                <fx:fileset dir="." includes="*.jar"/>
            </fx:resources>

            <fx:info
                title="${appName}"
                vendor="${vendorName}"/>
        </fx:deploy>
    </target>

これをWindowsで実行すると、以下のようなフォルダが得られる。

dist
└─bundles
    └─SimpleApp5
        │  msvcp100.dll
        │  msvcr100.dll
        │  packager.dll
        │  SimpleApp5.exe
        │  SimpleApp5.ico
        │  
        ├─app
        │      SimpleApp5.cfg
        │      SimpleApp5.jar
        │      
        └─runtime
            │  COPYRIGHT
            │  LICENSE
            │  README.txt
            │  THIRDPARTYLICENSEREADME-JAVAFX.txt
            │  THIRDPARTYLICENSEREADME.txt
            │  Welcome.html
            │  
            ├─bin
            │  │  attach.dll
            │  │  awt.dll
            │  │  bci.dll
            │  │  dcpr.dll
            │  │  decora_sse.dll
            │  │  deploy.dll
            │  │  dt_shmem.dll
            │  │  dt_socket.dll
            │  │  fontmanager.dll
            │  │  fxplugins.dll
            │  │  glass.dll
            │  │  glib-lite.dll
            │  │  gstreamer-lite.dll
            │  │  hprof.dll
            │  │  instrument.dll
            │  │  j2pcsc.dll
            │  │  j2pkcs11.dll
            │  │  jaas_nt.dll
            │  │  java.dll
            │  │  JavaAccessBridge-64.dll
            │  │  javafx_font.dll
            │  │  javafx_font_t2k.dll
            │  │  javafx_iio.dll
            │  │  java_crw_demo.dll
            │  │  jawt.dll
            │  │  JAWTAccessBridge-64.dll
            │  │  jdwp.dll
            │  │  jfr.dll
            │  │  jfxmedia.dll
            │  │  jfxwebkit.dll
            │  │  jli.dll
            │  │  jpeg.dll
            │  │  jsdt.dll
            │  │  jsound.dll
            │  │  jsoundds.dll
            │  │  kcms.dll
            │  │  lcms.dll
            │  │  management.dll
            │  │  mlib_image.dll
            │  │  msvcr100.dll
            │  │  net.dll
            │  │  nio.dll
            │  │  npt.dll
            │  │  prism_common.dll
            │  │  prism_d3d.dll
            │  │  prism_es2.dll
            │  │  prism_sw.dll
            │  │  resource.dll
            │  │  sawindbg.dll
            │  │  splashscreen.dll
            │  │  sunec.dll
            │  │  sunmscapi.dll
            │  │  t2k.dll
            │  │  unpack.dll
            │  │  verify.dll
            │  │  w2k_lsa_auth.dll
            │  │  WindowsAccessBridge-64.dll
            │  │  zip.dll
            │  │  
            │  ├─plugin2
            │  │      msvcr100.dll
            │  │      npjp2.dll
            │  │      
            │  └─server
            │          classes.jsa
            │          jvm.dll
            │          Xusage.txt
            │          
            └─lib
                │  accessibility.properties
                │  calendars.properties
                │  charsets.jar
                │  classlist
                │  content-types.properties
                │  currency.data
                │  flavormap.properties
                │  fontconfig.bfc
                │  fontconfig.properties.src
                │  hijrah-config-umalqura.properties
                │  javafx.properties
                │  javaws.jar
                │  jce.jar
                │  jfr.jar
                │  jfxswt.jar
                │  jsse.jar
                │  jvm.hprof.txt
                │  logging.properties
                │  management-agent.jar
                │  meta-index
                │  net.properties
                │  plugin.jar
                │  psfont.properties.ja
                │  psfontj2d.properties
                │  resources.jar
                │  rt.jar
                │  sound.properties
                │  tzdb.dat
                │  tzmappings
                │  
                ├─amd64
                │      jvm.cfg
                │      
                ├─cmm
                │      CIEXYZ.pf
                │      GRAY.pf
                │      LINEAR_RGB.pf
                │      PYCC.pf
                │      sRGB.pf
                │      
                ├─ext
                │      access-bridge-64.jar
                │      cldrdata.jar
                │      dnsns.jar
                │      jaccess.jar
                │      jfxrt.jar
                │      localedata.jar
                │      meta-index
                │      nashorn.jar
                │      sunec.jar
                │      sunjce_provider.jar
                │      sunmscapi.jar
                │      sunpkcs11.jar
                │      zipfs.jar
                │      
                ├─fonts
                │      LucidaBrightDemiBold.ttf
                │      LucidaBrightDemiItalic.ttf
                │      LucidaBrightItalic.ttf
                │      LucidaBrightRegular.ttf
                │      LucidaSansDemiBold.ttf
                │      LucidaSansRegular.ttf
                │      LucidaTypewriterBold.ttf
                │      LucidaTypewriterRegular.ttf
                │      
                ├─images
                │  └─cursors
                │          cursors.properties
                │          invalid32x32.gif
                │          win32_CopyDrop32x32.gif
                │          win32_CopyNoDrop32x32.gif
                │          win32_LinkDrop32x32.gif
                │          win32_LinkNoDrop32x32.gif
                │          win32_MoveDrop32x32.gif
                │          win32_MoveNoDrop32x32.gif
                │          
                ├─jfr
                │      default.jfc
                │      profile.jfc
                │      
                ├─management
                │      jmxremote.access
                │      jmxremote.password.template
                │      management.properties
                │      snmp.acl.template
                │      
                └─security
                        blacklist
                        blacklisted.certs
                        cacerts
                        java.policy
                        java.security
                        javaws.policy
                        local_policy.jar
                        trusted.libraries
                        US_export_policy.jar

SimpleApp5.exeという、本物の実行可能ファイルが作成されており、
従来であれば、launch4jなどのサードパーティのツールを使ってexe化し配布用パッケージを作成していたものが、標準の方法だけでexe化することができる。

また、これは配下のruntime下にコピーされているJREのサブセットを利用して実行されるように設定されている。


したがって、この「dist\bundles\SimapleApp5」以下のフォルダごと圧縮して他のマシンに持って行けば、マシンに既にJava8u40がインストールされているか問わず、そのまま利用可能となる。


※ なお、これはJREをまるごとコピーしたものではなく、ファイルをみると実行に必要のないツール類(*.exe)は含まれていない。

※ なお、JNLPを使ったウェブ配備用のファイルも作成されるが、今回は使う予定はないので無視している*9


これはWindowsの例であるが、同様にMacで実行すると、やはりMacで実行するためのアプリケーションバンドルが作成される。

とくにMacの場合はアプリケーションバンドルにすることでスクリーンメニュー上のアプリケーション名も、きちんと設定されるようになる、という重要なメリットがある。

結論

使ってみたところ、とくには嵌る点もなく、案外使いやすい。

複雑なものについては、まだ試していないので分からないのだが、簡単に使う分には、気軽に使える程度のものであることは分かった。


Scene Builderによるグラフィカルな編集ができるのは、やはり大きなメリットだと思う。


また、Scene Builderを使わずとも、FXML自体もわかりやすいので、パッと見て、どんな画面になるのか想像できる。

Swingのようにコードを読みながら画面がどう構成されるのかを考える難しさは軽減されており、レイアウトまわりの見通しが大変良いため、画面改修時の保守性が高いことは間違いないと思われる。


なので、とりあえず、今後はJavaFXの習得を兼ねて、簡単なところからJavaFXを使ってみたいと思う。


今回のコードは、GitHubに置いてあります。

今までに判明している注意点
  1. JavaFX8に対応していない環境だと画面が激しく乱れる。(サポートされていないグラボがあるらしい?)
    • MacBook2007LateのLion上に入れたJdk8で発生。Sceneがぐちゃぐちゃになり何も見えない。
  2. 動きがおかしなところがあったりする。たとえば、とても長いテーブルのホイールスクロールとか、かなり怪しい。(使い方が悪いのかも?)
  3. JavaFX標準のチャートのカスタマイズは大変難しい。(cssを使わざるを得ず、且つ、それが大変めんどくさい。)
  4. JogAmpや、ImageIOといった類がSwingやBufferedImageを想定しており、JavaFXでは直接使えないので、いろいろ苦労しそう。
  5. バージョンによって動きが変わることがある。バグが結構残存している感があり、updateのたびに改善されているようだ。

*1:http://www.oracle.com/technetwork/jp/java/javafx/overview/faq-1446554-ja.html#6

*2:JDT2015の櫻庭さんのセッションによると http://gluonhq.com/gluon-supports-scene-builder/ からビルド済みのものが取得できるらしい?

*3:JDT2015の櫻庭さんのセッションによると、Eclipse NetBeans等があればSceneBuilderはビルドできるらしい。

*4:初期化のためにをオーバーライドできる。

*5:自分でソースからビルドしたScene Builderの場合は連携がうまくゆかない場合もあるらしい。

*6:JDT2015で聞いてきたところによると

*7:JDT2015で聞いてきたところによると

*8:画像は、こちらのフリー素材からいただいたものをサイズ加工しています。 http://flowerillust.com/html/orange/orange1001.html

*9:いらないので作成したくないのだが、どうやっても必ず作成されるようだ。