JavaFX Hands on Lab

8/24 に JJUGJavaFX ユーザグループの共催で、JavaFX のハンズオンを行いました。

私がメインで説明をして、 id:aoe-tk さんと fukai_yas さんにヘルプしていただきました。

当日はなんと歩留まり 90% 以上!!

人数では隣でやっていた Java EE 7 のハンズオンに負けてしまうのですが、歩留まりや懇親会の参加率ではこちらの方が勝っていましたよww

さて、ハンズオンでは 2 つのアプリケーションを作成しました。一方が Hello, World! と、そのちょっとした拡張。もう一方が表とブラウザを使ったブックマーク的なアプリケーションです。

テキスト

当日はテキストには書いてないのですが、ちょっとした応用をやりました。また、ちょっと補足したいこともあるので、ここに書いておきます。

ラベルとテキストフィールドのバインド

ここで作ったサンプルはテキストフィールドとボタンとラベルから構成されていて、テキストフィールドに文字を入力して、リターンを入力するか、ボタンをクリックすると、ラベルにその入力した文字が反映されるというものでした。

でも、エンターキーやボタンをクリックしなくても、文字を入力したらすぐにラベルに反映させたくなることもありますよね。

そういう場合、Swing/AWT であれば、KeyEvent を拾って、それをもとにラベルに文字を追加するということを行ってきました。でも、これがまた、結構めんどうくさい。

JavaFX であれば、もっと簡単にリアルタイムの反映ができます。

どうやって? バインドです。

バインドは 2 つのプロパティを自動的に同期させる機構です。片方向のバインドもありしますし、双方向のバインドもあります。

たとえば、以下のコードでは、整数のプロパティの y が x にバインドしています。

        IntegerProperty x = new SimpleIntegerProperty();
        IntegerProperty y = new SimpleIntegerProperty();
        
        y.bind(x); // y を x にバインドする

        // x に 10 を代入                
        x.set(10);
        System.out.println(y.get()); // 10 が出力
        // x に 20 を代入                
        x.set(20);
        System.out.println(y.get()); // 20 が出力

ここでは片方向のバインドなので、y に値を設定することはできません。

さて、これをテキストフィールドとラベルにもおこなってしまおうというわけです。

Label クラスで表示する文字列のプロパティは textProperty() メソッドで取得できます。同じように、TextField クラスも textProperty() メソッドで取得できます。

そこで、コントローラクラスの initialize メソッドに次のように記述します。

        label.textProperty().bind(textField.textProperty());

これでテキストフィールドに何か入力すれば、すぐにラベルに反映されます。

KeyEvent を使うのに比べると、すごい簡単ですね。

しかし、本来やりたかったこととはちょっと違います。ここでは、Hello, X! の X の部分だけをテキストフィールドに入力させるということをやっていました。

しかし、先ほどのコードだとテキストフィールドの文字列とラベルの文字列が同一になってしまいます。

ではどうするかというと、プロパティ同士の演算をやってしまえばいいのです。

プロパティ同士の演算は、ユーティリティクラスの Bindings で定義されています。文字列の連結には、concat メソッドが使えます。

そこで、上のコードを下のように書き換えます。

        label.textProperty().bind(Bindings.concat("Hello, ", textField.textProperty(), "!"));

これで OK!

ChangeListener と InvalidationListener

後半のテーブルとブラウザを使用したサンプルでは、テーブルの選択行が変更されると、ブラウザで表示するというものでした。

この時、テーブルの選択行が変わったことを検知する部分を次のように記述していました。

        TableView.TableViewSelectionModel<Bookmark> selectionModel = table.getSelectionModel();
        selectionModel.selectedItemProperty().addListener(new ChangeListener<Bookmark>() {
            @Override
            public void changed(ObservableValue<? extends Bookmark> value, Bookmark old, Bookmark next) {
                String url = next.getUrl();
                engine.load(url);
            }
        });

しかし、これではまれに NullPointerException 例外が発生してしまうことがあります。といのも、chaged メソッドの第 2, 第 3 引数に null が渡ってくることがあるからです。

そのため、ここには null check が必要です。

            public void changed(ObservableValue<? extends Bookmark> value, Bookmark old, Bookmark next) {
                if (next != null) {
                    String url = next.getUrl();
                    engine.load(url);
                }
            }

ところで、ここでは ChangeListener インタフェースを使用しましたが、InvalidationListener インタフェースを使用することもできます。

両者の違いは、ChangeListener インタフェースがプロパティの値が変化したことを検知することに対し、InvalidationListener インタフェースがプロパティが使用されるときに値をチェックするということです。

このため、ChangeListener インタフェースではイベントが発生しても、InvalidationListener インタフェースではイベントが発生しないということがあります。

逆にいうと、InvalidationListener インタフェースを使用すれば、無駄なイベントを処理せずにすみます。

ただ、InvalidationListener インタフェースはちょっと使いにくい。といのも、InvalidationListener インタフェースの invalidated メソッドの引数の方が Observable インタフェースだからです。

Observable インタフェースを実装したクラスがプロパティになるのですが、Observable インタフェースにはリスナー登録のメソッドしか定義していないため、結局プロパティにキャストしてあげなくてはいけません。その前に instanceof で型のチェックをするわけですけど、instanceof にはジェネリクスの型が指定できません。

結局、めんどくさいので引数は使用しないということになりがち。

で、InvalidationListener インタフェースで書くとしたら、次のようになります。

        final TableView.TableViewSelectionModel<Bookmark> selectionModel = table.getSelectionModel();

        selectionModel.selectedItemProperty().addListener(new InvalidationListener() {
            @Override
            public void invalidated(Observable observable) {
                Bookmark bookmark = selectionModel.getSelectedItem();
                if (bookmark != null) {
                    engine.load(bookmark.getUrl());
                }
            }
        });

さて、ここでの例は ChangeListener インタフェースで書いても、InvalidationListener インタフェースで書いても、差はありません。

でも、ほんとに画面を更新する場合だけ値をチェックすればいい場合も多く、こういう場合は InvalidationListener インタフェースが有効になります。どちらも使えるようにしておけるといいですね。

ページのロード完了時のアニメーション

GitHub にあげてあるサンプルでは、Web ページのロード完了時にフェードインするアニメーションが書いてあります。でも、ハンズオンではもうちょっと派手なアニメーションを書きました。

派手といっても、複数のアニメーションを同時に行うことで派手に見せているだけです。

ここではフェードインと回転とスケーリングを一緒にやってみます。

もともとのコードはこれ。

        // ページのローディングが完了したら、フェードイン
        FadeTransition fadeIn = new FadeTransition(Duration.millis(1_000), webView);
        fadeIn.setToValue(1.0);
        fadeIn.play();

これを次のように変えました。

        FadeTransition fadeIn = new FadeTransition(Duration.millis(1_000), webView);
        fadeIn.setToValue(1.0);
                    
        RotateTransition rotate = new RotateTransition(Duration.millis(1_000), webView);
        rotate.setFromAngle(-360.0);
        rotate.setToAngle(0.0);
                    
        ScaleTransition scale = new ScaleTransition(Duration.millis(1_000), webView);
        scale.setFromX(0.1); scale.setFromY(0.1);
        scale.setToX(1.0); scale.setToY(1.0);
                    
        new ParallelTransition(fadeIn, rotate, scale).play();

RotateTransition クラスが回転、ScaleTransition クラスがスケーリングです。

そして、最後の ParallelTransition クラスで、複数のアニメーションを同時に行います。

まぁ、派手にしても意味はないんですけどね ^ ^;;

さて、ハンズオンですが、アンケートでも好評だというご意見が多かったです。その一方、Java EE と同時開催はやめて欲しいという意見も。

今回は、たまたま大きい会場がとれてしまったものの、そんな人数でハンズオンするのも大変なので、半分に分けましょうということで、こうなったのでした。次回はもうちょっと考えます。

とりあえず、ハンズオンの企画はこれからも続けようと思っているので、もしこういうハンズオンをやって欲しいという意見があれば、JJUG の ML や Twitter の @JJUG へお願いします!!