ひらおかゆみのなげやりブログ

もう、なげやりです…

WebView(JavaFX)のズーム機能を使ってみました

これはJavaFX Advent Calendar 2014の18日目のエントリです。19日の誕生日枠は @aoetk さんに先を越されてしまったので、今年は誕生日前夜祭枠です。昨年はiPhone風のメールクライアントを作ろうとして失敗しましたが、今年はHiDPI対応のWebブラウザをWebViewで作ってみました。まず、今年はちゃんと作れたので、GitHubの方からご紹介します。


yumix/webviewer · GitHub

事の発端

最初は、WindowsJavaFXのHiDPI対応について調べていました。

先日、念願のSurface Pro 3ユーザになりました。Surface Pro 3は2160×1440ピクセルという、数年前のPCと比べると4倍の広さのタッチスクリーンを持っているのですが、Eclipseを起動してみると、文字に比べてボタン・アイコン類がとても小さくなってとても使いづらいのです。この2年くらいで作ってきたサンプルも軒並みレイアウトぐちゃぐちゃだし。でも、WordやPowerPointは普通に表示されるのです。これが、HiDPIの世界?それが今回の挑戦のきっかけになりました。

HiDPIとは?

まず、HiDPIとは何でしょう?Windowsの画面解像度は96dpiに設定されていますが、それよりもずっと高い解像度のことを指すようです。iPhone 5sなど(私はiPhone 5sユーザーなので)のRetina液晶もHiDPIで、326dpiあるようです(出典)。Surface Pro 3の場合は実解像度が216dpiもあるそう。

ディスプレイの解像度が大きくなると、ディスプレイの物理的なサイズが同じなら文字やアイコンが小さくなり、それが過ぎると小さすぎて操作に支障をきたすでしょう。そこでHiDPI環境では視認できる文字やアイコンのサイズを拡大しているようです。ところが、JavaFXだとフォント以外にはこの拡大が適用されず、フォントがテキストフィールドに収まりきらなくなるなど、不具合が生じます。Eclipseでも起きているということは、たぶんJavaGUI全体の問題のような気がしています。

ところで、HiDPI関連の画面調整はと言うと、Windows 8.1にはコントロールパネルのディスプレイの設定箇所に「すべての項目のサイズを変更する」というスライダがあって、「小さくする」から「大きくする」まで4段階で変えられるようです。Surface Pro 3のデフォルト設定では大きいほうから2番目の設定になっていました。これは実はフォントやアイコンの拡大率と連動していて、小さいほうから100%、125%、150%、200%となっているようです(「すべてのディスプレイで同じ拡大率を使用する」にチェックを入れてみたら、スライダから倍率が明記されたラジオボタンに変わったので)。つまり、Surface Pro 3のデフォルトの拡大率は150%だということです。

 JavaFXでHiDPI対応を試みる

JavaFX HiDPI Windows」で検索してみると、青江 @aoetk さんがすでに調べていらしてWindowsJavaFXではサイズをピクセル指定するとそのまま(つまり拡大率を適用せず)ディスプレイのピクセルに対応してしまうそうです。MacJavaFXではDevice Independent Pixel(DIP)という仕組みに対応していて、Retinaのような高解像度ディスプレイでも元の画面レイアウトがだいたい維持されるようです。あくまで想像ですが、DIPではJavaFXでサイズをピクセル指定すると、ディスプレイの解像度に合わせて1ピクセル複数の実ピクセルで構成するように調整してくれるのでしょう。青江さんもWindowsJavaFXDIP対応がなされない限り、HiDPI対応は容易ではないと結論付けています。

青江さんのエントリを拝読した後、同じ検索で見つけた JavaFX DPI Scaling という記事も気になって読んでみました。この記事では、JavaFXのデフォルトフォントのサイズが解像度によって異なる(拡大率100%=12、125%=15、150%=18、200%=24)ことから、これを基準に解像度を推測して各部のサイズを決めるというものです。デフォルトフォントサイズ以外にも基準にできるサイズはいくつかあるようですが、このテクニックを使えばとりあえずのHiDPI対応はできそうです。

デフォルトフォントのサイズは Font.getDefault().getSize() で取得できます。取得できる値は拡大率に応じて12、15、18、24のいずれかなので、デフォルトフォントのサイズを12で割れば拡大率が算出できそうな気がします。拡大率100%想定の各部のサイズにデフォルトフォントサイズから算出した拡大率をかけてあげれば、なんとなくよさそうな結果が得られると思いませんか?

以前、JavaFXのSceneはツリー構造になっていると聞いたことがあって、ルートのAnchorPaneを渡してあげて再帰的にノードをたどりながらPaneやControlのサイズに拡大率を掛けてあげれば、すべて解決しそう…と思ったのですが私には無理でした。コントロールのサイズだけでなくmarginやpadding、HBoxやVBoxのspacingも考慮しなければならないし、PaneやControl間でサイズをバインドしているケースも難しそう。そして一番致命的だったのは、スタイルシートで画像に置き換えたボタンのリサイズがまるで見当つかない(スタイルシートで指定した背景画像そのものを拡大・縮小はできませんよね?)。もう汎用的なHiDPI対応は諦めました。そして私の手元には中途半端にHiDPI対応をした簡易Webブラウザが残りました。

WebViewのズーム機能を見つける

一応出来上がった簡易Webブラウザで適当なサイトを表示してみたら…何か、全体的に小さくありません?Google Chromeで同じサイトを開いたのと比較してみますが、どう見ても小さい(注:画像は完成版の簡易Webブラウザを使ったイメージです)。

f:id:yumix_h:20141217002932p:plain

(画像は http://www.jaxa.jp をブラウズしているところ)

WebViewのJavadocをよく調べてみたら、zoomProperty というプロパティがあって、これを操作すると表示倍率を変えられそう…ということで、webView.setZoom(1.5) としたら無事、ちょうどいい大きさで表示できました。

WebViewのズームと水平スライダをバインドする

でも、せっかく見つけたzoomProperty、もうちょっと使ってみたいですよね?ちょうど櫻庭さんのバインドに関するエントリを見ていたので、水平スライダをつけて、スライダとWebViewのズームをバインドしてみました。単純にバインドしただけでうまく連動しますね。ちょっと感動です。

でも、今現在の拡大率を知りたいのと、すぐに初期値に戻せるようにしたいので、ツールチップで拡大率を表示して、コンテキストメニューで初期値に戻せるようにしました。

さらにタッチ&ジェスチャーをバインドしてみる

さて、私のSurface Pro 3はタッチスクリーンを持っています。ということは、タッチ&ジェスチャーを目の前で試せるわけです。念のため、WebViewがジェスチャー(ピンチ)に対応しているかを調べて…標準では対応していませんね。ということで、水平スライダと同じ操作をジェスチャーでもできるように改造してみました。

まず用意するのは、WebViewに設定するジェスチャーイベントハンドラです。ピンチに対応するハンドラはonZoomです。

@FXML

public void onZoomWebView(ZoomEvent event) {

  double zoom = webView.zoomProperty().multiply(event.getTotalZoomFactor()).get();

  webView.zoomProperty().set(max(0.5, min(zoom, 4.0)));

}

ZoomEvent.getZoomFactor() か ZoomEvent.getTotalZoomFactor() でピンチの程度が取得でき、その値はそのまま拡大率として利用できるようです(Javadocにはそう書いてあった)。 ピンチイン/ピンチアウトした結果は getTotalZoomFactor() の方で取れます。getZoomFactor() は途中の状態を取得するらしいです。ただし、一旦スクリーンから指を離すと getTotalZoomFactor() の戻り値もリセットされてしまいます。複数回のピンチで拡大・縮小するような仕掛けにするためには、現在のWebViewのズームと getTotalZoomFactor() の戻り値をかけて新しいズームにする必要があります。

ただし、このコードを単純に追加すると例外がスローされて動きません。この場所でbound云々言っているからバインドの使い方が悪いの?

webView.zoomProperty().bind(zoomSlider.valueProperty());

原因は水平スライダをWebViewのzoomPropertyに一方向でバインドした状態で、さらにWebViewのonZoomをzoomPropertyでバインドしたから、みたいです。次のようにbindBidirectionalで双方向バインドにしたら無事動きました。

webView.zoomProperty().bindBidirectional(zoomSlider.valueProperty());

それから、拡大率の最大と最小は決めないとまずいことになります。水平スライダは動かせる範囲が決まっているので問題ないのですが、ジェスチャーの方は制限をかけないとどこまでも拡大・縮小して、そのうちアプリが落ちます。今回は50%~400%の間で制限するようにしました。

で、HiDPI対応はどこへ行った?

HiDPI対応もちゃんとしていますよ。

  • AnchorPane は prefWidth、prefHeight とも初期設定値×拡大率(バインド)
  • WebView.prefWidth は AnchorPane.prefWidth、WebView.prefHeight は AnchorPane.prefHeight - ヘッダHeight×拡大率にそれぞれバインド
  • 水平スライダの初期値は、フォントの拡大率に設定(Surface Pro 3ならば1.5)

たぶん、こういった調整の積み重ねで対応するしかないと思います。

補足: JavaFX Maven Pluginのこと

今回、JavaFX Maven Pluginというものを使ってみました。Java 8以降はJavaFXであっても普通のJava SEアプリとして作成できるので、特別なプラグインは不要なのですが、全部入りJARや.EXEも作ってくれるようなので利用してみました。

JavaFX Maven Pluginを追加すると、Mavenのターゲットに jfx: で始まる3つが追加されます。

  • jfx:jar -- 全部入りJARを作成する。
  • jfx:run -- jfx:jarでJARを作成して、それを実行する。
  • jfx:native -- JavaFXのネイティブ・パッケージを作成する。WIXInno Setupがあればインストーラまで作成する(なくても.EXEまでは作成できる)。

まとめ 

WebViewとジェスチャを組み合わせてズームできるようにしただけですが、スマホのブラウザみたいで面白いですよ。

(参考文献)

明日は、お誕生日枠の青江(@aoetk)さんです。Happy birthday!

ではまた。