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?

Lii
  • 11,553
  • 8
  • 64
  • 88
Chechulin
  • 2,426
  • 7
  • 28
  • 35

8 Answers8

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
Saikat
  • 14,222
  • 20
  • 104
  • 125
zhujik
  • 6,514
  • 2
  • 36
  • 43
  • 1
    Think you are missing a cast? Parent root = (Parent)loader.load(); – Paul Eden Feb 14 '14 at 23:53
  • 16
    Is this really the best way to do this? There is nothing provided by the JavaFX framework to archive this? – Hannes Oct 18 '14 at 15:42
  • 1
    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`](http://docs.oracle.com/javafx/2/api/javafx/fxml/FXMLLoader.html#getController()) sounds like it should be on `setController`.) – Bombe Feb 07 '15 at 11:42
  • 4
    @Bombe 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. – zhujik Feb 26 '15 at 10:31
  • ``controller.setStageAndSetupListeners(stage);`` has no such method. What should I do? – Martin Erlic Aug 07 '17 at 10:51
  • @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. – zhujik Aug 08 '17 at 07:33
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.

Robert Martin
  • 1,585
  • 1
  • 13
  • 20
  • 8
    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. – aardbol Jan 11 '16 at 10:20
  • 34
    `getScene()` still returns `null`. – m0skit0 May 24 '16 at 11:48
  • 13
    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()); ... } });` – Georgie Feb 22 '17 at 02:17
  • 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. – Aaron Chen Jan 17 '18 at 03:20
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);
...
Utku Özdemir
  • 7,390
  • 2
  • 52
  • 49
  • 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? – leobelizquierdo Apr 26 '16 at 18:53
  • @leobelizquierdo , got that getY() problem at this moment, did you find a sollution? – JonasAnon Mar 20 '17 at 15:12
  • 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? – tomorrow Aug 23 '20 at 13:34
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

Sandeep Kumar
  • 596
  • 5
  • 7
  • 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. – Adam Dec 21 '18 at 15:40
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*(...);
});
AdamOutler
  • 870
  • 4
  • 13
  • 29
0

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!!

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();
    });
}
0009laH
  • 1,960
  • 13
  • 27
carter
  • 59
  • 5
-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
     });
});
ndmeiri
  • 4,979
  • 12
  • 37
  • 45
fjqa86
  • 21