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

もう、なげやりです…

Windows版JavaFXでHiDPI対応をやってみた その1

こんにちは。

昨年12月のJavaFX Advent Calendarで果たせなかった(詳しくはこちら)、JavaFXのHiDPI対応をやってみました。まずは出来上がりから。


yumix/javafx-dpi-scaling · GitHub

実はちゃんとしたテストをしていないのですが、ローンチ・カスタマーの @btnrouge が彼の会社の製品に適用したところ全然問題なかったというので、それなりに動くとは思っています。

アプリケーションから使うのは基本的には org.yumix.javafx.scaling パッケージ、その中でも Scaling クラスになります。

package hidpi;

import org.yumix.javafx.scaling.Scaling;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class AppMain extends Application {

  @Override
  public void start(Stage stage) throws Exception {
    AnchorPane root = FXMLLoader.load(getClass().getResource("Sample.fxml"));
    
    // Scaling.getDefault() でインスタンスを取得
    // Scaling.compute(root) で root とその子のサイズを解像度に合わせて調整
    Scene scene = new Scene(Scaling.getDefault().compute(root));
    
    stage.setScene(scene);
    stage.show();
  }

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

}

現状、まだShapeに対応していないとか、テストほとんどやっていないとか、課題は残っていますが、根となるPaneを指定するだけでよろしくやってくれるようになりました。

 

今回やったことは、javafx.scene.Nodeと既知のすべてのサブクラスについて、スケーリングの影響がありそうな位置、サイズ、マージン、パディングに関するプロパティの値を補正するようなプログラムを組んで、それを再帰呼び出しでノードのツリー全体に適用するようにしたことです。この調査自体は私でなく @btnrouge がやってくれたのですが、彼が送ってくれた実装があまりにひどかった(全部staticメソッドにしていた)ので、調査結果だけ拝借して私が全部作り直しました。

GlassFishユーザー会の副会長さんが、まさか全部staticのC言語みたいなJavaプログラムを送ってくるとは思わなかったよ…)

 

次回は、Shape対応が終わったころに、設計みたいなことをまとめようと思ってます。

 

ではでは。

Java EE SDKとGlassFish

これはGlassFish Advent Calendar 2014の23日目のエントリです。昨日は @kikutaro_ さんの「AWS Elastic BeanstalkでJava 8 & GlassFish 4.1!」でした。

 

1. Java EE SDKについて

GlassFishのことを書けと言われても、Surface Pro 3をセットアップした時、JavaFX向けに最小限の開発環境だけを作ったので、そもそもGlassFishをダウンロードしなければいけないし、Java EE 7のことはよくわかっていないからその勉強も必要です。

幸い、OracleではJava EE SDKというものを提供していて、私のようにこれからJava EEを勉強する人たちのためにドキュメント、サンプル、実行環境が提供されています。そしてJava EE SDK付属の実行環境がGlassFishというわけです。

Java EE SDKで勉強し、開発したアプリケーションをデプロイする本番環境用のGlassFishは、GlassFish公式サイトからダウンロードできます。

本番環境に使っていいのはWebLogicじゃないの?と思った方、たぶん勘違いされています。本番環境はGlassFishWebLogicのどちらでもOKです。Oracleは、OracleによるGlassFishの商用サポートは止めると言いましたが、GlassFishを本番環境で使ってはいけないとは一言も言っていませんし、http://www.payara.co.uk/ のような企業が商用サポートを提供することも認めています。私の勝手な想像ですが、GlassFishはコミュニティが育てていくもので(その中にはもちろんOracleも含まれます)、商用サポートもコミュニティが自発的に進めて欲しい―そのことをOracleは私たちに伝えたいのではないでしょうか?

脱線してしまったので話を戻します。Java EE SDKのダウンロードは、Java EE の概要から行える…はずなのですが、どこもリンク切れで私はたどりつけませんでした。仕方がないのでURLの階層上ったり工夫をしてみたら、何とJava EE 7のチュートリアル仕様書、関連ダウンロードへのリンクが小奇麗にまとまっているじゃないですか!(ただし全部英語)


Home: Java Platform, Enterprise Edition (Java EE) 7 Release 7

忘れないうちに手順をまとめておきます。

  1. リリース・ノート http://www.oracle.com/technetwork/java/javaee/documentation/javaee7sdk-readme-1957703.html
  2. Java EE SDKのダウンロード http://www.oracle.com/technetwork/java/javaee/downloads/index.html
  3. Java EE SDKのインストール手順 http://www.oracle.com/technetwork/java/javaee/documentation/javaee7sdk-install-1957708.html
  4. チュートリアル "Your First Cup" https://docs.oracle.com/javaee/7/firstcup/index.html
  5. Java EE 7 チュートリアル https://docs.oracle.com/javaee/7/tutorial/index.html

リリース・ノートによると、付属のサンプルは本番環境では使ってはいけないそうです。これはライセンス上の問題ではなく、わかりやすさ優先のためセキュリティとかガン無視しているからだそうです。

2. Hello, Java EE 7 SDK!

Java EE 7 SDKもFull版とWeb Profile版の2種類があります。今回私がダウンロードしたのはWeb Profile版です。本番環境向けGlassFish 4.1と比較したところ、SDKにはglassfish4フォルダ(ディレクトリ)下にいくつか追加されていました。

他は、特に変わっていないはずです。管理コンソールを起動して、コンポーネント一覧から確認してみます。

さて、管理コンソールはGlassFish 4.1では英語onlyになってしまいましたが、日本語化する方法はあるようなので、必要に応じてやればよいかと。さて、ダッシュボードの "Installed Components" をみると、本番環境用のGlassFishに加え、以下のコンポーネントが追加されていることがわかります。

さらに "Available Add-Ons" に表示される以下のコンポーネントがFull版のSDKに追加されているコンポーネントのはずです。

これらJava EE SDK特有のコンポーネントを除くと、GlassFishそのものです。少なくとも私には、これらの差分以外でJava EE SDKの実行環境と本番環境向けGlassFishの区別ができませんでした。

3. Java EE 7の道のりは長い

ここまで来ても、実はまだ準備段階のようです。チュートリアルやサンプルはMaven形式なので、それをEclipse(や他のIDE)にインポートしなければなりません。Web Profile版を選択したはずなのにそれでもチュートリアルやサンプルはたくさんあるし、最初の目標だったチュートリアルは完遂どころか着手すらままならない状態です(完全にJava EEの規模なめてました…)。さらにその先、GlassFishカーネルの話まで考えると気が遠くなりそうです。@btnrougeが目指していたものは、実はとんでもなく幅広く、さらに奥が深いものだと今更ながら気づきました。先日、@btnrougeと話した時、彼は2015年内にGlassFishカーネルの全ソースコードを読破すると言っていたのですが…私はちょっとついていけない。Java EE自体、すべての機能について自信を持って説明できる人は、世界中に数えるくらいしかいないと思います。

私は諸先輩方の背中を眺めながら、自分のペースで、JavaFXとは違うJavaとして、Java EEというものを追いかけてみようと思いました。

 

明日は @backpaper0 さん、明後日は @n_agetsu さんの予定です。

 

(あとがき)

このエントリを書いているとき、ちょっとわからないことがあって@btnrougeに電話したのですが、Java EE 7 SDKなんてダウンロードした試しがない的な発言が…そんなものですかね?

誕生日を迎えて考えたこと

24歳になりました。

誕生日の24時間をすべて自宅以外で過ごすのは、多分生まれて初めてだと思います。両親や友人・知人からいろいろな形で祝福を頂き、満24年の当日を過ごすことができました。

24と言えば、0が発見されるまで特殊な意味を持つ数字でした。2、3、4、6、8、12のいずれでも割ることができるからです。これは24に限ったことではなく、60など他の12の倍数でも同じです。

0の概念が現在の10進法を支えているのなら、0の概念がなかった時代は実用面から約数が多い12進法が多く使われました。時刻や、天体の位置で使われる「分」「秒」など。

自分の誕生日より、自分の年齢の数字が気になっている、今日この頃です。

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!

ではまた。

JavaFXでiPhone風のメールクライアントを作る

ハッピーバースデー、私 & @aoetk さん!

ということで、私は23歳になりました。

最初にお断りしておきますが、これはJavaFX Advent Calendar 2013・19日目のエントリです。昨日、18日目は某瑞鳳教徒 (@btnrouge) のエントリ「Enterprise JavaFX for EUC」です。明日、20日目はMulticolorWorldさんです。

JavaFX Advent Calendarには昨年も参加していて、あまり大したことは書けなかったのですが、この1年で私も成長しました。その証として、JavaFXiPhone風のメールクライアントを作成してみました。メールクライアントを作成するにはJavaFXだけでなくJavaMailの知識も必要になりますが、そちらについてはJava Advent Calendar 2013の私のエントリ「JavaMailを手軽に使うライブラリ」で取り上げていますので、合わせてご覧ください。

私が今使用しているのは、香港版SIMフリーiPhonedocomo純正SIMをセットしたもので、iOS 6を継続して使用しています。今回作成するクライアントも、iOS 6標準のものを簡単にしたものです。

§1. 今年の挑戦ーJavaFXアプリケーションにWeld SEを適用する

少し前からWeld SEという、CDIJava SE環境で使うためのサブセットに興味を持っていました。Weld SEベースでJavaFXアプリケーションを作ってみたくなり、今回挑戦しました。NetBeansではMavenその他の連携がイマイチだったので、e(fx)clipse + JDK8で作ってみました。私にとっては意図しないところで勝手にコードを自動生成するNetBeansよりは、多少面倒でも教科書通りにコーディングすればとりあえず動くe(fx)clipseの方が使いやすいと思っています。

そしてその成果物が "quaoar" https://github.com/yumix/quaoar です

プロジェクト名 "quaoar" の由来は、太陽系で "Trans-Nepturian Objects" (TNO) と呼ばれている、海王星の外側を公転している無数の小惑星の1つ「Quaoar」です。

太陽系マメ知識: 小惑星 Quaoar について

TNOは最初の天体である1992 QB1(小惑星#15760、今日に至るまでなぜか命名されず、仮符号のQB1=キュー・ビー・ワンが定着してしまった稀有な例です)以後毎年のように発見され、「Quaoar」は2002年に発見された小惑星#50000(2002 LM60)です。大きさは直径にして月の約4分の1で、「Weywot」という衛星を従えています。小惑星番号がちょうど50000番というキリ番(この番号は本来冥王星のために予約されていたものですが、冥王星準惑星に分類され小惑星番号を振られた時点ではもう「Quaoar」の番号だったため、仕方なく冥王星には#134340を付けた、という話があります)であるだけでなく、「Quaoar」の公転軌道は離心率がほぼゼロ、つまり太陽の周りを真円の軌道で公転しています(海王星第1衛星のトリトンも離心率がほぼゼロです)。TNOの大半は離心率が大きいため、「Quaoar」は特殊な例だと言えます。なお「Quaoar」は現在、次期準惑星認定候補として挙げられている小惑星の1つでもあります。

§2. 画面レイアウトを作成してみる

まずは画面のデザインを決めます。残念ながら私は絵心がないので、普段使っているiPhone 4S (iOS 6) 標準のメールのデザインをパクります。今回は最初のバージョンなので、受信トレイ、メール本文、メール作成に絞ります。元ネタは次の3枚のスクリーンショットです。

 f:id:yumix_h:20131219034849p:plain

図1 受信トレイ

f:id:yumix_h:20131219034850p:plain

図2 受信メール詳細

f:id:yumix_h:20131219034851p:plain

図3 送信メール作成

これらのデザインを参考にPhotoshopで背景作って 、コントロールを配置していけばいいわけです。送信メール作成のキーボードについてはPC側に実物があるので、ソフトウェア・キーボードは不要だと判断しました。

 そして作成した背景画像がこれ。

f:id:yumix_h:20131219040514p:plain

 図4 背景画像

あとはこれをベースに画面をデザインすればいいわけですね。ボタンについても、スタイルシートで元の形を隠してしまえばOKなはず。

Schene Builder上でのプレビューですが、およそこんなイメージになります。

f:id:yumix_h:20131219051414p:plain

図5 Scene Builder上でのプレビュー結果

どうです?iOSっぽく見えませんか?

このようにして画面をデザインして、同時にControllerも実装してゆきます。

§3. JavaFXでUIを実装する

<TBD> CSSがなぜか読み込めなくて…12/19~12/20にかけて直します。ごめんなさい。

画面切り替えも、ここで実装します。

JavaFXでUIを作ります。今回はWeld SEでいろいろ実装するため、テンプレート的なところがあちこち変わります。まずはメインクラスから。

public class QuaoarApplication extends Application {
  
  private static Stage primaryStage;
  
  @Produces
  @PrimaryStage
  public Stage getPrimaryStage() {
    return primaryStage;
  }
  
  @Override
  public void start(Stage primaryStage) throws Exception {
    QuaoarApplication.primaryStage = primaryStage;
    
    Weld weld = new Weld();
    WeldContainer container = weld.initialize();
    QuaoarRunner quaoarRunner = container.instance().select(QuaoarRunner.class).get();
    quaoarRunner.start();
    weld.shutdown();
  }
  
  public static void main(String[] args) {
    launch(args);
  }
}

FXMLを読み込んでSceneを作成していく一連の流れは、全部メインクラスからQuaoarRunnerというBeanに移します。そしてWeldからQuaoarRunnerをロードすると 、QuaoarRunnerとそこからロードされるすべてのBeanで@Injectが使えるようになります。

このメインクラスではちょっとだけずるいことをしています。画面切り替えの時などにStageを操作する場面が出てくるのですが、引数で渡そうとすると画面のコントローラまで上手く渡らずNullPointerExceptionになります。そこでメインクラスのstaticフィールドにStageを設定して、さらにStageのプロデューサ・メソッドをメインクラスの中に作ってしまいました。

では、次にQuaoarRunnerの実装です。@InjectでStageをインジェクションしているところにご注目ください。

@Singleton
public class QuaoarRunner {
  
  @Inject
  @MailboxRootPane
  private AnchorPane mailboxRootPane;
  
  @Inject
  @MailReaderRootPane
  private AnchorPane mailReaderRootPane;
  
  @Inject
  @MailWriterRootPane
  private AnchorPane mailWriterRootPane;
  
  @Inject
  @PrimaryStage
  private Stage stage;
  
  public void start() throws IOException {
    Scene scene = new Scene(mailboxRootPane, 320, 480);
    stage.setScene(scene);
    stage.show();
  }
  
  public void select(WhichScene whichScene) {
    switch (whichScene) {
    case MAILBOX:
      System.out.println(mailboxRootPane);
      stage.getScene().setRoot(mailboxRootPane);
      break;
    case MAIL_READER:
      System.out.println(mailReaderRootPane);
      stage.getScene().setRoot(mailReaderRootPane);
      break;
    case MAIL_WRITER:
      System.out.println(mailWriterRootPane);
      stage.getScene().setRoot(mailWriterRootPane);
      break;
    default:
      break;
    }
  }
}

今回はFXMLLoaderを使うところもプロデューサを使って、全部@Injectでオブジェクトを作成しています。プロデューサは次のような実装にしています。

@Dependent
public class RootPaneProducer {

  @Inject
  private FXMLLoader loader;
  
  private static final String MAILBOX_ROOT_URI = "/org/yumix/quaoar/Mailbox.fxml";
  
  private static final String MAIL_READER_ROOT_URI = "/org/yumix/quaoar/MailReader.fxml";
  
  private static final String MAIL_WRITER_ROOT_URI = "/org/yumix/quaoar/MailWriter.fxml";
  
  @Produces
  @MailboxRootPane
  public AnchorPane getMailboxRootPane() throws IOException {
    return getRootPane(MAILBOX_ROOT_URI);
  }
  
  @Produces
  @MailReaderRootPane
  public AnchorPane getMailReaderRootPane() throws IOException {
    return getRootPane(MAIL_READER_ROOT_URI);
  }
  
  @Produces
  @MailWriterRootPane
  public AnchorPane getMailWriterRootPane() throws IOException {
    return getRootPane(MAIL_WRITER_ROOT_URI);
  }
  
  private AnchorPane getRootPane(String name) throws IOException {
    try (InputStream fxml = getClass().getResourceAsStream(name)) {
      return loader.load(fxml);
    }
  }
}

最初のプロデューサは、FXMLLoaderのロード処理を無理やり詰め込んだ形で、面白味はありませんが、2番目のはちょっと違います。元は http://qiita.com/opengl-8080/items/32a74a07e69ce7d38fc3 に載っているものをそのまま使っていたのですが、

loader.setControllerFactory(new Callback<Class<?>, Object>() {
  @Override
  public Object call(Class<?> param) {
    return instance.select(param).get();
  }
});

私は今回Java 8を使っているので、 

loader.setControllerFactory(param -> instance.select(param).get());

とさりげなくLambdaを使ってみました。Lambda使えるところは全部Lambda使った方が全然楽です。 

QuaoarRunnerには画面切り替えのためのコードがあります。selectメソッドがそれです。Stageと各画面のRootのAnchorPaneをインジェクションできるので、コントローラ側でやってもよいのですが、受信トレイと送信メール作成のように往復できる画面があると、Weld側で循環参照のようになって上手くいきませんでした。画面の切り替えをQuaoarRunnerに集中させるとそういったことがなくなり、スムーズにゆきます。

それ以外の部分は、いつものJavaFXとだいたい同じです。インジェクションも使えます。ただ、Weld SEの仕様でプロデューサの戻り値型にジェネリクスを使用できないようです。Java 8は全体的に型推論が強化されているのか、今まで型パラメータ化できなかった箇所でも型パラメータが使えるようになっています。Weld SEも早くJava 8に追従してほしいなと、思ってしまいました。

§4. "growslowly" を組み込む

やりたいことは、メール作成から「送信」をクリックすると、作成した内容を"growslowly"で送信するのと、Timerスレッドを使ってIMAPのメールボックスをポーリングして、ヘッダー情報と本文の先頭ををListView<FlowText>に詰めていくこと、それからListViewを選択するとメール全文を表示することです。

記事全体が長くなったのと、まだ未完成なので、次回のエントリで書きます。

§5. まとめ

私のPCはWindows 8.1で、標準のメールでOutlook.comもiCloudも読み書きできてしまいます。ということで今回はあくまで試作品という位置づけです。Windows 7ユーザーの方には少しはお役に立てるのかもしれませんが…でも勉強になりました。

最後に、今回の試作品もまた、以下のBSDライセンスで公開しています。私がGitHubで公開しているものは、私オリジナルのものは特に明記がなくてもBSDライセンスです。

Copyright (c) 2013, Yumi Hiraoka

All rights reserved.

 

Redistribution and use in source and binary forms, with or without modification,

are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of ditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND ONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 

JavaMailを手軽に使うライブラリ

ハッピーバースデー、私 & @aoetk さん!

ということで、私は23歳になりました。

最初にお断りしておきますが、これはJava Advent Calendar 2013・19日目のエントリです。同日のJava Advent Calndar 2013にも「JavaFXiPhone風のメールクライアントを作る」というタイトルで続編を公開する予定です。

さて、昨日、18日目はToshiaki Makiさんのエントリ「BloomFilterで効率的に大量データセットを持つ」でした。明日、20日目はJavaエバンジェリスト寺田さんの予定です。 

§1. JavaMailラッパー "growslowly" のご紹介

私は、諸般の事情からGitHubで "growslowly" https://github.com/yumix/growslowly というJavaMailのラッパーライブラリを公開しています。"growslowly"という名前は、ゆっくりでいいから育って欲しいという願いと、私の大好きなゆかち(井口裕香さん)の2nd シングル「Grow Slowly」を掛けています(ゆかちの楽曲の中でも一押しなのが「Grow Slowly」なのです)。SMTPの実装は相当前に完成していたのですが、受信側は長らく放置していました。今回JavaFXの簡易MUAを作ることにしたので、 それに先行してPOP3IMAP4のラッパー実装行いました。

§2. "growslowly" によるメール送信

"growslowly"の送信部はMailクラスのメソッドチェーンを利用し、通知メール程度なら1行で作成・送信できるように作ってあります。メールサーバの設定をすべてプロパティファイルに記述するのは事前準備が大変なので、JavaMailセッションのプロバイダをあらかじめ数種類用意しておいて、プロバイダの種類とメールアドレス、ログイン名、パスワードの各項目を指定すればセッションが作成できるようにしています。現時点での対応プロバイダは、GmailOutlook.com、iCloudと、私が契約しているISPであるOCNの4種類です。いずれの実装もメールサーバ固有のプロパティをファイルで保持していますが、そこは実装した私の都合なので、Sessionを作成できるのであればプロパティファイルを使用しなくても構いません。

@btnrougeSo-netBiglobeのプロバイダ実装を頼んでおいたはずなのですが、すっかり忘れられているようです。ちょうど彼が葉月ちゃんに現を抜かしている時期に依頼したのが間違っていたのかもしれません。

JavaMailでは受信側も同じセッションを使用しますので、この部分については共有できます。

では、"growslowly"によるメール送信サンプルです。Mailクラスがエントリポイントで、Builder風のインタフェースに仕上げています。

SessionProviderFactory factory = SessionProviderFactory.getInstance();
Session session = factory.getSession(OutlookProvider.class, "yumix@outlook.com", USERNAME, PASSWORD);
Mail.from("yumix@outlook.com").to(RECEIVER).subject("テストメール").message("growslowly のテストメール送信です。").send(session);

わかりやすさのために3行に分けていますが、実際には1行で記述することができます。また、できればSessionProviderSessionProviderFactoryCDIのインジェクションに置き換えたいのですが、そこは今後の課題として考えています。

§3. "growslowly" のIMAP/POP3対応

JavaMailの受信部分はIMAP4の利用を前提としながらも、POP3でもできるだけIMAP4に近い機能が提供できるように工夫されています。"grawslowly"の受信部はMailBoxクラスがエントリポイントとなり、そこを起点にMailFolderクラスがIMAPのメール・フォルダ階層を表現します。受信メールはMessageDecoderインタフェースを介して他のクラスに保存します。

  • MailBox : メールサーバへの接続と、メールボックス "INBOX" の取得を担当。"INBOX” は受信トレイに相当し、MailFolderのオブジェクトとして表される。
  • MailFolder : 配下のフォルダおよびメールの一覧を取得する。フォルダの取得はIMAP4のみ可能で、POP3の場合は空のフォルダ一覧が返される。
  • MessageDecoder<E> : MailBox.listMessagesクラスの引数に指定するデコーダのインタフェース。実装に当たってはJavaMailMessageMimeMessageの知識が必要です。フォルダ内のメッセージはMessageDecoderの具象クラスによりアプリケーションに合わせた形式に変換される。JavaMailの取得メッセージは遅延ロードになるため、確実にフォルダ内の全メッセージを取り出すためにも有効(だと思う)。

"growslowly"によるメール受信サンプルです。ここではIMAP4による受信例を出しますが、サブフォルダがないことを除けばPOP3でも同様の動きをします。

Session session = factory.getSession(OutlookProvider.class, "yumix@outlook.com", USERNAME, PASSWORD);
session.setDebug(false);
try (MailBox mailBox = new MailBox(session, IMAP)) {
  List<Summary&ht; msgs = mailBox.inbox().listMessages(new TestMessageDecoder());
  // do something
} catch (Exception e) {
  throw new RuntimeException(e.getMessage(), e);
}

§4. プロバイダごとの設定について

プロバイダごとの設定については、今後、修正が頻繁に発生するかと思われます。もちろん移行期間があるでしょうが、ブログに修正差分を掲載するよりは、GitHubのプロパティファイルを参照していただいた方が分かりやすいと思います。

受信プロトコルPOP3IMAP4か)、SMTP認証の要否(最近はSMTP認証必須のサーバが増えています)、SSL/TLSの設定要否がISPごとに異なっている項目です。私の知る限りでは、iCloudSMTP認証が少々厄介で、SSLをOFFにしつつもTLSをONにするという、少々特殊な設定が必要です。また、iCloudIMAP4接続に関する公式情報が内容が古いためかそのままでは使えないことも厄介でした。

§5. まとめ

JavaMailはかなり早い時期から存在するAPIのため、全体的に古臭さは否めません 。しかし、Java環境でメールの送受信を行う手段の第一選択肢であり、JavaMailなしでのメール送受信は少々辛いものがあります。

幸い、今回私が作成した"growslowly"のようなラッパー・ライブラリを用意することで、JavaMailの持つ古臭さをカプセル化することができ、より使いやすいインタフェースを提供できます。"growslowly"はJAX-RSJPAのように実行時例外のみ(具体的にはMailExceptionとそのサブクラス)スローするようにしており、また受信部ではAutoClosableインタフェースを実装しています。

"growslowly"はまだ未熟な実装です。しかし、JavaMailを簡単に使う方向性を示すものとしては、十分な役割を果たせるのではないでしょうか?

最後に、"growslowly"は以下のBSDライセンスで公開しています。

Copyright (c) 2013, Yumi Hiraoka

All rights reserved.

 

Redistribution and use in source and binary forms, with or without modification,

are permitted provided that the following conditions are met:

  • Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
  • Redistributions in binary form must reproduce the above copyright notice, this list of ditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND ONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;

LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 "growslowly"は名前の通り、本当にゆっくり成長しますので、長い目で見守っていただければ幸いです。

それでは、次は「JavaFXiPhone風のメールクライアントを作る」でお目にかかりましょう。