OpenJFX11 + OpenJDK11 + Maven で JavaFX を動かす

前回のエントリーはOpenJFXとOpenJDKでJavaFXを動かそうというものでした。

skrb.hatenablog.com

でも、OpenJFX SDKにパスを通さなくてはいけないなど、ちょっとめんどうだったのもたしか。

そんなおり、GluonのJoahn VosさんがOpenJFXをMavenでも使えるようにプルリク出してくれました。

github.com

この結果、Maven CentralにOpenJFXが登録されました!!

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

サンプルコードはこちら。

package net.javainthebox.hello;

import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.stage.Stage;

public class Hello extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        var label = new Label("Hello, JavaFX + Maven!");
        label.setAlignment(Pos.CENTER);
        var scene = new Scene(label, 200, 50);
        stage.setScene(scene);
        stage.show();
    }

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

}

JavaFXHello Worldです。

さて、これをコンパイル、実行するためのpom.xmlがこちら。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>net.javainthebox</groupId>
    <artifactId>hello</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>10</maven.compiler.source>
        <maven.compiler.target>10</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>11-ea+19</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.7.0</version>
                <configuration>
                    <release>11</release>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>exec-maven-plugin</artifactId>
                <version>1.6.0</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>java</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <mainClass>net.javainthebox.hello.Hello</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

このサンプルではjavafx.controlsモジュールを使用しているので、dependencyにはjavafx.controlsを指定しています。Maven Centralではモジュールのピリオドをハイフンで置き換えた名前になっています。

javafx.controlsはjavafx.graphics、javafx.baseに依存しているので、それらも自動的にロードされます。
これ以外にjavafx.fxml、javafx.media、javafx.swing、javafx.webモジュールもちゃんと登録されています。

では、まずはコンパイルしてみましょう。

C:\hello>mvn clean compile
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hello ---
[INFO] Deleting C:\Users\yuichi\Desktop\hello\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ hello ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\yuichi\Desktop\hello\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.7.0:compile (default-compile) @ hello ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\yuichi\Desktop\hello\target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.103 s
[INFO] Finished at: 2018-07-25T21:44:22+09:00
[INFO] ------------------------------------------------------------------------

C:\hello>

あっさり成功しました。もちろん、初回はモジュールのダウンロードがあるはずです。

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

C:\hello>mvn exec:java
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ hello ---

ここまで表示されれば、フレームが表示されているはずです。
OpenJFX SDKを使うよりは、とても簡単にJavaFXのアプリケーションを書けるようになりました。

ところで、JavaFXはネイティブライブラリが必要です。Maven Centralを見てみると、やっぱりプラットフォームごとに分けられてました。下のリンクはjavafx.graphicsのものです。

Central Repository: org/openjfx/javafx-graphics/11-ea+19

pom.xmlを見てみると、プラットフォームによって該当するJARファイルも依存があるように書かれてますね。

現状の問題点

Mavenで実行するとき、exec:javaであれば問題ないのですが、exec:execで別プロセスとして実行させると以下のようなメッセージを出して、実行ができません。

C:\Users\yuichi\Desktop\hello>mvn exec:exec
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< net.javainthebox:hello >-----------------------
[INFO] Building hello 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:1.6.0:exec (default-cli) @ hello ---
エラー: JavaFXランタイム・コンポーネントが不足しており、このアプリケーションの実行に必要です
[ERROR] Command execution failed.
org.apache.commons.exec.ExecuteException: Process exited with an error: 1 (Exit value: 1)
    at org.apache.commons.exec.DefaultExecutor.executeInternal (DefaultExecutor.java:404)
    at org.apache.commons.exec.DefaultExecutor.execute (DefaultExecutor.java:166)

ロードされたクラスを調べてみたのですが、Applicationクラスはロードされているものの、その他のJavaFXに関連するクラスがロードされていません。もうちょっと調べてみようと思ってます。

Using JavaFX with OpenJFX + OpenJDK

Hello, JavaFX developers using Oracle JDK!

You may know, we can't get Oracle JDK for free from Java SE 11. Therefore, we have no choice to move to OpenJDK from Oracle JDK.

But, JavaFX is NOT included in OpenJDK!!

You may think "OpenJFX is a project in OpenJDK project, isn't it?" Yes, OpenJFX is a project in OpenJDK project. But, OpenJFX isn't included in JDK project of OpenJDK project.

So, we should use OpenJFX with OpenJDK.

You are worried "I should build OpenJFX?" Don't worry! OpenJFX build can be downloaded in jdk.java.net site now.

But, even if your application is not modular application, you should set module configuration!!

I'll describe as follows. If you'd like to know only setting, you can skip to "JavaFX Configuration for OpenJFX."

Set up OpenJDK and OpenJFX

You can download OpenJDK and OpenJFX belowed URL.

JDK Builds from Oracle

OpenJFX
OpenJFX Early-Access Builds

In this article, I used early access of OpenJDK 11 on Windows.

OpenJDK 11
JDK 11 Early-Access Builds

I used openjfx-11-ea+13 that was released on 9 May and jdk-11-ea+15 that was released on 25 May.

After downloading openjdk-11-ea+15_windows-x64_bin.tar.gz, you should decompress it.

OpenJDK doesn't include installer, so you just add the path to jdk-11\bin directory.

In the same way, decompress openjfx-11-ea+13_windows-x64_bin-sdk.zip after download it.

There are 3 directories in javafx-sdk-11 directory: bin, legal and lib.

There are some native libraries (DLL in Windows) in bin directory, so you also add the path to bin directory.

There are some modular JAR files in lib directory.

Try to execute JavaFX sample application

First of all, let's try to compile and execute a sample application.

Here is the sample application.

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);
    }
}

The application indicates "OK" button only, and print out "Cliecked!" when you click the button.

Of course, I don't make module.

I located FxSample.java in javafx-sdk-11 directory.

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
Error: JavaFX runtime components are missing, and are required to run this application

C:\javafx-sdk-11>

Compile was done, but error occurred in execution!

I don't understand the error message. What is "runtime components"?

At first I thought JVM didn't load DLL, but the problem seems to be not that.

To check what happens, I add -verbose option for execution.

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
     <<snip, snip, snip>>

You can see what class was loaded by using -verbose option. For example, java.lang.Object class in java.base module was loaded firstly.

I checked all loaded classes, but JavaFX classes weren't loaded!!

That is why execution was failed.

I wonder why JavaFX classes weren't loadded in spite of setting classpath.

I remembered the case of Java EE modules such as JAXB in Java SE 9 or 10.

These modules are included in Java SE, but are not included in standard modulepath. Therefore, we should add --add-modules options when using these modules even if we execute non-modular application.

JavaFX case may be the same.

If so, we should set modulepath and --add-modules option.

JavaFX Configuration for OpenJFX

We set --module-path option or -p option for modulepath. value of modulepath is not file, but directory that modular jar files are located in.

In this case, I set lib directory for modulepath.

If modular application, we describes modular dependencies in module-info.java file. But, we use --add-modules option when non-modular application.

Which modules the application use? You can check them by jdeps tool.

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
    <<snip, snip, snip>>

We don't use -p option in jdeps tool, so use --module-path. The other option -s is for showing dependency summary only.

In the result, we can know FxSample class uses java.base, javafx.base, javafx.controls and javafx.graphics.

But, we don't need to set java.base module because it is loaded by default.

Moreover, javafx.controls module uses javafx.base and javafx.graphics. So, we should set only javafx.controls for --add-modules.

Then, let's execute!

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

If going well, you can see the window below.

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