2

I built a small application in JavaFX yesterday. I wanted to get the Scene of the application in the Controller class. I got an error every time I tried to get the scene in the controller class. I could set the OnKeyPressed-method on a Button in the Controller class, works fine. But it works only fine if the Button is selected.. I can get the scene in the Main-class method replaceSceneContent only. I have read this question already, but I call the getScene()-method in the initialize-method?? Thanks for any ideas!

Main class:

public class Main extends Application {

private Stage stage;

public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {
    stage = primaryStage;
    gotoMenu();
    primaryStage.show();
}

public void gotoMenu() {
    try {
        MenuController menu = new MenuController();
        menu = (MenuController) replaceSceneContent("Menu.fxml");
        menu.setApp(this);
    } catch (Exception ex) {
        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private Node replaceSceneContent(String fxml) throws Exception {
    FXMLLoader loader = new FXMLLoader();
    @SuppressWarnings("resource")
    InputStream in = Main.class.getResourceAsStream(fxml);
    loader.setBuilderFactory(new JavaFXBuilderFactory());
    loader.setLocation(Main.class.getResource(fxml));
    BorderPane page;
    try {
        page = (BorderPane) loader.load(in);
    } finally {
        in.close();
    }
    page.setOnKeyPressed(event -> {
        switch (event.getCode()) {
        case F11:
            if (stage.isFullScreen()) {
                stage.setFullScreen(false);
            } else {
                stage.setFullScreen(true);
            }
            break;
        default:
            break;
        }
    });
    Scene scene = new Scene(page);
    page.prefWidthProperty().bind(scene.widthProperty());
    page.prefHeightProperty().bind(scene.heightProperty());
    stage.setScene(scene);
    return (Node) loader.getController();
}}

Controller class:

public class MenuController extends BorderPane implements Initializable {

Main application;

@FXML
private Button button;

public void setApp (Main application) {
    this.application = application;
}

@Override
public void initialize(URL location, ResourceBundle resources) {
    button.getScene().setOnKeyPressed(e -> {
        switch(e.getCode()) {
        case A:
            System.out.println("A pressed!");
            break;
        default:
            break;
        }
    });
}}}
  • 1
    The `initialize()` method is called during the call to `loader.load(...)`, which is (necessarily) before the UI defined in the FXML is placed in a scene. – James_D Apr 21 '17 at 12:32
  • (As an aside, you should not both set the location *and* provide an input stream to the loader. Remove the input stream `in` entirely, and use the no-argument load method: `page = loader.load()`.) – James_D Apr 21 '17 at 12:35
  • 1
    1. Is there a method called after the UI placed the FXML? – GiantSunflower Apr 21 '17 at 15:17
  • 2. I tried to use the same code to load the FXML as in the SceneBuilder examples there: [link](http://www.oracle.com/technetwork/java/javafxscenebuilder-1x-archive-2199384.html), ist the code there deprecated? – GiantSunflower Apr 21 '17 at 15:17
  • 1. No. The UI is placed in the scene - and consequently the button's `scene` property becomes non-null, when you call `new Scene(page)`. The controller knows nothing about the scene, so if you want an event handler on the scene, you should register it in the method where you create the scene. – James_D Apr 21 '17 at 15:22
  • 2. Huh. Those examples do load the FXML like that. It's strange and I can see absolutely no reason to do that (apart from anything you become responsible for the input stream, instead of delegating that responsibility to the `FXMLLoader`, as is appropriate). All I can suggest is that those examples are *very* old... I would not use the stream at all in my own code. It makes even less sense, btw, that the controllers are subclasses of a node class (`BorderPane` in your example). That actually completely violates the MVC pattern. You should probably look for better examples. – James_D Apr 21 '17 at 15:28
  • 1. So there is no way to get the scene in the Controller, good to know.. I've deleted the InputStream:) Thanks James! – GiantSunflower Apr 21 '17 at 16:13
  • In theory, you could observe the button's `sceneProperty` and register a listener when it changes, but that just feels like a poor design, and the code would likely get complex. The bottom line is that attributes of the scene should be managed at the point where the scene is created and used. – James_D Apr 21 '17 at 16:15

2 Answers2

1

You can do

private Node replaceSceneContent(String fxml) throws Exception {
    FXMLLoader loader = new FXMLLoader();
    loader.setBuilderFactory(new JavaFXBuilderFactory());
    loader.setLocation(Main.class.getResource(fxml));

    BorderPane page = loader.load();
    MenuController controller = loader.getController();

    page.setOnKeyPressed(event -> {
        switch (event.getCode()) {
        case F11:
            if (stage.isFullScreen()) {
                stage.setFullScreen(false);
            } else {
                stage.setFullScreen(true);
            }
            break;
        default:
            break;
        }
    });
    Scene scene = new Scene(page);
    scene.setOnKeyPressed(event -> {
        if (event.getCode() == KeyCode.A) {
            controller.printA();
        }
    });


    page.prefWidthProperty().bind(scene.widthProperty());
    page.prefHeightProperty().bind(scene.heightProperty());
    stage.setScene(scene);
    return controller ;
}

with

public class MenuController extends BorderPane{

    // existing code...

    public void printA() {
        System.out.println("A!");
    }

}

Just a comment: it makes absolutely no sense for MenuController to be a subclass of BorderPane (or any other UI class). I left that in, in case you need it elsewhere, but it completely violates the MVC pattern.

Additionally, I'm not really sure why you want the key handler for A to be on the scene, and the key handler for F11 to be on the root of the scene. It seems these should both be registered with the scene. But again, I left it as you had it in the question.

James_D
  • 201,275
  • 16
  • 291
  • 322
  • I'll create some additonal controllers with fxml-files in the future. The replaceSceneContent-method returned a node in my code. I need a class which accepts this returned value. But that code is the same as in the oracle examples, too. Just wanted a working app :( Your code looks good, I'll make it compatible to diff controllers. Thanks! I made two switch statements because of multiple Controllers with different keyboard configurations in the future. F11 makes sense everywhere, but "a" maybe just in that controller. Very fast support here :) – GiantSunflower Apr 21 '17 at 19:41
0

Here's version 1.1 from my app. I added an if-clause to the setOnKeyPressed event handler. After the initialization of the controller is complete, a method turns the boolean controllerRunning to true. Finally I removed the InputStream, it's not needed.

If somebody needs an example:

Main class:

public class Main extends Application {

private Stage stage;
private boolean controllerRunning = false;
MenuController menu;

public void setControllerRunning(boolean controllerRunning) {
    this.controllerRunning = controllerRunning;
}

public static void main(String[] args) {
    launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {
    stage = primaryStage;
    gotoMenu();
    primaryStage.show();
}

public void gotoMenu() {
    try {
        menu = new MenuController();
        menu = (MenuController) replaceSceneContent("Menu.fxml");
        menu.setApp(this);
        menu.keyFunctions();
    } catch (Exception ex) {
        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
    }
}

private Node replaceSceneContent(String fxml) throws Exception {
    FXMLLoader loader = new FXMLLoader();
    loader.setBuilderFactory(new JavaFXBuilderFactory());
    loader.setLocation(Main.class.getResource(fxml));
    BorderPane page;
    try {
        page = (BorderPane) loader.load();
    } finally {
    }
    page.setOnKeyPressed(event -> {
        if (controllerRunning) {
            switch (event.getCode()) {
            case A:
                menu.printA();
                break;
            default:
                break;
            }
        }
        switch (event.getCode()) {
        case F11:
            if (stage.isFullScreen()) {
                stage.setFullScreen(false);
            } else {
                stage.setFullScreen(true);
            }
            break;
        default:
            break;
        }
    });
    Scene scene = new Scene(page);
    page.prefWidthProperty().bind(scene.widthProperty());
    page.prefHeightProperty().bind(scene.heightProperty());
    stage.setScene(scene);
    return (Node) loader.getController();
}}

controller class:

public class MenuController extends BorderPane{

Main application;

@FXML
private Button button;

public void setApp (Main application) {
    this.application = application;
}

public void keyFunctions() {
    application.setControllerRunning(true);
}
public void printA() {
    System.out.println("A!");
}
}