JavaFX’s user interfaces are fuelled by events. In fact, almost every action a user takes in a user interface will trigger an Event, which in turn will fire some executable code. To make a button do something in JavaFX you need to define the executable code usually by using a convenience method like setOnAction().

Then, when the button is clicked, JavaFX does the heavy lifting of fetching that pre-defined code, and executing it on the JavaFX Application thread.

Button events are pre-defined code that's executed when a button is clicked

It’s worth bearing in mind, there’s no guarantee your code will be executed immediately, but the delay between a user clicking a button and the action being completed is usually really short (we’re talking milliseconds). For most simple user interfaces, keeping your UI snappy won’t be an issue. If you’re experiencing some lag, you might want to consider outsourcing some thread work to background processes.

In JavaFX, a button event should be defined either by invoking the setOnAction() convenience method of a Button object, or by defining the onAction attribute of the Button object in an FXML document and linking it to a method that accepts an ActionEvent object as a parameter.

I’ll walk you through both ways now.

1. Using Java Code to define button events

If you have access to your Button in Java code, you can use the setOnAction() convenience method to define what happens when the button gets pressed. You can do this by defining an EventHandler<ActionEvent> object (which might sound terrifying if you’re new to Java or JavaFX). The good news is you can define the button’s effect using a lambda instead.

Here’s an example app using the EventHandler<ActionEvent> approach. Below, I’ll go through using the lambda approach.

package com.edencoding;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ButtonEventExampleApp extends Application{
    @Override
    public void start(Stage primaryStage) throws Exception {
        //create Scene Graph
        Button button = new Button("Press me!");
        StackPane root = new StackPane(button);
        Scene scene = new Scene(root, 300, 250);
        //Create Window
        primaryStage.setTitle("Button Example App");
        primaryStage.setScene(scene);
        primaryStage.show();
        //Define Button Action
        Label label = new Label("Pressed!");
        button.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                root.getChildren().setAll(label);
            }
        });
    }
}

Button setOnAction using a lambda

Instead, it’s much more common to define button actions using a lambda.

A button action can be defined using a lambda expression by providing a single argument (the event) as the lambda’s input and the executable code as either an expression or a statement block between curly braces.

In this case, we’ll define it using an expression.

package com.edencoding;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ButtonEventExampleApp extends Application{
    @Override
    public void start(Stage primaryStage) throws Exception {
        //create Scene Graph
        Button button = new Button("Press me!");
        StackPane root = new StackPane(button);
        Scene scene = new Scene(root, 300, 250);
        
        //Create Window
        primaryStage.setTitle("Button Example App");
        primaryStage.setScene(scene);
        primaryStage.show();
        //Define Button Action
        Label label = new Label("Pressed!");
        button.setOnAction(event -> root.getChildren().setAll(label));
    }
}

2. Using an FXML document to define button events

It can be more efficient to use the onAction attribute of the Button object. Especially if you’re using the FXML document-loading strategy of JavaFX’s MVC-oriented design.

Using the FXML approach, JavaFX does the heavy lifting of linking the button with the method in the Controller that’s going to define the button’s effect, and you don’t have to worry about EventHandler objects at all.

To set this up, you’ll need to load the FXML document using the FXMLLoader during app initialisation. The code to do this is in the drop down, but if you’re already using FXML documents, chances are you know how to create a view using an FXML file.

Once you have your main App code sorted, you just need to define two elements to create your button action:

  1. An onAction attribute in the Button object in the FXML file
  2. A method in the Controller that takes an ActionEvent as an argument and returns void.

FXML Document

<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.Label?>
<StackPane xmlns:fx="http://javafx.com/fxml/1" prefHeight="250.0" prefWidth="300.0"
           fx:controller="com.edencoding.controllers.Controller">
    <Button fx:id="button" text="Press Me!" onAction="#handleButtonPress"/>
    <Label fx:id="label" text="Pressed!" visible="false"/>
</StackPane>

Note you also need to define the link to the Controller using the fx:controller attribute in the root node of the View.

In this View, that’s the StackPane.

Java Controller

package com.edencoding.controllers;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
public class Controller {

    @FXML private Label label;
    @FXML private Button button;

    public void handleButtonPress(ActionEvent event) {
        button.setVisible(false);
        label.setVisible(true);
    }
}

If you haven’t used FXML documents before, and youre interested in how to do it properly, check out this article on full-MVC structure with JavaFX. It takes you through structure, code and a fully-coded up example of how to get FXML-based MVC running in JavaFX.

Should I use FXML or Java code?

Whether to use FXML or Java code probably depends on what you want to do, and the size of the application you want to create.

Generally speaking, the larger your application becomes, the more beneficial loading from FXML documents can be. I find that the streamlined view generation and MVC architecture enabled by FXML documents usually generates more slimmed-down, efficient code. And of course when it comes to button events, there’s no need to use Event Handlers (you just define a normal method in the Controller and JavaFX handles the rest!)

In smaller applications, there are more documents to handle when using the FXML approach (an extra FXML document and a Controller), which can seem more hassle than it’s worth. Plus, not everything is in Java code.

The next section’s a great example of this. Below, I want to take you through how to fire multiple events using a Button’s single click action. In each case, the example’s provided using Java-only code because it means a single copy-paste class which can run straight from your IDE.

Firing multiple events from a single button click

Sometimes, you just want to get more stuff done, just not all at once. Here are a few situations where you might find yourself needing to complete multiple actions from a single button click:

  1. Making multiple changes to the Model or View (e.g. loading a confirmation that the user’s action has been completed while updating the Model (underlying data) based on the user’s actions.
  2. The button click starts an asynchronous process of loading data and you want to fire an additional event when that process is complete.
  3. Creating some delayed effect that spawns another event at the end of it.

Making multiple changes to the Model or View

If you want to make more changes to your View or Model, some or all of which can be achieved within the Controller, you simply need to nest that code within the lambda expression.

Usually, if you’re intending to make changes to the Model, the Controller should invoke those methods in the Model to enable those changes to take place. This would be where that happens.

Java Code

public class ButtonEventExampleApp extends Application{
    Button button;
    Label label = new Label("Pressed!");
    StackPane root;
    @Override
    public void start(Stage primaryStage) throws Exception {
        //create Scene Graph
        button = new Button("Press me!");
        root = new StackPane(button);
        Scene scene = new Scene(root, 300, 250);
        //Create Window
        primaryStage.setTitle("Button Example App");
        primaryStage.setScene(scene);
        primaryStage.show();
        //Define Button Action
        button.setOnAction(event -> {
            updateButtonToLabel();
            updateUserOnActionResults();
        });
    }
    public void updateButtonToLabel(){
        Label label = new Label("Pressed!");
        root.getChildren().setAll(label);
    }
    public void updateUserOnActionResults(){
        Label statusLabel = new Label("Button clicked and removed!");
        root.getChildren().add(statusLabel);
        StackPane.setAlignment(statusLabel, Pos.BOTTOM_CENTER);
    }
}

Result

Starting an asynchronous process

Often, it’s easier (for structural reasons) to design an application that separates key resources from the UI code. One examples of this is interacting with a REST API or connecting to a database from JavaFX.

In these cases, you can’t immediately update the Model or View, but you do want to keep track of the task’s progress. JavaFX provides support for this with the Service and Task classes. Using a Service and Task, you can offload work onto a separate thread, and then fire another event once the asynchronous task has completed.

The JavaFX task object can be outsourced onto another Thread with a Service or ScheduledService

Java code

package com.edencoding;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ButtonEventExampleApp extends Application{
    Button button;
    Label label = new Label("Pressed!");
    StackPane root;
    @Override
    public void start(Stage primaryStage) throws Exception {
        //create Scene Graph
        button = new Button("Press me!");
        root = new StackPane(button);
        Scene scene = new Scene(root, 300, 250);
        //Create Window
        primaryStage.setTitle("Button Example App");
        primaryStage.setScene(scene);
        primaryStage.show();
        //Define Button Action
        button.setOnAction(event -> {
            performAsynchronousTask();
        });
    }
    private void performAsynchronousTask() {
        Service<String> service = new Service<>() {
            @Override
            protected Task<String> createTask() {
                return new Task<>() {
                    @Override
                    protected String call() throws Exception {
                        updateProgress(0, 5);
                        for (int i = 1; i <= 5; i++) {
                            Thread.sleep(1000);
                            updateProgress(i, 5);
                        }
                        return "Complete!";
                    }
                };
            }
        };
        button.textProperty().bind(
                Bindings.concat("Progress: ", service.progressProperty().multiply(100), " %")
        );
        service.setOnSucceeded(event -> {
            button.textProperty().unbind();
            updateLabelComplete();
        });
        service.start();
    }
    private void updateLabelComplete(){
        button.setVisible(false);
        label.setText("Complete!");
        root.getChildren().setAll(label);
    }

}

Result

Creating a delayed event from a button click

Finally, if you want to create a delayed effect, the PauseTransition is the object you’ll need. This transition is designed to take no action (but also not hang the thread) for a defined period of time, and allows you to define an event to fire once it’s done.

In this case, we’ll just wait for 5 seconds, although you could accomplish other things. Gmail is famous for having implemented this, allowing you to “un-send” emails for 5 seconds after you’ve clicked “Send.” Google isn’t actually “un-sending” your email. Instead, Gmail creates an Undo button and then just waits for 5 seconds before sending it, just to make sure you’re not having second thoughts.

Java code

package com.edencoding;
import javafx.animation.PauseTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Duration;
public class ButtonEventExampleApp extends Application{
    Button button;
    Label label = new Label("Pressed!");
    StackPane root;
    @Override
    public void start(Stage primaryStage) throws Exception {
        //create Scene Graph
        button = new Button("Press me!");
        root = new StackPane(button);
        Scene scene = new Scene(root, 300, 250);
        //Create Window
        primaryStage.setTitle("Button Example App");
        primaryStage.setScene(scene);
        primaryStage.show();
        //Define Button Action
        button.setOnAction(event -> {
            pauseFor5SecondsThenUpdateLabel();
        });
    }
    private void pauseFor5SecondsThenUpdateLabel() {
        //set up pause transition
        PauseTransition shortPause = new PauseTransition(Duration.seconds(5));
        shortPause.setOnFinished(event -> updateLabelComplete());
        //update label
        button.setVisible(false);
        label.setText("Pausing for 5 seconds...");
        root.getChildren().setAll(label);
        //start pause transition
        shortPause.playFromStart();
    }
    private void updateLabelComplete(){
        label.setText("Pause Complete!");
    }
}

Result

Conclusions

Creating button events is really simple. By invoking setOnAction, or onAction (Java code or FXML documents respectively), you can quickly connect a button click with a chunk of executable code.

JavaFX itself has a lot more support for events than this. In fact, you can fire events based on users interactions with almost any node. You can define events based on interactions from screen gestures to mouse movements, and each one can be set individually.

Given the massive amount you can do with events, they can seem a little complex and overwhelming. If you’re interested in learning more about what they are and how to deploy them, here’s a complete guide.