読者です 読者をやめる 読者になる 読者になる

JavaFX Maven PluginのWebサイトが復活して便利になっていた

JavaFX Maven

JavaFXMaven で開発する際に便利なプラグインとして JavaFX Maven Plugin というものがあります。これは javapackager を使ったビルド、パッケージング作業を Maven 経由で実行できるようにするプラグインです。

蓮沼さんの以下のブログエントリでも紹介されています。

www.coppermine.jp

NetBeans にも JavaFX 開発向けの Maven プロジェクトを作成するためのウィザードがあるのですが、こちらは Exec Maven Plugin を使って javapackager のコマンド実行をラップしただけのものです。パッケージングについても、依存ライブラリの JAR を全て解凍してから 1 つの JAR にパッケージングするようになっており、今ひとつという印象です。

この JavaFX Maven Plugin ですが、ドメインを維持することができなくなったのか、ドキュメントを載せていた Web サイトが途中で消滅してしまいました。 GitHub リポジトリの README でも Web Archive の方 にリンクが貼られているという状態ですw

ところが最近になって github.io 上に新たに Web サイトが作られるようになり、中々便利な作りになっていました。 (GitHub のコミットログを見ていると、メンテナが最初の作者とは別の人になったようで、そのことも影響している?)

http://javafx-maven-plugin.github.io/

以下のキャプチャを見てもらうと分かりますが、作りたいプロジェクトの内容を画面上でぽちぽち選択していくと、右側に pom.xml に記述すべき内容が吐き出されるようになっています。これを自分のプロジェクトの pom.xml にコピーすればいいわけです。最近こういうの増えましたねえ。

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

出力を JAR にするかネイティブパッケージにするか、ビルドは Maven JavaFX Plugin のゴールを直接叩くのか、それとも Maven ライフサイクルの中で実行するのか、などといった選択肢が用意されています。

以上、ちょっと嬉しかったのでお知らせまで。

javapackagerの紹介

JavaFX

このエントリは JavaFX Advent Calendar 2015 の 19 日目のエントリです。前日は id:yumix_h さんによる「 JavaFXで画面解像度を調べてみる 」でした。

今回は JDK に付属しているツールである javapackager について紹介します。このツール、私が見る限り公式のドキュメント以外では断片的な解説しか無い (主にネイティブパッケージの解説などでしか登場しない) ように見受けられるので、ここでこのツールができること全般について紹介したいと思います。

アプリケーション配布を巡る環境の変化

まず、javapackager のようなツールが登場した背景について触れたいと思います。これにはアプリケーション配布を巡る環境の変化が大きく関わっていると考えています。

既にご存じの通り、Java は "Write Once, Run Anywhere." を目指して作られたものであり、Java で実装、ビルドしたアプリケーションはどの環境でもそのまま動くことが期待されています。

この Java アプリケーションの配布ですが、元々はどのプラットフォームにも JRE がインストールされていることを前提とした考え方でした。クライアントアプリケーションの配布は実行可能 JAR の形態で行うことが普通です。サーバサイド Java では JRE の上にさらに Java EE コンテナのレイヤを敷き、アプリケーションは WAR もしくは EAR として配布しますね。

ところが、最近はこのように予めランタイムを別途インストールしておき、その上にアプリケーションをデプロイするスタイルはあまり好まれなくなっているように見受けられます。必要なものは1つのパッケージに全て含まれており、箱から出したらすぐ使えるような形態 (これを「自己完結型パッケージ」と呼びます) が好まれるようになっています。

これは OS ベンダ提供のアプリケーションストアという配布形態の登場が大きく関係していると考えています。アプリケーションストアから配布するアプリケーションはサンドボックス化されており、自己完結型であることが望まれています。エンドユーザとしても、アプリケーションストアからワンクリックでインストールが可能なスタイルに慣れてしまうと、わざわざランタイムをインストールするのは煩わしく感じられるようになるでしょう。特にデフォルトでは JavaFlash などがインストールされておらず、アプリケーションストアの利用率が高い Mac 環境ではその傾向が強いように思われます。

そしてもう一つの環境の変化はセキュリティです。どの OS、プラットフォームでも以下のようにセキュリティ対策が強化され、以前のように何も考えずに「野良アプリケーション」をほいほいインストールできなくなっています。

  • Java Web StartJava Applet では未署名のアプリケーションの実行はブロックされるようになりました。
  • Windows では未署名アプリケーションをダウンロードする際にはブラウザが警告を出します。
  • Mac では Gatekeeper という仕組みが導入され、デフォルトの状態では Mac App Storeからインストールしたアプリケーション、もしくはAppleデベロッパ登録し、そのデベロッパ ID で署名したアプリケーションのみが起動可能になっています。
  • FirefoxGoogle Chrome といった Web ブラウザの拡張機能でさえも、ブラウザベンダが運営する配布サイトからのみのインストールに制限されるようになりました。

JDK に同梱されている javapackager はこのようなクライアントアプリケーション配布を巡る環境の変化に対応できるようになっています。

javapackager の概要

javapackager は JDK に付属しており、Java アプリケーションのパッケージング、デプロイメントのためのツールです。

元々は javafxpackager という名前で、その名の通り JavaFX アプリケーションをパッケージングするための専用ツールとして JavaFX SDK に同梱されていました。JDK7 update6 以降は JDK に同梱されるようになり、Java アプリケーション全般に利用可能なツールになりました。JDK8 update20 以降は javapackager に名を変え、名実共に Java SE のためのツールに昇格 (?) しています。

javapackager が提供する機能は次のようなものになります。

  • 実行可能 JAR のビルド
  • 配布用パッケージの生成
    • Java Web StartJava Applet 向けバンドルの生成
    • 自己完結型パッケージの生成
      • インストール先の OS に合わせたネイティブインストーラの生成
      • JRE も含めたパッケージを生成し、OS 側にランタイムの事前インストールを要求しない
  • 配布プラットフォームに合わせたアプリケーションの署名
  • アプリケーションストア向けパッケージの生成

以降では上に挙げた javapackager の各機能について簡単に説明していきます。

なお、この javapackager の使い方を含む、クライアント Java アプリケーションのデプロイメント全般について詳細に解説した Oracle 公式のドキュメントがあります。日本語化もされています。詳細についてはこちらを参照してもらうとして、ここではつかみの部分を解説することにします。

Java Platform, Standard Editionデプロイメント・ガイド
http://docs.oracle.com/javase/jp/8/docs/technotes/guides/deploy/

javapackager 提供機能の簡単な説明

基本的な使い方

javapackager はコマンドラインツールです。javac や jar コマンドなどと同じディレクトリにインストールされているはずなので、これらコマンドへのパスが通っていれば使うことができます。また、Ant 向けタスクも用意されているのですが、ここではコマンドラインとしての使い方に限定して説明します。Ant での利用方法については上記ドキュメントを参照してください *1

次のような使い方をします。

$ javapackager コマンド [オプション]

javapackager に続けて実行したいタスクに応じたコマンドを指定するのが基本です。コマンド別にオプションがあり、必要に応じて指定します。コマンドは次の 5 種類があります。

コマンド 実行するタスク
-createbss CSS ファイルをバイナリ形式に変換する。
-createjar 実行可能 JAR を生成する。
-deploy デプロイ可能なパッケージを生成する。
-makeall -createjar と -deploy の両方を実行し、実行しているプラットフォームで生成可能な全ての形式のアーカイブを生成する。
-signjar JAR ファイルを署名する。

実行可能 JAR のビルド

まずは基本となる実行可能 JAR のビルドです。通常の jar コマンドが提供する機能に加え、次のような JavaFX 固有の機能を提供しています。

  • プリローダアプリケーションの指定
  • CSS のバイナリ化

1番目にあるプリローダとは、JavaFX アプリケーション本体が起動するまでの間に表示する小さなアプリケーションのことです。主にネットワークからアプリケーションをダウンロードする Java Web StartApplet で使われます。

2番目は CSS ファイルをバイナリ変換するというもので、CSS ファイルが巨大な場合などに、ファイルのパース速度を上げる目的で使用されます *2

-createjar コマンドを使って本機能を利用します。以下にコマンドの実行方法を示します。

$ javapackager -createjar -nocss2bin -appclass アプリケーションクラス名 -srcdir JARに含めるファイルのディレクトリ -outdir JARの出力ディレクトリ -outfile JARファイル名 -preloader プリローダクラス名

-nocss2bin オプションを指定すると、CSS ファイルのバイナリ化が実行されなくなります。未指定の場合だと CSS ファイルのバイナリ化が勝手に実行されてしまうので注意が必要です。 -appclass オプションで指定するのは main メソッドを含むクラスの名前です。マニフェストファイルは勝手に作ってくれますが、自分で追加の属性を指定したい場合は -manifestAttrs オプションに続けて "名前=値,名前=値,..." の形式で指定します。

配布用パッケージの生成

これが javapackager のメイン機能となります。アプリケーション JAR ファイルを準備した状態で -deploy コマンドを使って実行しますが、オプションの指定次第で様々なことができます。

まずは基本的な使い方を示します。以下に示すコマンドを実行すると、Java Web StartApplet 向けに JNLP ファイル及び Applet を実行する HTML ファイルが出力されます。

$ javapackager -deploy -outdir 出力ディレクトリ -outfile 出力ファイル名 -srcdir JARのあるディレクトリ -srcfiles 対象となるJARファイル名 -appclass アプリケーションクラス名 -name アプリケーション名称 -title アプリケーションタイトル

このコマンドを実行すると -name オプションで指定した名前の HTML、JNLP ファイルが出力されます。-title オプションは JNLP ファイル内の <title> タグに反映されます。また、これから説明する自己完結型パッケージの生成でも意味を持ちます。他にもオプションはあるのですが、詳細は javapackager コマンドのリファレンス ( Windows 向けMac、Linux 向け ) を参照してください。

ここでさらに -native オプションを指定すると、自己完結型パッケージを生成することが可能になります。インストール先の OS 向けのネイティブインストーラを生成し、アプリケーション専用の JRE も含めたインストールイメージを作成します。これにより、インストール先の OS に JRE の事前インストールを要求せず、また OS に予めインストールされている public JRE の影響も受けません。これならば「アプリケーションの検証が終わっていないので、JRE のアップグレードはしないでください (> <)」みたいなかっこ悪いことも言わずに済みますね。:)

-native オプションに続けて、次のバンドルタイプを指定することができます。

タイプ 説明
all 無指定の場合はこれが選ばれる。installer、image の両方を指定した場合と同じ結果になる。
installer コマンドを実行している OS が対応している全てのインストーラを生成する。
image アプリケーションインストールディレクトリの内容を展開する。Mac の場合は .app ディレクトリを作り、その下に展開する。
exe Windows の .exe インストーラを生成する。Inno Setup のバージョン 5 以上がインストールされている必要がある。
msi Windows の .msi インストーラを生成する。WiX Toolset のバージョン 3.8 以上がインストールされている必要がある。
dmg MacDMG パッケージ (ドラッグ&ドロップ形式のインストーラ) を生成する。
pkg Mac の PKG インストーラを生成する。
mac.appStore Mac App Store 用のパッケージを生成する。
rpm RPM パッケージを生成する。
deb Debian パッケージを生成する。

指定できるタイプはコマンドを実行する OS で利用可能なパッケージに限定されます。Windows では exe や msi を指定できますが、dmg や pkg、rpmdeb は指定できません。Mac App Store 向けのバンドルタイプもありますね!

-native オプションを使って、自己完結型パッケージを生成する場合、パッケージ固有のオプションを -Bオプション名=値 の形式で指定することができます。ここでは、WindowsMac 向けの代表的なオプションを紹介します。

Windows 向けパッケージ生成オプション

Windows の場合、-native exe もしくは -native msi オプションでインストーラを生成しますが、さらに次のようなオプションを指定することで、カスタマイズを行うことができます。

オプション 説明
-BappVersion=バージョン文字列 アプリケーションのバージョンを指定します。アプリケーションのプロパティで確認できるバージョンになります。
-Bicon=icoファイルのパス アプリケーションのアイコンファイル (.ico ファイル) を指定します。パスは -srcdir で指定するディレクトリからの相対パスになります。
-Bcopyright=コピーライト文字列 アプリケーションのコピーライト文字列を指定します。
-BlicenseFile=ファイルパス exe の場合にのみ有効で、インストーラに使用許諾契約を表示したい場合、そのファイルへのパスを指定します。
-BmenuHint=boolean インストール後、スタートメニューにショートカットを追加したい場合は true を指定します。
-BshortcutHint=boolean インストール後、デスクトップにショートカットを追加したい場合は true を指定します。
-BsystemWide=boolean アプリケーションをユーザローカルにインストールするか、システムレベルにインストールするかを選択します。
-Bwin.menuGroup=グループ名称 スタートメニューにショートカットを追加する場合、スタートメニューにグループを追加した上でその下にショートカットを作ります。そのグループ名を指定します。
-Bvendor=任意文字列 アプリケーションを提供する組織や個人名などを指定します。

結構色んな指定ができることが分かるでしょう。少し重要なのが -BsystemWide オプションです。true にした場合はシステムレベルでインストールされ、いわゆる Program Files ディレクトリ以下にイントールされるようになります。つまり、インストールに管理者権限が必要となります。false の場合はユーザローカルにインストールされます (インストールディレクトリは %LOCALAPPDATA%) 。この場合は管理者権限は不要です。

生成されたインストーラに対して、SignTool を使って署名することもできます。

以下に、実際のコマンド実行例を示します。自分が開発している Social Bookmark Viewer FX をパッケージ化したときのコマンドです。

> javapackager -deploy -native exe -outdir target -outfile SocialBookmarkViewer -srcdir target -srcfiles social-bookmark-viewer-fx-0.1-SNAPSHOT.jar -appclass aoetk.bookmarkviewer.MainApp -name "SocialBookmarkViewer" -title "Social Bookmark Viewer" -BappVersion=0.1 -BsystemWide=true -Bwin.menuGroup="Social Bookmark Viewer"

これを実行すると、次のような出力が得られます。
f:id:aoe-tk:20151219164935p:plain

生成されたインストーラを実行すると、次のようなおなじみのアプリケーションインストーラが立ち上がることになります。
f:id:aoe-tk:20151219165022p:plain

インストールが完了すると、スタートメニューにも登録されました!
f:id:aoe-tk:20151219165035p:plain

インストールディレクトリの下は次のようになっています。exe を実行して起動します。runtime ディレクトリの下には JRE のライブラリが入っています *3
f:id:aoe-tk:20151219165052p:plain

Mac 向けパッケージ生成オプション

Mac の場合は -native dmg-native pkg オプションを使いますが、Windows の場合と異なり、インストーラを生成するために特別なアプリケーションをインストールする必要が無いので、単純に -native だけを指定しちゃっても問題ありません。Mac 向けのカスタマイズオプションを示します。

オプション 説明
-BappVersion=バージョン文字列 アプリケーションのバージョンを指定します。アプリケーションのプロパティで確認できるバージョンになります。
-Bicon=icnsファイルのパス アプリケーションのアイコンファイル (.icns ファイル) を指定します。パスは -srcdir で指定するディレクトリからの相対パスになります。
-Bcopyright=コピーライト文字列 アプリケーションのコピーライト文字列を指定します。
-BlicenseFile=ファイルパス pkg の場合にのみ有効で、インストーラに使用許諾契約を表示したい場合、そのファイルへのパスを指定します。
-BsystemWide=boolean アプリケーションをユーザローカルにインストールするか、システムレベルにインストールするかを選択します。
-Bmac.CFBundleName=名称 アプリケーションメニューバーに表示するアプリケーション名称を -name オプションで指定した名称とは別の名称にしたい場合に指定します。
-Bmac.signing-key-developer-id-app=署名キー名 dmg の場合のオプション。Gatekeeper 向け署名を行いたい場合に指定します。キーがインストールされている場合はデフォルトでそれが使用されるようです。
-Bmac.signing-key-developer-id-installer=署名キー名 pkg の場合のオプション。Gatekeeper 向け署名を行いたい場合に指定します。キーがインストールされている場合はデフォルトでそれが使用されるようです。

Mac の場合、-BsystemWide は true にすると /Applications ディレクトリにインストールされます。Mac の場合はここにアプリケーションをインストールするのが一般的であるため、無指定の場合は true 扱いになります。false にした場合はユーザの Desktop ディレクトリがターゲットになります。

また、Mac 向けにはアプリケーションを "Gatekeeper Ready" にするための対応が入っていることが分かりますね。

Mac 向けのコマンド実行例も同様に示しておきますね。

$ javapackager -deploy -native -outdir target -outfile SocialBookmarkViewer -srcdir target -srcfiles social-bookmark-viewer-fx-0.1-SNAPSHOT.jar -appclass aoetk.bookmarkviewer.MainApp -name "SocialBookmarkViewer" -title "Social Bookmark Viewer" -BappVersion=0.1

これを実行すると、次のような出力が得られます。
f:id:aoe-tk:20151219021419p:plain

生成された DMG ファイルを起動すると、次のようなおなじみのインストール画面が出てきます。
f:id:aoe-tk:20151219021613p:plain

まとめ

このエントリでは javapackager の使い方について掴みの部分の紹介を行いました。最近のクライアントアプリケーション配布を巡る環境の変化に対応した、重要なツールであることを分かってもらえたらと思います。

このツールはクライアントアプリケーションだけでは無く、サーバアプリケーションでも使えると思います。最近はサーバアプリケーションもコンテナにデプロイするのでは無く、単一 JAR に全てをパッケージングしてデプロイする方法も好まれるようになってきました。javapackager を使えば、ランタイムごとパッケージングできるので、より配布が容易になるのではないでしょうか。

javapackager は今回紹介した内容の他にもまだ色々できることがあります (Mac App Store 向けパッケージの作成とか) 。詳しくは上に挙げた Oracle 公式のドキュメント読んでもらえればと。

明日は @ さんの予定です。

*1:というか、この公式ドキュメントでは Ant タスクで利用する方法ばかり説明されています

*2:バイナリ変換すると、ファイルの拡張子が ".css" から ".bss" に変わります。コード側もそれに合わせる必要があります。

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

JavaFX9に追加される機能が増えるかもしれません

JavaFX

このエントリは JavaFX Advent Calendar 2015 の 7 日目のエントリです。前日は id:bitter_fox / @ さんによる「 JavaFXを直接実行できるjshellを作った 」でした。 今年の Advent Calendar の自分の担当日はもう少し後だったのですが、空いていたので急遽ホットなネタで埋めることにしました。

先日、JavaFX コミュニティの間で、JavaFX を取り巻く現状について怒りをぶちまけた、以下のブログエントリが話題を呼んでいました。

Should Oracle Spring Clean JavaFX?
https://www.codenameone.com/blog/should-oracle-spring-clean-javafx.html

Java によるモバイルアプリケーション開発プラットフォームである Codename One の開発者である Shai Almog 氏 *1 によるエントリで、JavaFX の現状について、「Swing を置き換えるには到底至っていない」「Oracle 自体が JavaFX にコミットする姿勢を見せていない」とかなり厳しく批判しています。

このエントリは OpenJFX の ML でも話題になり、かなり激しい議論になりました。様々な意見が飛び交いましたが、やはり OracleJavaFX に対するコミットを疑問視する意見はかなり出てきました。
確かにここ最近の Oracle の行動には JavaFX から手を抜き始めているように見られてもおかしくないところが目に付きます。

Raspberry Pi 向け Oracle JDK での JavaFX サポートは停止し *2 、Scene Builder のバイナリ配布も停止してしまいました *3
Java9 と共にリリースされる JavaFX9 の新機能についても、大きな変更は Jigsaw 導入に対応して、これまで com.sun.javafx パッケージにあった API のうち、重要度の高いものを public するというものだけです。確かにこの対応は非常にリソースを割く作業であることは理解できますが、それにしても新しいコンポーネントの追加などが一切無いというのは寂しいものです。

ある程度議論が進んだところで、現在の JavaFX チームのリーダーである Kevin Rushforth 氏がコメントを挟んできました。

http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-December/018320.html

JavaFX はOpenJFX というコミュニティで開発を進めており、意見はオープンに取り入れるつもりであること、決して数が多くは無いけど、JavaFX9 には中々興味深い改善をするつもりだよ、といった内容です。
まず、JavaOne でも発表したことのようですが、JavaFX9 の新機能として以下のようなものをリストアップしていました。

  • A modularized JavaFX (into 6 core modules + deploy, swing interop, swt interop)
  • JEP 253 -- Control Skins & additional CSS APIs (proper support for third-party controls)
  • High DPI enhancements (full support on Windows; add support for Linux)
  • Public API for commonly used methods from internal packages:
    • Nested Event Loop
    • Pulse Listener
    • Platform Startup
    • Text API (HitTest, etc)
    • Static utility functions (under investigation)
  • New versions of WebKit and GStreamer

4 番目はこれまで internal だった API のうち、便利そうなものを public にするというものですが、確かに地味に良さげなものが並んでしますね。Text API とか気になる。

さらに Java9 リリース延期の提案 を受けて、もう少し機能追加しても良いかもとのコメントをしています。候補として次のような機能を挙げています。

  • Provide a JavaFX equivalent for JEP 272 / AWT ‘Desktop’ API
  • Make UI Control Behaviors public
  • UI Control Actions API
  • Public Focus Traversal API
  • JavaFX support for multi-resolution images
  • Draggable tabs
  • Image IO

1 番目は AWT 向けの JEP である JEP 272: Platform-Specific Desktop FeaturesJavaFX 向けにも提供しようというものです。この JEP はプラットフォーム固有のデスクトップ機能をより利用できるようにするというものです。過去には Java6 で一度強化が入っており、タスクトレイへのアクセスなどが可能になったりしましたが、久々にこの分野にテコ入れが入ることになります。

  • Mac のアプリケーションメニューの利用 (かつて Apple Java に含まれていた EAWT では提供されていました) 。
  • Windows タスクバー (Mac ではドック) のジャンプリストへのアクセス。
  • Windows タスクバー (Mac ではドック) でのプログレス表示。

よりネイティブなアプリケーションとして振る舞うことが可能になるので、この強化は是非とも入って欲しいなあと思っています。ましてや近年は javapackager を使ってネイティブアプリっぽく配布することが推奨されていますし。まあ、AWT には入るので、最悪 JavaFX 側に入らなくても何とかなるのではありますが、JavaFX 側から AWT の API を触るときは別スレッドにする必要があって面倒ですし。

3 番目とか 4 番目は Swing にある同名の API と同じかな。Action API は欲しいですねえ。

5 番目は HiDPI 環境におけるビットマップ画像の扱いのことでしょうか。WindowsMac 共に HiDPI 対応が入りましたが、ビットマップ画像の扱いは Mac 式になっており、整数倍にしか対応していません。Mac 式にしたのは恐らく暫定対応でしょうから、ここできちんと対応するということなのでしょう。

最後の Image IO も詳細は不明ですが、このあたりはまだまだ足りないところが多いので期待したいところですねえ。

こんな感じで思わぬところから、今後の機能強化について話が出てきました。幸い (?) Java9 のリリース延期はほぼ確実でしょうから、JavaFX9 ももう少し新しい機能が入った状態でリリースされることにはなりそうです。
まあ、そうは言ってもやっぱり OracleJavaFX に対してリソース抑えているよなあとは思いますが、それはまた別の話で。

明日は id:skrb / @ さんの予定です。

*1:元々は Sun で LWUIT の開発をされていた方のようです

*2:OpenJFX での開発は継続しています。

*3:Gluon が開発を継続し、 ここ でバイナリの配布も行っています。

JavaFX 8u60の新機能 (特にWindowsでのHiDPI対応について)

JavaFX

先日、JDK8 の機能アップデート版である JDK 8u60 がリリースされました。このバージョンのリリースノートは以下です。

http://www.oracle.com/technetwork/java/javase/8u60-relnotes-2620227.html

ですが、このリリースノートにはバンドルされている JavaFX 8u60 についての記述がありません。
以前から、JavaFX については 8u60 は安定性向上がメインのリリースになる、とのアナウンスがあったのですが、機能面でのアップデートが少し入っているようなので、自分が調べた範囲で分かったことをまとめます。

自分が調べた範囲では JavaFX 8u60 に次のアップデートが入っていることが分かりました。

  1. Windows で HiDPI 対応が入っている
  2. WebView の WebKit のバージョンが上がっている

Windows での HiDPI 対応について

まず 1 の Windows における HiDPI 対応についてです。こちらについては自分のブログでも以前に次のようなエントリを書いていました。

aoe-tk.hatenablog.com

そのエントリで「とにもかくにも JavaFXWindows でも Device Independent Pixel (DIP) に対応してくれるのが一番なんですけどねえ。」と結んでいたのですが、まさしくその対応が入ったことになります。

最近増えてきた高解像度端末では、そのままだと文字やアイコンなどが小さくなって読めたものじゃありません。そこで、Windows では物理的なスクリーンのサイズと解像度を見て、125%、150%、200%...と自動的にスケーリングを行うようになります。
私が使っている VAIO Tap 11 は 11 インチのディスプレイで解像度が Full HD (1920 x 1080) の端末なのですが、デフォルトで 125% のスケーリングが掛かっています。なのでこの端末だと、100px の長さのコンテンツが画面上では 125px を使って描画されるようになります (Windows のスケーリング設定を見てくれる処理系である場合) 。

これまでの JavaFX では、基準フォントサイズについては Windows のスケーリング設定を見て描画してくれたのですが *1 、プログラム上のピクセル指定についてはそのような考慮がありませんでした。それが 8u60 ではスケーリング設定の倍数を掛けたピクセル数で描画してくれるようになります。

詳細については OpenJFX の ML の以下のポストを参照してください。

http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-June/017337.html

と言うわけで自分の VAIO Tap 11 でもスケーリングが掛かる!と思っていたのですが、 変わりませんでした
調べてみたところ、 スケーリングの設定が有効になるのは、スケーリングが 150% 以上の場合 という制約が掛かっているようです。
JDK Bug System の次のチケットなどでこの点について議論されていました。

https://bugs.openjdk.java.net/browse/JDK-8129862

https://bugs.openjdk.java.net/browse/JDK-8130748

どうも、125% のスケーリングではフォントがぼやけて描画されてしまう問題を解決できず、125% ではスケーリングを切ることにした模様です。残念。 *2

ただ、起動引数を使って強制的にスケーリングの設定を掛けることはできます。Javaシステムプロパティ glass.win.uiScale を使って、次のように指定します。

$ java -Dglass.win.uiScale=125% -jar Application.jar

これを使って、自分の VAIO Tap 11 で Ensemble.jar を起動してみたスクリーンショットを示します。

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

きちんと 1.25 倍にされていることが分かりますね。フォントは確かに少しぼやけてましたが、そんなに気にするほどでもなかったかなあ。
次のアコーディオンのサンプルとかも分かり易いと思います。このアコーディオンは高さ、幅をピクセルで指定しています。

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

HiDPI 対応についてまとめるとこんな感じです。

  • Windows 上でスケーリングの掛かっている環境では、プログラム中のピクセル指定に設定された倍数を掛けた数のピクセル数で描画される。
  • ただしこの設定が有効になるのは 150% 以上から。
  • 強制的に任意のスケーリングを指定したい場合はシステムプロパティ glass.win.uiScale を使って指定する。
  • ビットマップ画像については、Apple 式の規則が適用され、整数倍の大きさの画像を用意しておけば、それが適用される。

詳細については上に示した OpenJFX の ML のポストを参照してください。

WebKit のバージョンアップについて

JavaFX 8u60 では WebView に使っている WebKit のバージョンが上がっています。主にセキュリティフィックスを取り込むためですが、少しだけ機能追加があるようです。

まず WebView の User-Agent の違いを示しておきます。

JavaFX 8u40
Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/537.44 (KHTML, like Gecko) JavaFX/8.0 Safari/537.44
JavaFX 8u60
Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/538.19 (KHTML, like Gecko) JavaFX/8.0 Safari/538.19

WebKit のバージョンが少し上がっていますね。

HTML5TEST を使ってみると、次のような違いがありました。

  • ECMA Script 6 の Promise に対応している。
  • URL API に対応している...らしい。
    • これどんな API
  • picture 要素に対応していないのに、なぜか srcset 属性には対応しているという結果が出る。
    • これは多分テストが何かおかしい。

大きな違いはありませんが、Promise に対応したというのは大きいですね。
8u40 でのテスト結果は こちら で、8u60 での結果は こちら で参照することができます。

なお、HTML5 で新たに追加されたフォーム要素でカレンダーや色のピッカーがまともに動いていない問題 (私の過去の このエントリ でも触れています) 、は未だに対応していませんねえ。
バグトラッカにも チケットが上がっています が、放置状態です...。

以上、JavaFX 8u60 の新機能について、私が調べた範囲では大体こんなところです。他にも何か情報があったら、教えてもらえると幸いです。

*1:そのため CSS で長さの単位に em を使用することで、HiDPI 対応することは可能でした。

*2:Surface Pro シリーズならば 150% 以上のスケーリングが掛かっているので、デフォルトでスケーリングが有効になっているはずです。誰か試して。

JavaFXのWebViewの検索を実現するのにもっと簡単な方法がありました

JavaFX

昨年の JavaFX Advent Calendar で次のようなエントリを書きました。 aoe-tk.hatenablog.com

このエントリでは、JavaFX の WebView を使ったアプリケーションに検索機能を実装する方法として、JavaScript のページ検索ライブラリを使う方法を紹介しています。
そこでは jQuery プラグインを使って実装していましたが、そのライブラリの CSSJavaScript オブジェクト定義が WebView で表示しているコンテンツのそれとバッティングする危険性がありました。

ところが、window.find() という非標準の関数があり、JavaFX の WebView がそれをサポートしていることを知りました。 *1
これを使えば特別なライブラリを読み込むこと無く、簡単に Web ページの検索機能を実現することができます。

以前の方法では、WebView 上のドキュメント読み込み完了時に検索のための jQuery プラグインの読み込みや、そのプラグインが要求する CSS クラスの定義の読み込みを行っていましたが、 window.find メソッドを使う場合はその必要はありません。何せ組み込みのメソッドなので。
件のエントリで解説した highlightPage() メソッドの実装を次のように変更します。

private static final String FIND_FUNCTION = "window.find(\"{0}\", false, false, true, false, true, false)";

private void highlightPage(Optional<String> word) {
    if (webEngine.getDocument() != null) {
        final String keyword = word.orElse("");
        if (!keyword.isEmpty()) {
            webEngine.executeScript(MessageFormat.format(FIND_FUNCTION, Encode.forJavaScript(keyword)));
        }
    }
}

単純に JavaScriptwindow.find メソッドを呼び出すように変更しています。
メソッドに渡す文字列に Encode.forJavaScript() というメソッドを噛ましていますが、これは OWASP Java Encoder Project というライブラリが提供する JavaScirpt 特殊文字をエスケープするための関数です。

また window.find メソッドは繰り返し呼ぶと、ページ内の次の一致箇所にフォーカスしてくれるので、次のように検索テキストフィールドのアクションイベントで highlightPage() メソッドを呼び出すようにし、検索フィールドにフォーカスがある状態で Enter キーを叩くと、次の一致箇所にフォーカスするようにしました。

<TextField fx:id="pageSearchBox" onAction="#handleSearchBoxAction" promptText="Find in page" HBox.hgrow="NEVER" />
@FXML
void handleSearchBoxAction(ActionEvent event) {
    highlightPage(Optional.ofNullable(pageSearchBox.getText()));
}

こうすると次のように検索フィールドにフォーカスがある状態での Enter キーをヒットすると、次の検索一致箇所にフォーカスが移動します。
f:id:aoe-tk:20150615001101p:plain

と言うわけで、JavaFX WebView で検索機能を実現する方法の訂正版でした。組み込みのメソッドを利用するので、この方法が一番良いと思われます。

実装の全体を確認されたい方は、私が開発している Social Bookmark Viewer FXBookmarkViewController.java 及び BookmarkView.fxml の実装を参照してください。

*1:この関数は元々 Netscape Navigator が独自に実装していた関数で、Firefoxレンダリングエンジンである Gecko もサポートし、WebKit もサポートしたようです。

JavaFXで2つのListViewのスクロールを同期する方法

JavaFX

とある目的があって (それがなんなのかはまた別途まとめます。) 、JavaFXListViewTableView について、同じ数のアイテムを持つ 2 つの ListView (TableView) のスクロール状態を同期する方法について調べてみました。

やりたいこと

次のように同じレコード数の ListView を並べます。
f:id:aoe-tk:20150522191146p:plain

片方をスクロールさせると、もう片方も一緒にスクロールさせるようにします。 どのスクロール手段 (マウスホイール、スワイプ、スクロールバーの操作) を用いても同期するようにします。
f:id:aoe-tk:20150522191220p:plain

Swing での実現方法

これを実現するにあたって、Swing で行った方法と同じ方法を使えないかを考えました。

Swing では、JListJTable をスクローラブルにするためには、JScrollPane に追加を行う必要があるのですが、この JScrollPane が保持する Model (BoundedRangeModel) を 2 つの JScrollPane で共有することで、簡単にスクロールを同期することが可能になります。
まさしく MVC パターンの利点ですね。

以下のサイトで詳しく説明されているので、参考にしてください。

ateraimemo.com

JavaFX では何もしなくても ListView や TableView はスクロール可能です *1 。ListView (TableView) 自身がスクロールバーを保持しています。
JavaFXScrollBar には value プロパティがあり、このプロパティがスクロール位置を示します。ということは、2 つの ListView (TableView) が保持する ScrollBar の value プロパティをバインドすることで、スクロールを同期させることが可能なはずです。

ListView が保持する ScrollBar の取得

ここで問題が発生します。ListView や TableView には、自身が保持する ScrollBar のインスタンスを取得するための公式の API が存在しないのです。どうやって ScrollBar のインスタンスを取得すればいいのでしょうか?

実は Node には lookuplookupAll という、CSS セレクタで自分の子供として保持している Node インスタンスを探すためのメソッドがあります。
そして、 ListView の CSS リファレンス を見ると、次のような情報が記載されています。
f:id:aoe-tk:20150522192012p:plain

ListView の子 Node の情報が記載されており、それぞれの Node の CSS スタイルクラスの情報が記載されています。 ScrollBar には .scroll-bar というスタイルクラスが設定されていることが分かりますね。これで ScrollBar のインスタンスを取得することができますね!

というわけで、ListView から ScrollBar のインスタンスを取得するためのコードスニペットを以下に示します。水平スクロールバーと垂直スクロールバーの 2 種類があることに注意してください。

private ScrollBar getScrollBar(ListView<String> listView, boolean vertical) {
    Set<Node> nodes = listView.lookupAll(".scroll-bar");
    for (Node node : nodes) {
        if (node instanceof ScrollBar) {
            ScrollBar scrollBar = (ScrollBar) node;
            if (vertical && scrollBar.getOrientation() == Orientation.VERTICAL) {
                return scrollBar;
            } else if (!vertical && scrollBar.getOrientation() == Orientation.HORIZONTAL) {
                return scrollBar;
            }
        }
    }
    throw new IllegalStateException("Not found!");
}

ScrollBar のインスタンスを取得できてしまえれば、後は value プロパティをバインドするだけです。次のようにバインドします。

@FXML
ListView<String> leftListView;

@FXML
ListView<String> rightListView;

private void bindScrollBar() {
    ScrollBar leftVerticalScrollBar = getScrollBar(leftListView, true);
    ScrollBar rightVerticalScrollBar = getScrollBar(rightListView, true);
    leftVerticalScrollBar.valueProperty().bindBidirectional(rightVerticalScrollBar.valueProperty());
    ScrollBar leftHorizontalScrollBar = getScrollBar(leftListView, false);
    ScrollBar rightHorizontalScrollBar = getScrollBar(rightListView, false);
    leftHorizontalScrollBar.valueProperty().bindBidirectional(rightHorizontalScrollBar.valueProperty());
}

これで垂直、水平スクロールが同期されます。

まだ罠がある

これで解決...と思いたいところですが、重大な注意点があります。それは ScrollBar インスタンスの生成タイミングです。 ListView や TableView はコンストラクタで new されたタイミングでは ScrollBar のインスタンスを生成しない のです。

JavaFXコンポーネント ( javafx.scene.control.Control を継承して作ったコンポーネント) はそれがシーングラフに追加され、実際にレンダリングされるタイミングまで見た目の部分を構築する処理を遅延するようになっています。
カスタムのコンポーネントを作成された方はご存じだと思いますが、Control には layoutChildren というフックメソッドがあり、コンポーネントのレイアウトを構築する処理はこのメソッドで行うように指示されています。

JavaFX のグラフィクス描画は保持 (retained) モードと呼ばれる仕組みになっており、描画命令は即座に実行せずまとめておき、まとまったタイミング (これを実行しているのが Pulse と呼ばれる、タイマー起動のイベントです) で一斉に実行します。
この方式は描画処理をまとめて実行するので効率が良い、ディスプレイのリフレッシュと同期できるのでなめらかな動きになるといった利点があり、WPFFlex など最近の GUI ツールキットは大抵この方式になっているようです。 *2

そのような考え方になっているため、コンポーネントのレイアウト処理についてもフックメソッドを用意して、まとめて処理するようになっているということです。

話を戻しますが、このように ListView (TableView) が保持する ScrollBar のインスタンスは生成が遅延されるので、例えば FXML コントローラーの initialize メソッドの処理で ScrollBar インスタンスを取り出そうとしても、この時点ではシーングラフに追加されていないので、null が返ってくることになります。

今回のコード例では次のように ScrollBar を取り出してバインドする処理をくくり出しておきます。 Platform.runLater で処理をくるんでいるのは、次の Pulse 実行後 (そこではレンダリング処理が終わっているので) に呼び出されるようにするためです。

public void requestBind() {
    Platform.runLater(this::bindScrollBar);
}

このメソッドを、FXML 読み込み側で次のように呼び出してもらうようにしました。

FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
Parent root = loader.load();
primaryStage.setTitle("Scroll Test");
primaryStage.setScene(new Scene(root));
primaryStage.show();
Controller controller = loader.getController();
controller.requestBind();

これで、最初に紹介したスクリーンショットのように、2 つの ListView のスクロールを縦も横も同期できるようになります。
全体のコードは Gist にアップしているので、そちらをご覧ください。

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

まとめ

まとめると次のようになります。

  • JavaFX でスクローラブルなコンテンツのスクロール状態を取得、更新する場合は、Swing と同様、スクロールバーオブジェクトが保持するステートを利用すれば良い。
  • JavaFX の ListView、TableView が保持するスクロールバーを取得するためには、現状 CSS セレクタ経由で取得するしか方法がない。
  • 上記方法でスクロールバーを取得する場合、レイアウト処理が一度行われた状態で取得するよう注意する必要がある。

スクロールバーあるいはスクロール状態を取得するための公開 API がないので、ややアクロバティックな方法になってしまっていますね。スクロール状態の取得、あんまり需要がないのかなあ?

さて、これはある目的を達成するための前調査だったんですよねえ。それについてはまた後ほど。

おまけ

Pulse をはじめとする、JavaFX の描画の仕組みについての話は、以前の JavaFX 勉強会で私の発表でも触れています。

また、「パーフェクトJava」の JavaFX のパートでもコンパクトかつ丁寧に説明されているので、こちらもおすすめです。

改訂2版 パーフェクトJava

改訂2版 パーフェクトJava

*1:というかほとんどの GUI ツールキットではそれが普通ですが...。

*2:反面、細かいチューニングが難しい、描画命令をキャッシュするのでメモリ消費が多くなるというデメリットもあります。

主要IDEのJavaFXプロパティへの対応状況

JavaFX IDE

はじめに

JavaFX 開発を行ったことがある人はご存じだと思いますが、JavaFX ではプロパティに対して既存の JavaBeans のプロパティとは異なる新しい API を導入しています (以降、JavaFX で導入された新しいプロパティ構文のことを「JavaFX プロパティ」と呼ぶことにします) 。
それについては自分のブログでも以前に「 JavaFX Advent Calendar 2012 26日目 GroovyのVetoableを使ったサンプルをJavaFXのバインディングを使って実装してみる 」というエントリを書いていました。

このエントリでも解説していますが、JavaFX では従来の getter、setter とは異なるプロパティの定義を行います。
NetBeansEclipseJavaFX 開発環境である e(fx)clipse では少し前からこのプロパティに対応していたのですが、つい最近、 IntelliJ IDEA でもしれっと JavaFX プロパティの生成に対応していたことに気付きました。
ということで、主要 3 大 IDEJavaFX プロパティへの対応状況についてまとめてみます。

JavaFX プロパティの記述について

まず、IDE がどのように JavaFX プロパティの記述をするべきかについて示します。

String 型の name という名前の読み書き両方が可能なプロパティは次のように記述します。

private StringProperty name = new SimpleStringProperty();

public StringProperty nameProperty() {
    return name;
}

public final String getName() {
    return name.get();
}

public final void setName(String name) {
    this.name.set(name);
}

JavaFX のプロパティは XXProperty 型 ( XX にはラップする値のクラスが入る) でラップします。そしてこの Property 型の get/set メソッドで値のやり取りをします。
外部に公開するときは "(プロパティ名)Property()" という名前のメソッドを宣言します。最低限これができていれば JavaFX の範囲では OK です。
さらに、既存の JavaBeans 仕様に合わせる必要があるときは getter/setter も上記コードのように実装します。サブクラスで置き換えられるのを防ぐために final 宣言するのが一般的です。

リードオンリーなプロパティの定義については、 上に挙げたエントリでも解説したように 二通りの方法がありますが、多くの場合は ReadOnlyXXWrapper 型を使って次のように記述します。

private ReadOnlyStringWrapper name = new ReadOnlyStringWrapper();

public ReadOnlyStringProperty nameProperty() {
    return name.getReadOnlyProperty();
}

public final String getName() {
    return name.get();
}

IntelliJ IDEA

まず、自分が最近実装に気付いた IDEA から見てみましょう。

IDEA の場合、先にフィールドの定義を行ってから、コード生成の Getter and Setter を実行します。
f:id:aoe-tk:20150428011717p:plain

getter/setter 生成の対象が JavaFX プロパティに相当する型であった場合... f:id:aoe-tk:20150428011902p:plain

次のようなコードが生成されます。 f:id:aoe-tk:20150428012414p:plain

ちゃんと JavaFX プロパティの形式に合わせたメソッドが生成されていますね!いつの間にこの機能を実装したんだろう?
ヘルプにはこの機能についての解説は一切ありませんでしたし、今までのリリースノートをひっくり返しても特にこの機能に関する記述は見当たりませんでした。
この機能の存在については 某社のあの人 も、 IntelliJ IDEA エバンジェリストのあの人 も知らないに違いない!

次にリードオンリープロパティについて見てみます。次のようなフィールドを宣言した状態でコード生成をしてみます。 f:id:aoe-tk:20150428013019p:plain

その結果は次の通り。 f:id:aoe-tk:20150428013048p:plain

ああ、残念。対応できてませんね...。本当はこうなって欲しいんです。 f:id:aoe-tk:20150428013201p:plain

ちなみに、フィールドを Wrapper ではなく、ReadOnlyXXProperty 型で宣言した場合は getter のみ生成します。これはバグでしょうね。後で YouTrack に起票しておきますかね。

NetBeans

次は NetBeans です。IDEA の時は予めフィールドを宣言しましたが、NetBeans の場合はフィールドの生成からウィザード形式で作るようになっています。 JavaFX プロジェクトの場合、コード生成リストに「Java FXプロパティの追加」というメニューが追加されています。*1
f:id:aoe-tk:20150428013719p:plain

これを選択すると次のようなダイアログが表示されます。 f:id:aoe-tk:20150428013920p:plain

フィールド名やプロパティの型、初期値などを指定できます。さらに読み書き可能かリードオンリーかも選べます。読み書き可能を選ぶと次のようなコードが生成されます。 f:id:aoe-tk:20150428014039p:plain

リードオンリーにしたい場合はダイアログの内容を次のように入力します。 f:id:aoe-tk:20150428013833p:plain

すると次のようなコードが生成されます。 f:id:aoe-tk:20150428014157p:plain

さすが Oracle が開発を引っ張っている IDE だけあって、ちゃんと対応できていますね。ただ残念なことに、このウィザードは JavaFX プロジェクトでしか出現しません。そのため、Maven で作ったプロジェクトではこのウィザードが出現しないのです。JavaFX 形式のプロパティは JavaFX アプリケーション以外でも利用可能なので、このような余計な制限は撤廃して欲しいですね。

Eclipse (e(fx)clipse)

最後は最も利用者が多いであろう Eclipse です。厳密には EclipseJavaFX 開発機能を追加するプラグインである e(fx)clipse についてですが。

e(fx)clipse の場合、IDEA の場合と同じように、予めフィールドを作成しておいてからコード生成の Generate JavaFX Getters and Setters を選択します。 f:id:aoe-tk:20150428014904p:plain

すると次のようなウィザードが起動され、対象となるフィールドを選択します。 f:id:aoe-tk:20150428015018p:plain

OK を選択すると次のようなコードが生成されます。オプションで final にするかを選択できるのもいいですね。 f:id:aoe-tk:20150428015117p:plain

フィールドが ReadOnlyXXWrapper 型の場合、次のようにちゃんとリードオンリープロパティのためのコードが生成されます。完璧ですね。 f:id:aoe-tk:20150428015304p:plain

というわけで、主要 3 大 IDEJavaFX プロパティへの対応状況についてまとめてみました。IDEA のリードオンリープロパティの対応について問題がありますが、どの IDE も基本的な対応がされていることが分かりますね。
JavaFX プロパティは記述がちょっと面倒だな、と思っていた方もいらっしゃったかも知れませんが、このように IDE のサポートも得られるようになったので、ご安心 (?) ください。

JavaFX で新しい形式のプロパティが導入されたことの意義についてはまた別の機会にまとめたいと思っています!

*1:Java と FX の間にスペースが入っているのはどういうことだ...