JavaFX Fundamentals & the Scene Graph

Handling a Simple Event

18 min Lesson 8 of 12

Handling a Simple Event

Every interactive GUI revolves around one central idea: something happens — the user clicks a button, presses a key, moves the mouse — and your code reacts. In JavaFX this is formalised through the event system. This lesson focuses tightly on the most common case you will write dozens of times: responding to a Button click. Once you understand how that works, every other event type follows the same pattern.

How the JavaFX Event System Works

When the user clicks a button, JavaFX constructs an ActionEvent object and delivers it to any registered event handler. An event handler is an object that implements EventHandler<T extends Event>, a functional interface with a single method:

package javafx.event; @FunctionalInterface public interface EventHandler<T extends Event> { void handle(T event); }

Because EventHandler is a functional interface, you can supply a handler as a lambda expression instead of writing an anonymous class. That is the idiomatic approach in modern JavaFX.

Registering a Handler with setOnAction

Button (and every other control that fires action events) exposes the convenience method setOnAction(EventHandler<ActionEvent> handler). You pass your lambda there and JavaFX calls it every time the button is activated — whether by mouse click, keyboard (Space/Enter), or touch.

import javafx.application.Application; import javafx.event.ActionEvent; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class ClickCounter extends Application { private int count = 0; @Override public void start(Stage primaryStage) { Label label = new Label("Clicks: 0"); Button button = new Button("Click me"); // Lambda handler: no anonymous class boilerplate needed button.setOnAction((ActionEvent event) -> { count++; label.setText("Clicks: " + count); }); VBox root = new VBox(12, label, button); root.setStyle("-fx-padding: 20; -fx-alignment: center;"); primaryStage.setScene(new Scene(root, 260, 130)); primaryStage.setTitle("Click Counter"); primaryStage.show(); } public static void main(String[] args) { launch(args); } }

Walk through the important points:

  • The lambda captures count and label from the enclosing scope. Because count is a field (not a local variable), mutation inside the lambda is perfectly legal.
  • The ActionEvent parameter is available inside the lambda but ignored here — it is useful when one handler is shared by multiple controls and you need to know which one fired.
  • Updating label.setText(...) inside the handler is safe because setOnAction handlers always execute on the JavaFX Application Thread.
The JavaFX Application Thread rule: All changes to the scene graph must happen on the JavaFX Application Thread (also called the UI thread). Event handlers fired by user interactions already run on that thread, so you can read and mutate nodes directly. You only need to think about threading when you start background work — covered in the next lesson.

Shorter Syntax: Dropping the Parameter Type

Java can infer the parameter type from context, so you can shorten the lambda:

button.setOnAction(event -> { count++; label.setText("Clicks: " + count); });

If the body is a single expression, you can drop the braces and semicolon entirely:

// Single-expression body: no braces, no semicolon button.setOnAction(event -> label.setText("Hello!"));

Using a Method Reference

When the handler logic is substantial enough to deserve its own method — or when the same logic is reused from multiple places — extract it as an instance method and pass a method reference:

import javafx.application.Application; import javafx.event.ActionEvent; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class MethodRefDemo extends Application { private int count = 0; private Label label; @Override public void start(Stage primaryStage) { label = new Label("Clicks: 0"); Button button = new Button("Click me"); button.setOnAction(this::handleClick); // method reference VBox root = new VBox(12, label, button); primaryStage.setScene(new Scene(root, 260, 130)); primaryStage.setTitle("Method Reference Demo"); primaryStage.show(); } private void handleClick(ActionEvent event) { count++; label.setText("Clicks: " + count); } public static void main(String[] args) { launch(args); } }
Prefer method references for non-trivial handlers. A lambda that exceeds three or four lines becomes hard to read inside a start() method that already constructs the scene graph. Extracting to a named method also makes it trivially testable in isolation.

One Handler, Multiple Buttons

When several buttons share the same response logic, register the same handler for all of them and use the event source to distinguish which button was clicked:

Button btnAdd = new Button("Add"); Button btnSubtract = new Button("Subtract"); Label result = new Label("0"); int[] value = { 0 }; // array trick: effectively final reference, mutable contents javafx.event.EventHandler<ActionEvent> handler = event -> { Button src = (Button) event.getSource(); if ("Add".equals(src.getText())) { value[0]++; } else { value[0]--; } result.setText(String.valueOf(value[0])); }; btnAdd.setOnAction(handler); btnSubtract.setOnAction(handler);

event.getSource() returns the Object that fired the event — cast it to Button and inspect its text, its user data (setUserData / getUserData), or its id (setId / getId) to branch your logic cleanly.

Do not compare button text to branch logic in real applications. Text changes when you internationalise the UI and will silently break your condition. Use button.getId() or a dedicated enum stored via setUserData() instead. The text comparison above is used here only for brevity.

addEventHandler vs setOnAction

The setOnAction shorthand registers exactly one handler; a second call replaces the first. When you need multiple independent handlers on the same control, use the lower-level API:

button.addEventHandler(ActionEvent.ACTION, event -> System.out.println("Handler 1")); button.addEventHandler(ActionEvent.ACTION, event -> System.out.println("Handler 2")); // Both fire on each click; neither removes the other.

To remove a specific handler later, keep a reference to the lambda (you cannot remove an anonymous lambda), or use a named method reference:

EventHandler<ActionEvent> logger = event -> System.out.println("Logging click"); button.addEventHandler(ActionEvent.ACTION, logger); // ... later: button.removeEventHandler(ActionEvent.ACTION, logger);

Summary

Handling a button click in JavaFX follows a clear three-step pattern: create the button, register a handler via setOnAction (or addEventHandler), and mutate the scene graph inside that handler. Use lambdas for concise inline logic and method references when the handler deserves a name. Because handlers fire on the JavaFX Application Thread, you can update the UI directly — no synchronisation needed. In the next lesson you will see what happens when handler work takes too long and how to offload it to a background thread without freezing the UI.