How to get stage from controller during initialization?
Asked Answered
H

8

103

I want to handle stage events (i.e. hiding) from my controller class. So all I have to do is to add a listener like this:

((Stage) myPane.getScene().getWindow()).setOn*whatIwant*(...);

But the problem is that initialization starts right after this code:

Parent root = FXMLLoader.load(getClass().getResource("MyGui.fxml"));

And before this code:

Scene scene = new Scene(root);
stage.setScene(scene);

Thus getScene returns null.

The only workaround I found by myself is to add a listener to myPane.sceneProperty, and when it becomes not null I get scene, add to it's windowProperty my listener handling which I finally retrieve stage. And it all ends with setting desired listeners to stage events.

I think there are too many listeners.

Is it the only way to solve my problem?

Hydromedusa answered 6/11, 2012 at 7:18 Comment(0)
A
129

You can get the instance of the controller from the FXMLLoader after initialization via getController(), but you need to instantiate an FXMLLoader instead of using the static methods then.

I'd pass the stage after calling load() directly to the controller afterwards:

FXMLLoader loader = new FXMLLoader(getClass().getResource("MyGui.fxml"));
Parent root = (Parent)loader.load();
MyController controller = (MyController)loader.getController();
controller.setStageAndSetupListeners(stage); // or what you want to do
Astronomical answered 6/11, 2012 at 8:18 Comment(6)
Think you are missing a cast? Parent root = (Parent)loader.load();Pyridoxine
Is this really the best way to do this? There is nothing provided by the JavaFX framework to archive this?Chimera
For some reason this does not work if you use the constructor without parameters and hand in the URL to the load() method. (Also, the javadoc on getController sounds like it should be on setController.)Seditious
@Seditious this is because the load() method with the URL parameter is a static method that doesn't set anything on the instance you are calling it on.Astronomical
controller.setStageAndSetupListeners(stage); has no such method. What should I do?Wichern
@santafebound that is a method that you should implement for yourself or you can call any method of your controller which you like. Its just a placeholder in my answer.Astronomical
M
125

All you need is to give the AnchorPane an ID, and then you can get the Stage from that.

@FXML private AnchorPane ap;
Stage stage = (Stage) ap.getScene().getWindow();

From here, you can add in the Listener that you need.

Edit: As stated by EarthMind below, it doesn't have to be the AnchorPane element; it can be any element that you've defined.

Maulstick answered 28/7, 2015 at 20:48 Comment(4)
Note that element can be any element with an fx:id in that window: Stage stage = (Stage) element.getScene().getWindow();. For example, if you only have a Button with an fx:id in your windows, use that to get the stage.Nathan
getScene() still returns null.Menorrhagia
Before initialization completes (e.g. in controller initialize method) this won't work because getScene() returns null. The original poster implies this is his situation. In this case alternative 1 given by Utku Özdemir below is better. If you don't need the stage then one listener is sufficient, I use this in initialize() to make an ImageView stretch: bgimage.sceneProperty().addListener((observableScene, oldScene, newScene) -> { if (oldScene == null && newScene != null) { bgimage.fitWidthProperty().bind(newScene.widthProperty()); ... } });Hallam
It solves my problem and I believe this is the best way. I think it's worth notice that 'ap' is the ID of AnchorPane given in either fx:id field in your .fxml file(If you're working with UI design with FXML) or the name you given to AnchorPane object. Generally it works fine for all XXXPane object.Evetta
R
33

I know it's not the answer you want, but IMO the proposed solutions are not good (and your own way is). Why? Because they depend on the application state. In JavaFX, a control, a scene and a stage do not depend on each other. This means a control can live without being added to a scene and a scene can exist without being attached to a stage. And then, at a time instant t1, control can get attached to a scene and at instant t2, that scene can be added to a stage (and that explains why they are observable properties of each other).

So the approach that suggests getting the controller reference and invoking a method, passing the stage to it adds a state to your application. This means you need to invoke that method at the right moment, just after the stage is created. In other words, you need to follow an order now: 1- Create the stage 2- Pass this created stage to the controller via a method.

You cannot (or should not) change this order in this approach. So you lost statelessness. And in software, generally, state is evil. Ideally, methods should not require any call order.

So what is the right solution? There are two alternatives:

1- Your approach, in the controller listening properties to get the stage. I think this is the right approach. Like this:

pane.sceneProperty().addListener((observableScene, oldScene, newScene) -> {
    if (oldScene == null && newScene != null) {
        // scene is set for the first time. Now its the time to listen stage changes.
        newScene.windowProperty().addListener((observableWindow, oldWindow, newWindow) -> {
            if (oldWindow == null && newWindow != null) {
                // stage is set. now is the right time to do whatever we need to the stage in the controller.
                ((Stage) newWindow).maximizedProperty().addListener((a, b, c) -> {
                    if (c) {
                        System.out.println("I am maximized!");
                    }
                });
            }
        });
    }
});

2- You do what you need to do where you create the Stage (and that's not what you want):

Stage stage = new Stage();
stage.maximizedProperty().addListener((a, b, c) -> {
            if (c) {
                System.out.println("I am maximized!");
            }
        });
stage.setScene(someScene);
...
Rockweed answered 18/6, 2015 at 8:19 Comment(3)
I'm trying to use this approach to populate a TableView and show a ProgressIndicator centered respect to the TableView but turns out that inside the events values for getX() and getY() aren't the same as if you use after all the initialization process end (inside a click event in a button) either for scene and stage, even stage getX() and getY() return NaN, any idea?Pernick
@Pernick , got that getY() problem at this moment, did you find a sollution?Jim
if I add pane to a scene that is already attached to a stage, this won't work, right? Shouldn't there be a check in the sceneProperty listener and only add the stageProperty listener if the stage is still null? Otherwise, just do what I need to right away?Transubstantiation
K
18

The simplest way to get stage object in controller is:

  1. Add an extra method in own created controller class like (it will be a setter method to set the stage in controller class),

    private Stage myStage;
    public void setStage(Stage stage) {
         myStage = stage;
    }
    
  2. Get controller in start method and set stage

    FXMLLoader loader = new FXMLLoader(getClass().getResource("MyFXML.fxml"));
    Parent parent = loader.load(); // Note the loader must be loaded before you can access the controller.
    OwnController controller = loader.getController();
    controller.setStage(this.stage);
    
  3. Now you can access the stage in controller

Kreit answered 15/10, 2014 at 4:17 Comment(1)
This was the winner for me. Robert Martin's answer worked at first, but then started throwing an error which I resolved by getting stage like this.Fining
T
3

Platform.runLater works to prevent execution until initialization is complete. In this case, i want to refresh a list view every time I resize the window width.

Platform.runLater(() -> {
    ((Stage) listView.getScene().getWindow()).widthProperty().addListener((obs, oldVal, newVal) -> {
        listView.refresh();
    });
});

in your case

Platform.runLater(()->{
    ((Stage)myPane.getScene().getWindow()).setOn*whatIwant*(...);
});
Tahoe answered 23/1, 2020 at 14:31 Comment(0)
S
1

Assign fx:id or declare variable to/of any node: anchorpane, button, etc. Then add event handler to it and within that event handler insert the given code below:

Stage stage = (Stage)((Node)((EventObject) eventVariable).getSource()).getScene().getWindow();

Hope, this works for you!!

Syrinx answered 11/8, 2018 at 10:13 Comment(0)
C
0

If you really want to avoid passing in the stage to the Controller, you could handle the observable properties of both the scene and the window using .sceneProperty() and .windowProperty() to store each instance once they have changed value:

private Stage stage;
private Scene scene;

@Override
public void initialize(URL location, ResourceBundle resourceBundle) {
    onSceneInitialized(nodeInScene, () -> {
        // do stuff with scene
        onStageInitialized(scene, () -> {
            // do stuff with stage
        });
    });
}

private void onSceneInitialized(Node node, Runnable onChanged) {
    node.sceneProperty().addListener((obs, past, present) -> {
        if (scene != null)
            return;

        scene = present;
        onChanged.run();
    });
}

private void onStageInitialized(Scene scene, Runnable onChanged) {
    scene.windowProperty().addListener((obs, past, present) -> {
        if (stage != null)
            return;

        stage = (Stage) present;
        onChanged.run();
    });
}
Canikin answered 22/6, 2023 at 23:34 Comment(0)
K
-2

You can get with node.getScene, if you don't call from Platform.runLater, the result is a null value.

example null value:

node.getScene();

example no null value:

Platform.runLater(() -> {
    node.getScene().addEventFilter(KeyEvent.KEY_PRESSED, event -> {
               //your event
     });
});
Kosiur answered 12/8, 2017 at 16:48 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.