Java 9で強化されたデスクトップ環境関連のAPIをJavaFXで使ってみる

このエントリは JavaFX Advent Calendar 2016 の 18 日目のエントリです。前日は id:nodamushi さんによる「 JavaFX9からPlatformに追加されるAPIについて 」でした。

はじめに

今回のエントリは JDK9 に新たに取り込まれる "JEP 272: Platform-Specific Desktop Features" を JavaFX から触ってみるというものです。

実は JDK9 では AWT/Swing/Java2D にかなり手が入ります。次のように多くの JEP が 9 には取り込まれます。

利用者に見えない内部的な強化や HiDPI 対応のような流石に対応しないとまずいものが中心ですが、その中でも JEP 272 はアプリケーション開発者側から見ても大きな機能追加になります。

まずはこの JEP 272 について紹介し、そしてこれが JavaFX からどの程度利用できるか調べてみた結果について述べていきたいと思います。

JEP 272 について

JEP 272 は "Platform-Specific Desktop Features" の名の通り、デスクトップ環境特有の機能を Java からも利用できるようにするというものです。それもプラットフォーム固有の機能も積極的に利用するというものです。

ご存じの通り Java のデスクトップ GUI アプリケーションでは、そのアプリケーションを実行するデスクトップ環境が提供している機能を利用することは中々難しかったりします。クロスプラットフォームアプリケーションの宿命かも知れません。

ですが、過去に Java 6 でそのための API が追加されたことがありました。

  • 特定のファイル・タイプに関連付けられたデフォルト・アプリケーションと対話する機能を提供する java.awt.Desktop クラス。
    • デスクトップ環境で設定されたデフォルトブラウザやエディタ、メーラなどを Java 側から起動することができます。
  • デスクトップ環境のシステムトレイにアクセスし、アプリケーション独自のトレイアイコン、メニューを追加できる java.awt.SystemTray クラス。

システムトレイを使ったりすると、よりネイティブアプリケーションっぽくなりますよね。ですが、その後の各 OS の進化に伴い、Java から使えないデスクトップ環境の機能がどんどん増えてきました。

例えば、Windows のタスクバー、Mac の Dock では次のようにプログレスバーを表示することができるようになっています。

f:id:aoe-tk:20161218003523p:plain:w1000 f:id:aoe-tk:20161218003543p:plain:w1000

バッジも表示できるようになっていますね。

f:id:aoe-tk:20161218003757p:plainf:id:aoe-tk:20161218003806p:plain:h94

でもこれらの機能には Java からアクセスすることができません。

また、Mac 特有の話ですが、Mac で実行するアプリケーションでは、メニューバーにアプリケーション名のメニューが通常のメニューの左に表示されます。アプリケーションの About ダイアログや環境設定ダイアログはここから開けるようにするよう Mac 上で動くアプリケーションは統一されています。でも、Mac 特有の話なので、Java アプリケーションはここにアクセスすることができません。

f:id:aoe-tk:20161218004452p:plain:w250

JEP 272 はこういったデスクトップ環境が提供する機能へ Java アプリケーションでもアクセスできるようにするものなのです。

提供されている機能を列挙します。

  • デスクトップ環境が起こすイベント (スリープ、サインアウト、フォアグラウンド/バックグラウンドの切り替えなど) に対してイベントリスナやイベントハンドラを登録する。
  • タスクバーへのアクセス。プログレス表示やバッジ表示、コンテキストメニューの追加を行える。
  • Mac 特有のアプリケーションメニューへのアクセス。

API レベルでは次のような追加になります。

  • java.awt.desktop パッケージの追加
    • デスクトップ環境で発生するイベントに対応した各種イベントクラスや、それに対するイベントリスナ、ハンドラが定義されている。
  • java.awt.Desktop クラスにメソッド追加
    • デスクストップ環境で発生する各種イベントに対して、イベントリスナの追加、ハンドラの登録を行うメソッドが追加されている。
  • java.awt.Taskbar クラスの新規追加
    • タスクバー (Mac での Dock) を操作するためのメソッドが定義されている。

この APIAWT の API です。でもこのエントリは JavaFX Advent Calendar のエントリです! なので、JavaFX からこれら機能を利用できるかを調べてみることにしましょう。

JavaFX から JEP 272 を利用する

それでは JavaFX から試してみることにします。ソースコードの全体は gist にアップしています。

https://gist.github.com/aoetk/7d5cc13e64239d1233e6dd879fed682e

Desktop クラスの利用

java.awt.Desktop クラスを使うと、デスクトップ環境で発生する様々なイベントに対して応答することができるようになります。まずはこのクラスのインスタンスを取得してみます。

if (Desktop.isDesktopSupported()) {
    Desktop desktop = Desktop.getDesktop();
    addAppEvents(desktop);
    setSystemMenuHandler(desktop);
} else {
    System.out.println("デスクトップはサポートされていません.");
}

実行している環境が Desktop クラスをサポートしているかを確認するメソッドがあるので、どの環境でも動かせるよう、必ずこれでチェックするようにしましょう。static メソッドである getDesktop() メソッドを使ってインスタンスを取得します。

デスクトップ環境で発生するイベントに対して応答できるようにしてみます。イベントが発生するとその旨を ListView に表示するようにします。スクリーンのスリープを例に取ると次のようになります。

// (中略)
@FXML
ListView<String> displayList;

private ObservableList<String> eventList = FXCollections.observableArrayList();
// (中略)

private void addAppEvents(Desktop desktop) {
    // (中略)
    if (desktop.isSupported(Desktop.Action.APP_EVENT_SCREEN_SLEEP)) {
        desktop.addAppEventListener(new ScreenSleepListener() {
            @Override
            public void screenAboutToSleep(ScreenSleepEvent screenSleepEvent) {
                addMessage("画面がスリープしようとしています.");
            }

            @Override
            public void screenAwoke(ScreenSleepEvent screenSleepEvent) {
                addMessage("画面がスリープから復帰しました.");
            }
        });
    } else {
        System.out.println("ScreenSleepEventはサポートされていません.");
    }
    // (中略)
}

// (中略)
private void addMessage(String msg) {
    Platform.runLater(() -> eventList.add(msg));
}

Desktop#addAppEventListner() メソッドを使って各種イベントリスナを登録します。画面スリープに対応するイベントは ScreenSleepEvent になります。イベント別にサポート有無をチェック可能なので、チェックするようにしましょう。

JavaFX からの利用に当たって注意点があります。それはイベントリスナの処理は AWT のイベントディスパッチスレッドで実行されるということです。このスレッドは JavaFX のアプリケーションスレッドとは別スレッドです。従って、 Platform.runLater() メソッドに処理をくるむ必要があります (addMessage() メソッドの実装に注目) 。

この他にも AppForegroundEvent (フォアグランド/バックグラウンドの変化に反応するイベント) 、 AppHiddenEvent (Mac 特有の「アプリケーションを隠す」に反応するイベント) 、SystemSleepEvent (システムのスリープに反応するイベント) 、 UserSessionEvent (ログインユーザのスイッチに反応するイベント) に対するリスナをセットして実行してみました。

Mac では全てのイベントに対応しています。アプリケーション起動後、「アプリケーションを隠して復帰 -> スクリーンをスリープして復帰 -> システムをスリープして復帰 -> ユーザスイッチをして復帰」を行った結果を示します。

f:id:aoe-tk:20161218020017p:plain:w532

バックグラウンドに回ったことを検知できるのはいいですね。そのタイミングで処理を停止してリソースの消費を防いだりするようなことができますね。

Windows 環境では残念ながら SystemSleepEventUserSessionEvent にしかサポートしていませんでした。システムをスリープ、復帰させた結果を示します。

f:id:aoe-tk:20161218020602p:plain:w301

Mac のアプリケーションメニューの利用

次に Mac のアプリケーションメニューにアクセスしています。これも Desktop クラスに対してハンドラを登録する形でカスタマイズします。まずは About メニューから。

if (desktop.isSupported(Desktop.Action.APP_ABOUT)) {
    // 現時点ではJavaFXでは何も起きない
    desktop.setAboutHandler(aboutEvent -> {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("オリジナルのAboutダイアログです.");
        alert.setHeaderText("設定");
        alert.show();
    });
} else {
    System.out.println("Aboutメニューはサポートされていません.");
}

Desktop#setAboutHandler() メソッドを使い、About メニューがクリックされた時の応答処理を登録します。オリジナルのダイアログを出そうとしたのですが...JavaFX ではそもそも About メニューの追加がされませんでした。( ;∀;)

ちなみに Swing アプリケーションで試したときはうまく動作しました。

次に設定メニューです。こちらは Desktop#setPreferencesHandler() メソッドを使い、設定メニューがクリックされた時の応答処理を登録します。

if (desktop.isSupported(Desktop.Action.APP_PREFERENCES)) {
    // 現時点ではJavaFXではエラーが起きる
    desktop.setPreferencesHandler(preferencesEvent -> {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setContentText("オリジナルの設定ダイアログです.");
        alert.setHeaderText("設定");
        alert.show();
    });
}

ですが、このコードを実行すると Cocoa 側から次のエラーメッセージが返ってきました...。

2016-12-17 21:21:08.978 java[1903:815004] *** -[__NSArrayM insertObject:atIndex:]: object cannot be nil
2016-12-17 21:21:08.984 java[1903:815004] (
    0   CoreFoundation                      0x00007fff88a2c452 __exceptionPreprocess + 178
    1   libobjc.A.dylib                     0x00007fff89405f7e objc_exception_throw + 48
    2   CoreFoundation                      0x00007fff88942a40 checkForCloseTag + 0
    3   AppKit                              0x00007fff8e19f8ce -[NSMenu insertItem:atIndex:] + 521
    4   libawt_lwawt.dylib                  0x00000001353c156f addMenuItem + 174
    5   libawt_lwawt.dylib                  0x00000001353c13f0 -[ApplicationDelegate _updatePreferencesMenu:enabled:] + 195
    6   JavaNativeFoundation                0x000000013536fd60 +[JNFRunLoop _performCopiedBlock:] + 17
(以下略)
)

AWT のフレームが生成されていることを前提に処理を進めているように推測されます。これはちょっと残念。なお、Swing で試したときはちゃんと動作しました。

というわけで、結論としては JavaFX では現状 Mac のアプリケーションメニューの利用はできないということになります。

Taskbar の利用

次にタスクバー (Dock) の利用を試してみることにします。バッジの表示とプログレス表示を試してみます。

if (Taskbar.isTaskbarSupported()) {
    Taskbar taskbar = Taskbar.getTaskbar();
    if (taskbar.isSupported(Taskbar.Feature.ICON_BADGE_NUMBER)) {
        taskbar.setIconBadge("10");
    } else {
        System.out.println("タスクバーのアイコンバッジへの数値登録はサポートされていません.");
    }
    addAction(taskbar);
} else {
    System.out.println("タスクバーはサポートされていません.");
}

タスクバーへのアクセスは Taskbar クラスを通して行います。インスタンスは static メソッドである Taskbar.getTaskbar() メソッドを使って取得します。

アイコンバッジのセットには Taskbar#setIconBadge() メソッドを使います。Mac 環境で試すと次のようにバッジの表示に成功しました!

f:id:aoe-tk:20161218174009p:plain:w423

Windows 環境は Taskbar クラスの利用そのものは可能だったものの、アイコンバッジの設定はサポートされていませんでした。まあ、Windows でのタスクバーアイコンへのバッジ登録が可能になったのは Anniversary Update からですしね。

次にプログレス表示を試してみます。次のように、JavaFX アプリ側のプログレス表示と、タスクバー側のプログレス表示を同時に行うようにしてみました。 AnimationTimer を使って表示しています。

AnimationTimer timer = new AnimationTimer() {
    private long startTime;

    @Override
    public void handle(long currentTime) {
        long elapsedTime = currentTime - startTime;
        if (elapsedTime > PROGRESS_TIME) {
            stop();
            progress.setProgress(1.0);
            startButton.setDisable(false);
        } else {
            double rateForProgressBar = Long.valueOf(elapsedTime).doubleValue() / PROGRESS_TIME;
            progress.setProgress(rateForProgressBar);
            int rateForTaskBar = (int) (rateForProgressBar * 100);
            if (taskbar.isSupported(Taskbar.Feature.PROGRESS_VALUE)) {
                taskbar.setProgressValue(rateForTaskBar);
            }
        }
    }

    @Override
    public void start() {
        startTime = System.nanoTime();
        progress.setProgress(0);
        if (taskbar.isSupported(Taskbar.Feature.PROGRESS_VALUE)) {
            taskbar.setProgressValue(0);
        } else {
            System.out.println("タスクバーのプログレス表示はサポートされていません.");
        }
        super.start();
    }
};

タスクバーのプログレス表示は Taskbar#setProgressValue() メソッドを使います。0 から 100 の間の数値をセットします。JavaFXプログレスバーは 0 から 1 と違うんですよね...。

Mac で試したところ、次のように Dock 側に進捗を表示できるようになりました!

f:id:aoe-tk:20161218175830g:plain

Windows は残念ながら未サポートでした。何でや、Windows7 の時からサポートしてたのに!

まとめ

というわけで JEP 272 の機能を JavaFX で試してみました。分かったことをまとめると次の通りです。

  • JavaFX からも JEP 272 の機能は一部を除き利用可能。
    • ただし、イベントリスナの処理は AWT のスレッドで実行される点に注意。
  • Mac のアプリケーションメニューにアクセスする機能は JavaFX からは使えない。
  • フル機能が使えるのは Mac だけ。Windows では一部の機能しかサポートしていない。
    • Linux では Ubuntu の Unity 環境でのみ使えるようです。今回は時間がなくて試せませんでした。

Mac から優先して実装されているのは理由があります。かつて Mac 環境向けの JavaApple 自身が開発、提供しており、こういた Mac 環境特有の機能にアクセスするために EAWT という API を提供していました。

Java 7 以降、Mac 向けの Java は OpenJDK で開発されるようになりますが、この EAWT は 7、8 にはバンドルが続いていました。ですが 9 からは提供をやめることになり、代替機能を Java 側で用意することになりました。それがこの JEP 272 であるということです。

ともあれ、よりネイティブアプリケーションっぽく振る舞えるような機能が実装されたのは嬉しいことです。今後のアップデートで Mac 以外の環境向けにも実装が進んでいくことでしょう。JavaFX 向けには 10 から同等機能の実装が予定されていますが、前倒しで実装とかされないかなあ。 *1

明日は誕生日枠の id:yumix_h さんの予定です。

*1:8 の時も u40 でかなりの機能追加が入ったことがあったので