3
votes

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

1 Answers

3
votes

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().