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のダイアログはそのようになっていますね)