3

What I'm trying to do

Load a stylesheet and apply it to the scene when a user clicks on a button

The issue

Calling getScene() returns null.

The class the function is in is the controller and root node of the scene, I'm using Scenebuilder 2.0 and have set the class to be the controller of the loaded fxml, it is a VBox.

VBox guiRootNode = null; // inside this instance is where the `getScene() call is`

try {
    FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml"));
    guiRootNode = (VBox) loader.load();
} catch (IOException e) {
    e.printStackTrace();
}

if (guiRootNode == null) {
    new Alert(Alert.AlertType.ERROR, "The GUI could not be loaded").showAndWait();
    Platform.exit();
} else {
    primaryStage.setScene(new Scene(guiRootNode));
}

The problem code is a member function inside the MainWindow class, the @FXML tag is so I can set the button to call it onAction() via the MainWindow.fxml.

@FXML
private void onDefaultCssClicked()
{
    // getScene() returns null
    getScene().getStylesheets().remove(getClass().getResource("dark.css").toExternalForm());
    getScene().getStylesheets().add(getClass().getResource("default.css").toExternalForm());
}

The complete code can be found at https://github.com/SebastianTroy/FactorioManufacturingPlanner however it doesn't represent a minimal code example by a long shot...

Similar Questions

JavaFX - getScene() returns null This QA assumes the getScene() call was done in an initialise function or during instantiation.

JavaFX getScene() returns null in initialize method of the controller In this QA the call is specifically in the initialise method, so not applicable here.

Things I have tried

  • I am not using the fx:root construct, checking that option in SceneBuilder causes the error javafx.fxml.LoadException: Root hasn't been set. Use method setRoot() before load.
  • Calling button.getScene() works fine, so I have my hack, but I'd like to understand the problem anyway.

Resolution

I stopped trying to make my controller the root gui object.

Basically I had asumed that the controller for a FX gui was the root GUI node, hence me making the controller extend the type of the root gui node. this of course isn't the case, the controller is and should be a seperate class which has some of the gui variables injected into it.

MCVE

MainWindow.fxml

<VBox xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.gui.MainWindow">
    <children>
        <Button onAction="#click" text="button" fx:id="button"/>
    </children>
</VBox>

MainWindow.java

public class MainWindow extends VBox {
    @FXML
    private Button button;

    @FXML
    private void click() {
        System.out.println("Controller scene: "+ getScene());
        System.out.println("Button scene: "+ button.getScene());
    }

}

Output on button click

Controller scene: null
Button scene: javafx.scene.Scene@4d6ed40a
Troyseph
  • 4,960
  • 3
  • 38
  • 61
  • 1
    Note: I've created+added a MCVE from your linked code, since we require the question to contain all the relevant info, not just a link to the info and your code isn't minimal... – fabian Nov 08 '17 at 21:29
  • 1
    @fabian Thank you, I thought an MCVE was desirable as opposed to mandatory, I thought the code I included was enough to explain my situation, I'll include an MCVE in the future – Troyseph Nov 08 '17 at 22:27

1 Answers1

3

The VBox loaded for the root of the fxml is a different instance than the instance used as root of the controller. You add the loaded node to a scene, but you do not add the controller to a scene so getScene() returns null.

loader.getRoot() == loader.getController()

yields false.

To use the same instance as the controller and the root, use the <fx:root> element and specify a instance of MainWindow as both root and controller:

<fx:root type="application.gui.MainWindow" xmlns:fx="http://javafx.com/fxml/1">
    <children>
        <Button onAction="#click" text="button" fx:id="button"/>
    </children>
</fx:root>
MainWindow mainWindow = new MainWindow();
FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml"));
loader.setRoot(mainWindow);
loader.setController(mainWindow);
loader.load();

... new Scene(mainWindow) ...

It may be convenient to do this from the constructor of MainWindow instead:

public MainWindow() {
    FXMLLoader loader = new FXMLLoader(MainWindow.class.getResource("MainWindow.fxml"));
    loader.setRoot(this);
    loader.setController(this);
    try {
        loader.load();
    } catch (IOException ex) {
        throw new IllegalStateException("cannot load fxml", ex); // or use a different kind of exception / add throws IOException to the signature
    }
}

This allows you to initialize+load a MainWindow using new MainWindow().

fabian
  • 80,457
  • 12
  • 86
  • 114
  • Then how on earth is it that my gui displays correctly (one instance) and the gui is correctly populated by my controller (another instance than the one in the scene)? – Troyseph Nov 08 '17 at 22:28
  • Having done what you suggest I get `javafx.fxml.LoadException: Controller value already specified.` I've specified the controller class in SceneBuilder which is the issue, however if I don't specify it, SceneBuilder cannot correctly autocomplete or suggest for me... – Troyseph Nov 08 '17 at 22:36
  • 2
    @Troyseph In your original version the `FXMLLoader` creates a new `VBox`, populates it with a new `Button`, then creates a new instance of the controller class `MainWindow`, injects the fields and sets the event handler, and returns the `VBox` instance (which contains the button). So `guiRootNode` contains the button, as desired. When you click the button, the event handler method is invoked on the *controller*, which is not the same object as `guiRootNode`. The controller instance is never added to a scene, so its scene is null. – James_D Nov 09 '17 at 11:45
  • @James_D I know I can do that, but then SceneBuilder doesn't sem to know what the controller class is, so auto-completion and such stop working, is this just an issue with SceneBuilder? – Troyseph Nov 09 '17 at 11:52
  • 1
    @Troyseph Yes, sorry, just reread your comment more carefully and you did explain that was the issue. Unfortunately, that is a limitation of Scene Builder; once you remove the `fx:controller` attribute, there is no longer any information from which Scene Builder can determine the controller class, and hence it can't do any auto completion. – James_D Nov 09 '17 at 11:57
  • 1
    @Troyseph So I guess two things. First, do you need to do it this way at all? In the code you posted, you can access the scene with `button.getScene()`, or if you explicitly want to get the scene from the root of the FXML, inject the root element into the controller (``, then `@FXML private VBox root ;` and `root.getScene()`). It's pretty unusual to make the UI root the same object as the controller; you typically want these to be different things. 2. Another workaround that lets you use `fx:controller` *and* set the controller is to use a `controllerFactory` on the. – James_D Nov 09 '17 at 13:36