[Presentation] JavaFX 8u40 のダイアログを探る

この記事は JavaFX Advent Calendar 2014、8 日目の記事です。昨日は @backpaper0 さんの「どうやってApplicationサブクラスの名前取ってきてんの?」でした。

Java SE 8 Update 40 より、JavaFX にダイアログが導入されることになりました。既に Update 40 の Early Access Build のインストーラー配布が公開されていますので、興味のある方はダウンロードして新機能を確かめてみることをお勧めします(ただし Early Access は本番環境への適用が禁止されているため、あくまで個人の範囲で)。

今回ご紹介するサンプルは https://github.com/btnrouge/javafx-dialogs にあります。JDK 8u40 以降でないと実行できないため、実行環境の JDK 8 のバージョンにはくれぐれもご注意ください。

JavaFX 8u40 のダイアログについては、11 月 25 日に開催された JavaFX Night にてセッションを担当しました。導入部分についてはセッション資料をご覧頂くのが一番かと思いますので、以下に掲載します。

JavaFX Night では、時間の都合で十分な量のサンプルを提示できなかったことと、ダイアログのカスタマイズについて「その気になればカスタマイズもできるよ!」のひとことで済ませてしまったため、この記事では主にそれらを中心にお話ししようと思います。

JavaFX 8u40 で導入予定のダイアログは、サードパーティのライブラリ ControlsFXDialog を参考にしていると思われます。その証拠に、画面レイアウトもよく似ています。

1. 2 通りのダイアログ表示方法

JavaFX のダイアログには 2 通りの表示方法があります。JavaFX Night でも取り上げた、Dialog#show() と Dialog#showAndWait() です。どちらもダイアログを表示し入力を待つ動作は同じですが、Dialog#show() はそれ以降に続く処理がすぐに実行されるのに対し、Dialog#showAndWait() はユーザー入力後に続く処理を実行します。

具体例を挙げましょう。まずは Dialog#show() の場合です。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Show");
alert.getDialogPane().setHeaderText("Header Text");
alert.getDialogPane().setContentText("Content Text");
alert.show();
System.out.println("message after alert#show()");

alert.show(); でダイアログを表示し、すぐに次の処理が実行されるため、ユーザーの入力を待つことなくコンソールに "message after alert#show()" と表示されます。

Dialog#show() はユーザー入力を待たないため、ダイアログからの応答を受け取るタイミングが存在せず、従って戻り値も void として宣言されています。

続いて、Dialog#showAndWait() の場合を見てみましょう。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Show and Wait");
alert.getDialogPane().setHeaderText("Header Text");
alert.getDialogPane().setContentText("Content Text");
alert.showAndWait();
System.out.println("message after alert#showAndWait()");

alert.showAndWait(); でダイアログを表示するところまでは同じですが、ユーザーの入力があるまで(ダイアログを閉じるまで)コンソールに "message after alert#showAndWait()" は表示されません。

基本的な使い分けとしては、

  • いわゆる従来型のダイアログとして使用する場合は Dialog#showAndWait()
  • Windows 8 のトースター通知のように処理を止めたくない場合は Dialog#show()

といった形になるでしょうか。

2. DialogPane の使い方

DialogPane は Dialog が保持できる唯一のコンテナーで、Dialog の dialogPane プロパティーを通じて操作することができます。DialogPane にはさらにいくつかのプロパティーが存在し、それらの調整だけでもダイアログのカスタマイズが可能です。

DialogPane のルック・アンド・フィールを制御するプロパティーの一覧を下記に示します。

DialogPane の主なプロパティー
プロパティークラス概要
graphic Node アイコン
headerText String ヘッダー文字列
header Node ヘッダー(headerText より優先)
contentText String メッセージ
content Node コンテンツ(contentText より優先)
expandableContent Node 詳細情報
expanded boolean 詳細情報の表示/非表示

headerText と header の関係、contentText と content の関係については JavaFX Night では触れませんでしたが、headerText と contentText がヘッダーとメッセージをそれぞれテキスト(内部的には Label にラップして header / content に設定)するのに対して、header と content はあらゆる Node ツリーを設定できるため自由なレイアウトが実現できます。header と content はデフォルトでは null となっており、header / content に Node を設定すると headerText / contentText の内容は無視される仕様になっています。多くの場合、凝ったダイアログを必要とするケースは多くないので、headerText と contentText を押さえておけば良いでしょう。

一般にエラーを通知するダイアログでは、初期表示ではエラーの詳細を隠し、必要に応じて詳細を表示する形式が多く見られます。そのようなダイアログを実現するために expandableContent と expanded が用意されています。expandableContent はデフォルトでは null になっています。expandableContent が null でない場合は expanded が有効になり、詳細情報の表示/非表示の制御ができるようになります。

expandableContent の想定される使い方としては、Alert ダイアログの AlertType.ERROR に原因となったエラーの詳細(例外スタックトレースなど)を表示するための領域を追加するような用途です。

なお、Dialog 自体にも headerText / contentText プロパティーが存在しますが、これらは DialogPane の headerText / contentText を呼び出しているだけです。

3. Alert のカスタマイズ

3.1. expandableContent プロパティ

Alert は DialogPane のプロパティを調整することで簡単にカスタマイズすることができます。ここでは expandableContent プロパティを設定して、エラー詳細情報を表示できるようにしてみましょう。

// error message
final String stackTrace = 
    "Exception in thread \"main\" java.lang.UnsupportedOperationException\n"
    + "\tat java.sql.Date.toInstant(Date.java:304)\n"
    + "\tat samples.time.Unsupported.main(Unsupported.java:6)";

Alert alert = new Alert(AlertType.ERROR);

// prepare expandable content
TextArea textArea = new TextArea(stackTrace);
alert.getDialogPane().setExpandableContent(textArea);

alert.setTitle("ERROR");
alert.setHeaderText("Error");
alert.setContentText("An exception was thrown in the application");
Optional result = alert.showAndWait();

ダイアログの実行イメージです。

exception.png

「詳細の表示」をクリックすると、expandableContent プロパティで設定した内容が表示されます。

exception-expanded.png

3.2. header プロパティおよび content プロパティ

headerText や contentText では単純な文字列しか出力できませんでした。しかし、出力したい内容を header または content に設定しておけば、任意の内容をダイアログに表示させることができます。

ここでは Alert の content に TextFlow(リッチテキスト)とイメージを設定したダイアログの例を見てみます。ソースコード中に contentText を設定していますがそれが無視されていることにも注目してください。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Custom content");
alert.setHeaderText("Header Text");
alert.setContentText("Content Text");

// Set content property
Text text1 = new Text("It's content Text ");
Text text2 = new Text("as Rich Text");
text2.setFill(Color.RED);
TextFlow textFlow = new TextFlow(text1, text2);
ImageView imageView = new ImageView(new Image(getClass().getResourceAsStream("someimage.png")));
VBox vbox = new VBox(4.0, textFlow, imageView);
alert.getDialogPane().setContent(vbox);

Optional result = alert.showAndWait();

ダイアログの実行イメージです。

custom-content.png

なお、header の場合も手順は同様です。

3.3. graphic プロパティ

Alert ダイアログのアイコンはコンストラクタ引数(AlertType)によって決まりますが、graphic プロパティを設定することで任意のイメージに差し替えることができます。この方法は AlertType.NONE の場合でアイコンを表示する場合にも用います。

では、アイコンを差し替えてみましょう。以下にソースコードを示します。

Alert alert = new Alert(AlertType.INFORMATION);
alert.setTitle("Custom icon");
alert.setHeaderText("Header Text");
alert.setContentText("Content Text");

// Set graphic property
ImageView imageView = new ImageView(new Image(getClass().getResourceAsStream("newicon.png")));
imageView.setFitWidth(48.0);
imageView.setFitHeight(48.0);
alert.getDialogPane().setGraphic(imageView);

Optional result = alert.showAndWait();

ダイアログの実行イメージです。

custom-graphic.png

4. TextInputDialog と ChoiceDialog のカスタマイズ

前章で示したように、Alert は比較的簡単にカスタマイズすることができます。しかし、TextInputDialog と ChoiceDialog については content をカスタマイズすることができません(contentText は可能)。これらのダイアログは content にあらかじめレイアウト(GridPane)が設定されており、かつレイアウトの設定タイミングをダイアログ側がコントロールしているため、拡張の余地がないのです。

なお、header プロパティおよび graphic プロパティについては、Alert 同様に設定が可能です。

5. 独自ダイアログの実装

Dialog のサブクラスを独自に作成することで、全く新しいダイアログを作成することもできます。こちらは Dialog クラスのサブクラスを直接作成するため手間はかかりますが、出来合いのダイアログに比べると自由度が高い利点があります。

注意点として、Dialog クラスのメソッドはすべて final 宣言されており、オーバーライドすることができません。ダイアログ内部のイベント取得タイミングを変更するなど、ダイアログの動作そのものを変えてしまうことはできないのです。

当初の予定では、Dialog から直接派生させた独自のダイアログを作成しようと考えていたのですが、ユースケースが思いつかなかったため割愛させていただきます。現在は JavaFX のソースコードも標準で添付されるようになったため、Alert などのソースを参考に各自工夫してみてください。

6. Dialog と Stage の関係

最後に Dialog と Stage の関係についてお話します。

実用上は、Dialog と Stage は似てはいるが別物として扱った方がわかりやすいでしょう。Dialog には Stage にも存在するプロパティーやメソッドをいくつも持っています。新たなウィンドウを開くという点でも同じです。

厳密に言うと、Dialog と Stage は "has-A" の関係にあります。ここから先は Dialog の実装に深く踏み込んだ話になるため、初見の方は読み飛ばしていただいて構いません。

Dialog は下請けクラス FXDialog のインスタンスを保持しています。FXDialog は抽象クラスであり、実際にはサブクラスの HeavyweightDialog のインスタンスとなります。そして HeavyweightDialog が DialogPane を root とする Scene を構築し(dialogPane に null を設定した場合は必ず新規の DialogPane が生成される)、Scene とダイアログの共通属性(リサイズ不可と画面中央への表示)を設定して Stage を作成します。

FXDialog とそのサブクラスは、Stage のメソッドとプロパティのうちダイアログのカスタマイズに利用可能なものを外部に提供しています。Dialog と Stage で共通のプロパティが多いのはこれが理由です。

Dialog は FXDialog(HeavyweightDialog)のコンストラクタに自身のインスタンスを設定し、ダイアログの Stage を生成してもらいます。Dialog のメソッドとプロパティは FXDialog が提供するメソッドとプロパティをほぼそのまま呼び出すだけです。

あまり多くのことを覚えたくなければ、Dialog と Stage は分けて考えた方が分かりやすいでしょう。

7. まとめ

JavaFX 2.x 時代から望まれていたダイアログが、来春には JavaFX 8 に追加されます。仕様策定に予想以上の時間がかかりましたが、まずまずの出来ではないでしょうか。2015 年 3 月には JavaFX 8u40 のリリースと共に標準でダイアログが利用できるようになるため、それまでの間に習熟しておくと良いことがありかもしれません。

最後になりましたが、今回サンプルで提示したソースコードは GitHub https://github.com/btnrouge/javafx-dialogs にホスティングしています。必要に応じて参照してください。


明日は気鋭の学生プログラマー @orekyuu が何か書いてくれると思います。大いに期待しましょう。