Search code examples
javafxkeyevent

Where to assign KeyEvent in Controller class in JavaFX


I'm trying to assign a Keyboard event to a specific keypress on a specific scene. This scene is loaded when a button is clicked on a previous scene.

My problem is: where would I assign this event to the keypress? I've tried implementing the Initializable interface to the controller class of said Scene, but when I try assigning the event in the initialize() method as shown in the code below, I get a null pointer exception, as the Scene is, apparently, null.

    @Override
public void initialize(URL arg0, ResourceBundle arg1) {
    this.pane.getScene().setOnKeyPressed(e -> {
        if(e.getCode()==KeyCode.E) {
            shoot();
        }
    });
}

If I tried doing this, instead

    @Override
public void initialize(URL arg0, ResourceBundle arg1) {
    this.pane.setOnKeyPressed(e -> {
        if(e.getCode()==KeyCode.E) {
            shoot();
        }
    });
}

The scene would load, but nothing would happen on pressing the E key.

The scene was built with SceneBuilder.


Solution

  • It's usually best to set key event listeners on the Scene. Note that the controller's initialize() method is called during the FXMLLoader.load(...) invocation, which is, of course, before the UI defined in the FXML file is added to the scene. Hence a call to getScene() in the initialize() method will return null.

    The simplest approach is probably just to register the key event listener on the scene at the point in the code where you load the FXML (typically here you will have a reference to the Scene), and call a method on the controller:

    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    
    public class App extends Application {
    
    
        @Override
        public void start(Stage stage) throws IOException {
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("Game.fxml"));
            Parent root = fxmlLoader.load();
            GameController controller = fxmlLoader.getController();
            Scene scene = new Scene(root);
            
            scene.setOnKeyPressed(e -> controller.shoot());
            
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    
    }
    

    where GameController is

    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.fxml.FXML;
    import javafx.scene.shape.Circle;
    import javafx.util.Duration;
    
    public class GameController {
    
        @FXML
        private Circle circle ;
        
        public void shoot() {
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.ZERO, new KeyValue(circle.radiusProperty(), 0)),
                    new KeyFrame(Duration.seconds(0.5), new KeyValue(circle.radiusProperty(), 200))
            );
            timeline.setOnFinished(e -> circle.setRadius(0));
            timeline.play();
        }
    }
    

    and, for completeness, Game.fxml is

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.Pane?>
    <?import javafx.scene.shape.Circle?>
    <?import javafx.scene.control.Button?>
    <?import javafx.geometry.Insets?>
    
    <Pane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" prefWidth="600" prefHeight="600" fx:controller="org.jamesd.examples.keyeventexample.GameController">
       <Circle fx:id="circle" centerX="300" centerY="300" radius="0" fill="coral"/>
    </Pane>
    

    If you prefer to keep everything in the controller, you can do a bit of fairly ugly work where you observe the current scene, adding the handler when a new scene is set. (For completeness I remove the handler if the UI is removed from a scene, which is not common.)

    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.event.EventHandler;
    import javafx.fxml.FXML;
    import javafx.scene.input.KeyEvent;
    import javafx.scene.shape.Circle;
    import javafx.util.Duration;
    
    public class GameController {
    
        @FXML
        private Circle circle ;
        
        public void initialize() {
            EventHandler<KeyEvent> keyPressListener = e -> shoot();
            
            circle.sceneProperty().addListener((obs, oldScene, newScene) -> {
                if (oldScene != null) {
                    oldScene.removeEventHandler(KeyEvent.KEY_PRESSED, keyPressListener);
                }
                if (newScene != null) {
                    newScene.addEventHandler(KeyEvent.KEY_PRESSED, keyPressListener);
                }
            });
        }
        
        public void shoot() {
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.ZERO, new KeyValue(circle.radiusProperty(), 0)),
                    new KeyFrame(Duration.seconds(0.5), new KeyValue(circle.radiusProperty(), 200))
            );
            timeline.setOnFinished(e -> circle.setRadius(0));
            timeline.play();
        }
    }
    

    and then of course your FXML-loading code doesn't have any extra work to do

    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    import java.io.IOException;
    
    public class App extends Application {
    
    
        @Override
        public void start(Stage stage) throws IOException {
            Parent root = FXMLLoader.load(getClass().getResource("Game.fxml"));
            Scene scene = new Scene(root);        
            stage.setScene(scene);
            stage.show();
        }
    
        public static void main(String[] args) {
            launch();
        }
    
    }