FXML Controller で Stage を使うためのアレコレ

FXML を使ってアプリケーションを作る場合、ロジック部分は Controller クラスを作って実装します。
その Controller のなかで javafx.stage.Stage オブジェクトを取得する方法をまとめます。

Stage オブジェクトを取得したい理由

すでに存在するウィンドウから新しいウィンドウを表示したい場合、次のような実装をします。

class OpenDialogController {

    void handleOpenButtonAction(ActionEvent event) {
        Stage newStage = new Stage()
        newStage.initModality(Modality.APPLICATION_MODAL)

        // ここでControllerが属する Stage のオブジェクトを渡したい…
        newStage.initOwner(????)    

        // 以下、省略
    }
}

ウィンドウにあたるのが Stage なので、新たなウィンドウを作る場合は Stage オブジェクトを生成します。
その際、Stage#initOwner(Window) を実装するのがお決まりです。このメソッドの引数には新たなウィンドウを開く際の親となるウィンドウ(Window もしくは、そのサブクラスの Stage)を指定します。

ですが、Controller クラスには自分の Stage を取得するための手段が実装されていません。
スーパークラスを実装しているわけではないので、getter があったりすることはない)

おいおい、どこから取ってくるんだ!?
という具合に今回悩みました。

よくあるダイアログを開くサンプルでは…

FXML を使っていないんですよね。
詳しくは述べませんが、FXML を使わない場合、javafx.application.Application クラスを継承したクラスの中ですべて実装できます。
そのため、親となる Stage オブジェクトを生成したものを子となる Stage に渡すのも簡単です。

FXML を使うと、View にあたる FXML のオブジェクト(?)も、Controller クラスのオブジェクトも勝手に作られます。
したがって、Controller クラスに対して Stage オブジェクトを渡す機会がないように思えます。

んで、ググって見ると同じように悩んでいる人たちがいたので参考にしてみました。

方法1:@FXML で対応づけしたノードからたどっていく

OTN Discussion Forums : How to access a stage from a FXML ...

ここのQ&Aを参考にしました。

class OpenDialogController {

    @FXML Button openButton

    void handleOpenButtonAction(ActionEvent event) {
        Stage newStage = new Stage()

        Window window = openButton.getScene().getWindow()
        newStage.initOwner(window)

        // 以下、省略
    }
}

@FXML で対応づけしたノード(javafx.scene.Node)があれば、Node#getScene() で Scene が取得できるので、その Scene#getWindow() で Window オブジェクトを取得することができます。

わかってしまえば、当然そうやってたどれるでしょうが…と思う方法です。

ただ、上記であげた参考サイトにも書かれてますが、エレガントな方法ではないですよね。

方法2:Controller オブジェクトに値を渡す方法が実は存在した

JavaFX: How to get stage from controller during initialization? - Stack Overflow

ここのQ&Aを参考にしました。

class OpenDialogController {

    Stage thisStage

    void setThisStage(Stage stage) {
        thisStage = stage
    }

    void handleOpenButtonAction(ActionEvent event) {
        Stage newStage = new Stage()
        newStage.initOwner(thisStage)

        // 以下、省略
    }
}

Controller クラスは Stage オブジェクトをプロパティとして保持するようにして、setter メソッドで外からセットできるようにします。
こうすれば、Stage オブジェクトを使いたい場合はプロパティから取得すればいいだけになります。

setter を使って Stage オブジェクトをセットするのが Application クラスになります。

class OpenDialogApp extends Application {

    @Override
    void start(Stage stage) {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("OpenDialog.fxml"))
        Parent root = loader.load()

        OpenDialogController controller = (OpenDialogController) loader.getController()
        controller.setThisStage(stage)

        stage.setScene(new Scene(root))
        stage.show()
    }
}

FXMLLoader#getController() で FXML に対応する Controller オブジェクトを取得できます。
Controller オブジェクトをつかめたら、あとは Stage を setter でセットしてあげるだけです。

(Stage だけでなく、他のパラメータもこれで Controller に渡せそうです)

通常は次のように FXMLLoader クラスの static メソッドの load() を使って Parent を取ってきますが、それだと FXMLLoader#getController() を使うことができません。FXMLLoader クラスのインスタンスを一度取得するというのがミソですね。

void start(Stage stage) {
    Parent root = FXMLLoader.load(getClass().getResource("OpenDialog.fxml"))

    stage.setScene(new Scene(root))
    stage.show()
}

まとめ

シーングラフの構造とか、きちんと勉強しておくべきですね。
JavaFX で遊んでいるときは「困ったら調べて」の繰り返しなので、断片的な知識だけになってしまってます。

もっといい方法があるようでしたら、どなたかご教授ください。m(_ _)m