Event Handling in JavaFX
Writing GUI applications requires that program control be driven by the user's interaction with the GUI. When the user moves the mouse, clicks on a button, or selects an item from a menu an "event" occurs.
The javafx.event package provides the basic framework for FX events. The Event class serves as the base class for JavaFX events. Associated with each event is an event source, an event target, and an event type.
- Source — GUI objects (e.g., a Button object) which generate an ActionEvent (e.g., when the button is clicked). The source implements the EventTarget interface.
- Listener — An object that subscribes (listens) for a particular type of event to occur. The listener must implement an interface like EventHandler or InvalidationListener1.
- Type — An additional classification that are used to specify the specific type of event. For example, a MouseEvent has the following types:
ANY
,MOUSE_CLICKED
,MOUSE_PRESSED
,MOUSE_RELEASED
,MOUSE_MOVED
,MOUSE_DRAGGED
,MOUSE_ENTERED
,MOUSE_EXITED
,MOUSE_ENTERED_TARGET
,MOUSE_EXITED_TARGET
,DRAG_DETECTED
.
The operating system or windowing system captures generated events and notifies the listener objects when a type of event they are listening for has occurred. The operating system notifies the listener object by sending a message to (calling) the event handling method. If no listener object is subscribed to listen for the event that occurred, the event is ignored.
Types of Events
There are many subclasses of the Event
class:
- ActionEvent — widely used to indicate things like when a button is pressed.
- MouseEvent — indicates mouse activity.
- DragEvent — indicates a drag-and-drop gesture.
- KeyEvent — indicates that a keystroke has occurred.
- ScrollEvent — indicates scrolling by mouse wheel, track pad, touch screen, etc...
- TouchEvent — indicates a touch screen action
EventHandler I — Separate Class
The EventHandler
interface is a functional interface with one method:
void handle(T event)
. Here we will focus on enabling functionality when
a button is pressed. In order to do this, we need to:
- Create an object that implements the
EventHandler<ActionEvent>
interface - Tell it to listen for events initiated by the button. To do this we pass the object above to the
Button.setOnAction()
method - The system notifies an event hanlder by calling the handler's
handle()
method. - The
handle()
method is passed a reference to theActionEvent
object.
Sample Code
Here is an example of a simple JavaFX application that has one button:
public class SimpleGUI extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new StackPane();
Button clickMe = new Button("Click Me");
clickMe.setOnAction(new ClickMeHandler());
root.getChildren().add(clickMe);
primaryStage.setTitle("Simple GUI");
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
}
Here we:
- Create a button that says "Click Me" on it
- Add an event handler to the button
- Add the button to a stack pane
- Set the title for the application
- Add the stack pane to a scene and add the scene to the stage
- Show the stage
The ClickMeHandler
class implements the EventHandler
class:
public class ClickMeHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
Button button = (Button)event.getSource();
if(button.getEffect()==null) {
button.setEffect(new BoxBlur());
} else {
button.setEffect(null);
}
}
}
Here we:
- The
ClickMeHandler
class implements theEventHandler
specifically forActionEvent
s - We can get the source of the event by calling
getSource()
- Since we know that the event source is a
Button
, we can cast it to aButton
- We then toggle placing a BoxBlur effect on the button
EventHandler II — Inner Class
Now let's add a label to the GUI. Instead of blurring the button, let's blur the label when the button is clicked. Now we have a problem. Can you identify the problem?2
The following code solves that problem.
public class SimpleGUI extends Application {
private Label label;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new StackPane();
label = new Label("Here is some text that can be manipulated with the button above.");
Button clickMe = new Button("Click Me");
clickMe.setOnAction(new ClickMeHandler());
root.getChildren().addAll(label, clickMe);
primaryStage.setTitle("Simple GUI");
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
private class ClickMeHandler implements EventHandler<ActionEvent> {
@Override
public void handle(ActionEvent event) {
if(label.getEffect()==null) {
label.setEffect(new BoxBlur());
} else {
label.setEffect(null);
}
}
}
}
This may seem strange, but we are actually declaring a class within another class. Here ClickMeHandler
is known as an inner class.
- An inner class is declared inside the body of the outer class, but outside of all methods.
- An inner class can access all members of the enclosing class (including those private guys).
- As a result, the
handle()
method has access to thelabel
attribute of theSimpleGUI
class. - An inner class can be private to the outside world, i.e., an instance of an inner class cannot be instantiated (created) independently.
- You can have more than one inner class in an enclosing class
EventHandler III — Anonymous Inner Class
If we had more buttons, each button could have a corresponding inner class that implements an event handler for the button. In all of these situations, it likely that we will only ever create one object from each of these inner classes. If we are only going to create one object from the class, we can make use of an anonymous inner class. Here's how:
public class SimpleGUI extends Application {
private Label label;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new StackPane();
label = new Label("Here is some text that can be manipulated with the button above.");
Button clickMe = new Button("Click Me");
clickMe.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
if(label.getEffect()==null) {
label.setEffect(new BoxBlur());
} else {
label.setEffect(null);
}
}
});
root.getChildren().addAll(label, clickMe);
primaryStage.setTitle("Simple GUI");
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
}
- Notice that here we actually define the class right where we want to use it.
- One strange thing about the syntax is that it looks like we are instantiating an interface, i.e.,
new EventHandler<ActionEvent>()
. - Notice that the
{ ... }
block immediately following is the implementation of the inner class.
EventHandler IV — Lambda Expression
Hopefully the above discussion has helped you get a better understanding of the different approaches to handling events. The following sample is similar to the one in the previous section, but here we replace the anonymous inner class with a lambda expression.
public class SimpleGUI extends Application {
private Label label;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new StackPane();
label = new Label("Here is some text that can be manipulated with the button above.");
Button clickMe = new Button("Click Me");
clickMe.setOnAction(event -> {
if(label.getEffect()==null) {
label.setEffect(new BoxBlur());
} else {
label.setEffect(null);
}
});
root.getChildren().addAll(label, clickMe);
primaryStage.setTitle("Simple GUI");
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
}
- The
EventHandler
is a functional interface, i.e., it has only one method. - Lambda expressions are a new syntax introduced in Java 8 that allows one to treat functionality as a method argument.
- Functional interfaces can be represented with a lambda expression.
Grand Finale
In order to separate the code associated with handling the event, I recommend extracting it into a helper method. The above class can be rewritten as:
public class SimpleGUI extends Application {
private Label label;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
Pane root = new StackPane();
label = new Label("Here is some text that can be manipulated with the button above.");
Button clickMe = new Button("Click Me");
clickMe.setOnAction(event -> handleClickMe(event));
root.getChildren().addAll(label, clickMe);
primaryStage.setTitle("Simple GUI");
primaryStage.setScene(new Scene(root, 400, 300));
primaryStage.show();
}
private void handleClickMe(ActionEvent event) {
if(label.getEffect()==null) {
label.setEffect(new BoxBlur());
} else {
label.setEffect(null);
}
}
}
If the lambda expression is just a call to another method, we can replace the lambda expression with a method reference. Here we would replace:
clickMe.setOnAction(event -> handleClickMe(event));
with:
clickMe.setOnAction(this::handleClickMe);
which makes the handleClickMe(ActionEvent event)
method get called whenever
the button generates an ActionEvent
.
The InvalidationListener
is used for observable controls like
sliders, menus, etc.. The ChangeListener interface may be used
instead of the InvalidationListener
if, in the listener, you
need to know the new value of the item being observered.
One of the problems with the approach taken in EventHandling I is that the event handler object does not have access to any GUI elements except the button (since we can get it by calling getSource()
).