OpenJFX + OpenJDK で JavaFX を動かす

今までOracle JDKJavaFXを使っていたみなさま、こんにちは。

なんと、Java SE 11からOracle JDKは無償版がなくなりました!(開発のみであれば、無償でも使用できますけど...) しかたないので、OpenJDKを使わざるをえないと思われている方も多いと思います。

ところが、OpenJDKにはJavaFXが含まれていないのです!!

「えっ、OpenJFXってOpenJDKのプロジェクトじゃないの?」と思われるかもしれません。確かにOpenJFXはOpenJDKのプロジェクトなのです。でも、OpenJDKのプロジェクトがJava SEのRIであるOpenJDKに含まれるというわけではありません。

OpenJFXも、Java SEのRIであるところのOpenJDKには含まれていないのです。

とはいっても、OpenJFXを自分でビルドして使うのはハードルが高すぎますね。そうしたら、OpenJFXのビルドを公開してくれるようになりました!

これで、OpenJFXとOpenJDKでJavaFXを使えます!!

ところが、結論を先にいうと、「OpenJFX + OpenJDKはモジュールではないアプリケーションでも、モジュールを指定しないと動きません」です。

以下に解説しますが、手っ取り早くやり方を知りたい人は途中をすっ飛ばして、「OpenJFXでのJavaFXの起動設定」を読んでください。

OpenJDKとOpenJFXのセットアップ

OpenJFXのダウンロードは下のURLからできます。

OpenJFX Early-Access Builds

http://jdk.java.net はOpenJDKも公開しています。今回は、まだリリース前ですがOpenJDK 11のEaryly Accessを試してみましょう。使用したのは、5月25日に公開されたjdk-11-ea+15です。

OpenJDKはインストーラーは含まれていないので、自分でtar.gzのファイルを展開して、binにパスを通しておきます。

次にOpenJFXです。OpenJFXは5月9日に公開されたopenjfx-11-ea+13を使用しました。

ダウンロードしたopenjfx-11-ea+13_windows-x64_bin-sdk.zipを展開すると、bin、legal、libの3つのディレクトリが含まれていることが分かります。binディレクトリにはネイティブライブラリ(Windowsの場合はDLL)が配置されているので、ここにもパスを通しておきます。

libディレクトリにはJARファイルがモジュールごとに配置されてます。

とりあえず、JavaFXのサンプルを実行してみる

では、JavaFXのプログラムをビルドして、実行してみましょう。

今回のサンプルはこちら。

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

public class FxSample extends Application {
    public void start(Stage stage) {
        Button button = new Button("OK");
        button.setOnAction(e -> System.out.println("Clicked!"));

        Scene scene = new Scene(button, 300, 200);
        stage.setScene(scene);

        stage.setTitle("FxSample");
        stage.show();
    }

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

ボタンを表示して、ボタンをクリックすると"Clicked!"と標準出力に出力するだけのプログラムです。

もちろん、モジュールにはしません。

では、さっそくコンパイルして、実行してみましょう。

C:\javafx-sdk-11>java -version
openjdk version "11-ea" 2018-09-25
OpenJDK Runtime Environment 18.9 (build 11-ea+15)
OpenJDK 64-Bit Server VM 18.9 (build 11-ea+15, mixed mode)

C:\javafx-sdk-11>javac -cp lib\* FxSample.java

C:\javafx-sdk-11>java -cp lib\*;. FxSample
エラー: JavaFXランタイム・コンポーネントが不足しており、このアプリケーションの実行に必要です

C:\javafx-sdk-11>

コンパイルはできたのですが、実行ができません!

しかも、このエラーメッセージ、全然意味が分かりません。ランタイムコンポーネントって何なんでしょう?

はじめはDLLが読み込まれていないのかと思ったのですが、どうやら違ったようです。

そこで、何が起こっているのか知るためにverboseオプションをつけて起動してみました。

C:\javafx-sdk-11>java -verbose -cp lib\*;. FxSample
[0.008s][info][class,load] opened: C:\jdk-11\lib\modules
[0.018s][info][class,load] java.lang.Object source: jrt:/java.base
[0.019s][info][class,load] java.io.Serializable source: jrt:/java.base
[0.019s][info][class,load] java.lang.Comparable source: jrt:/java.base
[0.023s][info][class,load] java.lang.CharSequence source: jrt:/java.base
[0.024s][info][class,load] java.lang.String source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.AnnotatedElement source: jrt:/java.base
[0.025s][info][class,load] java.lang.reflect.GenericDeclaration source: jrt:/java.base
[0.026s][info][class,load] java.lang.reflect.Type source: jrt:/java.base
[0.027s][info][class,load] java.lang.Class source: jrt:/java.base
     <<以下、長くなるので省略>>

verboseオプションをつけると、どのクラスがロードされているのが分かります。たとえば、一番初めにロードされたのがjava.baseモジュールに含まれるjava.lang.Objectクラスだということが分かります。

ロードされたクラスをチェックしてみると、なんとJavaFXのクラスがまったくロードされていないのでした。

それじゃ、実行できるわけがないですね。

しかし、クラスパスは指定しているのに、なぜクラスがロードされていないのでしょう?

ここで、思い浮かぶのがJava SE 10でのJAXBなどJava EE系のモジュールの扱いです。これらのモジュールは標準のモジュールパスからは外されているので、使用するときにはモジュールでないアプリケーションであっても--add-modulesで指定する必要があるのです。

もしかしたら、ここで起こっていることも同じことなのかもしれません。

だとしたら、解決は簡単で、モジュールパスにlibを追加して、ロードするモジュールを指定すればいいのです。

OpenJFXでのJavaFXの起動設定

では、実際に起動設定を見ていきましょう。

モジュールパスは--module-pathオプションもしくは-pオプションで指定します。モジュールパスで指定するのはモジュールが配置されているディレクトリです。

モジュール化したアプリケーションであれば、どのモジュールを使用するかはmodule-infoに記述します。

しかし、モジュールではないアプリケーションでは、--add-modulesオプションで指定する必要があります。

どのモジュールを使用しているかはjdepsコマンドで調べることができます。

C:\javafx-sdk-11>jdeps --module-path lib -s FxSample.class
FxSample.class -> java.base
FxSample.class -> javafx.base
FxSample.class -> javafx.controls
FxSample.class -> javafx.graphics
javafx.base -> java.base
javafx.base -> java.desktop
javafx.controls -> java.base
javafx.controls -> javafx.base
javafx.controls -> javafx.graphics
    <<以下、省略>>

jdepsコマンドでは、モジュールパスの指定に-pは使用できないので、--module-pathを使用します。もう1つの-sオプションはシンプル表示のためのオプションです。

この結果を見ると、FxSampleクラスはjava.base、javafx.base、javafx.controls、javafx.graphicsの4つのモジュールを使用していることが分かります。

しかし、java.baseモジュールはデフォルトでロードされるので、指定する必要はありません。

また、javafx.controlsモジュールはjavafx.baseモジュールとjavafx.graphicsモジュールを使用しています。このため、javafx.controlsだけを指定すれば、javafx.baseモジュールとjavafx.graphicsモジュールは自動的にロードされます。

この結果、--add-modulesオプションで指定するのはjavafx.controlsモジュールだけでよいことが分かりました。

では、実行してみましょう。

C:\javafx-sdk-11>java -p lib --add-modules javafx.controls FxSample

これで、以下のようなウィンドウが表示されるはずです。

ControlsFX 準備編 その2

このEntryは JavaFX Advent Calendar の最終日のエントリーです。

昨日は id:yumix_h さんの JavaFXのHTMLEditorの機能と限界 でした。

今日もControlsFXの紹介です。

23日のエントリー でモジュールではないアプリケーションからの使い方を紹介しました。

今日はモジュールアプリケーションからControlsFXを使用します。

でも、その前にProject Jigsawのモジュールについて簡単に解説しましょう。ここでは、JavaFXでの例で示していますけど、他のモジュールアプリケーションでも同じようなことはあるはずです。

モジュール

module-info.javaがあればモジュールです。そして、module-info.javaには依存性(requires文)、公開範囲(exports文)を記述します。

基本的にはこれだけですが、FXMLを使用している場合、コントローラクラスがリフレクションでアクセスされるため、それを許可するopens文も欠かせません。

では、前回のサンプルがどのようなモジュールに依存しているか調べてみましょう。そのためには、jdepsコマンドを使用します。

C:\controlsfx\controlsfxdemo>jdeps -cp lib\controlsfx-9.0.0.jar -s dist\controlsfxdemo.jar
controlsfxdemo.jar -> lib\controlsfx-9.0.0.jar
controlsfxdemo.jar -> java.base
controlsfxdemo.jar -> javafx.fxml
controlsfxdemo.jar -> javafx.graphics

java.base、javafx.fxml、javafx.graphicsの3つのモジュールに依存していることが分かります。ただし、java.baseだけはmodule-info.javaに記述する必要はありません。

公開するのは、net.javainthebox.fxパッケージなので、これを含めてmodule-info.javaを記述します。

ここでは、モジュール名をnet.javainthebox.controlsfxdemoとします。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

しかし、これだけではビルド、実行させることができません。もちろん、ControlsFXの扱いのためです。

controlsfx-9.0.0.jarはモジュールではないので、注意する必要があります。

モジュールの種類

実をいうと、モジュールには3種類あります。

module-info.javaが存在するのが普通のモジュールですが、それ以外に2種類のモジュールがあります。

  • 通常のモジュール
  • 自動モジュール (Automatic Module)
  • 名前なしモジュール (Unnamded Module)

Automatic ModuleもUnnamed Moduleもmodule-info.javaがないモジュールです。これらの違いはモジュールが直接依存しているのか、間接的に依存しているかという違いです。

  • モジュールが直接アクセスするJARファイル -> Automatic Module
  • モジュールがアクセスするJARファイルがアクセスするJARファイル -> Unnamed Module

ということになります。

moudle-info.javaを含むモジュールはクラスパスを使用することができず、すべてモジュールパスで扱います。モジュールでないJARファイルもモジュールパスで指定したディレクトリにおけば、モジュールとして扱われるのです。

そして、そのモジュールとして扱われるJARファイルをAutomatic Moduleと呼びます。

モジュールパスでアクセスするということは、module-info.javaに記述しなくてはいけません。そのため、Automatic Moduleとして扱われるJARファイルはモジュール名が必要になります。

この場合、マニフェストファイルMANIFEST.MFのAutomatic-Module-Name属性で指定された名前を使用します。Automatic-Module-Name属性が指定されていない場合、JARファイルの名前がモジュール名として使用されます(バージョン表記などが含まれていても、それは取り除かれます)。

controlsfx-9.0.0.jarファイル場合、Automatic-Module-Name属性は記述されていません。そのため、ファイル名からcontrolsfxがモジュール名として使用されます。

さらに、Automatic Moduleが使用するJARファイルがUnnamed Moduleとして扱われます。これは名前がないので、今までと同じようにクラスパスで指定します。

では、module-info.javaにAutomatic Moduleのcontrolsfxモジュールを追加してみます。

module net.javainthebox.controlsfxdemo {
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これで、ビルドはできるようになります。しかし、実行してみると...

C:\controlsfx\controlsfxdemo>java -p dist;lib -m net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo
Exception in Application start method
java.lang.reflect.InvocationTargetException
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication(Unknown Source)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.base/java.lang.reflect.Method.invoke(Unknown Source)
        at java.base/sun.launcher.LauncherHelper$FXHelper.main(Unknown Source)
Caused by: java.lang.RuntimeException: Exception in Application start method
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.launchApplication1(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication$2(Unknown Source)
        at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.NoClassDefFoundError: javafx/scene/control/TextField
        at java.base/java.lang.ClassLoader.defineClass1(Native Method)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
        at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown Source)
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadTypeForPackage(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadType(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.importClass(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processImport(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.processProcessingInstruction(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
        at javafx.fxml/javafx.fxml.FXMLLoader.load(Unknown Source)
        at net.javainthebox.controlsfxdemo/net.javainthebox.fx.ControlsFXDemo.start(ControlsFXDemo.java:14)
        at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(Unknown Source)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(Unknown Source)
        at java.base/java.security.AccessController.doPrivileged(Native Method)
        at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(Unknown Source)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
        at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source)
        ... 1 more
Caused by: java.lang.ClassNotFoundException: javafx.scene.control.TextField
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
        at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
        ... 33 more
Exception running application net.javainthebox.fx.ControlsFXDemo

C:\controlsfx\controlsfxdemo>

javafx.scene.control.TextFieldクラスが見つからないという例外が起こってしまいました。

これはcontrolsfxがJavaFXのモジュールに依存しているためです。しかし、Automatic Moduleでは依存性が記述できないため、TextFieldクラスを含むモジュールをロードできないためです。

その場合、controlsfxが依存するモジュールも一緒にmodule-info.javaに記述します(起動時オプションで指定する方法もあります)。

controlsfxが依存しているモジュールを調べるために、jdepsコマンドを使用してみましょう。

C:\controlsfx\controlsfxdemo>jdeps -s lib\controlsfx-9.0.0.jar
controlsfx-9.0.0.jar -> java.base
controlsfx-9.0.0.jar -> java.desktop
controlsfx-9.0.0.jar -> javafx.base
controlsfx-9.0.0.jar -> javafx.controls
controlsfx-9.0.0.jar -> javafx.graphics
controlsfx-9.0.0.jar -> javafx.media
controlsfx-9.0.0.jar -> javafx.web

ここで表示されたすべてのモジュールを記述する必要はありません。実際に使用しているモジュール、ここではTextFieldクラスが含まれるjavafx.controlsモジュールだけを追加すればOKです。

module net.javainthebox.controlsfxdemo {
    requires javafx.controls;
    requires javafx.fxml;
    requires javafx.graphics;

    requires controlsfx;
    
    exports net.javainthebox.fx;
    opens net.javainthebox.fx to javafx.fxml;
}

これでモジュールのアプリケーションからControlsFXを使用することができます。

なお、NetBeansではビルドはできるものの、実行はエラーになってしまいます。いろいろ試してみたのですが、結局実行できませんでした。

なので、実行だけはコマンドラインから行っています。

準備編だけで長くなってしまったので、個々のコントロールについてはまた後日。

ControlsFX 準備編

このEntryは JavaFX Advent Calendar の22日目のエントリーです。

昨日は id:hagi44 さんの 業務で JavaFX をちょっとだけ使ってみた でした。明日は id:yumix_h さんが再び登場です。

なんか、ITproの連載が終わってから、完全にJavaはオフモードになってしまっていますが、久しぶりにblog書くのではてな記法もぜんぜん覚えていないというていたらく。

まぁ、仕事ではJavaJavaFXも使っているので、Javaに触っていないというわけではないのですが...

ということで、今日はControlsFXを紹介しようと思います。

ControlsFX

ControlsFXはJonathan Gilesさんを中心にJavaFXのコントロールを提供するためのプロジェクトです。

ControlsFXのコントロールの中には、DialogのようにJavaFXの標準コントロールに採用されたものもあります。

今年のJavaOneのDuke's Choice Awardも獲得しています!

とことが、JonathanがOracleからMicrosoftに転職してしまったのです!

ということで、Duke's Choice Award受賞記念と、転職のはなむけにControlsFXを紹介していきます!!

ControlsFXが提供しているコントロールおよびその周辺機能としては、以下のようなものがあります。

  • Actions
  • Borders
  • BreadcrumbBar
  • ButtonBar
  • CheckComboBox / CheckListView / CheckTreeView
  • Decoration / Validation
  • Dialogs
  • FXSampler
  • Glyph font pack support
  • GridView
  • HiddenSidesPane
  • HyperlinkLabel
  • InfoOverlay
  • ListSelectionView
  • MasterDetailPane
  • Notifications / NotificationPane
  • PlusMinusSlider
  • PopOver
  • PropertySheet
  • RangeSlider
  • Rating
  • SegmentedButton
  • SnapshotView
  • SpreadsheetView
  • TaskProgressView
  • TextFields
  • Top Quality JavaDocs!
  • Translations

このリストはContorlsFXのサイトからコピペしてきたものなので、私もよくわかっていないものも入ってます ^ ^;;

今日は準備編として、ControlsFXを使えるところまで紹介します。

ダウンロードとサンプルの実行

ControlsFXは現状、2つのバージョンが公開されています。

  • Java SE 9用のControlsFX 9.0.0
  • Java SE 8用のControlsFX 8.40.14

当然、Java SE 9用のControlsFX 9.0.0を使っていきます。

ダウンロードは ダウンロード用のリンク から行います。

ダウンロードするファイルはcontrolsfx-9.0.0.zipです。このファイルを展開すると、ライセンスのテキストとJARファイルが3つあります。

この中のcontrolsfx-samples-9.0.0.jarファイルがサンプルです。さっそく実行してみましょう。

C:\controlsfx-9.0.0>java -jar controlsfx-samples-9.0.0.jar
Initialising FXSampler sample scanner...
        Discovering projects...
                Found project 'ControlsFX', with sample base package 'org.controlsfx.samples'
WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.getInstance()
WARNING: Please consider reporting this to the maintainers of impl.org.controlsfx.ReflectionUtils
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

実行はできましたけど、なんか怒られてます。

これはJava SE 9のモジュール化の影響で、隠蔽しているクラスをリフレクションで使用しているために警告されているのです。メッセージにあるように--illegal-access=warnを実行時オプションとして付記すれば、警告はでますが、問題なく実行できます。

逆に--illegal-access=warnを付けないと、実行できたとしても、リフレクションが正常に動作しない部分があるはずです(確かめてないですけど)。

では、--illegal-access=warnを付けて、実行してみましょう。

C:\controlsfx-9.0.0>java --illegal-access=warn -jar controlsfx-samples-9.0.0.jar
Initialising FXSampler sample scanner...
        Discovering projects...
                Found project 'ControlsFX', with sample base package 'org.controlsfx.samples'
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.getInstance()
WARNING: Illegal reflective access by impl.org.controlsfx.ReflectionUtils (file:/C:/controlsfx-9.0.0/controlsfx-9.0.0.jar) to method com.sun.javafx.css.StyleManager.addUserAgentStylesheet(java.lang.String)

先ほどとはメッセージが変わっています。警告はしたけれども、実行できているということです。

実行すると、以下のようなウィンドウが表示されます。

右側の項目を選択すれば、その機能のデモが中央に表示されます。そして、右側に説明が表示されます。

たとえば、Bordersを選択すると、こんな感じ。

こんな感じで動かしてみれば、どういうコントロールが提供されているか分かるはずです。

Scene Builderへの組み込み

コントロールを使うのであれば、Scene Builderで使えなければ!!

ということで、Scene Builderでも使えるようにしてみましょう。

Scene Builderの左側のコントロールのリストの上に歯車アイコンがあります。

そこをクリックすると、下のようなメニューが表示されます。

で、JAR/FXML Managerを選択します。すると、Library Managerダイアログが表示されます。

ローカルにあるJARファイルを追加する場合は3番目の[Add Library/FXML from file system]を選択します。すると、ファイルチューザーが表示されるので、ダウンロードしたcontrolsfx-9.0.0.jarを指定します。

すると、JARファイルに含まれているコントロールが一覧表示されます。

必要なコントロールをチェックして (というか、全部チェックすればOKですが)、[Import Components]を選択すれば、OK。

すると、コントロールの一覧に[Custom]グループが追加されて、そこに今追加したコントロールが表示されます。

Mavenなどのレポジトリからダウンロードすることもできます。その場合は一番上の[Search repositories]もしくは、次の[Manually add Library from repository]を選択します。

[Manually add Library from repository]から説明しましょう。この項目を選択すると、Add Library from Repositoryダイアログが表示されます。

上の図のようにGroup IDにはorg.controlsfx、Artifact IDにはcontrolsfxを指定します。すると、バージョンが選択できるようになるので、9.0.0を選択します。

[Search repositories]の場合、先ほどのGroup IDもしくはArtifact IDをどちらかを入力すると、それに合致するライブラリを表示するので、追加します。

これで、標準のコントロールと同じようにControlsFXのコントロールを使用することができます。

試しに、AnchorPaneにCustomTextFieldを貼ってみたのが、下の図です。

このFXMLはこんな感じになってました。

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

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import org.controlsfx.control.textfield.CustomTextField?>


<AnchorPane prefHeight="200.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1">
   <children>
      <CustomTextField fx:id="textField" layoutX="74.0" layoutY="63.0" promptText="Custom TextField" AnchorPane.leftAnchor="50.0" AnchorPane.rightAnchor="50.0" AnchorPane.topAnchor="50.0">
         <font>
            <Font size="16.0" />
         </font>
      </CustomTextField>
   </children>
</AnchorPane>

ControlsFXを使用する (非モジュールアプリケーション)

といっても、普通のライブラリと同じです。

ここでは、NetBeansのNightly Buildを使います。

それにしても、NetBeansはいつになったら、Java SE 9に対応したバージョン出してくれるんでしょうねぇ...

試しに、controlsfxdemoというプロジェクトを作成しました。

Mavenのプロジェクトではなく、普通のプロジェクトなので、ライブラリにcontrolsfx-9.0.0.jarを追加してください。

では、先ほどのFXMLをそのまま使ってみます。

メインクラスをnet.javainthebox.fx.ControlsFXDemo、FXMLをControlsFXDemoView.fxml、コントローラクラスをnet.javainthebox.fx.ControlsFXDemoControllerとしました。

プロジェクトの構成はこんな感じ。

ControlsFXDemoクラスはFXMLをロードするだけです。

package net.javainthebox.fx;

import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class ControlsFXDemo extends Application {
    
    @Override
    public void start(Stage stage) throws IOException {
        AnchorPane root = FXMLLoader.load(getClass().getResource("ControlsFXDemoView.fxml"));
        
        Scene scene = new Scene(root);
        
        stage.setTitle("ControlsFX Demo");
        stage.setScene(scene);
        stage.show();
    }

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

FXMLは先ほどのFXMLにコントローラクラスの指定を加えてます(整形してあります)。

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

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import org.controlsfx.control.textfield.CustomTextField?>

<AnchorPane prefHeight="200.0" prefWidth="400.0" 
            xmlns="http://javafx.com/javafx/9.0.1" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="net.javainthebox.fx.ControlsFXDemoController">
   <children>
      <CustomTextField fx:id="textField" 
                       layoutX="74.0" layoutY="63.0" 
                       promptText="Custom TextField" 
                       AnchorPane.leftAnchor="50.0" AnchorPane.rightAnchor="50.0" AnchorPane.topAnchor="50.0">
         <font>
            <Font size="16.0" />
         </font>
      </CustomTextField>
   </children>
</AnchorPane>

ControlsFXDemoControllerクラスは、以下の通り。というか、何もやってないです。

package net.javainthebox.fx;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import org.controlsfx.control.textfield.CustomTextField;

public class ControlsFXDemoController implements Initializable {

    @FXML
    private CustomTextField textField;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // TODO
    }    
    
}

これで実行してみましょう。

何も問題ないはず。

問題なのは、モジュールアプリケーションにした場合なのですが、それは次回。

JavaFXでバンプマッピング

このEntryは JavaFX Advent Calendar の最終日です。今年は誰も落とさずに、Adventしました!!すばらしい!!!!

昨日は @masanori_msl さんの JavaFX + Apache POIでSpreadsheet操作 でした。

さて、バンプマッピングです。

なぜ、バンプマッピングかというと、JavaFX Advent Calendar の 19 日目のひらおかさんの JavaFX で小惑星を描いてみよう で、私が Twitter で次のようにコメントしたからです。

とコメントしたのにもかかわらず、バンプマッピングのことってどこかに書いたことないなぁと思い出したわけです。

バンプマッピングとは

バンプ (bump) はデコボコのことです。ようするに、3D のオブジェクトの表面を擬似的にデコボコに描画することをいいます。

あくまでも擬似的なので、ほんとにデコボコを描くわけではないのですが、意外に使えるのです。

ちなみに、ほんとにデコボコを作るマッピングとしては、ディスプレイスメントマッピングというものがありますが、JavaFX ではサポートしていないのです ><

で、バンプマッピング

バンプマッピングにもいろいろあるらしいのですが、もっとも一般的で JavaFX でもサポートしている法線ベクトルを使用する方法を紹介します。

物体の表面の接線に直交している法線というのものを考えます。高校の数学でも法線出てきているので、思い出してね。

法線は英語だと Normal といいます。

その物体に当たった光は、その法線との入射角と同じ角度で反射します。

表面が均一だと、法線の向きもそろうのでキレイに反射します。鏡の表面のようなものです。でも、表面が荒れていて乱雑だと、法線の向きもバラバラになるので、反射光もどこに向かうか分からなくなります。すると、表面がマッドな感じになるわけです。

一般的には法線はベクトルとして扱います。3D CG をやっているとよく出てくる、法線ベクトルというやつです。

前述したように法線は表面の接面に直交したベクトルなのですが (3D なので接線ではなくて接面です)、これを意図的に変えてみようというのがバンプマッピングの基本的な考えです。

どういうことかというと、表面が均一でも下の図のように法線を違う方向にあえて向けてしまうのです。

すると、人間はあたかもそこにデコボコがあるかのように認知してしまうわけです。上の絵の場合、突起ですが、へっこみも同じように法線を内向きにすれば可能です。

でも、法線ベクトルを変更しているだけなので、本当にデコボコがあるわけではありません。あくまでも擬似的なものです。

バンプマッピングの準備

さて、バンプマッピングをやりたいわけですが、それに先駆けてやらなければならないことがあります。法線ベクトルを先に準備しなければならないということです。

オブジェクトにテクスチャを貼るように、法線ベクトルを表したマップを準備するのです。

マップの各ピクセルが法線ベクトルを表すようにします (厳密にはテクスチャと同じように、UV マッピングするので、ピクセルではないのですが...)。

では、どうやって各ピクセルで法線ベクトルを表すかというと、RGB の値にベクトルの x 座標 y 座標 z 座標を割り当ててしまいます。

といっても、そんなの作れないと思いますよね。実際には、ツールで作ってしまいます。

ここでは Photoshop を使った方法を紹介します。

今回はサンプルとして球に火星のテクスチャを貼る場合を考えてみます。火星の地形図は WikipediaGeoTemplate/mars を使用しました。

https://upload.wikimedia.org/wikipedia/commons/thumb/b/b7/Mars_G%C3%A9olocalisation.jpg/720px-Mars_G%C3%A9olocalisation.jpg

まずはこのイメージを Photoshop に読み込みます。そして、メニューバーの [フィルタ] - [3D] - [法線マップを生成] を選択します。

すると、下図のようなダイアログが表示されます。これは生成した法線マップでバンプマッピングしたものです。これで、法線マップがちゃんと使えそうかどうかを確認します。

ここでは、球に貼ろうとしているので、そのままデフォルトでかまいません。もし、違う形状に貼ろうとするのであれば、左下の [オブジェクト] を変更します。

他のパラメータは基本的には触らなくても大丈夫です。

これで、[OK] すると、元々の表示が紫主体の表示に変化しているはずです。後はこれを保存するだけです。

昔の赤青フィルタで作った 3D の映画のようですね。

他のツールでも同じように作成できるはずです。フリーのツールもあるので、ググってみてください。私はもっぱら Photoshop を使っているので、他のツールのどれがいいのかよく分からないのですが ^ ^;;

さて、これで法線マップができました。

JavaFXバンプマッピングをする

準備はできたので、JavaFXバンプマッピングをしてみましょう。

といってもそんなに難しいことではありません。JavaFX でテクスチャを貼る時に使用する javafx.scene.paint.PhongMaterial クラスを、バンプマッピングでも使用します。

    PhongMaterial material = new PhongMaterial();

    // テクスチャの設定
    material.setDiffuseMap(new Image(テクスチャの画像のURL文字列));

    // バンプマッピングの設定
    material.setBumpMap(new Image(法線マップ画像のURL文字列));

    // オブジェクトにマテリアルを設定
    mars.setMaterial(material);

setDiffuseMap メソッドがテクスチャで、setBumpMap メソッドが法線マップです。後はこれを 3D のノードにセットするだけです。

上のコードでは変数 mars が Sphere クラスのオブジェクトになっています。

これでおしまい。準備の方が大変なぐらいですね。

バンプマッピングをしたものと、していないものを比較して見ましょう。

左がバンプマッピング有り、右が無しです。

テクスチャを貼っただけでも何となくデコボコに見えますが、バンプマッピングをするとさらにデコボコが大きくなるような感じです。微妙といえば微妙かもしれませんが...

また、本当にデコボコを作っているわけではないので、縁の部分などはアラが見えてしまいます。それでも、十分使えると思いませんか。

というわけで、JavaFXバンプマッピングでした。

ソースは gist にあげてあります。

JavaFX Bump Mapping Demo

JavaFXのGUI構築ツール、Scene BuilderでFXML編集

このEntryは JavaFX Advent Calendar の 22 日目です。

昨日は @fukai_yas さんの ScalaFXのCell描画を実装する でした。

明日は kimukou さんです。

さて、Scene Builder ですが、ここでは解説しません。

というのも、今日公開された ITpro の 最新Java情報局 で書いているからです。

JavaFXのGUI構築ツール、Scene BuilderでFXML編集

JavaFX の解説はこれで 4 回目。ぜひ、1 回目から読んでください!!!!

ということで、ステマかつ手抜きのエントリーでしたw

Sooner or Later

このEntryは JavaFX Advent Calendar の16 日目です。

昨日は @khasunuma さんの JavaFX から Payara Micro を呼び出す際の注意点 でした。

明日は @nodamushi さんです。

そして、この Entry は Java Puzzlers Advent Calendar 2016 の 16 日目でもあります。

昨日は @khasunuma さんの Puzzle : String concatination でした。

そして、明日も @khasunuma さんです。

さて、JavaFX で Puzzle を書くわけですが、GUI は出てきません。

GUI の機能ではないのですが、JavaFX で重要な機能の 1 つにバインドがあります。みなさん、バインド使ってますか?

バインドは JavaFX のプロパティ間で値を自動的に同期させるための機構です。

たとえば、以下のコードではどうでしょう。

    IntegerProperty x = new SimpleIntegerProperty(0);
    IntegerProperty y = new SimpleIntegerProperty();
    y.bind(x);

    x.set(10);
    System.out.println(y.get());

JavaFX ではプロパティを なんちゃらProperty というクラスで表します。この なんちゃらProperty クラスを使うと、バインドももれなく着いてくるというわけです。

で、y は x にバインドしているので、x が変更されると y も勝手に値が更新されます。

なので、このコードを実行すると 10 が出力されます。

バインドは自動で同期してくれるだけではなく、バインドが遅延処理されることも特徴です。

上のコードだと、x が変更されたら、y が変更されるのではありません。y を使用する時に、x が変更されているかどうかをチェックします。この遅延処理によってムダな同期処理を減らすことができるのです。

さて、ここからが Puzzle です。

次のコードを実行したらどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
        y.addListener((observable, previous, present) -> {
            System.out.println("Change");
        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

他の Puzzlers Advent の人はちゃんと 4 択にしているんですけど、Java Puzzlers の本はそうでないんですよね。 4 択にするのは、あくまでもプレゼンテーションのためです。

なので、ここでは選択肢は出しません。実行したら、どうなるでしょうか?

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

答えは、次のようになります。

Change
Change
Change
Change
20

たぶん大方の人は、こうなると想像していたと思います。パズルにもなにもなっていないように感じられるかもしれません。

この問題は JavaFX をある程度知っている人が陥るパズルになっているのです。

試しに、次のコードではどうなるでしょう。

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;

public class SoonerOrLater {
    public static void main(String... args) {
        IntegerProperty x = new SimpleIntegerProperty(0);
        IntegerProperty y = new SimpleIntegerProperty();
        y.bind(x);
        
//        y.addListener((observable, previous, present) -> {
//            System.out.println("Change");
//        });
        y.addListener(observable -> {
            System.out.println("Change");
        });

        x.set(10);
        x.set(20);

        System.out.println(y.get());
    }
}

先ほどのパズルのうち、一方の addListener メソッドをコメントアウトしただけです。

実行すると、次のようになります。

Change
20

Change が出力されるのは、2 回だと思いませんでしたか?

ところが、Change が出力されるのは、1 回です。

ラムダ式を使っているので、分かりにくくなっているのですが、x に登録しているリスナはそれぞれインタフェースが異なります。

引数が 3 つのラムダ式が ChangeListener インタフェース、1 つラムダ式が InvalidationListener インタフェースです。

ChangeListener インタフェースは、Java Beans の PropertyChangeListener インタフェースと同様に、プロパティの値が変更されたら、コールされます。

一方の InvalidationListener インタフェースはプロパティが invalid になった時にコールされます。これはどういうことかというと、遅延処理に関係しているわけです。

プロパティの使用時にプロパティの値が valid かどうかを調べて、invalid だったらコールされるのです。

上のコードでは y が使用されるのは、最後の出力の時だけなので、その時に valid かどうかチェックされます。

x は 2 回変更されていますが、valid かどうかのチェックはその後に行うので、結果的に 1 度しかコールされません。

ところが、ChangeListener インタフェースは異なります。バインドされているターゲットの値が変更されてもコールされます。つまり、その都度、値が変化しているかどうかチェックするわけです。

問題は、2 種類のリスナを登録するとどうなるかということです。

バインドのターゲットの値が変更されたら、ChangeListener インタフェースのメソッドがコールされます。この時に、古い値と新しい値を引数に渡します。このプロパティの値を取得するということは、プロパティを使用するということになります。

つまり、valid かどうかのチェックが行われてしまうのです。

ChangeListener インタフェースのメソッドは 2 回コールされるので、InvalidationListener インタフェースのメソッドも 2 回コールされてしまいます。

そして、最後の y.get() 時にも valid かどうかのチェックは行われるのですが、x.set(20) の後に変更はないので、値は valid です。このため、InvalidationListener インタフェースのメソッドはコールされないのです。

このパズルの教訓です。

  • バインドしているプロパティの値をチェックしたいからといって、安易に ChangeListener インタフェースを登録するのはやめよう

Puzzle のネーミング

Joshua Bloch のパズルのネーミングって、とってもウィットに富んでいます。中には日本ではよく分からない習慣や文化に基づいているものもあるので、全部理解できるわけではないのですが...

Java Puzzlers の翻訳をされた柴田さんもパズルのネーミングの意味を調べるのが大変だったらしいですし。

でも、こういうウィットやユーモアを含ませたネーミングというのは、とても重要だと思うわけです。

みんな、パズルを作る時にこういうところまで気にすると、もっとパズルがおもしろくなるのになぁと個人的には思っています。

今回の Sooner or Later ですが、日本語にすると「遅かれ早かれ」ということです。

遅いか早いかはよく分からないけど、最終的にはリスナのメソッドがコールされるよということです。もちろん、遅延処理が絡んでくるからこういうネーミングにしたわけですが、どうでしょうか?

Scene Builder 小ネタ 3つ

このEntryはJavaFX Advent Calendarの11日目です。

昨日は @Yucchi_jp さんの HitInfoを少しだけ… でした。

明日は @boochnich さんです。

すいません、小ネタです。

Scene Builder をビルドする

みなさん、Scene Builder使ってますか? 便利ですよね。

でも、Oracleバイナリパッケージを配布しなくなってしまったのが... めんどくさいんですかねぇ。まぁ、Gluonがバイナリパッケージを配布してくれているからいいのですが。

とはいえ、せっかくのOSSなのですから、ビルドしてみたいと思いませんか。

JavaFX、というかOpenJFXをビルドするにはいろいろと準備が必要なのでめんどうなんですけど、Scene Builderだけであれば簡単です。

さっそくやってみましょう。

なお、今回はJava SE 8用のScene Builderをビルドします。

まずは、OpenJFXをクローンします。残念ながら、Scene Builderだけをクローンすることはできないのです。ちなみに、OpenJDKが使っているのはMercurialです。

hg clone http://hg.openjdk.java.net/openjfx/8u-dev/rt

クローンが完了しましたか。そうしたら、rt/apps/scenebuilderを見てみましょう。SceneBuilderAppディレクトリがScene BUilder本体、SceneBuilderKitディレクトリがIDEに組み込むなどの用途で使用するライブラリになってます。

OpenJFXのビルドにはgradleを使うのですが... なんとSceneBuilderAppのディレクトリを見てみるとnbprojectというディレクトリがあります。

これが何を意味するかというと、NetBeans用のプロジェクトになっているということです。

さっそくNetBeansで読み込んでみましょう。

SceneBuilderApp単独ではビルドできないので、SceneBuilderKitも一緒に読み込みます。

後は、SceneBuilderAppプロジェクトでF6すれば、ビルドして実行します。

JavaFXのアプリケーションは実行するとJARファイルも作ってくれます。distディレクトリにSceneBuilderApp.jarファイルができているはずです。後は-jarオプションで実行すればOK。

java -jar SceneBuilderApp.jar

とはいえ、dist/libにあるSceneBuilderKit.jarも一緒に使ってます。なので、他の場所にコピーする時はSceneBuilderKit.jarも忘れずに。

ルートコンテナを変更する

たとえば、NetBeans で FXML を生成すると、勝手に AnchorPane をルートコンテナとする FXML を生成しますよね。「空のFXML」を生成するといっても、ルートコンテナだけは勝手に設定してしまいます。

でも、AnchorPane を使いたいわけじゃないんだよ、他のコンテナを使いたいんだよ、ということも多いのでは。

そんな時、どうするかというと、IDEテキストエディタで FXML を編集すればいいのですが... そんな時でも、Scene Builder を使いたいわけです。

Scene Builder には Wrap in という機能があります。これを使えば、ルートコンテナを変更することもできるのです。

Wrap in は任意のノードをコンテナで包み込む機能です。

たとえば、AnchorPane に Button が貼られているとします。この Button を、AnchorPane の直下ではなく、間にコンテナ、たとえば FlowPane を挟みたいような場合に使用します。

これをやるには、Wrap in したいノードを選択します。選択するのは Hierarchy の処でも、中央のエディタ部分でもどちらでも OK。

そして、右クリックでポップアップメニューを表示させ、[Wrap in] を選択します。すると、下の図のようにコンテナの一覧がサブメニューに表示されるので、使用したいコンテナを選択します。

ノードを直接右クリックしなくても、メニューバーの [Arrange] - [Wrap in] でもおなじことができます。

Wrap in すると、ノードとコンテナの間に、指定したコンテナが挟まります。

この機能、どんなノードに対してもできるので、コンテナに対してもできます。つまり、ルートコンテナでも OK ということです。

ルートコンテナに対して Wrap in をしても、ちゃんと名前空間の定義やコントローラクラスへのリンクが保ったままにしてくれます。

ただし、スタイルシートは引き継がないので、注意が必要です。

たとえば、次のような FXML があったとしましょう。

<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" 
            xmlns:fx="http://javafx.com/fxml/1" fx:controller="ViewController">
    <stylesheets>
        <URL value="@view.css"/>
    </stylesheets>
</AnchorPane>

これを BorderPane で Wrap in すると、次のようになります。

<BorderPane xmlns="http://javafx.com/javafx/8.0.60" 
            xmlns:fx="http://javafx.com/fxml/1" 
            fx:controller="ViewController">
   <center>
      <AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0">
          <stylesheets>
              <URL value="@view.css" />
          </stylesheets>
      </AnchorPane>
   </center>
</BorderPane>

CSS の view.css がルートコンテナに引き継がれていません。また、サイズは設定されていないのは、しかたないですね。

コントローラクラスの雛形を作る

「コントローラクラスの雛形を作るのにいい方法ないですか」と FB で質問されたのですが、意外にみなさん Scene Builder の機能を知らないのですね ^ ^;;

コントローラクラスの雛形を作るのは簡単。

メニューバーの [View] - [Show Sample Controller Skelton] を選択すれば OK です。

たとえば、某連載用に作った次の FXML でコントローラクラスを作ってみましょう。

<VBox alignment="CENTER" prefHeight="100.0" prefWidth="400.0" spacing="12.0" 
      xmlns="http://javafx.com/javafx/8.0.60" 
      xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="DictionaryController">
   <children>
      <HBox alignment="CENTER" spacing="20.0">
         <children>
            <TextField fx:id="keyField" prefColumnCount="20" promptText="Key" />
            <Button mnemonicParsing="false" onAction="#search" text="Search" />
         </children></HBox>
      <Label fx:id="valueLabel" />
   </children>
</VBox>

この FXML は fx:id で結びつけられた要素が 2 つ (keyField, valueLabel)、イベント処理が 1 つ (searchメソッド) あります。

では、[View] - {Show Sample Controller Skelton] を選択してみると...

というダイアログが表示されます。ファイルには落とせないので、左下の[コピー]を選択すれば、クリップボードに保存してくれるので、後はペーストするだけ。

でも、このスケルトン、間違っているんですよね ^ ^;;

何が違うかというと、ActionEvent の import 文が抜けてます。まぁ、import 文は IDE で補完してくれるから、問題ないといえば、ないのですが。

ちなみに、右下の [Full] を選択すると、コードが次のようになります。

import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

public class DictionaryController {

    @FXML
    private ResourceBundle resources;

    @FXML
    private URL location;

    @FXML
    private TextField keyField;

    @FXML
    private Label valueLabel;

    @FXML
    void search(ActionEvent event) {

    }

    @FXML
    void initialize() {
        assert keyField != null : "fx:id=\"keyField\" was not injected: check your FXML file 'dictionaryView.fxml'.";
        assert valueLabel != null : "fx:id=\"valueLabel\" was not injected: check your FXML file 'dictionaryView.fxml'.";

    }
}

これだと、ちゃんと ActionEvent の import 文も含まれています。

ちなみに、initializeメソッドが Initializable インタフェースの initialize メソッドではないところに注意が必要ですね。

ところで、このダイアログ、違和感ないですか?

今の Scene Builder は L8N されていないのですが、なぜかこの [コピー] ボタンだけ日本語になっているのです。なんでなんでしょうね?