JavaFX 1.2 非同期処理の概要(3)ー 非同期処理の進捗を通知する
まったく人気のないJavaFXネタですが、当面は続くと思われます。使ってみると思った以上に面白いです。
今回は非同期処理の概要としては最終回。非同期処理の進捗を表示するような場合にどうするかです。これまでの非同期処理は終わった時に関数が呼び出されるため、長い処理が今どのくらいの進捗なのかが解りませんでした。プログレスバーや進捗率などを表示することはRIAとしては必要不可欠な要素の1つかと思いますので、紹介します。
尚、ネタはこちらのBlogからです。また、今後改良される可能性が非常に高いことはお断りしておきます*1。
実行サンプルはこちら。
進捗をMyLongTaskに伝える
それではタスクの進捗状況をMyLongTaskに伝え、それを表示しましょう。JavaFX 1.2ではこの仕組みを提供するフレームワークは準備されていますが、未完成な状態のようです。現時点では、開発者が色々と実装しなければなりません。最終的にはSwingWorkerのような統一されたAPIが用意されるとは思います。
MyLongTaskHandler.java
最初に作成するのは、MyLongTaskHandlerは進捗に変化があった時に進捗状態を通知するインターフェイスです。このインターフェイスは新しく作成します。
package jp.deathmarch.javafx.sample.async; public interface MyLongTaskHandler { void onProgress(int value); }
MyLongTaskResultHolder.java
特に変更はありません。
package jp.deathmarch.javafx.sample.async; import java.util.concurrent.atomic.AtomicReference; public class MyLongTaskResultHolder { private final AtomicReference<String> result = new AtomicReference<String>(); public String getResult() { return result.get(); } public void setResult(String result) { this.result.set(result); } }
MyLongWorker.java
次に非同期に実行される処理を記述したクラスです。しつこいようですが、このクラスのrun()メソッドは非EDTで実行されます。忘れないように書いておくべきでしょう。
package jp.deathmarch.javafx.sample.async; import com.sun.javafx.functions.Function0; import javafx.async.RunnableFuture; import javafx.lang.FX; // 非EDTで実行されるタスク // 決してGUIの描画に関わるような処理を記述してはいけない public class MyLongWorker implements RunnableFuture { private final MyLongTaskResultHolder result; private final MyLongTaskHandler handler; public MyLongWorker(MyLongTaskResultHolder result, MyLongTaskHandler handler) { this.result = result; this.handler = handler; } // このメソッドは非EDTで実行される @Override public void run() throws Exception { System.out.println("MyLongTask start."); for (int i = 0; i < 10; i++) { try { Thread.sleep(500); // sleep 500ms * 10 onProgress(i+1); } catch (InterruptedException e) { // do nothing } } result.setResult("Complete!"); System.out.println("MyLongTask end."); } // 進捗状況を通知する。 // そのまま呼び出ささずにEDTで処理させる private void onProgress(final int value) { FX.deferAction(new Function0<Void>() { @Override public Void invoke() { handler.onProgress(value); return null; } }); } }
コンストラクタの引数にMyLongTaskHandlerを追加しています。進捗状況の通知はこのインターフェイスを通して行います。また、進捗が解りやすいようにsleepは0.5秒毎に10セットに変更しています。それぞれの処理が終わった時にonProgressを呼び出しているだけです。
onProgressメソッドでは、FXクラスのdeferActionメソッドを使い、MyLongTaskHandlerに進捗を通知しています。この部分は次のようにしてはいけません。
private void onProgress(int value) { handler.onProgress(value); }
これがマズイ理由はEDT以外からの処理だからです。onProgressではGUIに関連する処理が行われている筈なので、この処理は必ずEDTで行わなければなりません。
Swingではこのような場合、SwingUtils.involeLaterを使用しましたが、JavaFXの場合は、javafx.lang.FX#deferActionを使用します。細かいことは兎も角、『後でEDTでやってくれ』という意味は同じです。
これで、各進捗でEDTを通して進捗が通知されるようになりました。
MyLongTask.fx
修正点はMyLongTaskHandlerを継承するようになった事と、JavaTaskBaseのインスタンス変数の初期化を行っている事です。
JavaFXではmixinが可能な為、インターフェイスでもextends句に記述します。実装に関してはoverrideを使うので差はありません。progressはJavaTaskBaseに定義されたインスタンス変数ですが、ここにハンドラーを経由して通知された値をセットしています。
package jp.deathmarch.javafx.sample.async; import javafx.async.JavaTaskBase; import javafx.async.RunnableFuture; public class MyLongTask extends JavaTaskBase, MyLongTaskHandler { init { maxProgress = 10; // 進捗の最大値 progress = 0; // 初期の進捗値 } var result: MyLongTaskResultHolder; // return RunableFuture implement override function create(): RunnableFuture { result = new MyLongTaskResultHolder(); return new MyLongWorker(result, this); } public function getResult():String { return result.getResult(); } override function onProgress(value: Integer): Void { progress = value; } }
また、initブロックにてmaxProgressとprogressの値を設定しています。どちらもpublic-read protectedなインスタンス変数です。
Main.fx
ほとんど変わりませんが、進捗を表示するTextが追加されています。こちらもbindを使ってあっさりとした記述ですね。
package jp.deathmarch.javafx.sample.async; import javafx.stage.Stage; import javafx.scene.Scene; import javafx.scene.text.Text; import javafx.scene.text.TextOrigin; var text:String; var myLongTask:MyLongTask = MyLongTask { onStart: function() { text = "onStart"; } onDone: function() { text = "onDone: {myLongTask.getResult()}"; } }; myLongTask.start(); Stage { title: "Async Sample1" scene: Scene { width: 100, height: 50 content: [ Text { textOrigin: TextOrigin.TOP content: bind text } Text { y: 20 textOrigin: TextOrigin.TOP content: bind "{myLongTask.percentDone}%" } ] } }
まとめ
3回に渡ってJavaFXの非同期処理を解説してきましたが、基本的には非同期タスクを作成し、フレームワーク側がスレッドの管理を行うようになっています。JavaTaskBaseの実装は公開されていませんが、恐らくは数スレッドを管理するExecutorServiceがいて、そこにRunnableでラップしてsubmitしているのではと思われます。
現時点では定期的な処理を別スレッドで行うようなフレームワークは用意されていませんが、おそらくJavaFX 2.0の頃には実装されるのではないでしょうか?現時点で作る場合でも、それほど労力は使わずに実装できそうです。
また、現時点ではJavaのインターフェイスを使わないと非同期処理ができません。これは次のように記述すれば簡単に(ほとんど)JavaFXの世界のみで実現できるため、近い将来用意されると思います。
ActionRunnableFuture.java
public class ActionRunnableFuture implements RunnableFuture { private final Function0<Void> func; public ActionRunnableFuture(Function0<Void> func) { this.func = func; } @Override public void run() throws Exception { this.func.invoke(); } }
Main.fx
class FxActionTask extends JavaTaskBase { public-init var action: function(): Void; override function create(): RunnableFuture { return new ActionRunnableFuture(action); } } FxActionTask { action: function() { java.lang.Thread.sleep(5000); } onDone: function() { println("END"); } }.start();
ポイントはFunction0クラスがFX世界の関数をJava世界で扱うためのクラスという点でFX世界のFxActionTaskクラスのaction関数をそのままコンストラクタの引数に渡しているところです。
こうすることで、FxActionTaskを利用する側としては、何を非同期で行うかをactionに定義し、終わったらどうするかをonDoneに定義するだけで済むわけです。
*1:とはいえ仕組みを知るのは重要