JavaFXのWindows環境におけるHiDPI対応について調べたことのメモ

先日のVAIO Tap 11レポートのエントリで、こんなことを書いていました。

ただ、高 DPI スケーリングに対応していないアプリケーションが多いことが分かりました。
(中略)
NetBeans はデフォルトだとコンソールの文字が豆粒のようになってしまい、とても悲しかったです (タスクバーの大きさに対するコンソールの文字の大きさに着目) 。

そのうち JavaFX で高 DPI に対応するにはどうすればいいか、調べてみるつもりです。

というでちょっと調べてみましたが、まだいい対応策が見つかっていないので、以下に現在分かっている点だけをとりあえず列挙します。

  • Windows 環境について言えば、JavaFXプログラム内でサイズ指定に使うピクセルは Device Independent ではない
    • つまり、JavaFXのプログラムで指定した数値がそのままスクリーン上のピクセル数になる。
    • なお、Mac では既に Device Independent になっているらしいが、手元に Retina Mac がないので確認できず。
  • Screen クラスには getDpi() メソッドがあり、これを用いて実行環境上の DPI を取得することは可能。これを使ってプログラム上でスケールの調整をすることは可能。
    • とは言え、シーングラフ上の各コンポーネントのサイズを一々設定し直すのは面倒。
    • Java コード上で指定しているピクセルはいいとして、外部 CSS についてはファイルを作り直す羽目になる。
    • ルートのレイアウトコンテナに対して、Scale を設定することでまとめて拡大すれば良いように思われるが、Transform による変形はレイアウト境界に対して影響を及ぼさないため、レイアウトコンテナ上の Node 達がレイアウトコンテナから「はみ出す」ことになってしまう。

というわけで現状 Windows 環境では HiDPI に対応したアプリケーションを作るのは容易ではない感じです。
一番やりやすいワークアラウンドは、CSS のピクセル指定部分をテンプレート化しておいて、まとめて置き換える方法ですかね。もしかしたら単位に em を使えばいいのかな?

とにもかくにも JavaFXWindows でも Device Independent Pixel (DIP) に対応してくれるのが一番なんですけどねえ。
JIRA にもこのようなチケットが上がっていますが、Windows の世界でも高解像度端末が増えてきていますし、早く対処してもらいたいところです。

現在分かっていることはこんなところです。もう少し調べて何か分かったらまたエントリをアップします。

ListViewやTableViewのセルをカスタマイズする方法 (JavaFX Advent Calendar2013 7日目)

このエントリは JavaFX Advent Calendar 2013 の 7 日目のエントリです。前日は蓮沼 (@) さんによる、「e(fx)clipseで作るJavaFXアプリケーション」でした。

はじめに

Twitter やブログなどで JavaFX に関するエントリを見ていると、ListViewTableView のセルの表示をカスタマイズする方法がよく分からずに苦戦している人をよく見かけます。
このようなリスト系コンポーネントの表示をカスタマイズする方法は、実はどの GUI ツールキットでも大体やり方が似ているのですが、HTML を使った UI の開発ばかりをやっている人には馴染みがないかも知れません。
そこで、少し地味ではありますがこの方法について解説します。また、セルのカスタマイズをする際には性能面で注意が必要なポイントがあるので、それについても触れたいと思います。
なお、ListView も TableView も基本的には同じような方法でセルのカスタマイズが行えるので、以降の説明は ListView に絞って行います。

ListView の仕組み

リストのセルの表示をカスタマイズする方法を理解するためには、まずは ListView の仕組みを知っておいた方がいいと思うので、まずはそこから説明します。
まずは次のようなシンプルなリストを見てみましょう。

このリストは 500 件のアイテムを表示しています。大量にアイテムがあるので、大半が隠れていて、スクロールを行う必要があります。
一見すると隠れている領域にもセルのインスタンスがあるかのように思えますね。
ところが、シーングラフの状態を確認することができる、Scenic View を使って見てみると...。

なんと表示されている範囲のインスタンスしか生成されていないのです! (ListCell インスタンスの数に注目してください)
ListView はこのように表示されている範囲のセルのインスタンスだけを作り、スクロールが発生するとセルの表示をスクロール位置に対応したデータに合わせて切り替えることで、あたかも隠れた範囲から新しいセルが入ってきたかのように見せかけているのです。
こうすることで、大量のデータを表示しても性能が落ちないようにしているのです。

セルのカスタマイズ方法

それではセルのカスタマイズ方法について説明しましょう。セルの表示をカスタマイズするには次の 2 つのことを行う必要があります。

  1. カスタムの Cell クラスを作り、表示したいレイアウトを実装する
  2. ListView に対して上で作ったカスタムの Cell クラスを使うように CellFactory を設定する

具体例を示しながら説明していきます。今、自分用にソーシャルブックマークサービスDiigo のビューアを JavaFX で作ろうと考えているのですが、それのブックマークリスト部分を先に作ってみることにします。
セルの表示を次のようにすることを考えてみます。

上から順にタイトル、コメント、タグを表示します。タグはハイパーリンクとして横に並べるようにします。

まずはリストのデータソースとなるモデルを用意します。次のような JavaFX 形式の JavaBean として作ります。

/**
 * ブックマークを表すモデル.
 */
public class BookmarkModel {
    private StringProperty title = new SimpleStringProperty();
    private StringProperty comment = new SimpleStringProperty();
    private ObservableList<String> tagList = FXCollections.observableArrayList();

    /**
     * コンストラクタ.
     * @param title タイトル
     * @param comment コメント
     * @param tagList タグのリスト
     */
    public BookmarkModel(String title, String comment, List<String> tagList) {
        this.title.set(title);
        this.comment.set(comment);
        this.tagList.addAll(tagList);
    }

    public StringProperty titleProperty() {
        return title;
    }

    public StringProperty commentProperty() {
        return comment;
    }

    public ObservableList<String> getTagList() {
        return tagList;
    }
}

このオブジェクトを ObservableList にくるんで ListView にデータとして渡すことになります。

次はカスタムの Cell クラスを作りましょう。

public class BookmarkCell extends ListCell<BookmarkModel> {
    private VBox cellContainer;
    private Text txtTitle;
    private Text txtComment;
    private HBox tagContainer;
    private boolean bound = false;

    public BookmarkCell() { // (1)
        initComponent();
        initStyle();
    }

    private void initStyle() {
        txtTitle.setFont(new Font("System Bold", 18.0));
    }

    private void initComponent() { // (2)
        cellContainer = new VBox(5);
        txtTitle = new Text();
        VBox.setVgrow(txtTitle, Priority.NEVER);
        txtComment = new Text();
        VBox.setVgrow(txtComment, Priority.ALWAYS);
        tagContainer = new HBox(); // (3)
        VBox.setVgrow(tagContainer, Priority.NEVER);
        cellContainer.getChildren().addAll(txtTitle, txtComment, tagContainer);
    }

    @Override
    protected void updateItem(BookmarkModel bookmarkModel, boolean empty) { // (4)
        super.updateItem(bookmarkModel, empty);
        if (!bound) { // (5)
            txtTitle.wrappingWidthProperty().bind(getListView().widthProperty().subtract(25));
            txtComment.wrappingWidthProperty().bind(getListView().widthProperty().subtract(25));
            bound = true;
        }
        if (empty) {
            setText(null);
            setGraphic(null);
        } else { // (6)
            txtTitle.setText(bookmarkModel.titleProperty().get());
            txtComment.setText(bookmarkModel.commentProperty().get());
            tagContainer.getChildren().clear();
            List<String> tags = bookmarkModel.getTagList();
            for (String tagName : tags) {
                tagContainer.getChildren().add(createTagLink(tagName));
            }
            setGraphic(cellContainer);
        }
    }

    private Hyperlink createTagLink(String tagName) {
        Hyperlink hyperlink = new Hyperlink(tagName);
        hyperlink.setTextFill(Color.BLUE);
        return hyperlink;
    }
}

カスタムの Cell は javafx.scene.control.Cell を継承して実装しますが、ListView 向けには javafx.scene.control.ListCell という基底クラスが用意されているので、通常はこれを継承して実装します。 (TableView の場合は javafx.scene.control.TableCell を継承します)
型パラーメータには ListView のデータソースとなるクラスを指定します。今回の場合、先ほど上で作った BookmakrModel を指定することになります。

カスタムの Cell では updateItem() メソッドをオーバーライドします。
このメソッドは初期表示やスクロール時など、セルの表示を更新する必要があるときにコールバックとして呼び出されます。
第1引数には、このセルが表示を担当することになったデータソース (この例では BookmarkModel のインスタンス) が渡されます。第2引数にはこのセルが表示するデータが空であるかが渡されます。(空の場合に true)
第1引数に渡されたデータを使って、セルの表示内容を更新することになります。Cell クラスは Label や Button のスーパークラスである javafx.scene.contorol.Labeled を継承しており、setText() メソッドを使ってテキストを、setGraphic() メソッドを使って任意のグラフィックを表示するために設定することができます。
よって、任意のコンテンツをセルに表示したい場合、表示する Node を作って、この updateItem() メソッド内において、setGraphic() メソッドの引数にその Node を渡せばいいわけです。

ここで1つ注意点があります。それは、updateItem() メソッド内での Node オブジェクトの生成は極力避ける、ということです。
updateItem() メソッドはスクロールを行った場合など、高頻度で呼び出される可能性があります。そのため、このメソッド内で Node オブジェクトの生成を行うと、その負荷はかなりのものとなります。
従って、可能な限り Node オブジェクトの生成は Cell クラスの初期化時に行い、updateItem() メソッドではプロパティやレイアウトの変更にとどめるようにします。

以上を踏まえて今回の実装例について解説します。
まず、Cell クラスのコンストラクタで初期化可能な Node のインスタンスを全て作っています。(1)
実際に Node の生成を行っているのは initComponent() メソッドです。(2)
セルでは上からブックマークタイトル、コメント、そしてタグのリストを並べるのですが、これは VBox を作って配置します。
ブックマークタイトル、コメントの表示には javafx.scene.text.Text を使います。
タグについては BookmarkModel が保持するタグの数に応じて動的に変化するので、これは updateItem() メソッドで生成するようにしていますが、そのタグを水平に並べるための HBox だけをここでは生成して、コンテナとなる VBox に追加しています。(3)

続いて updateItem() メソッドの実装です。(4)
まず、ブックマークタイトルやコメントのテキストがセルの幅に合わせて折り返されるように、親となる ListView の width プロパティと、それぞれの wrappingWidth プロパティをバインドしています。(5)
コンストラクタの中で行わず、最初に updateItem() メソッドが呼び出されたときにこのバインドを実行している理由は、コンストラクタが呼び出される時点ではまだセルが配置される ListView と結びつけられていないからです。
そして、第2引数が false の場合に、引数として渡される BookMarkModel オブジェクトから値を取り出して、Text や HyperLink オブジェクトに設定しています。(6)
今回は少し手を抜いて、updateItem() メソッドの中で HyperLink を生成していますが、さらに性能を重視するのなら、コンストラクタである程度の数のインスタンスを作ってキャッシュしておいた方が良いでしょうね。

それでは、このカスタムのセルを使ってみることにします。まずは FXML から。

<BorderPane id="BorderPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0" prefWidth="460.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="aoetk.SampleListViewController">
  <center>
    <ListView fx:id="listView" prefHeight="200.0" prefWidth="200.0" />
  </center>
</BorderPane>

BorderPane の真ん中に ListView を貼っているだけです。続いて Controller の実装です。

public class SampleListViewController implements Initializable {
    public ListView<BookmarkModel> listView;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        listView.setCellFactory(new Callback<ListView<BookmarkModel>, ListCell<BookmarkModel>>() { // (1)
            @Override
            public ListCell<BookmarkModel> call(ListView<BookmarkModel> listView) {
                return new BookmarkCell();
            }
        });
        ObservableList<BookmarkModel> bookmarkModels = createBookmarkModels();
        listView.setItems(bookmarkModels); // (2)
    }

    private ObservableList<BookmarkModel> createBookmarkModels() { // (3)
        final String longComment = "長いコメントのサンプルです。長いコメントのサンプルです。長いコメントのサンプルです。"
                + "長いコメントのサンプルです。長いコメントのサンプルです。長いコメントのサンプルです。長いコメントのサンプルです。"
                + "長いコメントのサンプルです。長いコメントのサンプルです。長いコメントのサンプルです。長いコメントのサンプルです。";
        final String normalComment = "普通の長さのコメント。";
        ObservableList<BookmarkModel> bookmarkModels = FXCollections.observableArrayList();
        for (int i = 0; i < 500; i++) {
            String comment = "";
            if (i % 3 == 0) {
                comment = longComment;
            } else if (i % 3 == 1) {
                comment = normalComment;
            }
            BookmarkModel model = new BookmarkModel(
                    "ブックマークタイトル" + i, comment, Arrays.asList("tag1", "tag2", "tag3"));
            bookmarkModels.add(model);
        }
        return bookmarkModels;
    }
}

initialize() メソッドで ListView にセルの設定を行っています。ListView#setCellFactory() メソッドを使い、セルを生成する CellFactory を設定しています。(1)
CellFactory は、javafx.util.Callback インターフェースを実装したクラスとなります。call() メソッドで、使いたい Cell クラスのインスタンスを返すようにします。
後はデータの設定です。createBookmarkModels() メソッドにて、500個の BookmarkModel のインスタンスを生成して (3) 、そのリストを ListView のアイテムとして設定しています。(2)

これを実行すると次のようにカスタマイズしたセルを使った ListView が表示されます。

いかがでしたでしょうか。ちょっと回りくどいと感じる方もいるかもしれません。ですが、このような作りになっていることでモデルとビューが綺麗に分離できますし、リストの項目数が多くても描画性能へ与える影響を小さくすることができるというメリットがあります。
また上でも述べたように、JavaFX に限らず、GUI アプリケーションではリスト系コンポーネントの表示をカスタマイズしようとしたら、大体これと同じような方法を採ります。なので他の GUI プラットフォームで開発するときにも同じ考え方が通用するので、是非この機会に覚えてもらえたらと。

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

追記

ソースコード全体は gist にアップしています。ご参照ください。
https://gist.github.com/aoetk/7827455

JavaOne報告会でJavaFXについての発表&LTを行ってきました

10/19 に実施されたJavaOne 2013 サンフランシスコ報告会 Tokyoにて、JavaFXのアップデートについての発表とLTを行ってきました。

当日のJavaFXアップデートの資料は以下の通りです。

JavaOne2013報告会 JavaFX Update from Takashi Aoe

JavaOne初参加の身でありながら、例年は櫻庭さんが担当されているポジションを引き継ぐことになったので、とても緊張しました。
自分の前に寺田さん、大山さん、櫻庭さんが発表がありました。3人の発表で場が大盛り上がりした状態で自分の発表に入ったのですが、自分の発表になると雰囲気がしーんとした感じになってしまったので、「うわ、これは失敗かなあ...」とショックを受けたのですが、後から聞いた話だと結構皆さんそれなりに楽しんで聞いて頂けたようで、ほっとしています。
そうは言ってもこのような大きな場で長丁場の発表をするのにはまだまだ修行が必要だと言うことがよーく分かりました。精進します。

本編のJavaFXアップデートに続いて、LTでもお話しをしてきました。こちらはJavaOneではちょっと珍しい感じのHadoopのセッションについてです。
JavaFXの方は真面目な感じで行ったので、こちらはちょっとくだけた感じにしました。
発表資料は次の通りです。

JavaOne2013報告会 LT資料 Hadoopの話を聞いてきた from Takashi Aoe

今のところSlideShareでのページビューはLTの方がちょっと上ですね。何だかんだ言ってHadoopは注目ネタですかね。

さて、ブログの方のJavaOne報告もまだ半分ほど残っていますね。こちらも早めにアップします。(^^;;

意外に優秀なJavaFX WebViewのHTML5フォーム対応

久々のポストです。(そういやFxGlassFishMonitorの解説も1回書いただけでさぼってるなー。こちらも早くまとめねば)

今、仕事で完全に社内向けの管理用Webアプリケーションを作っているのですが、どうせ完全に内部向けなら古いブラウザなんか気にせずHTML5で追加されたフォームをばんばん使っちゃえ♪ って感じで作っています。
で、ふと、「JavaFXのWebViewはWebKitベースなんだけど対応状況はどうなんだろう?」と思って調べてみたら意外に対応状況が優秀だったことがわかりました。さすがにChromeには負けますが、Safariよりは対応が進んでいたりします。と言うわけでその点についてまとめてみます。

以下のコードを表示してみることにします。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>HTML5フォームテスト</title>
    <style>
:invalid {
  border-color: #e88;
  -webkit-box-shadow: 0 0 5px rgba(255, 0, 0, .8);
  -moz-box-shadow: 0 0 5px rbba(255, 0, 0, .8);
  -o-box-shadow: 0 0 5px rbba(255, 0, 0, .8);
  -ms-box-shadow: 0 0 5px rbba(255, 0, 0, .8);
  box-shadow:0 0 5px rgba(255, 0, 0, .8);
}
    </style>
  </head>
  <body>
    <form action="" method="GET">
      <p>
        number: <input type="number" min="0" max="100" name="number">
      </p>
      <p>
        date: <input type="date" name="date">
      </p>
      <p>
        search: <input type="search" name="search">
      </p>
      <p>
        url: <input type="url" name="url">
      </p>
      <p>
        email: <input type="email" name="email">
      </p>
      <p>
        month: <input type="month" name="month">
      </p>
      <p>
        range: <input type="range" min="0" max="100" name="range">
      </p>
      <p>
        color: <input type="color" name="color">
      <p>
      <p>
        meter: <meter value="60" min="0" max="100"></meter>
      </p>
      <p>
        progress: <progress max="100">進行中だよ</progress>
      </p>
      <input type="submit" value="submit"name="submit">
    </form>
  </body>
</html>

HTML5で追加されたフォームのうち代表的なものを記述しています。invalid 疑似クラスを指定しており、指定したフォームに対して妥当でない値が入力された場合にはボーダーの色を変更するようにしています。ブラウザが対応していれば、妥当でない値を入れると色が変わるようになります。

まず、これをChrome (バージョン 28.0.1500.71) で表示すると次のようになります。

今回指定した全てのフォームに対応しています。number や date、month、range、color に対しては入力補助のコンポーネントが表示されており、url や email に対しては、妥当でない値を入れるとCSSでの指定通りにボーダーが赤くなっていることが分かります。 にもちゃんと対応していますね。
date については次のようなカレンダーピッカーを出すことができます。

Firefox (22.0) では次のように、url、email、meter、progress には対応していますが、number、date、month、range、color には対応しておらず、入力補助コンポーネントも出ません。

Safari (6.0.5) についてもMac版は意外と対応が遅れており、date、month、color が対応していません。iOS版はもう少し対応が進んでおり、date や month ではあのドラム型の入力コンポーネントが出てきます。

Opera (12.15 *1 ) は一番対応が早かったブラウザで、今回指定した全てのフォームに対応しています。

では、JavaFXではどうでしょうか。バージョン2.2のWebViewを使って表示した結果は次の通りです。

何と、search と color 以外は全て対応しているのです!number や date、month は一見対応していないように見えますが、実はフォーカスしてカーソルキーを上下すると数値や日付の値を入力できるようになっています。(画像では分からないですが)

で、この件についてTwitterでさくらばさんにさらに次のような情報をもらいました。

と言うわけでJDK8 Early Access (1.8.0-ea-b99) をダウンロードして、JavaFX8のWebViewでも見てみることにしました。
結果は次の通りです。

何とdate、month、colorでピッカーが用意されるようになりました!でも、このバージョン (8 Build b99) ではバグがあるのかクリックしても何も反応しませんでした。(´;ω;`)ブワッ
まあ、正式版が出るまでには対応されるでしょうw

ちなみにJavaFX8では DatePicker コンポーネントが追加されています。こんな感じです。

バグが直るときっとWebViewでもこんなピッカーが表示されるようになるのでしょう。

というわけでJavaFX WebViewのHTML5フォーム対応状況についてでした。

おまけ

今回、JDK8でJavaFXアプリを作るためにNetBeans7.4betaをダウンロードして使ってみたのですが、NetBeans7.4ではMavenベースのJavaFXプロジェクトを作れるようになっていました。これはうれしい!

(追記)
さすがにIEの結果がないのはちょっとあれだと思ったので、VMを立ち上げてWin8上のIE10での結果も撮って追加しました。

IE10は number、url、email、range、progress に対応していました。ただし number は妥当性検証のみで入力補助はありません。range の表示がWin8らしいですね。

*1:Blinkになる前のバージョンです

JavaFX2.2でダイアログを作る方法

はじめに

TwitterJavaFX関連の話になったときにしばしば見かけるのが、「ダイアログ表示できないの?」というコメントです。
JavaFXにはOSレベルでのウィンドウを作成するための Stage クラスがあり (Swingの JFrame 相当のクラスです) 、これを使えばダイアログの作成は可能です。
でも、Swingの JOptionPane に相当するユーティリティクラスは残念ながらありません。
次のバージョンに当たるJavaFX8ではSandboxプロジェクトで開発されていたのですが、いつの間にやら ControlsFX というオープンソースプロジェクトに独立していました。これもJavaFX8にならないと使えません。

と言うわけでJavaFX2ではダイアログを表示するためには自分で実装する必要があるのですが、一度やり方を覚えてしまえばまあそんなに面倒でもないです。
また、現在ベータ版の Scene Builder1.1 ではダイアログを作成するためのひな形が用意されていたりします。
ここではScene Builderのひな形を使ってダイアログを作成する方法についてまとめてみます。

今回作ってみるダイアログ

今回は懐かしのSwingSet2にあったダイアログを作ってみましょう。以下に示すものです。

Yesと答えると「コンピュータの前にいないで外へ出ろ」と説教されるやつですw
YesとNoについてはそれぞれ新たなメッセージダイアログが次に開き、Cancelが選ばれたら何も表示せずにそのまま閉じます。
Yesがデフォルトボタンに設定されていて、キーボードでEnterを押すとYesが選択されます。また、Escを押すとキャンセル扱いになり、そのまま閉じます。

Scene Builderでダイアログのデザインを作る

ではJavaFXでこれを作ってみましょう。
先にも述べたように、Scene Builder1.1 にはダイアログのひな形が用意されています*1。メニューから [ファイル] - [テンプレートから新規作成] - [アラートダイアログ] を選択します。

すると次のようにダイアログのひな形になるFXMLが生成されます。

ちなみにメニューのスクリーンショットを見てお気づきになったかと思いますが、CSSやリソースバンドルを一緒に生成してくれるメニューもあります。
これをベースに自分の好みに合わせてちょいちょいといじっていけばいいわけです。今回は次のように仕上げました。

具体的に行った変更点は次の通りです。

  • ImageView部分に60×60の画像を設定。
    • 今回はCacooを使って自作しました。
  • 文章だけを表示したいので、太字のタイトルラベルを削除。
  • Yesボタンをデフォルトボタンに設定。
    • プロパティの [Default Button] にチェックを入れます。こうすると、Enterキーを押されたときに選択されるボタンに設定されます。
  • Cancelボタンをキャンセルボタンに設定。
    • プロパティの [Cancel Button] にチェックを入れます。こうすると、Escキーを押されたときに選択されるボタンに設定されます。

FXMLは次のようになりました。

<GridPane hgap="14.0" maxHeight="+Infinity" maxWidth="+Infinity" minHeight="-Infinity" minWidth="-Infinity" vgap="20.0" xmlns:fx="http://javafx.com/fxml" fx:controller="aoetk.dialogsample.ConfirmDialogController">
  <children>
    <ImageView fitHeight="60.0" fitWidth="60.0" pickOnBounds="true" preserveRatio="true" GridPane.columnIndex="0" GridPane.halignment="CENTER" GridPane.rowIndex="0" GridPane.valignment="TOP">
      <image>
        <Image url="@confirm.png" />
        <!-- place holder -->
      </image>
    </ImageView>
    <VBox alignment="CENTER_LEFT" maxHeight="+Infinity" maxWidth="+Infinity" minHeight="-Infinity" prefWidth="400.0" spacing="7.0" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.valignment="CENTER">
      <children>
        <Label fx:id="detailsLabel" text="message" textAlignment="LEFT" wrapText="true">
          <font>
            <Font name="HGPGothicE" size="13.0" />
          </font>
        </Label>
      </children>
    </VBox>
    <HBox maxHeight="-Infinity" maxWidth="+Infinity" minHeight="-Infinity" minWidth="-Infinity" prefWidth="300.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
      <children>
        <HBox id="HBox" alignment="CENTER">
          <children>
            <Button id="btnCancel" cancelButton="true" mnemonicParsing="false" onAction="#handleBtnCancelAction" text="Cancel" HBox.hgrow="NEVER">
              <HBox.margin>
                <Insets right="14.0" />
              </HBox.margin>
            </Button>
          </children>
          <HBox.margin>
            <Insets />
          </HBox.margin>
        </HBox>
        <Pane maxWidth="+Infinity" HBox.hgrow="ALWAYS" />
        <Button id="btnNo" cancelButton="false" minWidth="80.0" mnemonicParsing="false" onAction="#handleBtnNoAction" text="No" HBox.hgrow="NEVER">
          <HBox.margin>
            <Insets />
          </HBox.margin>
        </Button>
        <HBox id="HBox" alignment="CENTER">
          <children>
            <Button id="btnYes" defaultButton="true" minWidth="80.0" mnemonicParsing="false" onAction="#handleBtnYesAction" text="Yes" HBox.hgrow="NEVER">
              <HBox.margin>
                <Insets left="14.0" />
              </HBox.margin>
            </Button>
          </children>
        </HBox>
      </children>
    </HBox>
  </children>
  <columnConstraints>
    <ColumnConstraints hgrow="NEVER" maxWidth="-Infinity" minWidth="-Infinity" />
    <ColumnConstraints halignment="CENTER" hgrow="ALWAYS" maxWidth="+Infinity" minWidth="-Infinity" />
  </columnConstraints>
  <padding>
    <Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
  </padding>
  <rowConstraints>
    <RowConstraints maxHeight="+Infinity" minHeight="-Infinity" valignment="CENTER" vgrow="ALWAYS" />
    <RowConstraints maxHeight="-Infinity" minHeight="-Infinity" vgrow="NEVER" />
  </rowConstraints>
</GridPane>

同じようにして、メッセージダイアログのデザインも作成します。

Javaコード側の実装

さて、お次はJavaコード側の実装です。まずは先ほど作ったダイアログのFXMLに対してコントローラーを作ります。

public class ConfirmDialogController implements Initializable {
    @FXML
    private Label detailsLabel;
    private DialogOption selectedOption = DialogOption.CANCEL;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }

    public DialogOption getSelectedOption() {
        return selectedOption;
    }

    public void setMessage(String msg) {
        detailsLabel.setText(msg);
    }

    @FXML
    void handleBtnYesAction(ActionEvent event) {
        handleCloseAction(DialogOption.YES);
    }

    @FXML
    void handleBtnNoAction(ActionEvent event) {
        handleCloseAction(DialogOption.NO);
    }

    @FXML
    void handleBtnCancelAction(ActionEvent event) {
        handleCloseAction(DialogOption.CANCEL);
    }

    private void handleCloseAction(DialogOption selectedOption) {
        this.selectedOption = selectedOption;
        getWindow().hide();
    }

    private Window getWindow() {
        return detailsLabel.getScene().getWindow();
    }
}

注目するのは各ボタンに割り当てられたハンドラメソッド (handleBtnYesAction、handleBtnNoAction、handleCancelYesAction) です。
選択結果を自身のプロパティに保存した後に、ダイアログ上のコンポーネントから Scene 、Window をたどって、自身のウィンドウを隠しています (handleCloseAction メソッド) 。
また、ダイアログに表示するメッセージはコントローラーが受け取るようにしています。
メッセージダイアログのコントローラーについても大体これと同じような実装になっています。

続いてダイアログを開く側のコードです。

    @FXML
    private void handleButtonAction(ActionEvent event) {
        try {
            // 確認ダイアログの表示
            FXMLLoader loader = new FXMLLoader(getClass().getResource("ConfirmDialog.fxml"));
            loader.load();
            Parent root = loader.getRoot();
            ConfirmDialogController controller = loader.getController();
            controller.setMessage("今日の外の天気は晴れですか?");
            Scene scene = new Scene(root);
            Stage confirmDialog = new Stage(StageStyle.UTILITY);
            confirmDialog.setScene(scene);
            confirmDialog.initOwner(button.getScene().getWindow());
            confirmDialog.initModality(Modality.WINDOW_MODAL);
            confirmDialog.setResizable(false);
            confirmDialog.setTitle("Select an Option");
            confirmDialog.showAndWait(); // ダイアログが閉じるまでブロックされる

            // 確認ダイアログの選択結果に応じたメッセージダイアログの表示
            switch (controller.getSelectedOption()) {
            case YES:
                showMessageDialog("コンピュータで遊んでないで外に出よう。\nビーチに行って太陽の日を浴びたらどうでしょう。");
                break;
            case NO:
                showMessageDialog("屋内にいて様々なものから保護されているのはいいことです。");
                break;
            }

        } catch (IOException ex) {
            Logger.getLogger(DialogSampleViewController.class.getName()).
                    log(Level.SEVERE, "読み込み失敗", ex);
        }
    }

FXMLLoader を使って、確認ダイアログのFXMLを読み込んでいます。
このコード例で示しているように、FXMLLoader からコントローラーのインスタンスを取得することが可能で、表示したいメッセージを渡していますね。
注目してもらいたいのは Stage のインスタンスを生成している部分です。

            Stage confirmDialog = new Stage(StageStyle.UTILITY);
            confirmDialog.setScene(scene);
            confirmDialog.initOwner(button.getScene().getWindow());
            confirmDialog.initModality(Modality.WINDOW_MODAL);
            confirmDialog.setResizable(false);
            confirmDialog.setTitle("Select an Option");
            confirmDialog.showAndWait(); // ダイアログが閉じるまでブロックされる

StageStyle.UTILITY を渡してインスタンスを生成していますが、これはウィンドウの装飾を最小限にするというオプションです。つまり、ウィンドウを閉じるボタンしか表示されません。*2
また、initOwner メソッドを使って、親ウィンドウのインスタンスを渡しておき、さらに initModality メソッドModality.WINDOW_MODAL を渡す事で、親ウィンドウに対してモーダルになるように設定しています。
そして、Stage#showAndWait メソッドを使ってダイアログウィンドウを表示しますが、このメソッドはウィンドウが閉じられるまでスレッドをブロックします。

従って、showAndWait 呼び出しの後はダイアログが閉じられた状態になっているので、次のように確認ダイアログコントローラーが保持している選択肢を取得して、次のメッセージダイアログに表示するメッセージを決めて、ダイアログを表示しています。

            switch (controller.getSelectedOption()) {
            case YES:
                showMessageDialog("コンピュータで遊んでないで外に出よう。\nビーチに行って太陽の日を浴びたらどうでしょう。");
                break;
            case NO:
                showMessageDialog("屋内にいて様々なものから保護されているのはいいことです。");
                break;
            }

showMessageDialog メソッドについてはもう説明するまでもないでしょう。(最後に全コードをアップしたgistのURLを示します)

このコードを実行すると次のように動きます。

とまあこんなところです。一度やり方を覚えてしまえばまあそんなに手間でもないでしょう。たぶん...きっと...。
JavaFX8、Java8に先駆けて先行リリースされないかなあ。

全コードはgistにアップしています。ご参照ください。(画像は自分で用意してくださいね)
https://gist.github.com/aoetk/5652577

*1:Scene Builderで表示されるダイアログもこのひな形を使って作っているようです。

*2:ちなみに StageStyle.UNDECORATED にすると、ウィンドウのボタンが一切表示されなくなります。ダイアログにはこっちの方がいいかも知れません。(Macのダイアログはそのようになっていますね)

FX GlassFish Monitorの解説 (見映え編)

Java The Nightのデモでお見せしたFX GlassFish Monitorの作りについての解説です。まずは反響が大きかった見映えのところから説明したいと思います。

一番目に付いたのはウィンドウ枠だと思います。OSのウィンドウ枠は全く見えず、周囲が何か光っていますね。
JavaFXではコンポーネントにドロップシャドウエフェクトを追加することができ、今回もそれを利用しているのですが、OSのウィンドウ枠に相当する Stage クラスには適用することはできません。

そこで次のような方法で実現しました。

  • Stage 及び Scene は透明にする。
  • その上に、ドロップシャドウエフェクトを効かせた Rectangle を貼り付ける。
    • この Rectangle の大きさは Scene よりシャドウの幅の分だけ小さくする。
  • その上にレイアウトコンテナ (BorderPaneAnchorPane) を貼り付ける。

図示するとこんな感じです。

メインウィンドウ (監視項目のツリーが並ぶウィンドウ) について、Stage の上に Scene を組み立てている部分のJavaコードと、FXML のコードを示しておきます。

    @Override
    public void start(Stage stage) throws Exception {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("view/MainView.fxml"));
        loader.load();
        Parent root = loader.getRoot();
        MainViewController controller = loader.getController();
        controller.setParentStage(stage);
        Scene scene = new Scene(root, 924, 700, Color.TRANSPARENT);
        stage.initStyle(StageStyle.TRANSPARENT);
        stage.setScene(scene);
        stage.show();
    }
<StackPane id="StackPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="700.0" prefWidth="924.0" xmlns:fx="http://javafx.com/fxml" fx:controller="aoetk.fxglassfishmonitor.view.MainViewController">
  <children>
    <Rectangle arcHeight="5.0" arcWidth="5.0" fill="#1e90ff00" height="690.0" stroke="$x1" strokeType="INSIDE" width="914.0">
      <effect>
        <DropShadow blurType="GAUSSIAN" spread="0.7">
          <color>
            <Color blue="0.878" green="1.000" red="0.000" fx:id="x1" />
          </color>
        </DropShadow>
      </effect>
    </Rectangle>
    <BorderPane fx:id="containerPane" onMouseDragged="#handleMouseDragged" onMousePressed="#handleMousePressed" prefHeight="200.0" prefWidth="200.0" styleClass="container">
      <center>
        <ScrollPane prefHeight="200.0" prefWidth="200.0">
          <content>
            <Pane fx:id="drawRegion" prefHeight="200.0" prefWidth="200.0" />
          </content>
        </ScrollPane>
      </center>
      <top>
        <HBox fx:id="boxTitle" prefHeight="50.0" prefWidth="200.0">
          <children>
            <Text fill="$x1" fontSmoothingType="LCD" stroke="WHITE" strokeType="OUTSIDE" strokeWidth="0.0" text="FX GlassFish Monitor" HBox.hgrow="NEVER">
              <font>
                <Font name="System Bold Italic" size="28.0" />
              </font>
            </Text>
            <Region prefHeight="200.0" prefWidth="200.0" HBox.hgrow="ALWAYS" />
            <Button fx:id="btnExit" mnemonicParsing="false" onAction="#handleBtnExitAction" styleClass="close-button" text="Exit" HBox.hgrow="NEVER" />
          </children>
          <padding>
            <Insets bottom="20.0" left="10.0" right="10.0" top="10.0" />
          </padding>
        </HBox>
      </top>
      <StackPane.margin>
        <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
      </StackPane.margin>
    </BorderPane>
  </children>
  <stylesheets>
    <URL value="@../asset/style.css" />
  </stylesheets>
</StackPane>

Rectangle とレイアウトコンテナ (BorderPane) の貼り合わせには StackPaneを使っています。
StackPane にマージンを指定していて、その上に置いたコンテンツがドロップシャドウの幅の分だけ隙間を空けるようにしています。

BorderPane の onMousePressed と onMouseDragged にイベントハンドラを設定しています。これは Stage を透明にしたために、そのままではウィンドウ枠が消えてウィンドウの移動ができなくなるため、自分でドラッグ移動のイベントを実装しているのです。

スタイルシートはこんな感じです。

.root {
    -fx-base: #212020;
}

.container {
    -fx-background-color: rgba(0, 0, 0, 0.7);
}

.close-button {
    -fx-border-color: white;
    -fx-border-width: 2px;
    -fx-border-radius: 5px;
    -fx-background-color: transparent;
    -fx-text-fill: white;
    -fx-font-size: 16px;
    -fx-cursor: hand;
}

container クラスがレイアウトコンテナに適用するもので、色を黒で透明度を 70% に設定しているだけです。
close-button クラスはExitボタンに適用したスタイルで、背景を透明にしてあのような見た目にしているのが分かりますね。

と言うわけで見映えについての解説でした。大して手数を掛けずに実現可能だと言うことが分かりますね。

コード全体についてはもう少し整理してからGithubのURLを公開する予定です。もうしばらくお待ちを。

おまけ

解説に使った図はScene Bulderを使って描きました。が、やっぱりお絵かきにはまだまだ機能不足ですね。Rectangle を平行四辺形に変形する部分は直接FXMLを手打ちしました...。

Java Day Tokyoに参加&発表してきました

5/14 (火) に開催されたJava Day Tokyoに参加し、さらにその中のセッションの1つである、Java The Nightに登壇しました。
まさかこんな大きなイベントで自分が発表する側に立つことになるとは思わず、とても緊張しましたが良い経験になりました。
このエントリではイベントの感想についてまとめたいと思います。

Java The Nightでの発表について

Java The Nightで自分は「監視ツールでみるJavaFXJava EEの魅力」と題して発表しました。セッション資料はSlideShareにアップしています。

Java Day Tokyo 2013 Java the Night 監視ツールでみるJavaFXとJava EEの魅力 from Takashi Aoe

登壇することになった経緯ですが、Oracle寺田さんからTwitterのダイレクトメッセージで突如お願いされましたw
寺田さんからの要望は、「デモの際におもしろ、おかしく、やっていただくことはできますでしょうか。」でした。
随分ハードル高いなあと思いつつも、折角の機会なので登壇させて頂くことになりました。

発表の内容としては、JavaFXで作ったGlassFishの監視アプリケーションのデモを通して、自分がJavaFXJava EEの魅力と思っている点について伝えるというものでした。
インパクト重視という要望だったので、JavaFXで作るアプリは派手めな、中二病っぽい感じの路線で行くことにしました。

JavaFXについてここで伝えたかったことは、プログラムを使って何かやりたいタスクがあったときに、JavaならばJavaFXが加わったことで、このように綺麗なGUIで成果をアウトプットできるようになるということです。
CUIもいいけど、GUIでやると楽しいですよ。
このアプリケーションのルックスについては予想以上に反響があったようですが、実はそんなに手数を掛けなくてもこのような見た目にすることが可能です。この点については後ほど別エントリで解説します。

Java EEについては、運用フェーズにおける監視という側面からの利点を伝えようとしました。Java EE APサーバーはいずれもサーバー上で稼働するサービス、アプリケーションに対して豊富な監視オプションを提供しています。
Webアプリケーションフレームワークはともすると開発の側面ばかりに目が行きがちですが、ソフトウェアのライフサイクル全体で考えると運用も重要です。その点も考慮して選択を考えてもらったらと思います。

各セッションについての感想

それでは自分が参加した各セッションについて軽く感想を書いていきます。

基調講演

基調講演ではJava SE、Java FX/Embedded、Java EE、コミュニティについて、それぞれのOracleのキーパーソンから現状と今後の展望について説明するというものでした。
特に自分の印象に残ったのが、OracleがM2Mを重視しているように見えたことです。今後は様々なデバイスがネットワークでつながるようになるため、確かに次にソフトウェア開発の分野でホットになるのはここかも知れません。そして恐らくこの分野でもライバルになるのはAndroidでしょうね...。

Raspberry Pi NightHacking

Pro JavaFX 2の著者の一人であるStephen Chinさんによる、Raspberry Pi上でのJavaFXアプリケーションの実行についてのセッションでした。
目の前でJavaFXアプリケーションを構築して、それをRaspberry Piに移植動かすところまでを見せてもらいました。
驚いたのは、本当にそのままのJavaFXアプリケーションが動いているということです。エフェクトとかも普通に動いていました。質問したところ、動かないのはWebViewとMediaViewだけとのことです。
Raspberry Pi向けのJava SEのプロファイルは結構小さいのですが、それでもJavaFXアプリケーションがここまで動くというのには色んな可能性を感じさせてもらいました。

Java IDE の最新トレンド

EclipseNetBeansIntelliJ IDEAというJava界三大IDEについて、Eclipse派代表として竹添さん、NetBeans派代表としてきしださん、IntelliJ派代表として今井さんが語り合うというとても楽しいセッションです。
モデレータの山本裕介さんがIntelliJユーザーと言うこともあり、ややIntelliJにバイアスが掛かっていたような気がしましたが。(^^;;
スライドが三者三様でこちらもそれぞれの個性が出ていて面白かったです。竹添さんはきっちりPowerPointで、今井さんはrstテキストで、そしてきしださんは安定の手書き資料w
なお、自分は基本的にNetBeans派ですが、IntelliJもちょこちょこ触りますし *1 、もちろん仕事の必要上Eclipseも結構触ります。
ディスカッションで自分の印象に残ったのは以下の点です。

  • Eclispeは確かにセッティングが面倒だけど、Eclipseが出たての頃はみんなその作業をとても楽しんでいた。
    • 確かに自分もEclipseが登場したての頃は、とてもわくわくしながら拡張を楽しんでいたのを覚えています。
  • InteillJはJava、Groovy、Scalaを1つのプロジェクトに混在させて開発できる。
  • NetBeansはJenkins連携ができる。ただしメニュー名は "Hudson" だけどw
  • (テキストエディタ派の人に向けて) IDEの利点は「IDEが良い書き方を教えてくれる」ところにありますよ。
Groovy, Clojure, Scala, VisageでのJavaFX活用

こちらもStephen Chinさんによるセッションです。Groovy、ClojureScalaVisage *2JRubyでのJavaFXのサポート状況について順に説明してもらいました。
ちなみにChinさんはScalaFXVisageのコミッタをされています。
JavaFXのサンプルアプリケーションとしてよく使われる、Vanishing Circleを例に、他の言語で実装した例を見せてもらいました。

特に自分がいいなと思ったのがGroovyFXです。@FXBindableアノテーションを付けることで、JavaFX形式のプロパティを生成してくれるのはとても嬉しい!
自分の以前のエントリでも解説しましたが、JavaFX形式のプロパティの記述はちょっと面倒なのです。
ScalaFXもとてもScalaらしいアプローチでDSLを設計していて、こちらも好感が持てました。
VisageJavaFX Script時代から言語仕様が強化されているのですね。

なお、Chinさんは日本の漫画やアニメが相当好きなようで、各言語についてアニメに例えてらっしゃいました。以下の通りですが自分のよく知らないモノもあるw

タブレット用の JavaFX アプリケーション開発

JavaFXエヴァンジェリストであり、Pro JavaFX2の著者の一人である、Jim Weaverさん *3 による、JavaFXのマルチタッチ関連APIの解説と、タブレット向けアプリケーションを作る上での注意点について説明するというものでした。
タブレットのターゲットはWindows8でした。実際にSurface Proを操作しながら解説していました。

マルチタッチ関連APIの解説の内容は自分が先日JavaFX勉強会で行ったものとほぼ同内容でしたw
タブレット向けアプリケーションを開発する上での注意点は、やはりコントロールの大きさでした。そのためにデザインにはなるべくCSSを活用し、まずはrootクラスのフォントを32px程度にするのが良いとのことでした。

結構説明が早く終わってしまったので、質問がいっぱい出ていました。自分は高解像度端末におけるスケーリングの対応について聞いたのですが、現時点では端末に合わせて自分でサイズを調整するしかないようです。でも、後で気付いたのですが、確かJavaFX8で導入されるModenaテーマはスケーリングの変化に対応していたような?

Java The Night

最後は自分も登壇したJava The Nightです。みなさん実に色んな持ちネタを披露してもらって、とても楽しめました。
驚いたのはほとんどの人がJavaFXを使っていたことです!自分が考えている以上にJavaFXJava開発者の間で認知度を上げているのかも知れません。

最後に寺田さんから発表されたJava7のAPIドキュメント日本語化のニュースは驚きました。Oracle本体を説得して日本語化にこぎつけた日本Oracleの皆様方には感謝です!


このように、聴講、発表共にとても楽しい一日を過ごすことができました。このような素晴らしい場を提供して頂いた日本Oracleの皆様には厚く御礼申し上げます。ありがとうございました。

*1:特にJavaFXサポートが入ってからは、JavaFXサポート機能がなかなか強力なので結構触るようになりました。

*2:OracleディスコンにしてしまったJavaFX Scriptオープンソース化したものです

*3:ちなみに先日のJJUG CCCでは櫻庭さんのお誘いで、Jimさんとお昼ご飯を食べに行きました。