このエントリは JavaFX Advent Calendar 2016 の 18 日目のエントリです。前日は id:nodamushi さんによる「 JavaFX9からPlatformに追加されるAPIについて 」でした。
はじめに
今回のエントリは JDK9 に新たに取り込まれる "JEP 272: Platform-Specific Desktop Features" を JavaFX から触ってみるというものです。
実は JDK9 では AWT/Swing/Java2D にかなり手が入ります。次のように多くの JEP が 9 には取り込まれます。
- JEP 251: Multi-Resolution Images
- JEP 256: BeanInfo Annotations
- JEP 258: HarfBuzz Font-Layout Engine
- JEP 262: TIFF Image I/O Plugin
- JEP 263: HiDPI Graphics on Windows and Linux
- JEP 265: Marlin Graphics Renderer
- JEP 272: Platform-Specific Desktop Features
- JEP 283: Enable GTK 3 on Linux
利用者に見えない内部的な強化や 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 では次のようにプログレスバーを表示することができるようになっています。
バッジも表示できるようになっていますね。
でもこれらの機能には Java からアクセスすることができません。
また、Mac 特有の話ですが、Mac で実行するアプリケーションでは、メニューバーにアプリケーション名のメニューが通常のメニューの左に表示されます。アプリケーションの About ダイアログや環境設定ダイアログはここから開けるようにするよう Mac 上で動くアプリケーションは統一されています。でも、Mac 特有の話なので、Java アプリケーションはここにアクセスすることができません。
JEP 272 はこういったデスクトップ環境が提供する機能へ Java アプリケーションでもアクセスできるようにするものなのです。
提供されている機能を列挙します。
- デスクトップ環境が起こすイベント (スリープ、サインアウト、フォアグラウンド/バックグラウンドの切り替えなど) に対してイベントリスナやイベントハンドラを登録する。
- タスクバーへのアクセス。プログレス表示やバッジ表示、コンテキストメニューの追加を行える。
- Mac 特有のアプリケーションメニューへのアクセス。
API レベルでは次のような追加になります。
- java.awt.desktop パッケージの追加
- デスクトップ環境で発生するイベントに対応した各種イベントクラスや、それに対するイベントリスナ、ハンドラが定義されている。
- java.awt.Desktop クラスにメソッド追加
- デスクストップ環境で発生する各種イベントに対して、イベントリスナの追加、ハンドラの登録を行うメソッドが追加されている。
- java.awt.Taskbar クラスの新規追加
- タスクバー (Mac での Dock) を操作するためのメソッドが定義されている。
この API は AWT の 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 では全てのイベントに対応しています。アプリケーション起動後、「アプリケーションを隠して復帰 -> スクリーンをスリープして復帰 -> システムをスリープして復帰 -> ユーザスイッチをして復帰」を行った結果を示します。
バックグラウンドに回ったことを検知できるのはいいですね。そのタイミングで処理を停止してリソースの消費を防いだりするようなことができますね。
Windows 環境では残念ながら SystemSleepEvent
と UserSessionEvent
にしかサポートしていませんでした。システムをスリープ、復帰させた結果を示します。
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 環境で試すと次のようにバッジの表示に成功しました!
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 側に進捗を表示できるようになりました!
Windows は残念ながら未サポートでした。何でや、Windows7 の時からサポートしてたのに!
まとめ
というわけで JEP 272 の機能を JavaFX で試してみました。分かったことをまとめると次の通りです。
- JavaFX からも JEP 272 の機能は一部を除き利用可能。
- ただし、イベントリスナの処理は AWT のスレッドで実行される点に注意。
- Mac のアプリケーションメニューにアクセスする機能は JavaFX からは使えない。
- フル機能が使えるのは Mac だけ。Windows では一部の機能しかサポートしていない。
Mac から優先して実装されているのは理由があります。かつて Mac 環境向けの Java は Apple 自身が開発、提供しており、こういた 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 でかなりの機能追加が入ったことがあったので