復活していたQtJambi

QtJambiの開発は引き継がれていた

OracleからJavaのクライアントテクノロジーに関するロードマップが発表され、それについて色々思うことを垂れ流した次のブログを投稿したのですが、あれからもう5年になるのですね。

aoe-tk.hatenablog.com aoe-tk.hatenablog.com

このエントリにて、次のようなことを書いていました。

汎用的なクロスプラットフォーム GUI で一番成功しているのはやはり Qt でしょうか。モバイルへの進出にも成功していますし。開発言語は C++ ですが、他のプログラミング言語へのバインディング も多いです。ですが、Java バインディングである Jambi が死んでしまったんですよね...。

QtのJavaバインディングであるJambiについて、このエントリを書いたときは死んだような状態になっていたのですね。ところが最近QtJambiという名称として (以前は "Qt Jambi" とQtとJambiの間にスペースが入っていました) 復活していたことに気付きました。

github.com

これは元々NokiaがQt JambiのOSS化にあたって作った 旧Qt Jambiのリポジトリ とは別のリポジトリです。 どうもOmix Visualizationという会社が最新のQtに対応したフォークを作成して開発を引き継いだようです。旧リポジトリでは対応していなかったQt5系はもちろん、6系も最新の6.5にまで対応したリリースがあります。

コミット履歴を調べてみると *1 一番最初のコミットは2015年9月で、旧リポジトリの最後のコミットが2015年9月であるのを見るに、旧プロジェクトの開発終了後すぐに新しいリポジトリを作ったようです。旧プロジェクトにてコミッタ間で話し合いがあってフォークが決まったとかですかねえ。

リポジトリ作成は2015年9月ですがリリースタグの付いた最初のリリースは2020年8月で、ここからプロダクトとしてリリースしていくステップに入ったようです。 Qt本家Wikiの言語バインディングのページ に追加されたのもその時からでした。旧プロジェクトの終了後、5年のブランクを経て復活した形ですね。

触ってみる

見つけたからには触ってみることにしましょう。なお自分はQtによる開発の経験は過去に一切ありません🙂

QtJambiはJPMSに従ったモジュール化がされています。次のAPIドキュメントのトップメージにモジュール一覧が列挙されています。Qtが提供する膨大なライブラリを概ねカバーしているようです。3DやWebViewなどはもちろん、BluetoothNFC、センサーなど実に膨大な範囲をカバーしていることが分かりますね。

https://doc.qtjambi.io/latest/

基本的なものは全て qtjambi モジュールに入っています。モジュール別にMaven Centralに登録されているので、Mavenを使っている場合は pom.xml に次のように依存設定を追加します。実行にあたってはプラットフォームに応じたネイティブライブラリも必要なので qtjambi-native-<os>-<architecture> も追加する必要があります。

    <dependencies>
        <dependency>
            <groupId>io.qtjambi</groupId>
            <artifactId>qtjambi</artifactId>
            <version>6.5.0</version>
        </dependency>
        <dependency>
            <groupId>io.qtjambi</groupId>
            <artifactId>qtjambi-native-windows-x64</artifactId>
            <version>6.5.0</version>
        </dependency>
    </dependencies>

これでプログラムを作成できます。ごく簡単なウィンドウとメニューを持つアプリケーションを作ってみます。

package aoetk.qtsamle;

import io.qt.gui.QAction;
import io.qt.widgets.QApplication;
import io.qt.widgets.QLabel;
import io.qt.widgets.QMainWindow;
import io.qt.widgets.QMenu;

import static io.qt.core.QObject.tr;
import static java.util.Objects.requireNonNull;

public class WindowSample {

    public static void main(String[] args) {
        QApplication.initialize(args);
        QMainWindow mainWindow = new QMainWindow();
        QMenu menu = requireNonNull(mainWindow.menuBar()).addMenu(tr("&File"));
        QAction quitAction = requireNonNull(menu).addAction(tr("&Quit"));
        requireNonNull(quitAction).triggered.connect(QApplication::quit);
        mainWindow.setCentralWidget(new QLabel(tr("Hello Qt!"), mainWindow));
        mainWindow.show();
        QApplication.exec();
        QApplication.shutdown();
    }

}

実行するにあたっては予めQtがインストールされている必要があります。 インストーラのダウンロードページ からQtのインストールを行います。予めユーザー登録を行う必要がある点に注意してください。非常に巨大なライブラリなのでインストールには結構時間が掛かります。

作ったJavaアプリケーションを実行する際、JVMシステムプロパティ java.library.path にQtのライブラリパスを指定します。OS別に次のようなパスになります。

  • Windows
    • <path to>\Qt\<version>\msvc2019_64\bin
  • Linux
    • <path to>/Qt/<version>/gcc_64/lib
  • macOS
    • <path to>/Qt/<version>/macos/lib
    • macOSの場合はさらに起動引数として -XstartOnFirstThread を指定する必要あり

Windows環境のIntelliJでの実行設定の例を示しておきます。

プログラムを実行すると次のようにメニューとラベルを持つウィンドウが起動しました。

使い方を学ぶには?

汎用クロスプラットフォームGUIツールキットとしては最強と思われるQtへのバインディングが提供されたことで、再びJavaにもスタンドアロンGUIアプリケーションを作るための強力な手段が加わりました。

現時点での問題点は余りにも情報が少ないという点ですね。GoogleTwitter、Stack Overflowなどを検索してもこの新しいQtJambiへの言及がほとんど見られていません。本当に一握りの人間しかこれの存在に気付いていないような気がしますw

基本的にドキュメントは GitHubプロジェクト上のWiki からたどれる範囲のみで、Qtについての基本概念は知っていることを前提とした記述になっています。APIドキュメントについても本家C++版のページへのリンクを示すだけのクラスが多いです。

自分はC++の開発経験がゼロに等しいので、C++版のドキュメントを読むのはちょっと辛いんですよねえ。幸いQtは有名どころのプログラミング言語に対してはほぼバインディングを提供しており、例えばPythonバインディングであるQt for Python (PySide) については公式からも豊富なドキュメントが提供されているので、これでQtを勉強しますかねえ。

www.qt.io

というわけでいつの間にか復活していたQtJambiについての紹介でした。

*1:コミット数はほぼリリース単位になっていて少なく、クローズドのリポジトリで開発してリリースのタイミングで一気にパブリックリポジトリにpushしているスタイルのようです。

Java 11ではPublic JREが本当になくなりました

Java 11 の登場で Java を取り巻く環境は様々な転換点を迎えることになりました。散々言われている Oracle からのリリース方法の変更の話もありますが、もう1つ、Public JRE の消滅があります。

実際に JDK11 をインストールして色々変化があったので、このエントリではその情報を共有したいと思います。と言っても Twitter ではこの件に関して頻繁につぶやいていたので、それを引用しながらの内容になります。

そもそも Public JRE って何?

一言で言うと「 あなたとJAVA, 今すぐダウンロード 」からダウンロードしてインストールするソフトウェアのことですw

JDK と異なり、Java アプリケーションの実行に必要なモジュールだけを OS のシステムレベルでインストールします。開発者ではなくエンドユーザー向けのソフトウェアです。次のような役割を担います。

  • Executable JAR ファイルの実行
    • Explorer や Finder といったファイラーから JAR ファイルをダブルクリックすることで Java アプリケーションを起動できるようにします
  • ブラウザ Java Plug-in 経由で起動される Java Applet、Java Web Start アプリケーションの実行
  • デスクトップ上での Java Web Start アプリケーションの実行
    • これもファイラーから JNLP ファイルをダブルクリックすることでアプリケーションの起動が可能になります
    • クライアント側に実行に必要な JAR のキャッシングも行い、バージョンが変わったときだけネットワークから差分データを取得するようなこともします

JDK11 でどう変わったか

そして、Java 11 ではこれが無くなりました。Oracle JDK 11 をインストールしたときのつぶやきを引用します。

こんな感じで本当に Public JRE のインストールがなくなりました。しかし、この時点では前のバージョンは残っており、今後 Public JRE の無償サポート (Java 8 の JRE については個人向けに対しては 2020 年までサポートを継続) が終了したときにはどうするのかという疑問点が残っていました。

Java 定期アップデート時の JRE 10 の扱い

その答えは先日の定期アップデートの時に判明しました。Public JRE をインストールしている場合、自動でアップデートチェックが走り、更新があった場合はインストールダイアログが表示されます。今回の場合、自分のマシン環境には Java 10 の Public JRE しか入っていませんでした。これは Java 11 のリリースと共に無償サポートは終了しました。

以下、そのときのつぶやきを引用します。

そうです、自動アップデートのタイミングで無償サポート対象外の JRE のインストールを検出した場合はそれをクリーンアップするように促されます。このようにして古い JRE が放置されないようにちゃんと考えていたのですね。

こうして私の環境では JAR をダブルクリックしても何も起きなくなり、当然のことながら Java Applet は実行できなくなりました。

今後の Java アプリケーションの配布はどうするのか?

このようにして、システムレベルでインストールする JRE は今後無くなっていきます。では、どうやって Java アプリケーションをエンドユーザーに届けるのかというと、アプリケーション開発者が JRE と共にアプリケーションを配布することになります。

Java 9 からは配布用の JRE を生成するための jlink というツールが付属しています。アプリケーション側がちゃんとモジュラー化していれば、アプリケーションの実行に必要なモジュールだけを含んだ JRE を生成することができます。今後はこれを利用しましょう。

ただこの jlink、コマンドライン起動を想定していて、起動はシェルスクリプト (Windows 向けには bat ファイル) の形になっており、GUI アプリケーション向きではありません。そのためには javapackager と言うツールがあったのですが (過去の私の このエントリこのエントリ で取り扱っています) 、このツールは JavaFX に付属していたものであり、 Java 11 で JavaFX が切り離された ため、JDK から無くなってしまいました。

そこで、OpenJDK では JDK 向けに javapackager 相当のものを作ろうという JEP が起案されました。

http://openjdk.java.net/jeps/343

名前は jpackager にするつもりみたいです。早く出てきて欲しいですね。

Java Client Roadmap Updateによせて (後編)

というわけで先日アップした次のエントリの後編です。

aoe-tk.hatenablog.com

前回は年寄りの思い出話という感じでしたがまさかの大きな反響を頂いて驚いています。後編については JavaFX や Swing、そしてクロスプラットフォーム GUI の今後について思うところを書いていきたいと思います。

JavaFX は今後どうなる?

今回の決定で JavaFXJDK リリースから分離されることになったわけですが、逆に言うと JDK のリリースサイクルに縛られること無く開発を進められることになります。そして、私の感覚からすると、当面 JavaFX が廃れるような心配はしなくていいと見ています。

JavaFXJava EE と同様によりオープンソースコミュニティに今後の開発をゆだねることになりましたが、JavaFX のコミュニティは今でもとても盛り上がっています。OpenJFX の ML では今でも盛んに議論がされていますし、先のホワイトペーパーでも「情熱的なコミュニティ」であると賞賛しています。

実は今回の発表の少し前に OpenJFX のリーダーである Kevin Rushforth 氏から OpenJFX コミュニティの今後の方向性について問いかけがありました。

http://mail.openjdk.java.net/pipermail/openjfx-dev/2018-February/021335.html

この議論では大変な盛り上がりを見せ、より外部の人が入りやすくなるような雰囲気にしようという方向になりました。その一環として GitHub に OpenJFX のリポジトリのミラーも作られ、PR を受け付けられるようにしています (OCA へのサインは必要です) 。

github.com

このように、JDK のリリースサイクルに縛られず、より外部も参加しやすくなることで、かえって開発のスピードが上がる可能性もあると考えています。前編でも述べたように海外では結構採用事例があり、Gluon のように JavaFX をビジネスのメインにしている会社もあります *1

と言うわけで、Java の標準 GUI では無くなりましたが、優秀なライブラリとしてあり続けるだろうと思っています。前編でも述べましたが、とても開発しやすい API ですし、JavaGUI を作るときの最有力候補であることは変わらないと思っています。コミュニティとの連携も取りやすくなる方向になっていますし、業務で JavaFX を採用したところがあってもそんなに心配しなくてもいいと思います。

私自身はここ数年フロントエンドとは余り縁の無い仕事をしている事もあり、業務で JavaFX を触る機会は無かったのですが、自分の身の回りでで GUI を使った道具が必要な時は JavaFX で作っていました。今後もそのためには JavaFX を使い続けると思っています。

Swing について

紆余曲折を経て、Java のデフォルト GUI は Swing に戻った感があるのですが、前編でも述べたように OpenJDK では再び Swing に力が入りそうな雰囲気になっています。Swing に JavaFX の良いところ (FXML とかバインディングとか) が移植されるような流れにならないかなあ。

私は Swing は一定の成功を収めたと思っています。何より Swing が覇権を握っている分野があります。それは IDE です。NetBeans と JetBrains の IDE だけじゃないかと突っ込まれそうですが、特に JetBrains IDE のここ最近の躍進ぷりはすごいです。JavaScriptPythonPHPRuby、Go など実に様々な言語コミュニティで人気を博しています。

クロスプラットフォーム GUI の今後

今回の件でクロスプラットフォーム GUI というのはやっぱり難しいものだなとは思いました。でも需要があるのは確かです。ゲームエンジンの様にユースケースをより絞ったものは普及していますしね。

汎用的なクロスプラットフォーム GUI で一番成功しているのはやはり Qt でしょうか。モバイルへの進出にも成功していますし。開発言語は C++ ですが、他のプログラミング言語へのバインディング も多いです。ですが、Java バインディングである Jambi が死んでしまったんですよね...。 *2

後は有力候補としてはやはり Electron なんですかねえ。私は好きじゃないんですよ。ぶっちゃけ Chrome ブラウザそのものなので、Electron アプリを 1 つ立ち上げると (潤沢にリソースを使う) Chrome ブラウザが余計に 1 つ立ち上がるようなものなので、使う側としてそんなに好きじゃないんです。Web 開発の難しさをデスクトップ GUI アプリ開発に持ち込みますし、そんなに JavaScript + CSS で開発したいですか?と言いたい感じです。まあ古典的なスタイルの GUI 開発が好きな人間としては、WebComponents に期待しているところです。

モバイル OS 戦争に敗れ去った MS は Xamarin に力を入れていますし、最近は Flutter なんてのも登場しましたし、今後もクロスプラットフォーム GUI へのトライアルは色々な形で出てくるでしょうね、というところで雑感垂れ流しのエントリを締めくりりたいと思います。

*1:この会社は JavaFXiOSAndroid 向けアプリを開発できる、Gluon Mobile などを販売しています。

*2:3 年ほどコミットがなく、公式サイトもドメインが乗っ取られています...。

Java Client Roadmap Updateによせて (前編)

既にご存知の方も多いと思いますが、先日 Oracle から JavaFX をはじめとする、Java のクライアントテクノロジーについて今後のロードマップが発表されました。

https://blogs.oracle.com/java-platform-group/the-future-of-javafx-and-other-java-client-roadmap-updates

上記ブログエントリでは主に JavaFX の今後の扱いについて述べていますが、以下のホワイトペーパーにはそのほかに Applet や Java Web Start、そして Swing/AWT といった Java のクライアントテクノロジー全般の今後のロードマップについて記載されています。

http://www.oracle.com/technetwork/java/javase/javaclientroadmapupdate2018mar-4414431.pdf

このホワイトペーパーを読んで、まあ色々思うことがありました。Java のクライアントテクノロジーについての過去も振り返りながら私が感じたことを色々語りたくなったので、このエントリを書きました。まあ巷で言う「ポエム」ってやつですね。

長くなったので前後編に分けて書くことにしました。

ホワイトペーパーに書かれていたこと

ホワイトペーパーに記載されていた内容を簡単にまとめると次の通りです。

  • Oracle Java SE 11 からは JavaFX を同梱しないことを確定
    • これまで Oracle JDK には JavaFX を同梱、OpenJDK には同梱しないという状況だったが Oracle JDK も同様になる
    • 今後も OpenJDK には同梱されないという状況はそのまま
    • JavaFXJava のデフォルト GUI ツールキットではなくなることを意味する
  • Applet のサポートは予告通り Java SE 8 までで、Java SE 11 には含まれなくなる
  • Java Web Start についても Java SE 11 以降には含まないことを明言
    • これは今回の発表で初めて明らかになったこと
  • Swing と AWT については Java SE 11 においても開発を継続する

JavaFX については後ほど述べるとして、注目してもらいたいことは Applet、Web Start の扱いです。 これらに対する実行環境のアップデートが提供され続けるのは Java SE 8 のサポート期間の間だけ ということです。

よって、特に Applet を使った業務システムを展開しているところは Java SE 8 の無償アップデート提供期間中に対応を検討する必要があります。特にブラウザ Java Plug-in 部分はクライアント PC のセキュリティに直結するため、アップデートせずに放置するという選択肢は危険です。対応策としては次のようになります。

  • Oracleの商用サポートを契約し、継続して実行環境のアップデートを受け取る
  • アプリケーションを改修し、ブラウザ上で動かすのではなく、JRE を同梱したスタントアロンアプリケーションとしての配布に切り替える

というわけで、このロードマップ発表で Java のクライアントテクノロジーは転換点を迎えることになりました。これまでの経緯をちょっと振り返ってみようかなと思います。

Java クライアントテクノロジーの変遷について振り返り

Java 誕生時の GUI

Java は誕生時から AWT という GUI ツールキットを同梱していました。Java の主な目的の1つに Web ブラウザ上で GUI アプリケーションを動かせるようにすることがあったからです。

AWT は画面に描画する GUI 要素は WindowsMac といった実行環境のコントロールを使っていました。それに対して抽象化層を提供するものであったため、用意された GUI 要素は最大公約数的なものでした。そこで、AWT をベースに、Java2D を使用して全ての GUI 要素を自分で描画するSwing というツールキットが登場します。これは当時 Netscape 社が独自に開発したものに Sun が目を付けて共同開発したものです。

Swing はどの OS でも動く GUI を構築できるということで非常に画期的でしたが、如何せん登場時期が早すぎました (20世紀のことです) 。当時のクライアント PC のスペックでは動作が重すぎました。私は MMX Pentium 166MHz のマシンで初めて Swing を動かしたのですが、全ての動きがスローモーションで笑っちゃうしかなかったのを覚えています。

ですがこれにめげることなく継続的にアップデートを続け、JDK1.4 あたりでようやく実用的な速度が出るようになり、この頃から業務アプリケーション向けクライアント開発をはじめ、そこそこ採用されるようになってきた印象があります。私もこの時期に Swing での開発を経験しています。

Swing の発展

そして Java 5 から 6 に掛けた辺りで、当時既に普及していた Web アプリケーションに対して、当時の HTML の表現力の限界からリッチクライアントが再び見直される流れが出てきます。特に Macromedia (現在は Adobe に吸収) が Flash を業務クライアントとして推すことに熱心で、ここから Flex が誕生します。その流れを受け、Java SE 6 の頃に Sun は Swing 関連の技術に大きなてこ入れを行います。

  • Java.net にデスクトップ専用のプロジェクトができる
  • Java Plug-in の強化
  • デスクトップ環境と連携する API の追加
    • 特定のファイル・タイプに関連付けられたデフォルト・アプリケーションと対話する機能を提供する java.awt.Desktop クラス
    • デスクトップ環境のシステムトレイにアクセスし、アプリケーション独自のトレイアイコン、メニューを追加できる java.awt.SystemTray クラス
  • Swing 開発のためのアプリケーションフレームワークプロジェクトとその関連技術の開発が始まる

RIA の潮流と JavaFX の登場

この頃が Swing の全盛期だったかなあと思っています。そして大体同じ頃に別の流れが出てきました。それがコンシューマ向け Web を対象とした RIA の流れです。MicrosoftSilverlight をリリースした辺りから Flash と共に次のような特徴を備えたものとして注目され始めました。

  • HTML の限界を超えた操作性を提供
  • Web ブラウザ上でもデスクトップ上でも実行可能
  • お仕着せのコンポーネントでは無く、デザイナが GUI 要素を自由自在にデザインできる
    • この頃 iPhone がマルチタッチによる操作性と流麗なグラフィックの GUI をひっさげてデビューしたことも大きな影響を与えていたと思います

もちろん Java にも Applet や Web Start という同様の技術がありました。Swing も GUI 要素を自分で描画する仕組みになっているため、頑張ればどんな見た目にすることもできます。しかし、デザイナが扱うのには向いていないプラットフォームでした。

そこで Sun が目を付けたのが、Sun が買収した企業である SeeBeyond に所属していた Christopher Oliver 氏が開発していた Form Follows Function (F3) というプロジェクトでした。 これは確か Swing を開発しやすくするための DSL だったと思います。 (さくらばさんより指摘があり、これは正しくないとのことでした。F3 は Flash + ActionScriptDSL を目指したもので、たまたま実行の土台として Swing を使っていたとのことです。) Sun はこの技術をベースに JavaFX というプロジェクトを立ち上げ、次のような特徴を備えたコンシューマ向け GUI 開発、実行環境としてデビューさせます。

最初は Swing のラッパーコンポーネントという印象もあった JavaFX ですが、Sun はこれを AWT に頼らない GUI ツールキットとして発展させます。Prism エンジンを開発して GPU を積極的に活用する新世代の Java GUI ツールキットになっていきました。この頃から Sun は Swing への投資を急速にトーンダウンさせ、先ほど挙げた Swing App Framework 関連の JSR は中止してしまいました。これは当時 Swing 開発者からの反発もかなり大きかったことを覚えています。

ともかく Sun は JavaFX をすごくアピールしていました。Sun の開催するカンファレンスでは JavaFX を使った流麗なインターフェースをよくデモしていました。ですが、最も JavaFX を浸透させたかったスマートフォンへの展開を失敗してしまいます。旧 Windows Mobile に提供したのみで、水面下で Android への採用も持ちかけていたという話を聞いたこともあるのですが、現実はそうでないことは皆さんのよく知るところです。

そして Oracle による Sun 買収というイベントが起きます。

Oracle の買収と JavaFX の方向転換、発展

Oracle の買収で JavaFX がどうなるか不安視されましたが、JavaFX は引き続き盛り上げていくことを宣言します。ですが、OracleJavaFX をコンシューマ向け開発では無く、あくまで業務向け開発のツールにすることを重視しました。...とは Oracle ははっきり言っていませんが、その後の動きや Oracle のビジネス領域から考えれば明白でした。

JavaFX 2.0 において、JavaFX Script を廃止することが決まりました。そして、Java + FXML で開発するというスタイルに変わります。当然のことながらこれは大騒ぎになりました。 *1

ですが、結果としてこれは JavaFX にはプラスとなりました。本来の Java 開発者層が JavaFX に関心を持ち、注目度がぐっと上がりました (そのかわり Web デザイナ層は離れていきましたが...) 。古くささが見えてきた Swing と比べ、次のような点が優れていました。

  • FXML による GUI 構造の分離
  • プロパティとバインディング API によるリアクティブなスタイルでのビュー更新
    • これは遅延実行の仕組みを備えるなど、他のプラットフォームと比べても優れていました
  • Lambda Ready

私が JavaFX を本格的に触りだしたのもやはりこの時期でした。Flex による RIA 開発経験をしたこともあり、ユーザーに様々な UX を提供可能な RIA の可能性に目を向けていたところ、Java で開発可能な RIA プラットフォームが登場したので注目をしたのです。Flex と比べて優れているところも多いと評価していました。

2013 年の Java Day Tokyo の Java the Night に登壇したとき はこんなツールをデモして、JavaFX の魅力をアピールしたりしてました。

Oracle は積極的に JavaFX の開発を進め、JavaFX 8 までは盛んに機能追加も行い、今後 Java の標準 GUIJavaFX になると宣言しました。JavaOne で iOSAndroidバイスでの稼働をデモしたりと、Oracle は本気だと私もすごく期待していました。

主に欧州を中心に業務系システムやトレーディングツールなどでの採用例が出てくるようになります。NASA など科学技術研究の現場での採用も多かったように思います。 Eclipse GEF も最新版では JavaFX ベースになっています。

そして Oracle の方向転換

ですが、JavaFX 8 のリリースがピークでした。JavaFX 8 リリースの後辺りから次のように徐々に暗雲が垂れ込んできました

  • iOSAndroid への搭載の話はトーンダウンし、OpenJFX でソースだけ公開してお茶を濁す
    • 肝心の JVM 部分の公開がなかった
  • Raspberry Pi での実行にも積極的になったかと思えば、1 年後にはトーンダウン
    • 最初は Oracle Java SE Embedded に JavaFX を同梱していましたが、途中からやめてしまいました
  • Scene Builder も 2.0 リリースを最後にメンテナンスモードに

Java 9 では Jigsaw 対応で手一杯で新機能の追加はほとんどなく、明らかにリソースを絞っているように見えました。そして今回の発表につながります。

ホワイトペーパーを見る限り、OracleJava のクライアントテクノロジーに対する興味がほとんど無いことが分かります。現在は Web ファースト、モバイルファーストの流れであると結論付けています。まあ Oracle の立ち位置的にここまで見てくれたのが奇跡的ですらあったのかも知れませんが。

なお OpenJDK コミュニティは AWT、Swing、Java2D の強化に積極的で、そういう意味では様々な紆余曲折を経て Java 標準 GUI の座は再び Swing に戻ってきたのかも知れません。

と言うわけで Java のクライアントテクノロジーの歴史を振り返ったところで前編終了。長くなりすぎて疲れましたw 後編では JavaFX 、そしてクロスプラットフォーム GUI の今後について思うところを書こうかなと思っています。

*1:当時のさくらばさんの怒り様はまあすごかったです (^^;;

JDK9でのjavapackagerについて

はじめに

以前自分の blog にて JDK に付属しているツールである javapackager について紹介したことがあります。このツールは主にクライアントサイド Java アプリケーションを配布可能な形態でパッケージングするためのツールです。ネイティブインストーラも生成することができます。

aoe-tk.hatenablog.com

このエントリではネイティブパッケージに含まれるランタイムについて、次のようなことを述べていました。

昔は JDK を丸ごと放り込むという豪快な感じになっていましたが、最近は結構スリムアップしました。JDK9 の Jigsaw が入るともっと効率よくなるでしょう。

そして遂に Java9 がリリースされました。JDK9 の javapackager のマニュアル には次のような記載があります。

For self-contained applications, the Java Packager for JDK 9 packages applications with a JDK 9 runtime image generated by the jlink tool.

確かに jlink と連動してランタイムイメージを作ると記載されています。つまり、module-info.java を作っていれば、必要なモジュールだけを含んだランタイムイメージを作成することになり、アプリケーションの配布サイズが小さくなることが期待されます。というわけで早速 JDK9 の javapackager を試してみることにしました。

パッケージ対象となるアプリケーション

まず、パッケージ対象となるアプリケーションを作ります。とてもシンプルな JavaFX アプリケーションとして作ります。

package aoetk.sample;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.StackPane;
import javafx.scene.text.Text;
import javafx.stage.Stage;

public class SampleApp extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception{
        primaryStage.setTitle("Packager Sample");
        StackPane stackPane = new StackPane();
        stackPane.getChildren().add(new Text("Packager Sample"));
        primaryStage.setScene(new Scene(stackPane, 300, 275));
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

ウィンドウの真ん中にテキストを表示するだけのとてもシンプルな JavaFX アプリケーションです。これを対象にパッケージングを行ってみます。

JDK8でのパッケージング

まず、JDK8 でコンパイルし、パッケージングしてみます。パスを JDK8 に通します。

>set PATH=C:\Program Files\Java\jdk1.8.0_144\bin;%PATH%

>javac -version
javac 1.8.0_144

>where javapackager
C:\Program Files\Java\jdk1.8.0_144\bin\javapackager.exe

JDK8 の javac でコンパイルし、JAR を作ります。

>javac -d bin src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:32    <DIR>          .
2017/10/08  00:32    <DIR>          ..
2017/10/08  00:32             1,354 packager-sample.jar
               1 個のファイル               1,354 バイト
               2 個のディレクトリ  366,187,679,744 バイトの空き領域

この JAR に対し、ネイティブインストールイメージを作ります。今回はインストール後のイメージサイズを知りたいので、 -native の引数に image を渡してインストールイメージのみを作ります。

>javapackager -deploy -native image -outdir package -outfile packager-sample -srcdir artifact -srcfiles packager-sample.jar -appclass aoetk.sample.SampleApp -name "jdk8-sample" -title "JDK8Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK8Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk8-sample
"モジュール: [java.rmi, java.sql, javafx.web, jdk.charsets, java.logging, java.xml.crypto, java.xml, jdk.xml.dom, jdk.jfr, java.datatransfer, jdk.packager.services, jdk.httpserver, javafx.base, jdk.net, java.desktop, java.naming, javafx.controls, java.prefs, java.security.sasl, jdk.naming.rmi, jdk.zipfs, java.base, jdk.crypto.ec, jdk.management.agent, java.management, java.sql.rowset, javafx.swing, jdk.jsobject, jdk.sctp, java.smartcardio, jdk.unsupported, jdk.jdwp.agent, jdk.scripting.nashorn, java.instrument, java.security.jgss, jdk.management, java.compiler, javafx.graphics, jdk.security.auth, java.scripting, javafx.fxml, jdk.dynalink, javafx.media, jdk.accessibility, java.management.rmi, jdk.naming.dns, jdk.security.jgss, jdk.localedata]をランタイム・イメージに追加しています。"
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk8-sample ディレクトリの下にインストールイメージが作られます。これはインストール後にインストールディレクトリに展開される構成そのものです。

f:id:aoe-tk:20171009000534p:plain

ディレクトリサイズは次のように 167MB と、単純なアプリケーションにしては随分大きなサイズになっていることが分かります。

f:id:aoe-tk:20171009000554p:plain

JDK9でのパッケージング

では Java9 で導入されたモジュールシステムを利用してパッケージングしてみることにしましょう。環境を JDK9 に変更します。

>set PATH=C:\Program Files\Java\jdk-9\bin;%PATH%

>javac -version
javac 9

>where javapackager
C:\Program Files\Java\jdk-9\bin\javapackager.exe

モジュールの設定を行います。まずはこのアプリケーションがどのモジュールを利用しているかを調べてみましょう。

>jdeps -s artifact\packager-sample.jar
packager-sample.jar -> java.base
packager-sample.jar -> javafx.base
packager-sample.jar -> javafx.graphics

java.base モジュールの他に javafx.base モジュール、 javafx.graphics モジュールに依存していることが分かります。 javafx.graphics モジュールは javafx.base モジュールに依存しているので module-info.java は次のように javafx.graphics モジュールへの依存を記載すれば OK です。

module aoetk.sample.packager {
    requires javafx.graphics;
    exports aoetk.sample;
}

これを JDK9 のコンパイラを使ってコンパイルし、JAR を作ります。

>javac -d bin src\module-info.java src\aoetk\sample\SampleApp.java

>javapackager -createjar -nocss2bin -appclass aoetk.sample.SampleApp -srcdir bin -outdir artifact -outfile packager-sample.jar

>dir artifact
 ドライブ C のボリューム ラベルは Windows です
 ボリューム シリアル番号は ACFE-5623 です

 C:\Users\aoe\develop\packager-sample\artifact のディレクトリ

2017/10/08  00:52    <DIR>          .
2017/10/08  00:52    <DIR>          ..
2017/10/08  00:52             1,642 packager-sample.jar
               1 個のファイル               1,642 バイト
               2 個のディレクトリ  366,004,936,704 バイトの空き領域

これを同じように javapackager を用いてインストールイメージを作るのですが、モジュールを使ったアプリケーションをパッケージングする場合は指定する引数が異なります。ですが、現在の javapackager のマニュアル はこの変更に追いついていません...。執筆時点で javapackager のコマンドラインオプションについて正確な記述があったのは JEP 275: Modular Java Application Packaging のみでした。

具体的には -srcdir-srcfiles-appclass の指定が無くなり、モジュールを使った Java アプリケーションを java コマンドで実行するときと同じようにモジュールパス (対象モジュールの JAR が置かれているディレクトリ) を -p (もしくは --module-path) で、実行クラスを -m (もしくは --module) でモジュール名とクラス名を組み合わせて指定します。

>javapackager -deploy -native image -outdir package -outfile packager-sample -p artifact -m aoetk.sample.packager/aoetk.sample.SampleApp -name "jdk9-sample" -title "JDK9Sample" -BappVersion=1.0 -Bwin.menuGroup="JDK9Sample"
アプリケーション・バンドルを作成しています: C:\Users\aoe\develop\packager-sample\package内のjdk9-sample
モジュールaoetk.sample.packagerは存在しません。
"モジュール: [aoetk.sample.packager]をランタイム・イメージに追加しています。"
モジュールaoetk.sample.packagerは存在しません。
警告: Windows Defenderが原因でJavaパッケージャが機能しないことがあります。問題が発生した場合は、リアルタイム・モニタリン グを無効にするか、ディレクトリ"C:\Users\aoe\AppData\Local\Temp\"の除外を追加することにより、問題に対処できます。
結果のアプリケーション・バンドル: C:\Users\aoe\develop\packager-sample\package

これで jdk9-sample ディレクトリの下にインストールイメージが作られます。

f:id:aoe-tk:20171009000635p:plain

ディレクトリサイズを見ると 84.9MB と JDK8 の場合に比べて半分程度になっていることが分かります。確かに Jigsaw の効果が出ていますね!

f:id:aoe-tk:20171009000656p:plain

インストールイメージの中身について

なお、インストールイメージの中身を調べてみると、こちらも興味深いものがありました。 アプリケーションや JRE の JAR が見当たらない のです! runtime ディレクトリの下を覗いてみると、何やら modules というそれっぽいファイルがあります。

f:id:aoe-tk:20171009000713p:plain

このファイル、JAR か JMOD ファイルかと思いきや、特に ZIP 圧縮されていません。バイナリエディタで覗いてみると時々 CAFEBABE が登場しており、単にクラスファイルを 1 つのファイルにまとめたもののように見受けられます。色々調べてみましたが、このファイルが何であるかの解説を見つけられませんでした。誰か知っている人いますか?

(2017/10/10) 追記

id:MATSUZAKI 様よりコメントで情報を頂きました。jimage 形式のファイルであるとのことです。jimage については Java Magazine の Vol.25 に説明がありました。

jimage形式は、モジュール化されたランタイムに必要なクラスやリソースを管理するコンテナの形式です。 jimageファイルは、従来のようなzipベースの圧縮ではなく、クラスやリソースを高速に検索できるようにインデックスが付けられています。jimageのコンテンツ領域には、そのイメージのすべてのクラスとリソースが含まれており、位置情報にひも付けて管理されています。

JDK には jimage というコマンドがあったので (なお、現時点での JDK9 のドキュメントにはこのコマンドについての説明は見当たらず...) 、このコマンドで中身を閲覧してみました。

>jimage list modules
jimage: modules

Module: aoetk.sample.packager
    META-INF/MANIFEST.MF
    aoetk/sample/SampleApp.class
    module-info.class

Module: java.base
    META-INF/services/java.nio.file.spi.FileSystemProvider
    com/sun/crypto/provider/AESCipher$AES128_CBC_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_CFB_NoPadding.class
    com/sun/crypto/provider/AESCipher$AES128_ECB_NoPadding.class
    ...

確かにクラスファイルやリソースファイルが含められていますね。

まとめ

ということで、Project Jigsaw の恩恵で、Java SE 9 からはアプリケーションのインストールイメージをより絞って配布が可能であることが分かりました。OracleOracle Java SEサポート・ロードマップ において、Java アプリケーションの配布は (予め配布先に Java をインストールさせるのではなく) JRE も一緒にバンドルした自己完結型パッケージングでの配布を推奨しています。jlink と javapackager を最大限に活用してきましょう。

getterやsetter以外のメソッドをJavaBeansのアクセッサにする

ネット上で、Java への dis として「なんで Java のプロパティアクセッサはわざわざ get/set で始まる名前のメソッドにしないといけないんだ!」というのをよく見かけます。ですが実は JavaBeans の仕様としてはいわゆる getter/setter でないメソッドもプロパティのアクセッサにすることができます。余りにも JavaBeans のことがボロクソに言われるのでかっとなって書きましたw

まずは以下の Java クラスを見てください。

/**
 * getter/setterを使わないJavaBeansの例.
 */
public class Test {
    private String わーい;

    public String すごーい() {
        return わーい;
    }

    public void たーのしー(String myProperty) {
        this.わーい = myProperty;
    }

    @Override
    public String toString() {
        return "Test{" +
                "わーい='" + わーい + '\'' +
                '}';
    }
}

いわゆる getter/setter がありませんね。このクラスを次のように Introspector に掛けてプロパティを抽出し、実行してみます。

import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

/**
 * Beanの利用例.
 */
public class JavaBeansSampleApp {
    public static void main(String[] args) {
        try {
            Test test = new Test();
            BeanInfo testBeanInfo = Introspector.getBeanInfo(test.getClass());
            PropertyDescriptor[] propertyDescriptors = testBeanInfo.getPropertyDescriptors();
            for (PropertyDescriptor pd : propertyDescriptors) {
                if ("わーい".equals(pd.getName())) {
                    System.out.println("わーいプロパティのsetter: " + pd.getWriteMethod());
                    System.out.println("わーいプロパティのgetter: " + pd.getReadMethod());
                    pd.getWriteMethod().invoke(test, "なにこれー?");
                }
            }
            System.out.println(test);
        } catch (IntrospectionException | InvocationTargetException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

結果は次のようになります。

わーいプロパティのsetter: public void aoetk.sample.Test.たーのしー(java.lang.String)
わーいプロパティのgetter: public java.lang.String aoetk.sample.Test.すごーい()
Test{わーい='なにこれー?'}

set で始まっていない たーのしー() メソッドが setter として認識され、get で始まっていない すごーい() メソッドが getter として認識されています。プロパティの writeMethod を実行するとちゃんと たーのしー() メソッドが実行されて値が設定されています。

からくりについて解説しましょう。次のように Bean に対応した BeanInfo 実装クラスを用意し、Bean と同じパッケージに置きます。以下の例では BeanInfo インターフェースのデフォルトメソッドを実装した SimpleBeanInfo クラスを継承し、必要なメソッドだけ実装しています。

/**
 * {@link Test}のJavaBeans情報.
 */
public class TestBeanInfo extends SimpleBeanInfo {
    @Override
    public PropertyDescriptor[] getPropertyDescriptors() {
        try {
            return new PropertyDescriptor[]{
                    new PropertyDescriptor("わーい", Test.class, "すごーい", "たーのしー")
            };
        } catch (IntrospectionException e) {
            return null;
        }
    }
}

ここでは getPropertyDescriptors() メソッドをオーバーライドし、 PropertyDescriptorインスタンスを返しています。もうお分かりですね。ここでこの JavaBean のプロパティの情報 (プロパティ名、アクセッサメソッド) を返しているわけです。

Introspector の API ドキュメント には次のような解説が記載されています。

Fooクラスについては、情報の問い合わせ時にnull以外の値を提供するFooBeanInfoクラスがあれば、明示的な情報を取得できます。まず、ターゲットのBeanクラスの完全指定されたパッケージ名に「BeanInfo」を付加して新規のクラス名とし、BeanInfoクラスを検索します。これに失敗した場合は、この完全指定されたパッケージ名の最後のクラス名にあたる部分を使って、BeanInfoパッケージ検索パスに指定されたパッケージごとに該当クラスを検索します。
(中略)
クラスの明示的なBeanInfoが見つからない場合は、低レベルのリフレクションを使ってクラスのメソッドを調べ、標準設計パターンを適用して、プロパティのアクセス用メソッド、イベント・ソース・メソッド、publicメソッドを識別します。
(以下略)

つまり、JavaBeans を作るとき、本来は対になる BeanInfo 実装クラスを用意し、Bean の情報を提供する必要があるのです。ですが、毎回それをやっては面倒なので、便宜的にあの命名規則が用意されているわけです。CoC の先駆けだったわけですね (ちなみに JavaBeans は 20 世紀に誕生した仕様です) 。

この BeanInfo ですが次のような情報を提供することができます。

  • プロパティの情報
  • イベントの情報
  • メソッドの情報
  • アイコン

アイコンとかイベントって何?と思いましたか。JavaBeans は本来可搬性のあるソフトウェアコンポーネント、特に GUI コンポーネントを開発するために作られた規格です。つまり、IDE の「ポトペタツール」に追加して使えるソフトウェア部品を作るためのものなのです。BeanInfo ではそのツールに対して提供する情報を定義します。

f:id:aoe-tk:20170426222842p:plain

こうして考えると、単なる DTO として JavaBeans を使うのはオーバースペックであることも分かりますね。なぜこんなにあちこちで JavaBeans が使われるようになったのかは歴史的な経緯が色々あるのですが、まあそれはまたの機会で。

なお、このエントリでやったいたずらは業務のコードではやらないでくださいね。いたずらに混乱を招くだけです。酒の席でのネタにとどめてくださいw

全体のコードは gist にアップしてあります。

https://gist.github.com/aoetk/e5a09f67f2ebbc206d0770b21116b69e

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 でかなりの機能追加が入ったことがあったので