われプログラミングする、ゆえにバグあり

私だって価値を創造してみたいのです

JavaFXがApplicationThread以外からのUIの更新に厳しくなってた対策

JavaFXJavaGUIのアプリケーションを作るのに便利です。

Java8とともにJavaFX8がリリースされたので、以前にJavaFX2.2で作成したアプリをJava8(JavaFX8)で置き換える作業を現在行っています。

わりと簡単にできるだろうと楽観していたのですが、いきなり困った事がありました。

ApplicationThread以外からはUIを変更できない。

表題の通りです。どこかで聞いたことがあるようなないような...
Androidでありますね。これ。Androidの場合は"UIThread"って名称だったと思いますけど、ここでは同じようなものだと考えればいいです。

わかってたのなら今更言うなやし。と思われるかもしれませんが、
これ、JavaFX2.2までは可能だったんです。
(稀にApplicationThread以外から更新ダメってエラーが出る部分がありましたけど、むしろそのケースを潰してた方が楽なレベルでした)
ですが、JavaFX8からはほぼ完全に(?)ApplicationThread以外からのUI更新は駄目っぽいです。


実際にそのコード例を書いてみます。

JavaFX2.2では問題ないが、JavaFX8では問題のあるコード
Service<?> service = new Service<Void>() {

    @Override
    protected Task<Void> createTask() {
        return new Task<Void>() {

            @Override
            protected Void call() throws Exception {
                File file = new FileChooser().showOpenDialog(owner);
                System.out.println(file);
                return null;
            }
        };
    }
};
service.start();

service.start(); から実行されているので、別スレッドでの処理になっています。
そして、new FileChooser().showOpenDialog(owner); の部分で(JavaFX2.2では)ファイルチューザが表示されるはずです。が、JavaFX8では表示されません。

 

これをJavaFX8で対応するためにServiceを使わないように書き換えるのは大変です。
なので、以下の方法で対応しました。

JavaFX8のために対策をしたコード
class FxUtils {
    public static <V> V getFromApplicationThread(Callable<? extends V> callable) throws Exception {
        if (Platform.isFxApplicationThread()) {
            return callable.call();
        }
        RunnableFuture<V> future = new FutureTask(callable);
        Platform.runLater(future);
        return future.get();
    }
}

Service<?> service = new Service<Void>() {

    @Override
    protected Task<Void> createTask() {
        return new Task<Void>() {

            @Override
            protected Void call() throws Exception {
                File file = FxUtils.getFromApplicationThread(() -> {
                    return new FileChooser().showOpenDialog(owner);
                });
                System.out.println(file);
                return null;
            }
        };
    }
};
service.start();

違いは、FxUtils.getFromApplicationThread() を呼んでいる部分です。
ApplicationThread で実行しないといけない処理だけをラムダ式にして渡しています。
このメソッドでは"現在"の Thread を調べて、ApplicationThread ならばそのまま実行。そうでなければ Platform.runLater() に FutureTask を渡します。そうすると ApplicatoinThread で実行してもらえます。あとは結果が返るのを待つだけです。(FutureTask: この場合、 future.get() はラムダ式が値を返すまで待ちます。戻り値はその値になります。)

 

まとめ

未調査な部分もあります

今回のコードようにはっきりと ApplicationThread 以外からUIを更新していることがわかっていれば対策はできますが、ApplicationThread 以外から Property を変更して、それが UI に影響がでるような Bind(callback) をしてるとなるとどうなんだろうなとか...
まだ調べてません。