JavaFXで丸型ボタンを作ってみる (FXML + CSS + ベクター画像縛り)

はじめに

VAIO Tap 11 を購入したこともあり、Windows8 をよく使うようになったわけですが、Windows8 の Store App を使っていると、次のようなタッチでの利用を意識した丸型で大きめのボタンをよく見かけるようになりました。

で、これと同じようなものを JavaFX でも作ってみようとしました。その際に次のような条件を課すことにしました。

  • FXML と CSS の範囲だけで作る。
    • Java コード側にデザインに関するコードを含めないようにする。
  • アイコンにはビットマップではなくベクターの画像を使う。
    • スケールできるようにしたい。

以下、手順について順に示していきます。

ボタンの作成

まず、ボタンの外形については CSS を使えば簡単に円形にすることができます。
またアイコンについては、JavaFX の Button クラスには graphic プロパティというものがあり、このプロパティに任意の Node を設定することでボタンに画像を表示できるようになります。

というわけでまずは CSS の準備です。ボタンに適用するクラスを作成します。

.circle-button {
    -fx-pref-width: 4.0em;
    -fx-pref-height: 4.0em;
    -fx-background-radius: 2.0em;
    -fx-background-color: null;
    -fx-border-radius: 2.0em;
    -fx-border-color: black;
    -fx-border-width: 2.0px;
}

ポイントは次の通り。

  • background (背景) と border (境界) の両方を調整する必要があります。
  • ボタンの幅、高さと、background、border の角の丸みを調整する radius プロパティを揃えることで円形にします。

さらにマウスオーバー、クリック、無効化の状態に対応するため、hover、pressed、disabled 疑似クラスのスタイルも設定します。

.circle-button:disabled {
    -fx-border-color: rgb(0, 0, 0, 0.7);
}
.circle-button:hover {
    -fx-background-color: #dcdcdc;
}
.circle-button:pressed {
    -fx-background-color: black;
    -fx-border-color: white;
}

無効化の場合は半透明にし、マウスオーバーの場合はうっすらと背景に色をつけるようにしています。クリックされた時には色を反転させています。

このクラスをボタンに適用することで次のような見た目のボタンになります。

アイコンの作成

続いてアイコンに使うベクター画像についてです。JavaFXjavafx.scene.shape パッケージにある各種クラスを利用することでベクター画像を作ることができます。

ですが、凝った画像を Java コードや FXML コードでごりごり書きたくないですよね。 *1
できれば IllustratorInkscape などのベクターグラフィックツールで作成した画像を使ったり、あるいは ネット上でフリーで配布されている素材とかを使いたいところです。

実は JavaFXSVG 形式のパスを取り扱うことができます。先ほど言及した javafx.scene.shape パッケージには SVGPath というクラスがあります。
また、CSS でも Region クラス (Control や Pane などのスーパークラス) には -fx-shape というプロパティがあり、SVG のパス文字列を指定することができます。

以上を踏まえて Scene Builder を使ってベクター形式のアイコン画像を準備する手順について示します。

まず、使いたい SVG 画像をテキストエディタで開き、そのパス文字列をコピーします。

Scene Builder で SVGPath オブジェクトを追加し、その Content プロパティに先ほどコピーしたパス文字列を貼り付けます。すると表示がそのアイコンに変わるはずです。

ボタンにアイコンを設定する

それではボタンに対して準備した画像を設定します。
FXML 上で Button オブジェクトの graphic プロパティに対して先ほど用意した SVGPath オブジェクトを次のように設定すれば OK です。

<Button layoutX="162.0" layoutY="51.0" mnemonicParsing="false" styleClass="circle-button">
   <graphic>
      <SVGPath content="M10,16 10,0 0,8z" styleClass="button-icon-shape" />
   </graphic>
</Button>

これを Scene Builder 上で簡単に行う方法があります。次のように、Hierarchy ビューで Button に対して Shape オブジェクト (ここでは SVGPath オブジェクト) をドラッグアンドドロップすることができ、こうすると、Buttton の graphic プロパティにその Shape オブジェクトが設定されるようになります。

ちなみに CSS では Button クラス (正確には基底クラスの Labeled クラス) に -fx-graphic というプロパティがありますが、このプロパティには URI しか指定できないため、ビットマップ画像のパスを指定するしかありません。

イコン画像もマウスクリックに反応させて色を変えるようにする

これで大体できあがりなのですが、先ほどボタンの疑似クラス設定で、マウスクリック時に色を反転させるように設定したことを覚えているでしょうか?
イコン画像についてもそれに追随させて色を反転させることを考えます。

ここは CSS の機能を利用して実現します。次のように子孫セレクタを使って、クリックされたボタンの子孫に当たる SVGPath に対して、色を反転させる設定が適用されるようにしています。

.circle-button:pressed SVGPath {
    -fx-fill: white;
}

できあがりの例

こんな感じで JavaFXWindows Store App にあるような丸型ボタンを作ることができました。
以下に完成例のスクリーンショットを載せておきます (アイコンの画像は このサイト で配布されているものを使わせてもらいました) 。

この作成例のソースコードは gist にアップしてあります。
https://gist.github.com/aoetk/b5b9a03e1033057224aa

*1:Scene Builder がもうちょっとお絵かきツールとしての機能も強化してくれるといいのですが...。

きしださんの文字列連結のやつをCharBufferでやってみる

多分色んな人が既にやっていて何番煎じになっているか分かりませんが、きしだ (id:nowokay / @) さんの「StringBuilderを使ったクソコードはどこまで遅いか 」「Java8時代の文字列連結まとめ」について自分もちょっと遊んでみました。

というのも今日の昼飯時に社内でもこの話が話題になって、そこで弊社の某モヒカンより「性能面でセンシティブな場面で String を使うことを考えるな。CharBuffer 使え。」との言葉を賜りましたからです。
と言うわけで以下のコードを試してみました。

    public static String charBufferJoin() {
        CharBuffer buffer = CharBuffer.allocate(7995);
        buffer.put('[');
        for (int i = 0; i < strarray.length; ++i) {
            if (i != 0) {
                buffer.put(',').put('[');
            }
            buffer.put(strarray[i]).put(']');
        }
        buffer.flip();
        return buffer.toString();
    }

結果は、

charBufferJoin:1261ms
stringJoin:2800ms
stringJoiner:2135ms
streamListJoin3:2362ms
streamListParallelJoin3:3677ms
stringBuilderJoin:1870ms
stringBuilderJoinMem:1652ms
stringBuilderFuckingJoin:2480ms

確かに速いっぽい。 *1 前述の某モヒカンのお言葉は正しかったようです。

おしまい。

*1:あと、ぼくのマシン、きしださんのマシンよりかなり遅いっぽい...。

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 の世界でも高解像度端末が増えてきていますし、早く対処してもらいたいところです。

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

VAIO Tap 11 購入レポート

先週、SONYタブレット PC である VAIO Tap 11 を買っちゃいました。
VAIO を購入するのは実に 13 年振りです (Windows マシンを購入するのも 11 年振りくらい) 。
1 週間使ってみての感想とかをまとめてみました。

購入のきっかけ

今家にある情報端末は次のような感じです。

はい、Apple 製品ばっかりです。母艦の PC が MBP なのですが、デスクトップ代わりに購入したので、持ち運びにはちょっと辛いです。
外に PC を持ち運ぶ際には仕方なくこれを外に持ち出すのですが、会社で支給された MBA の携帯性を体験すると辛くなってしまいました。
iPad を使うという手もあるのですが、これはよく言われているようにコンテンツの消化には最高なのですが、これで何かを書いたり作ったりするのはちょっと厳しいです。
外付けキーボードを付けても、キーの入力に本体の処理速度が追い付かないなど、やはり PC の使用感にはほど遠いものでした。第一、iPad ではプログラミングができない!

そこで登場した Windows8。PC とタブレットの融合を狙ったあのインターフェースなら、もしかして MBAiPad の役割をこれ 1 台で満たすことができるのではないか?の期待を持ったわけです。
次のような条件を満たすマシンが出たら買いたいな、と思ったのですが、Windows8 リリース直後では中々これに適合するデバイスが出てきませんでした。

  • タブレットとキーボードが分離可能であること。
  • タブレット部が軽量であること。
    • 最低限 1kg は切っていて欲しい。
  • WindowsRT ではなく Windows8 であること。
    • これまでの Windows アプリケーションが動かせないなんてあり得ない!
    • というか RT だったら開発できない。
  • 解像度はフル HD 以上。

最近になってようやくこの条件を満たせそうなマシンが出てくるようになり、最終的に Surface Pro 2 とこの VAIO Tap 11 が候補に残りました。
そうこうしているうちに Surface Pro 2 は生産が追い付かなくなって受注停止、一方 VAIO の方は値引きキャンペーンを開始したので、やや衝動的にぽちってしまいましたw *1

次のようなスペックで購入しました。

  • ホワイトモデル
  • Core i3
  • SSD 256GB
  • メモリー 4GB

外観

外観はこんな感じです。ディスプレイサイズが 11 インチなので、タブレットとしてはやはり大きく感じます。手にした重さについては大きさの割に軽く感じられます。

背面はこんな感じ。白は高級感があっていいです。スタンドは Surface と違い、角度が無段階になっています。

タブレット本体は薄いのですが、ファンはあります。この辺は PC っぽさがあります。

付属のキーボードです。Surface と違い、完全に分離しており、無線で本体につながります。非常にしっかりしたキーボードです。ただ、トラックパッドが...。

キーボードはこのように本体にふたとしてぺたっと貼り付けられます。というか、キーボードはこのようにして本体に装着しないと充電できません。
下に見えているのはスタイラスで、これもしっかりした作りで使い易いです (電池が必要ですが) 。

iPad 4th gen と重ねてみました。iPad をさらに「長く」したような感じですね。

使ってみた感想

ハードウェア面

ディスプレイはとてもクリアです。ここはさすが SONY と言ったところ。
解像度はフル HD (1920 * 1080) 。200ppi 弱になり、さすがに Retina Mac には少し及びませんが、文字はとてもくっきりです。ある程度目を近づけないとドットが目立ちません。
11 インチでこの高解像度であるため、そのままでは全てが小さく表示されてしまうので、デフォルトで 125% の DPI スケーリング (120dpi) に設定されていました。 *2
ただ、このスケーリングでも字が小さいと感じる人がいそうだと思いました。人によってはもう一段階スケールを上げた方が良いかもしれません。

先述したようにキーボードの打ち味はとても良いです。下手なノート PC よりよっぽど良かったりします。分離式なので好きな場所にキーボードを置けるというのもいいです。
ただ、トラックパッドの操作性がひどい。 カーソルが思ったように動かないし、かと思うとタイピングしている最中に手のひらが触れてしまったら反応して事故が起きたり...。
トラックパッドを殺すことのできるハードスイッチがあったので、タイピングしているときはこのスイッチを入れるようにした方が良さそうです。
別途マウスを調達した方がいいかなあと思いました。特にデスクトップモードで作業しているときはタッチだけでマウスの代用をすることは不可能なので。

タブレットとしての使い勝手ですが、タッチパネルは iPad と比べるとちょっと精度が落ちるかなあと感じるところがありました。たまにタッチしても反応しなかったりすることが。やはり Apple はこの辺りをとても良くチューニングしています。
とは言え、Windows8 自体の操作性がとてもいいので、基本的にタッチパネルでの操作感は良好です。
大きさの割には軽い (780g) ため、持ち運びやすい点もグッドです。
ただ、横持ちで使っているときはいいのですが、アスペクト比が 16:9 であるため、縦持ちにするととても細長くなって、ちょっと気持ち悪いです。
Web や横書きの文書を閲覧するときは縦持ちの方がいいのですが、これではちょっと違和感が大きいですね。タブレットとして使うのならば iPadアスペクト比が一番丁度良いと思います。

ソフトウェア面

最初に面食らったのが、インストールされている OS が 8.1 ではなく、無印の Windows8 だった点です。
そのためいきなり OS のインストール作業をする羽目に...。
Windows8 ではデスクトップモードの操作性にかなり問題がありましたが、8.1 で大分マシになりましたね。

色々言われている Windows8 ですが、タブレットとして使った場合の操作性は素晴らしいです。
あのスタート画面も、アプリを開くことなくサマリの情報が表示されるので、これはこれで便利です。
Store アプリケーションは操作性の統一が iOS 以上に徹底されています。次の規則を覚えると、どのアプリケーションでもすぐに使い方を覚えられるようになります。

  • 上下のエッジスワイプでコンテキストメニューの表示
  • 右のエッジスワイプで出てくるチャームメニューで他アプリとの連携
  • サマリ/詳細の切り替えにセマンティックズームが使える (ピンチイン、アウトで表示を切り替えられる)

他アプリと連携するコントラクト (Andoroid のインテントに相当) もとても便利です。
問題は、肝心の Windows Store の品揃えが壊滅的なことです。TwitterFacebook、Flipboard、Evernote といった超メジャーどころはありますが、いずれも iOSAndroid 版と比べると機能が少なく、かなりおざなりな感じです。操作感はいいのに...。
そのため、iPad の完全な代替になるかというと、ちょっと厳しいです。

デスクトップモードについては、キーボードが使い易いこともあって、ノート PC として使っても快適に感じます。
ただ、高 DPI スケーリングに対応していないアプリケーションが多いことが分かりました。
ぼやっと拡大されるアプリケーションはまだいいですが、文字が豆粒のようになってしまうのは困りものです。
例を挙げるとデスクトップ版 EvernoteJava を使ったアプリケーションなどです。
NetBeans はデフォルトだとコンソールの文字が豆粒のようになってしまい、とても悲しかったです (タスクバーの大きさに対するコンソールの文字の大きさに着目) 。

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

まとめると、タブレットとして使っても、ノート PC として使っても、細かいところで不満点はありますが、基本的に快適です。
iPadMBA の役割を 1 台で満たすデバイスを手に入れるという野望がどの程度満たされたかというと、次のような理由から 50% くらいかなあという感想です。

  • 縦持ちで使いにくい。
  • Store アプリが貧弱であるため、タブレットとして使った場合に制約が多い。
  • トラックパッドの操作性が良くない。

でも、これなら出先でプログラミングや Office 文書の編集ができるし、しかも Web や SNS のチェックとかはタブレットの操作性で行えるので、今後外へ持ち歩くデバイスは基本的にこれになると思います。
まあ良い買い物したと思っています。 *3

おまけ

Windows タブレットの事実上の標準ベンチマークとなっているアレですが、Corei3 だけあってさすがに快適に動きます。

ただし、バックグラウンドで別のタスクが走っている場合は極端に遅くなります。
あと、結構ファンが回るので、バッテリーの消費が激しくなる点には注意です。 *4

*1:でも VAIO Tap 11 の方も発注してから納品するまで1ヶ月掛かったんですけどね。

*2:Surface Pro は同じフル HD でも 150% に調整されているようです。Surface の方がディスプレイサイズが1インチ小さいからですかね。

*3:あと、図らずも SONYVAIO を手放すことになったので、その意味でも記念購入になっちゃいましたね。

*4:ぶっちゃけ艦これFlash の作りはかなりチューニングの余地があると思っているんですけどねえ...。

GlassFishで仮想ディレクトリマッピングを行う方法 (GlassFish Advent Calendar 23日目)

このエントリは GlassFish Advent Calendar 2013 の 23 日目のエントリです。前日は id:makoyanagisawa さんによる「@btnrougeが教えてくれなかったこと」でした。

当初、今年の Java Day Tokyo で披露した FX GlassFish Monitor に絡めて、GlassFish が公開している監視項目の REST API の話にしようと思ったのですが、蓮沼さんの第8日目のエントリでやられてしまったので、別の小ネタにします。

今回取り上げるのは、WAR の外にあるコンテンツをアプリケーションのドキュメントルートにマッピングする方法です。仮想ディレクトリマッピングと呼ばれるやつです。 *1

例えば次のディレクトリに存在するコンテンツを http:////content にマッピングしたいとします。

/home/hoge/external/content/

その場合、glassfish-web.xml に次のような記述を追加します。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
  ...
  <property name="alternatedocroot_1" value="from=/content/* dir=/home/hoge/external"/>
</glassfish-web-app>

glassfish-web-app 要素の子要素である property 要素に alternatedocroot_* (* には連番が入る) という名前で設定を記述します。複数設定したい場合は連番を 1, 2, ... と続けます。
値には "from=(コンテキストパス以下のパス) dir=(マッピングしたいドキュメントルート)" という形式で記述します。
from で指定したパスがサブディレクトリとして付くことに注意してください。この例では /home/hoge/external/content 以下を見に行きます。

開発者以外の人が管理しているコンテンツ (例えばヘルプドキュメントとか) がある場合にはこうすると WAR に含めずに済むので便利ではないでしょうか。

というわけでちょっとしたネタでした。
明日は蓮沼 (@) さんの予定です。

*1:WebLogic だと weblogic.xml 要素で設定するやつです。

Java EE Managed Beanについて (Java EE Advent Calendar2013 19日目)

このエントリは Java EE Advent Calendar 2013 の 19 日目のエントリです。前日は @さんによる、「JSF 2.2 でさらに便利になったMarkupを使ってみよう」でした。

はじめに

Java EEの仕様の中でも立ち位置が微妙すぎて、かの金魚本でもガン無視されちゃっている Java EE Managed Bean のお話を取り上げます。
(JSF の Managed Bean のお話じゃないのでご注意を!)
Managed Bean は JSR-316 Java EE 6 の一部として含まれている仕様で、仕様書である "Managed Beans 1.0 Specification" は本文がたったの 11 ページというとてもあっさりとした仕様です。
重厚長大なイメージのある Java EE 仕様の中で、このあっさりさは際立っているように思えます。

Managed Bean とは?

Managed Bean は Java EE アプリケーションサーバーのコンテナによって管理される JavaBean です。もう少し具体的に言うと EJB のスーパーセットです。
コンテナの管理下に入ることにより、次のような機能がサポートされます。

  • リソースのインジェクション
  • ライフサイクルに応じたコールバックメソッド
  • インターセプタの適用

EJB と異なり、次のような機能はサポートされません。

作り方

Managed Bean の作り方は簡単です。POJO に @javax.annotation.ManagedBean アノテーションを付与するだけで OK です。
POJO でいいのですが、次のような制約があります。

  • final ではない
  • 抽象クラスではない
  • 内部クラスの場合、static なクラスであること (ネストクラスであること)

Serializable であることは強制されません。
JSFアノテーション (@javax.faces.bean.ManagedBean) を間違えて付けてしまわないように注意しましょう。

@ManagedBean("myManagedBean")
public class MyManagedBean {
    @Resources
    private OtherResource otherResource;
    /**
     * コンポーネント初期化後に呼び出されるメソッド
     */
    @PostConstruce
    public void setUp() {
    }
    /**
     * コンポーネント破棄前に呼び出されるメソッド
     */
    @PreDestroy
    public void cleanup() {
    }
}

@ManagedBean アノテーションの属性には JNDI ルックアップを行う際に使う名前を指定します。Managed Bean を利用するのに後述の @Resource アノテーションしか使わないのならば省略しても OK ですが、JNDI ルックアップを行う際には必ず記述する必要があります。

EJB と同様、@PostConstruct、@PreDestroy アノテーションを使うことができ、インスタンス生成直後、インスタンス破棄前に処理を挟み込むことが可能になります。
また、@Resource を使って他のコンテナ管理リソースをインジェクトすることもできます。
基本的な作り方は EJB と全く変わらないことが分かると思います。

使い方

Managed Bean の使い方は次の2通りです。

  1. @Resource アノテーションによるインジェクション
  2. JNDI ルックアップ

まず、@Resource アノテーションを使ったインジェクションはとても簡単です。インジェクトしたいフィールドなり setter メソッドなりに指定すれば OK です。

@Stateless
public class MyEJB {
    @Resource
    private MyManagedBean myManagedBean;
    ...
}

JNDI ルックアップを行う場合、ルックアップに使用する名前空間は次のようになります。

アプリケーションレベルの名前空間
java:app//
モジュール内での名前空間
java:module/

はデプロイしたアーカイブの名前 (WAR ファイルの名前など) になります。
は @ManagedBean アノテーションに指定した属性になります。EJBCDI と違って、デフォルトでは名前が生成されないので注意してください。
モジュールをまたいで利用することは少ないでしょうから、通常は後者の名前空間を使って取得することになるでしょう。

    InitialContext ic = new InitialContext();
    MyManagedbean myManagedBean = (MyManagedbean) ic.lookup("java:module/myManagedBean");

使いどころ

コンテナ管理オブジェクトにして、コンテナ提供の機能 (特にリソースインジェクション) を使いたいけど、EJB を使うまでもない、という場面で使うことになるでしょうか。
要はトランザクション境界にする必要のないコンポーネントの場合に使うといったところですかね。
以前自分が開発した JAX-RS アプリケーションでは、次のコンポーネントを Managed Bean にしました。

  • Repository クラス (DAO クラス)
  • JAX-RS のリソースクラス

そして、トランザクション境界となる Service コンポーネントを Stateless Session Bean としています。

...と書きましたが、それだったら CDI 使ったら良いんじゃないのー、という突っ込みが入りそうです。いや全くその通りだと思います。

Java EE 6 の仕様策定時、CDI はぎりぎりまで EE 6 に入るかどうか微妙で、Managed Bean はそんな中産まれた、微妙な立ち位置の仕様に見えます。

今年の JavaOne で、Java EE の将来を考えるというパネルディスカッションを聴いたのですが、そこでもこれからは CDI に寄せた方がいいんじゃないの?という雰囲気でした。
なので、今後は CDI を使うようにした方がいいように思えます。個人的には Managed Bean のシンプルな仕様は結構気に入っているんですけどね。

分からないこと

Managed Bean のインスタンス管理方式については仕様では明確になっていません。
アプリケーションサーバーの実装に任されており、恐らく Singleton Session Bean と同じ (1つしかインスタンスを作らない) 方式になっているものと思われますが、実際どうなんでしょう?
SLSB のようにインスタンスプールを作っている実装もあったりするのでしょうか?

というわけで、Managed Bean という今後日の目を見ることはないであろう、かわいそうな仕様についての紹介でしたw

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

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