2016年7月7日木曜日

JavaFX で Windows のシステムトレイを使う

JavaFX で Windows のシステムトレイを使おうとした場合、
JavaFX 内での解決方法はなく、
Java 6 で導入された java.awt.TrayIcon を使わないといけないようです。

TrayIcon 自体の使い方は


といったページなどでまとめられているので、そちらを参照して下さい。

ここでは TrayIcon を JavaFX で使う場合の注意点について触れます。

  1. TrayIcon に登録した Listner から、JavaFX の UI に対して操作を行う場合には、直接操作を行うのではなく、 Platform.runLater() を使って JAT コンテキスト内で処理されるようにする
  2. アプリケーションの終了は System.exit() ではなく、JavaFX で Platform.setImplicitExit(false) した上で Platform.exit() を使う。
    その上で exit する際には必ず明示的に SystemTray.getSystemTray().remove() を呼んで SystemTrayIcon を消すようにする
という2点です。

ズバリ、この点については次のページを参考にしています。

が、一応、私なりの補足を書いておくと…

注意点の1点目は JavaFX の GUI スレッド用の対応です。

JavaFX の解説書や JavaFX でスレッドを扱う場合の記事などで良く触れられますが、 JavaFX は GUI 用に独自のスレッド JavaFX Application Thread (JAT) を持っていて、GUI の操作は全て JAT のスレッドのコンテキスト内から行わないといけません。
しかし TrayIcon は、 JavaFX ではなく awt のためスレッドのコンテキストが分かれてしまっています。 そのため、TrayIcon に登録した Listner 側からは JavaFX GUI の操作が直接できません。

そういう場合には Platform.runLater() を使うと、JAT とは別のスレッドコンテキストから JAT に対して処理依頼が出来るようになります。
(runLater() で依頼された処理は、JAT 側のキューにためられて順次処理がされます)

例えば、TrayIcon をダブルクリックしたら、JavaFX のメインウィンドウ(primaryStage)を隠したい(hide() したい)場合などは次のような感じになります。
SystemTray tray = SystemTray.getSystemTray();
TrayIcon icon = new TrayIcon(ImageIO.read(getClass().getResourceAsStream("hoge.png")));
icon.setImageAutoSize(true); // hoge.png を trayicon サイズに自動調整
icon.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        Platform.runLater(() -> {
            primaryStage.hide();
        });
    }
});
tray.add(icon);
注意点の2点目は、JavaFX での終了処理の作法に絡んだ対応です。

JavaFX アプリは Platform.exit() を使うのが作法であります。
Application の Javadoc によれば
JavaFXアプリケーションを明示的に終了するには、Platform.exit()を呼び出すことをお薦めします。代替方法としてSystem.exit(int)を直接呼び出すことが許容されますが、Applicationはstop()メソッドを実行できなくなります。 
とありますので、stop() に何も実装していないのであれば、System.exit() でも良さそうではあります...
ではありますが、副作用も読み切れないので、Platform.exit() を使うという前提で話を進めます(笑

TrayIcon を使うのであれば、JavaFX のメインウィンドウを閉じる場面が多々あると思われます。
それでアプリケーション自体が終了しないための指定として Platform.setImplicitExit(false) を呼び出して、 JavaFX アプリの終了は内部で Platform.exit() を呼び出した場合だけにします。

ただ、Platform.exit() 呼び出し後に TrayIcon をそのままにしておくと、awt のスレッドが残ってしまい、アプリケーションが終了しません。
そこで、Platform.exit() と一緒に必ず SystemTray.getSystemTray().remove() を呼び出して TrayIcon の後始末=awt スレッドの終了もされるようにします。

私自身は、Application#stop() に実装をして、 Platform.exit() の呼び出し時に必ず呼び出されるようにしてみました。
これでうまく行く感じです。
ざっと実装の概要だけ書くとこんな感じです。
public class Main extends Application {
    private TrayIcon icon;
    @Override
    public void start(Stage primaryStage) {
        // start で TrayIcon の登録処理を実装
        Platform.setImplicitExit(false);
        SystemTray tray = SystemTray.getSystemTray();
        icon = new TrayIcon(ImageIO.read(getClass().getResourceAsStream("reload_12x14.png")));
        // 本当はここで Listener 登録をしているが、ここでは省略
   
        tray.add(icon);
    }
    @Override
    public void stop() throws Exception {
        // stop で TrayIcon の登録処理を実装
        SystemTray.getSystemTray().remove(icon);
        super.stop();
    }
}

二点目ですが、
「javafx platform.exit system.exit」といった Google 検索をした結果を見ていると、
Platform.exit();
System.exit();
なんて、2つ組み合わせての呼び出しも見受けられます。
どうしても強制的&確実にアプリケーション全体を終わらせたい場合には、こういう手もアリなのかもしれません…


0 件のコメント:

コメントを投稿