黒地に黄色のクローラー

この記事は、JavaFX Advent Calendar 2015 の 17 日目の記事です。

昨日は id:c9katayama さんの PDFBoxとFXGraphics2Dを使って大きなPDFをレンダリングする でした。

明日は @yumix_h さんです

タイトルの黒地に黄色のクローラーといえば、あれですよ、あれ。

明日の公開のあの映画。そう Star Wars です。こんなやつです。

http://img.lum.dolimg.com/v1/images/episode-3-crawl_4d843f60.jpeg

過去に、プレゼンのはじめに Star Wars のパロディのムービーを作ったことがある櫻庭ですから、この波に乗らないわけにはいきません。

ということで、JavaFX であのクローラーを作ってみようというわけです。

3D でやってみる

あのクローラーは文字列が奥のほうに移動しているので、まずは単純に 3D の API を使って、z 軸方向に移動させてみます。

でも、単に文字列を移動させても、台形の形にはならないので、文字列を x 軸を回転軸にして回転させます。ようするに、文字列を寝かせるわけですね。そして、z 軸方向に移動させるわけです。

まず、3D にするにはカメラを設定しなくてはいけません。

通常、2D の GUI では平行透視法のカメラが設定されています。このため、奥にあるノードでも小さく表示されることはありません。でも、3D の場合は、奥にあるノードは小さく表示しなくてはなりません。そのためには、それようのカメラ、つまり透視法を使用したカメラを設定します。

これは簡単で、Scene に PerspectiveCamera を設定します。

        Scene scene = new Scene(root);
        scene.setCamera(new PerspectiveCamera());

これで、奥の方のノードは小さく表示されるようになりました。

さて、次は文字列を寝かせる処理です。これはノードの setRotate メソッドで行います。しかし、単に setRotate メソッドをコールすると、z 軸を中心に回転してしまいます。これを x 軸を中心にさせるには setRoateAxis メソッドを使用します。

        Text text = new Text(crawl);

        text.setRotationAxis(new Point3D(1.0, 0.0, 0.0));
        text.setRotate(-80.0);

2 行目で、x 軸を表す Point3D オブジェクトを生成して、setRotationAxis メソッドに指定しています。そして、3 行目で後ろに 80 度倒す処理をします。JavaFX では角度はラジアンではなくてディグリーを使用することに注意してください。

準備ができたので、z 軸方向に移動するアニメーションを作成してみましょう。z 軸方向に移動する場合でも、移動は移動なので、TransTranslateTransition を使用します。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setToZ(1_000);
        trans.setInterpolator(Interpolator.LINEAR);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

プログラムの全体はブログの最後の方にまとめて載せました。

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

確かに、奥に移動する感じはできましたけど、なんかちょっと違う...

奥の方に移動すると、x 軸方向に回転させた角度が変化してしまうのが気になります。3D なので、当たり前といえば当たり前なのですが。

また、いくら z 軸上で奥に移動させても、見えなくなるほどにはなりません。そのため、さいごの方の挙動が止まっているように見えてしまいます。

単純に 3D で移動させるよりも、もうちょっといい方法があるような気がします。もうちょっと模索してみましょう。

3D と 2D のアニメーションを組み合わせる

次に思いついたのが、3D の表現と 2D のアニメーションを組み合わせる方法です。

文字列を後ろに寝かせるのは 3D で行うとして、奥に移動するアニメーションは疑似的に 2D で表せないかということです。具体的には y 軸方向の上に移動するアニメーションと、縮小するアニメーションを組み合わすということです。

y 軸方向に移動させるアニメーションは先ほどと同じように TranslateTransition を使用します。また、拡大縮小には ScaleTransition を使用します。

まずは、移動のアニメーションです。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(1_000);
        trans.setToY(-100);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

単に y 軸上を動かしているだけです。

そして、拡大縮小です。はじめは拡大しておいて、徐々に縮小します。ただし、文字列を寝かしているため、z 軸方向にも大きさがあります。そのため、z 軸方向も拡大縮小します。

        ScaleTransition scale = new ScaleTransition(Duration.millis(20_000), text);
        scale.setFromX(5.0);
        scale.setFromY(5.0);
        scale.setFromZ(5.0);
        scale.setToX(0.01);
        scale.setToY(0.01);
        scale.setToZ(0.01);
        scale.setInterpolator(Interpolator.LINEAR);
        scale.setCycleCount(Animation.INDEFINITE);
        scale.play();

なお、複数のアニメーションを同時にやるためには ParallelTransition を使うこともありますが、そんなにきっちり合わす必要がなければ、単に複数のアニメーションを play してあげれば大丈夫です。

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

先ほどよりはいい感じになっていると思うのですが、どうでしょう。

しかし、これもアニメーションで移動させていると、文字列の寝ている角度が変わってしまうのが気になります。まぁ、こちらも、当たり前といったら当たり前ですが。

そこで、移動と同時に角度を変化させるアニメーションも一緒に行ってみます。

角度を変化させるアニメーションは RotateTransition です。

        RotateTransition rotate = new RotateTransition(Duration.millis(20_000), text);
        rotate.setToAngle(-80.0);
        rotate.setInterpolator(Interpolator.EASE_OUT);
        rotate.setCycleCount(Animation.INDEFINITE);
        rotate.play();

角度を変化させているといっても、ほんのちょっとです。それでも、見た目はかなり変化します。

ここでは回転の中心を設定していませんが、それははじめに文字列に対して設定しているからです。

これで同時に行うアニメーションは 3 つになりましたが、効果はどうでしょう。さっそく実行してみましょう。

かなりよくなったと思いませんか。これはこれでいいかもしれませんが、他の方法も模索してみましょう。

すべて 2D のアニメーションで行う

さいごに、3D はあきらめて、すべて 2D で行ってみましょう。もちろん、Star Wars の Episode IV の頃には CG はなかったのですから、2D のアニメーションでやっているはずです。

ということは、2D のアニメーションで実現するということは、原点回帰になるのかな?

もちろん、2D なので、Scene にカメラを設定させる必要もありません。

しかし、問題になるのは文字列を寝かせることができるかどうかです。実をいうと、これはエフェクトを使うことで、比較的簡単にできます。

どんなエフェクトかというと PerspectiveTransform です。先ほどまで使用していた PerspectiveCamera と同じく、透視法が使えるエフェクトというわけです。

PerspectiveTransform はノードを囲む四隅を任意の点に写像することができるエフェクトです。AffineTransform が実現するアフィン変換だと平行は平行に変換されますが、PerspectvieTransform では平行は維持されません。つまり長方形を台形などに変換することができるのです。

変換には四隅の座標を指定します。

        PerspectiveTransform perspective = new PerspectiveTransform();
        perspective.setUlx(300.0);
        perspective.setUly(200.0);

        perspective.setUrx(500.0);
        perspective.setUry(200.0);

        perspective.setLlx(0.0);
        perspective.setLly(500.0);

        perspective.setLrx(800.0);
        perspective.setLry(500.0);

        text.setEffect(perspective);

それぞれ 2 行ずつで、上左、上右、下左、下右の座標を指定しています。ここでは、ルートのコンテナの座標を台形にするような変形になります。

そして、後は先ほどと同じように y 軸上を移動するアニメーションと拡大縮小を組み合わせます。ただし、拡大縮小はノードの真ん中を中心に拡大縮小してしまうため、上記の変換と合わせると変な位置に変換されてしまいます。

そこで、Scale を使用して、拡大縮小の中心を変化させています。Scale を使ってアニメーションをするため、Transition ではなく Timeline を使用しています。

        TranslateTransition trans = new TranslateTransition(Duration.millis(20_000), text);
        trans.setFromY(400);
        trans.setToY(0);
        trans.setInterpolator(Interpolator.EASE_OUT);
        trans.setCycleCount(Animation.INDEFINITE);
        trans.play();

        Scale scale = Transform.scale(4.0, 4.0, 400.0, 200.0);
        text.getTransforms().add(scale);
        Timeline timeline = new Timeline(
                new KeyFrame(Duration.millis(19_000),
                        new KeyValue(scale.xProperty(), 0.3), 
                        new KeyValue(scale.yProperty(), 0.3)),
                new KeyFrame(Duration.millis(20_000),
                        new KeyValue(scale.xProperty(), 0.001), 
                        new KeyValue(scale.yProperty(), 0.001))
        );
        timeline.setCycleCount(Animation.INDEFINITE);
        timeline.play();

拡大縮小のアニメーションを 2 段階にしているのは、よりそれっぽさを出すためです。

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

PerspectiveTransform と Scale を組み合わせているせいか、アニメーション開始時の拡大している状態では少しノードの表示が荒れてしまっていますが、動きは一番それらしいのではないでしょうか。

ということで、3 種類の方法で Star Warsクローラーを実現してみました!

これで、時代の波に乗れたかなww

プログラム

3D でやってみる


Crawler Demo 1

3D と 2D のアニメーションを組み合わせる


Crawler Demo 2

すべて 2D のアニメーションで行う


Crawler Demo 3

JavaFX と Project Jigsaw

この記事は、JavaFX Advent Calendar 2015 の 12 日目の記事です。

昨日は @y_q1m さんの JavaFX アプリケーションに隠し Control を搭載する でした。

明日は @tomo_taka01 さんです

ごめんなさい。今日も小ネタです。

JavaOne に行ってからというもの、Project Jigsaw が面白くなっていろいろやってます。

ところで、JavaFXJava SE 9 ではあまり新機能が盛り込まれないようなのです。Feature Complete がちょっと伸びたので、もう少し盛り込むようですが...

で、なんで新機能が少ないかというと、Project Jigsaw への対応が大変だったからということらしいのです。JavaFX 9 については、 id:aoe-tk さんの JavaFX9に追加される機能が増えるかもしれません を見ていただくとして、「じゃあ JavaFX のモジュールはどうなったの?」というところをちょっと調べてみました。

Java SE 9 の java コマンドには -listmods というオプションが追加になって、標準のモジュールの一覧を表示することができます。まずは、これを試してみましょう。

C:\>java -listmods 2>&1 | grep javafx -
javafx.base@9.0
javafx.controls@9.0
javafx.deploy@9.0
javafx.fxml@9.0
javafx.graphics@9.0
javafx.media@9.0
javafx.swing@9.0
javafx.web@9.0

8 個のモジュールが JavaFX に関連するようです。

基本となるのが javafx.base で、コントロールは javafx.controls というようになっているのはわかるのですが、javafx.graphics などがよく分かりません。javafx.base が基本というのは分かりますけど、基本というのはどのぐらいの範囲、つまりどういうクラスが含まれているのかなどもよく分かりません。

これを調べるために、簡単なクラスで試してみました。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

単にステージを表示するだけのクラスです。これが含まれる JAR ファイルが test.jar だとします。で、この JAR ファイル (or クラス) がどのようなモジュールを使用しているかを調べるには、jdeps コマンドを使用します。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.graphics

オプションの -s はサマリだけを出力させるオプションです。

ステージを表示するだけで、他には何もやっていないのだから javafx.base に依存しているだろうと思ったら、javafx.graphics に依存していました。もちろん、javafx.graphics は javafx.base に依存しているでしょうから、間接的には依存はしているのですが...

-s オプションを外すと、もうちょっと詳しく見ることができます。

jdeps test.jar
test.jar -> java.base
test.jar -> javafx.graphics
   <unnamed> (test.jar)
      -> java.lang
      -> javafx.application
      -> javafx.stage

やはり、Application クラスや Stage クラスは javafx.graphics モジュールに含まれるようです。

ちょっとサンプルを改造してみましょう。

public class Test extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        Label label = new Label("Label");
        
        StackPane root = new StackPane(label);
        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();
    }
    
    public static void main(String... args) {
        launch(args);
    }
}

こうすると、依存しているモジュールには javafx.control が出てくるはず。

jdeps -s test.jar
test.jar -> java.base
test.jar -> javafx.controls
test.jar -> javafx.graphics

やはり、javafx.controls に依存していました。

しかし、javafx.base や javafx.graphics はよく分からないままです。こういう時は、モジュールを直接調べてみましょう。

一般的にモジュールには JAR ファイルを使用するのですが、標準のモジュールは JAR ファイルではなく、JMOD ファイルになっています。モジュールが JAR ファイルの時は jar コマンドで調べられるのですが、JMOD ファイルの時は jmod コマンドを使用します。どちらもオプションとして -p (--print-module-descriptor) を使用すれば、モジュールの情報を表示してくれます。

さっそくやってみましょう。JMOD ファイルは JDK のディレクトリの jmods ディレクトリにあります。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.base.jmod -p

Name:
  javafx.base@9.0
Requires:
  jdk.jfr
  java.base [ MANDATED ]
  java.desktop
Exports:
  com.sun.javafx to [javafx.controls, javafx.graphics, javafx.swing]
  com.sun.javafx.beans to [javafx.controls, javafx.graphics, javafx.fxml]
  com.sun.javafx.collections to [javafx.controls, javafx.graphics, javafx.swing, javafx.media]
  com.sun.javafx.binding to [javafx.controls, javafx.graphics]
  com.sun.javafx.event to [javafx.controls, javafx.graphics]
  com.sun.javafx.logging to [javafx.graphics]
  com.sun.javafx.property to [javafx.controls]
  com.sun.javafx.runtime to [javafx.graphics]
  javafx.beans
  javafx.beans.binding
  javafx.beans.property
  javafx.beans.property.adapter
  javafx.beans.value
  javafx.collections
  javafx.collections.transformation
  javafx.event
  javafx.util
  javafx.util.converter
Conceals:
  com.sun.javafx.property.adapter
Hashes:
  Algorithm: SHA-256
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  jdk.jfr: 6hhBHpCTp8ZkLutorg8GIFCp+D9MrL++mIsTh6grzyE=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

外に公開しているパッケージは Exports の項に書いてあります。実をいうと、Exports の部分は順番がぐちゃぐちゃだったので、ソートしなおしてあります。

これを見ると、javafx.base は、意外にもグラフィックに関する部分はまったく含んでおらず、プロパティやバインドに関するクラスだけを含んでいるようです。

ただし、Requires の項に、java.desktop が含まれていることに注意が必要です。java.desktop は AWT や Swing が含まれているモジュールです。これに依存しているので、まったくグラフィックに関連がないというわけではないことが分かります。

次に、javafx.graphics です。

C:\Program Files\Java\jdk1.9.0\jmods>jmod javafx.graphics.jmod -p

Name:
  javafx.graphics@9.0
Requires:
  java.base [ MANDATED ]
  java.desktop
  java.xml
  javafx.base [ PUBLIC ]
Exports:
  com.sun.glass.ui to [javafx.web, javafx.media]
  com.sun.glass.utils to [javafx.web, javafx.media]
  com.sun.javafx.application to [javafx.web, javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.cursor to [javafx.deploy, javafx.swing]
  com.sun.javafx.css to [javafx.controls, javafx.deploy]
  com.sun.javafx.css.converters to [javafx.deploy]
  com.sun.javafx.css.parser to [javafx.deploy]
  com.sun.javafx.embed to [javafx.deploy, javafx.swing]
  com.sun.javafx.font to [javafx.web]
  com.sun.javafx.geom to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.geom.transform to [javafx.web, javafx.controls, javafx.swing, javafx.media]
  com.sun.javafx.iio to [javafx.web]
  com.sun.javafx.jmx to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.menu to [javafx.controls, javafx.deploy]
  com.sun.javafx.perf to [javafx.deploy]
  com.sun.javafx.scene to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.scene.input to [javafx.web, javafx.controls, javafx.swing]
  com.sun.javafx.scene.text to [javafx.web, javafx.controls]
  com.sun.javafx.scene.traversal to [javafx.web, javafx.controls]
  com.sun.javafx.sg.prism to [javafx.web, javafx.swing, javafx.media]
  com.sun.javafx.stage to [javafx.controls, javafx.deploy, javafx.swing]
  com.sun.javafx.text to [javafx.web, javafx.deploy]
  com.sun.javafx.tk to [javafx.web, javafx.controls, javafx.deploy, javafx.swing, javafx.media]
  com.sun.javafx.util to [javafx.web, javafx.controls, javafx.fxml]
  com.sun.prism to [javafx.web, javafx.media]
  com.sun.prism.image to [javafx.web]
  com.sun.prism.paint to [javafx.web]
  com.sun.scenario.effect to [javafx.web]
  com.sun.scenario.effect.impl to [javafx.web]
  com.sun.scenario.effect.impl.prism to [javafx.web]
  javafx.animation
  javafx.application
  javafx.concurrent
  javafx.css
  javafx.css.converter
  javafx.geometry
  javafx.print
  javafx.scene
  javafx.scene.canvas
  javafx.scene.effect
  javafx.scene.image
  javafx.scene.input
  javafx.scene.layout
  javafx.scene.paint
  javafx.scene.shape
  javafx.scene.text
  javafx.scene.transform
  javafx.stage
Conceals:
   <<省略>>
Hashes:
  Algorithm: SHA-256
  javafx.base: vKYsvlQtkuWc5pwKzHKlUfIp9lXa4SAjnU1TniODA74=
  java.xml: 1FV2E7DCyzOAroiUpxIyJMSg+AzeDI5qm1SmeFyqv4U=
  java.desktop: AE4yDbzkIjSeteMHRe9ZMpEkjE/u4PMGSL7WCwpEyOw=
  java.base: Jjip1psj45/C01FcGzXZc83cwqFHAZVVL3wSXJLiMpc=

こちらも、ソートしなおしてあります。

Applicationや、Scene、Stage も javafx.graphics に含まれていることが分かります。また、イベント関連、CSS 関連なども含まれています。

ちょっと意外だったのが、Animation や Shape も含まれていることです。こういうクラスたちは、外だしされるかと思ってましたけど、基本のところに含まれるんですね。

私がいつも使っているクラスたちは、ほぼ javafx.graphics で済んでしまうということも分かりました。あんまり、コントロール使わないんですよね ^ ^;;; もちろん、仕事では使ってますけど。

ということで、今日はここまで。

Interpolator で補間

この記事は、JavaFX Advent Calendar 2015 の 9 日目の記事です。

昨日も私の JCConf で JavaFX について発表してきた でした。

明日は @kokuzawa さんです

今日は小ネタ。

アニメーションで重要な概念に Interpolator があります。Interpolate が「補間する」という意味なので、Interpolator は「補間をするもの」ぐらいですかね。

コンピュータでアニメーションをする時は、ある時点での状態から、次の時点での状態を指定します。JavaFX で Timeline を使うのであれば、ある時点の状態を示すのが KeyFrame になります。

通常は複数の KeyFrame でアニメーションを行うわけですが、その KeyFrame 間の状態をどのようにつなげていくかということが補間です。

たとえば、移動する場合であれば、ある地点から他の地点へ移動する時に、等速運動で移動するのか、それとも重力落下のように等加速度運動で移動するのかなどさまざまな方法があります。等速運動で移動するのであれば、ある時点から次の時点までに 4 回の画面の書き換えがあれば、1 回の書きかえごとに 1/4 ずつ移動すればいいことになります。

実際には、何回の書き換えが行われるのかは GPU の性能などによって異なるため、補間曲線を定義しておきます。

通常、補間曲線は x 軸が時間、y 軸が状態の変化量を表すのですが、汎用的に使用できるように通常は開始点を原点、終了点を (1, 1) の点とします。

たとえば、等速運動だとこんな補間曲線になります。

この補間曲線があれば、どのように補間すればいいかすぐに分かるわけです。そして、これを保持しているのが Interpolator というわけです。

ところで、自然界で等速運動はあまり見かけることはありません。動作のしはじめはゆっくりなど、動作によって非線形に状態の変化が起こります。

よく使われるのが、はじめゆっくり、さいごゆっくり、はじめとさいごの両方ゆっくりという 3 種類です。たとえば、車が発進する時はアクセルを踏んでゆっくり加速しはじめ、途中は等速運動、さいごはブレーキを踏んでゆっくり止まります。これは、はじめとさいごの両方ゆっくりに当てはまります。

で、この 3 種類は一般的に Ease In、Ease Out、Ease Both (Ease In Out ということもあります) と呼ばれます。Interpolator クラスにもこの 3 種類はそれぞれ EASE_IN、EASE_OUT、EASE_BOTH と定数定義されています。

そのほかに、等速運動を表す LINEAR と、セルアニメーションのように離散的に状態が変化する DISCRETE の、合計 5 種類が定数として定義されています。

それぞれの定数の補間曲線で描いてみましょう。

EASE_IN

EASE_OUT

EASE_BOTH

DISCRETE

この 5 種類がどのように補間されているのか理解するには、実際にアニメーションにしてみるのが手っとり早いです。

5 種類並べて同時に移動させた結果がこちら。プログラムはさいごに載せました。

ちなみに、Timeline では LINEAR がデフォルト、Transition では EASE_BOTH がデフォルトになっています。

ここまでが前振りで、ここからが本題です。

この Interpolator は自分で任意の補間曲線を設定することができます。設定するには SPLINE メソッドか、TANGENT メソッドを使用します。ここでは、SPLINE メソッドを使用します。

SPLINE メソッドは開始点 (0, 0) と終了点 (1, 1) を通るスプライン曲線を補間曲線として設定するメソッドです。引数は 3 次スプライン曲線なので、引数は 2 つのコントロールポイントの座標になります。それを図示したのが、下図です。

たとえば、EASE_BOTH とは逆にロケットスタートして中だるみ、さいごにまたダッシュするような Inerpolator であれば、こんな感じで設定できます。

    Interpolator interpolator = Interpolator.SPLINE(0.2, 0.8, 0.8, 0.2);

で、なんで補間のことを話題にしているかというと、補間関数を下の図のようにして、予備動作を実現できないかと考えたわけです。

予備動作というのは英語だと anticipation といって、何らかの動作を行う前にする動作のことです。これはアニメーションのバイブルともいえる Disney Animation: The Illusion of Life に書かれている 12 の原則の 1 つなのです。下のリンクは英語版の方ですが、日本語版もあります。

The Illusion of Life (Disney Editions Deluxe)

The Illusion of Life (Disney Editions Deluxe)

下の図のように走り出すドナルドがちょっと後ろに体重をかけて、これからロケットスタートをするぞという動作です。アニメではよくある表現ですよね。

http://www.viz.tamu.edu/faculty/parke/ends489f00/section6/donald.gif
The Twelve Principles of Animationより引用

で、上の図のようにはじめマイナス側に補間する補間関数であれば、この予備動作ができるのではと思ったわけです。

で、やってみました。

    Interpolator interpolator = Interpolator.SPLINE(0.2, -1.0, 0.6, 0.6);

そうしたら、怒られちゃいました...

Caused by: java.lang.IllegalArgumentException: Control point coordinates must all be in range [0,1]
	at com.sun.scenario.animation.SplineInterpolator.<init>(SplineInterpolator.java:98)
	at javafx.animation.Interpolator.SPLINE(Interpolator.java:199)
	at InterpolatorDemo.start(InterpolatorDemo.java:26)

どうやら、コントロールポイントの座標は (0, 0) と (1, 1) の四角の中だけのようです。楽して予備動作をしようと思ったのに、残念。

もちろん、複数の KeyFrame、もしくは複数の Transition を SequentialTransition でつなげたアニメーションを使えば予備動作できるんですけどね。

簡単にできないかなぁと思ったわけです。まぁ、そんなに楽してはできないというのが、今日の教訓でした。

長くなってしまったわりには、内容がないエントリーだ ><


Interpolator Demonstration

JCConf で JavaFX について発表してきた

この記事は、JavaFX Advent Calendar 2015 の 8 日目の記事です。

昨日は id:aoe-tk さんの JavaFX9に追加される機能が増えるかもしれません でした。

台湾の JUG である TWJUG が主催している JCConf で CfP をしていたので、軽い気持ちで応募したら受かっちゃいました。JCConf は、JJUG でいうところの CCC のようなカンファレンスです。

日本でも話したことがないことを話すつもりはないので、Swing から JavaFXマイグレーションの話をすることにしました。

資料はもともと英語で作っているので、そのままでいいやと思っていたのですが... 見直してみたら、やっぱりここも直したい、あそこも直したいで半分ぐらい手を入れてしまいました。

そのせいで、喋りの準備が全然できなかったというのは、言い訳にしかすぎないんですけどね。

いちおう、原稿は作って、そのキーワードを抜き出したカンペを作ってスマホですぐに見られるようにしていたのですが、やっぱりそれを見る余裕はありませんでした ><

で、資料はこちら。

Swing がメインテナンスモードだし、もうこれから新しい機能は入らないから、Swing から JavaFX に移行することを考えようという内容です。

マイグレーションの例を 2 つ、ライブコーディングしようとしていたのですが、実際には 1 つ目の簡単な例だけでかなり時間を食ってしまって、2 つ目はライブではできませんでした。

そのせいもあって、後半はグダグダ。英語のプレゼンはもっと入念に練習しないとダメですね。

喋っている時は気がつかなかったのですが、Q&A の時に JRuby の Charles Nutter さんが聞いているのに気がつきました!!!

話している時に気がつかなくてよかった!気がついていたら、緊張してなおさらグダグダになってしまうところでした。

講演の後、ちょっとだけ話をして、おもしろかったといってもらえたので、よかったです。

ちなみに、台湾での JavaFX の認知度は低かったようです。というか、やっぱりサーバーサイドの人がほとんどで、Swing を使ったこともないという人ばかりでした。セッションの後、TWJUG の @kojilin さんとも喋ったのですが、台湾ではデスクトップアプリを Java で作るということはあまりないようですね。

まぁ、日本でもそんなにメジャーというわけではないですが、こういうセッションをやるとかなりの割合の人が Swing を使っていたりするのですが...

今日は JavaFX の技術的な話はぜんぜんないですけど、まぁこういうエントリーがあってもいいでしょう ^ ^;;

明日の Advent Calendar は空きなので、誰か埋めて!!

Wheel or Touch

この記事は、JavaFX Advent Calendar 2015 の 4 日目の記事です。

昨日は蓮沼さんの MSI/EXE インストーラ版 Scene Builder を HiDPI 対応させるには です。明日は y_q1m さんです。

この Advent Calendar の 2 日目の id:torutk さんの 記事 の中で、マウスホイールでもスクロールイベントが発生してしまって困るとありました。えっ、そんなの簡単に判別できるのにと思って、ツィートしようと思ったのですが、いや待て、blog にした方が Advent Calendar が埋まるということで、ここに書いてますww

みんな ScrollEvent だけでタッチかホイールか判別しようとしているのが間違っているんですよ。ScrollEvent は GestureEvent のサブクラスで、GestureEvent はジェスチャーのイベントを扱うクラスです。

でも、ジェスチャーなんて高レベルな API なわけですよ。もっと低レベルで扱えばいいのです。

ようするに、タッチのイベントである TouchEvent を使うのです。

ちょっとしたテストプログラムを書いてみました。

public class WheelOrTouch extends Application {

    private boolean touch;

    @Override
    public void start(Stage stage) throws Exception {
        Pane pane = new Pane();
        pane.setPrefSize(600, 400);

        // タッチの開始
        pane.setOnTouchPressed(event -> {
            touch = true;
        });

        // タッチの終了
        pane.setOnTouchReleased(event -> {
            touch = false;
        });

        pane.setOnScroll(event -> {
            // タッチの有無でジェスチャーかホイールを判別
            if (touch) {
                System.out.println("TOUCH " + event.getEventType());
            } else {
                System.out.println("WHEEL " + event.getEventType());
            }
        });

        Scene scene = new Scene(pane);
        stage.setScene(scene);
        stage.show();
    }

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

これで、タッチしてみました。結果はこう。

TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
WHEEL SCROLL
WHEEL SCROLL
WHEEL SCROLL
WHEEL SCROLL

あれっ、最後に WHEEL SCROLL が出てしまってる!!

と予定調和的につっこんでみました。

ここで、WHEEL SCROLL が出ているのは、慣性スクロールのためです。そして、慣性スクロールかどうかは GestureEvent クラスの isInertia メソッドで判別できます。

ということで、ちょっと変更したのがこちら。

public class WheelOrTouch extends Application {

    private boolean touch;

    @Override
    public void start(Stage stage) throws Exception {
        Pane pane = new Pane();
        pane.setPrefSize(600, 400);

        // タッチの開始
        pane.setOnTouchPressed(event -> {
            touch = true;
        });

        // タッチの終了
        pane.setOnTouchReleased(event -> {
            touch = false;
        });

        pane.setOnScroll(event -> {
            // タッチの有無、もしくは慣性スクロールでジェスチャーかホイールを判別
            if (touch || event.isInertia()) {
                System.out.println("TOUCH " + event.getEventType());
            } else {
                System.out.println("WHEEL " + event.getEventType());
            }
        });

        Scene scene = new Scene(pane);
        stage.setScene(scene);
        stage.show();
    }

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

これで、タッチなのかホイールなのか判別できます。タッチした結果がこちら。

TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL
TOUCH SCROLL

マウスホイールした結果がこちら。

WHEEL SCROLL
WHEEL SCROLL
WHEEL SCROLL
WHEEL SCROLL

ちゃんと判別できました!!

でも、getTouchCount で判別した方が簡単かなぁ...

JavaQne 2015

もうずいぶん日時が経ってしまいましたが、1 月 24 日に福岡で行われた JavaQne 2015 に参加してきました。JavaOne じゃなくて、JavaQne です。ちなみに Q は九州の Q です。

きしださんから JavaFX の話をということだったのですが、JavaFX の新しめのところを話してもキョトンとされるだけなのは重々承知しています。なので、比較的食いつきやすい Swing からの移行に関して話をしてきました。

ビックリしたのが Swing 経験者の数。数人いればいい方だと思ってたのですが、半分ぐらいの人が Swing を使っていた経験ありでした。

内容は JJUG CCC 2014 Fall で話したものの焼き直しです。でも、CCC では前半で時間を使いすぎて、後半がかなり駆け足になってしまったので、今回は前半は抑え気味にしました。それでも、最後はちょっと時間が足りなかった ><

それでも、そこそこ反応がよかったのでよしとしてください。

資料はこちら。

前半は JavaFX の紹介と、主な機能について。後半が Swing との差分とマイグレーションという 2 部構成にしました。

前半はおいておいて、後半。

Swing と JavaFX の一番の違いは、なんといっても FXML と CSS だと思っています。

GUI の構造を全部 Java で書かなくてはいけないなんて、ほんと苦痛でしかないです。コードも見づらいので、コードの保守もめんどうです。

Scene Builder でポトペタで FXML を作って、必要に応じて FXML を編集すれば OK。後はイベント処理部分をコントローラクラスに記述するだけ。これだけでかなり楽になります。

JavaFXCSS はちょっとくせ者で、HTML の CSS と同じだと思っているとすぐにイタい目にあいます。JavaFX の部品のプロパティが分かっていないと書けないんですよね。

どういうプロパティがあるのかは Scnene Builder の CSS Analyzer を使えば分かります。CSS Analyzer はかなり便利でデフォルト値も分かるし、現在適用している値も分かります。

できれば、Scene Builder で CSS ファイルを直接編集できるといいのですが...


次は GUI 部品の話。Swing だとコンポーネントJavaFX だとコントロールと呼びます。

単純な部品は Swing も JavaFX もだいたい同じです。Button や Label などは、プロパティもほぼ一緒ですし。

ちょっと変わってくるのが、ComboBox やダイアログ。JavaFX のダイアログは Java SE 8u40 から提供されますが、Swing の JOptionPane クラスのようなユーティリティクラスはないので、自分で組み立てます。とはいうものの、自由度は結構高いので、いろいろできます。

全然違うのが、JavaFX のコントロール名の最後に View がつく部品。ListView、TreeView、TableView、TreeTableView の 4 種類です。特に使用頻度が高い TableView については後述します。

レイアウトも Swing と JavaFX では違います。

Swing では、コンテナ + レイアウトマネージャ の組み合わせでレイアウトを行います。これに対して、JavaFX ではコンテナがレイアウト機能を含んでいます。

なので、レイアウトマネージャに対応した何たら Pane を探せば OK。たとえば、FlowLayout であれば FlowPane、BorderLayuot であれば BorderPane など。

実際にレイアウトを組み立てるのは FXML なので、クラス名が分かれば、後は Scene Builder でポトペタできます。

非同期処理は Swing が SwingWorker クラス、JavaFX が Service クラス、Task クラスで表します。


ということで、実際に Swing のサンプルを JavaFX に移植してみました。サンプルは Twitter のツィート検索ツールです。

Swing の GUI の構造から FXML を作成し、それをロードするメインクラスを作成します。次に、FXML に対応して、イベント処理を行うコントローラクラスを作成します。

このサンプルにはテーブルが含まれているのですが、ここが移植の一番めんどうなところです。

Swing の JTable は内部のモデルとして TableModel インタフェースを実装したクラスを保持しています。通常は DefaultTableModel クラスもしくは AbstractTableModel クラスのサブクラスとして実装します。

TableModel インタフェースの一番キモになるメソッドが getValueAt(int row, int column) です。つまり、テーブルモデル内でどのようにデータを保持しているかはおいておいたとしても、テーブルモデルを使う側からすると 2 次元の表の列と行を指定して、その値を取得できるようにすればいいわけです。

これに対して、JavaFX の TableView はモデルとして JavaBeans を使用します。そして、JavaBeans であるモデルクラスのプロパティとテーブルのカラムをバインドさせます。

JTable では単なる 2 次元の表だったのが、TableView ではカラムに意味を持たせるわけです。

そして、そのモデルクラスのリストを TableView に渡してあげると、モデルオブジェクトのプロパティの値を対応するカラムに表示します。

この考え方の違いを理解しないと、TableView 使いにくいになってしまいがち。でも、モデルとしての JavaBeans と、テーブルの対応が分かれば、TableView の方が理解しやすいと思います。

ただ、今の書き方だとカラムとプロパティのバインドがちょっとめんどうなんですよね。

非同期処理もやり方が違うだけで、やろうとしていることは変わりありません。Service/Task の書き方さえ分かれば、SwingWorker からの移行も簡単にできるはずです。

結局、GUI でやることなんてそんなにガラッと変わるわけではありません。GUI 部品を配置して、イベント処理を行うという基本的な考えは同じです。

なので、Swing を使っていたのであれば、JavaFX への移行は結構簡単にできるはずです。

JavaFX でプラグインを使う

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

昨日は @orekyuu さんの 24 日目なのに、タイトルは 25 日目になっている JavaFX Advent Calendar25日目 ~ JavaFXで夢のCanvasライフ でした。


いつも、アニメーションなどの描画ネタが多いのですが、今日は趣を変えて JavaFX のアプリケーションでプラグインを作ろうと思います。

意外に知られていないと思いますが、Java SE にはプラグインを作るのに便利なクラスがあります。

そのクラスは java.util.ServiceLoader クラスです。

ServiceLoader クラスについては、ずいぶん前に ITpro の Java 技術最前線に記事を書いたので、そちらをぜひご参照ください。

「Java SE 6完全攻略」第11回 コンポーネントのロードを行うServiceLoader

ようするに ServiceLoader クラスはプラグインを検索して、ロードしてくれるクラスです。

たとえば、プラグインを表すインタフェースを foo.Bar インタフェースとしましょう。

そして、プラグインごとに Jar ファイルを作成しますが、その時に META-INF/services/foo.Bar というファイルを作成します。その foo.Bar ファイルには foo.Bar インタフェースを実装したクラス名を記述します。

これだけでその Jar ファイルをクラスパスに含めておけば、プラグインとしてロードすることができます。

ここでは、プラグインの仕組みだけを示すために、簡単なアプリケーションを作ることにします。アプリケーションには複数のタブがあり、そのタブの中身をプラグインで表示させるということにします。

こんな感じ。

プラグインは、ファクトリとプラグインの本体という構造にしたいと思います。

まずファクトリは PluginFactory インタフェースとします。

package net.javainthebox.fxplugin.plugin;

import java.util.Optional;

public interface PluginFactory {
    String getName();
    Optional<Plugin> createPlugin();
}

getName メソッドはタブの名前を返すためのメソッドプラグインの本体は createPlugin メソッドで生成します。

プラグインの JAR ファイルは、このインタフェース名と同じファイルを作成します。つまり、META-INF/services/net.javainthebox.fxplugin.plugin.PluginFactory ファイルです。

そして、プラグインの本体は Plugin インタフェースです。

public interface Plugin {
    Node getContent();
}

そして、このプラグインをロードするためのアプリケーションはすごい簡単なものにしました。

public class Main extends Application {
    
    @Override
    public void start(Stage stage) {
        StackPane root = new StackPane();
        
        TabPane tabs = new TabPane();
        root.getChildren().add(tabs);

        loadPlugins(tabs);
        
        Scene scene = new Scene(root, 300, 250);
        
        stage.setTitle("FXPlugin");
        stage.setScene(scene);
        stage.show();
    }
    
    private void loadPlugins(TabPane tabs) {
        ServiceLoader<PluginFactory> loader
                = ServiceLoader.load(PluginFactory.class);
        
        loader.forEach(factory -> {
            Tab tab = new Tab(factory.getName());
            factory.createPlugin().ifPresent(plugin -> tab.setContent(plugin.getContent()));
            
            tabs.getTabs().add(tab);
        });
    }

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

ここで、プラグインをロードしているのは loadPlugins メソッドです。

ServiceLoader オブジェクトは load メソッドで生成できます。load メソッドの引数はロードするプラグインのインタフェースです。

ServiceLoader クラスは Iterable インタフェースを実装しているので、forEach メソッドを使用できます。

まず、ファクトリの getName メソッドを使用して、Tab オブジェクトを作成します。

そして、プラグインの本体は createPlugin メソッドで生成します。この返り値は Optional クラスなので、値があるときだけ、Tab オブジェクトのコンテンツをセットするようにしました。

これでプラグインをロードする部分はできました。

プラグインの作成

では次にプラグインを作成してみましょう。

ここではシンプルなプラグインということで、ボタンが 1 つだけあるプラグインを作成します。

まずはファクトリクラスです。

public class ButtonPluginFactory implements PluginFactory {

    @Override
    public String getName() {
        return "Button";
    }

    @Override
    public Optional<Plugin> createPlugin() {
        try {
            return Optional.of(new ButtonPlugin());
        } catch (IOException ex) {
            return Optional.empty();
        }
    }
}

getName メソッドは Button を返すだけです。

createPlugin メソッドは IOException 例外が発生したら、空の Optional オブジェクトを返します。それ以外は ButtonPlugin オブジェクトを使用します。

では ButtonPlugin クラスを見てみましょう。

public class ButtonPlugin implements Plugin {
    private AnchorPane content;
    private ButtonPluginViewController controller;
    
    public ButtonPlugin() throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("ButtonPluginView.fxml"));
        content =loader.load();
        controller = loader.getController();
    }

    @Override
    public Node getContent() {
        return content;
    }
}

ここでは、FXML をロードしています。

FXMLLoader オブジェクトを生成しているのは、コントローラクラスを取得するためです。ここではコントローラクラスを直接アクセスしていませんが、大規模なアプリケーションの場合はコントロールクラスからモデルへアクセスするなど、コントローラクラスの取得が必要な場合が多くあります。

そういうときのためにも、FXMLLoader オブジェクトを生成しておく方がいいと思います。

いちおう FXML ファイルも示しておきましょう。

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

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


<AnchorPane prefHeight="200.0" prefWidth="300.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="netjavainthebox.fxplugin.buttonplugin.ButtonPluginViewController">
   <children>
      <Button layoutX="117.0" layoutY="75.0" mnemonicParsing="false" onAction="#action" style="-fx-font-size: 24;" text="OK" AnchorPane.bottomAnchor="75.0" AnchorPane.leftAnchor="117.0" AnchorPane.rightAnchor="116.0" AnchorPane.topAnchor="75.0" />
   </children>
</AnchorPane>

コントローラクラスは、ボタンがクリックされたらダイアログを表示するだけです。

ダイアログを使用しているので、このサンプルをビルド、実行するには、JDK 8u40 が必要です。

public class ButtonPluginViewController implements Initializable {

    @FXML
    private void action(ActionEvent event) {
        Alert alert = new Alert(AlertType.INFORMATION);
        alert.setTitle("Button Plugin");
        alert.getDialogPane().setHeaderText("Button Plugin");
        alert.getDialogPane().setContentText("Button Plugin");
        alert.show();
    }

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

そして、忘れてはいけないのが META-INF/services/net.javainthebox.fxplugin.plugin.PluginFactory ファイルです。

ファイルの中身は PluginFactory インタフェースの実装クラス名です。

netjavainthebox.fxplugin.buttonplugin.ButtonPluginFactory

もう 1 つ、同じように ListPlugin というのも作成しました。

そして、アプリケーションの実行時には、2 つのプラグインの JAR ファイルをクラスパスに付け加えます。Windows だったら、こんな感じで実行します。

java -cp PluginContainer.jar;ButtonPlugin.jar;ListPlugin.jar net.javainthebox.fxplugin.container.Main

実行すると、タブが 2 つ表示されます。

ちゃんとプラグインがロードできました。

ここでは、簡単なアプリケーションですが、これを応用すれば、複雑なアプリケーションもできるはずです!

サンプルのコードは GitHub で公開しています。

FXPlugin