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

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

もう、なげやりです…

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.