1
votes

I'm trying to add a key event handler to an editable ComboBox in a simple JavaFX application. Since the Scene Builder doesn't provide access to the TextField in the ComboBox, I have to add the event handler in code.

Here is my attempt to add the handler.

Main class

package sample;

import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.stage.Stage;

public class Main extends Application {

    @FXML
    private ComboBox combo;

    @Override
    public void start(Stage primaryStage) throws Exception{
        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
        Parent root = loader.load();
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();

        Controller c = loader.getController();
        combo.getEditor().setOnKeyTyped(c::handleComboKeyPress);
    }


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

Controller class

package sample;

import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyEvent;

public class Controller {
    @FXML
    private ComboBox combo;

    public void handleComboKeyPress(KeyEvent ke)
    {
        System.out.print("key press. ");  // debugging
        String query = combo.getEditor().getText();
        System.out.println(query);   // debugging
    }
}

FXML (sample.fxml)

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.layout.Pane?>

<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="300.0" prefWidth="400.0" xmlns="http://javafx.com/javafx/10.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <children>
      <ComboBox fx:id="combo" editable="true" layoutX="75.0" layoutY="34.0" prefWidth="150.0" promptText="City, State" visibleRowCount="5" />
   </children>
</Pane>

This crashes with a null pointer exception on the last line of the start method. The problem is combo doesn't have a value yet because (I assume) the FXML loader runs in a separate thread and isn't finished by the time my code tries to call getEditor.

What is a more appropriate way to set the event handler?

Edit: added complete source code

2
Were are you defining combo and where it is initialized? - acarlstein
As Slaw suggested Main#combo is not the same as Controller#combo, Main#combo is probably null when you're attempting to set it. You need to setup a delegate on your sample.Controller to pass through to the Controller#combo.getEditor() or something along those lines. - kendavidson
@barrybrown it comes from your loader. You're loading sample.fxml which has the fx:controller set. That is the controller that gets injected and initialized. - kendavidson
The FXMLLoader is aware of the Class of your controller; in your case, from the fully qualified name specified by fx:controller. When you load the FXML file it creates an instance of the controller for you. Later, the FXMLLoader obtains the Fields of the Class and searches them for ones whose names match the fx:ids. When a match is found it attempts to reflectively set the Field to the object created from the FXML element. This injection only happens with the controller instance — the FXMLLoader is not aware of every object instance in your application. - Slaw
Note the @FXML annotation is only necessary when the field or method is non-public. The annotation is basically just telling the FXMLLoader it's okay to try and access the non-public member. Also, you hypothesized your issue was related to threads—it is not. Everything the FXMLLoader does happens on the thread that called load. In other words, the code you've shown is sequential. For some more about the fundamentals of FXML, read Introduction to FXML. - Slaw

2 Answers

1
votes

The start method needs access to the ComboBox variable declared in the Controller class. It's not sufficient to simply annotate a ComboBox variable with @FXML in the Main class.

I solved the problem by adding a getComboBox method to the Controller class. This will return the ComboBox instance referred to by the combo variable.

public ComboBox getComboBox()
{
    return combo;
}

In the Main class, use this method to get to the editor underlying the ComboBox:

Controller c = loader.getController();
c.getComboBox().getEditor().setOnTyped(c::handleComboKeyPress);

Here is the revised Main class:

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
        Parent root = loader.load();
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 400, 300));
        primaryStage.show();

        Controller c = loader.getController();
        c.getComboBox().getEditor().setOnKeyTyped(c::handleComboKeyPress);
    }

Better Solution

As suggested by @Slaw below, an alternative (and better) solution is to set the keyTyped handler in the Controller. Use an initialize method which gets automatically called by the loader if it exists.

public void initialize()
{
    combo.getEditor().setOnKeyTyped(this::handleComboKeyPress);
}

Here is the entire Controller class (minus the imports):

public class Controller {
    @FXML
    private ComboBox combo;

    public void handleComboKeyPress(KeyEvent ke)
    {
        // Do stuff
        System.out.println("key pressed.");
    }

    public void initialize()
    {
        combo.getEditor().setOnKeyTyped(this::handleComboKeyPress);
    }
}
1
votes

Yet another option is to hook the event handler directly in the fxml. Not entirely certain if this is safe in all contexts (not a fxml gal, just interested :) - but the fx:reference seems to allow access to (arbitrary?) properties of elements.

The following snippet sets the eventHandler for keyTyped to the combo's editor:

<ComboBox fx:id="combo" editable="true" />
<fx:reference source="combo.editor" onKeyTyped="#handleTyped" /> 

Which relieves the controller from coding anything except the handler method:

@FXML 
private ComboBox<String> combo;

@FXML 
private void handleTyped(KeyEvent ev) {
    System.out.println("ev: " + ev);
}

Update: this seems to not work in fx8 (thanks, Matt for the heads up!) - worksforme in fx11 (didn't test any other)