JavaFX Controls, Layouts & FXML

Common Controls

18 min Lesson 1 of 12

Common Controls

JavaFX is built around a scene graph: every visual element is a Node, and those nodes are arranged in a tree that ends at a Stage (the OS window). Before you design anything complex you need to be comfortable with the four workhorses of every form: Button, Label, TextField, and CheckBox. This lesson examines each one — its API surface, its event model, and the patterns experienced developers use when wiring them together.

The Application Skeleton

Every JavaFX program extends javafx.application.Application and overrides start(Stage primaryStage). That method is the entry point for your UI code; the platform calls it on the JavaFX Application Thread after launching the runtime.

import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class ControlsDemo extends Application { @Override public void start(Stage stage) { VBox root = new VBox(12); // 12 px spacing between children root.setPadding(new javafx.geometry.Insets(16)); // controls added below ... Scene scene = new Scene(root, 400, 300); stage.setTitle("Controls Demo"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
The JavaFX Application Thread: All UI code — creating nodes, reading their state, modifying them — must run on the JavaFX Application Thread. If you spawn a background thread and need to update the UI from it, wrap your update in Platform.runLater(() -> { ... }).

Label

A Label is a non-interactive text node. Its primary job is to display read-only information or act as a caption for another control. You create it with a string literal or bind it to a dynamic property.

import javafx.scene.control.Label; Label greeting = new Label("Welcome to the app"); greeting.setStyle("-fx-font-size: 16px; -fx-font-weight: bold;"); // Associate the label with an input field for accessibility Label nameLabel = new Label("Full name:"); nameLabel.setLabelFor(nameField); // nameField defined below

For dynamic text you should prefer property binding over repeatedly calling setText(). For example, to show a running character count:

Label countLabel = new Label(); // Bind the label text to the length of a TextField's text property countLabel.textProperty().bind( nameField.textProperty().length().asString("%d characters") );

TextField

A TextField accepts single-line text input. The most important member is textProperty(), a StringProperty that you can observe, bind, or read at any time.

import javafx.scene.control.TextField; TextField nameField = new TextField(); nameField.setPromptText("Enter your full name"); // placeholder text nameField.setPrefWidth(280); // Read the value on demand String current = nameField.getText(); // React immediately to every keystroke nameField.textProperty().addListener((obs, oldVal, newVal) -> { System.out.println("Changed from: " + oldVal + " to: " + newVal); }); // React when the user presses Enter nameField.setOnAction(e -> System.out.println("Submitted: " + nameField.getText()));
Use the text property, not polling. Calling getText() inside a button handler is fine for small forms, but attaching a ChangeListener to textProperty() keeps your logic reactive and makes validation straightforward — you validate as the user types rather than only on submit.

If you want to restrict input to numbers you can filter the change before it is applied using a TextFormatter:

import javafx.scene.control.TextFormatter; import javafx.util.converter.IntegerStringConverter; TextFormatter<Integer> formatter = new TextFormatter<>( new IntegerStringConverter(), 0, change -> change.getControlNewText().matches("-?\\d*") ? change : null ); ageField.setTextFormatter(formatter);

Button

A Button fires an ActionEvent when clicked. You attach a handler with setOnAction(EventHandler<ActionEvent>) or, since Java 8, a lambda.

import javafx.scene.control.Button; Button submitBtn = new Button("Submit"); // Lambda handler — concise and idiomatic submitBtn.setOnAction(e -> handleSubmit()); // Default button: activated by pressing Enter anywhere in the scene submitBtn.setDefaultButton(true); // Cancel button: activated by pressing Escape Button cancelBtn = new Button("Cancel"); cancelBtn.setCancelButton(true); cancelBtn.setOnAction(e -> stage.close());

Buttons can be disabled programmatically, which is a common pattern for preventing submission before all required fields are filled:

// Disable the button whenever the name field is empty submitBtn.disableProperty().bind( nameField.textProperty().isEmpty() );
Why binding beats if-statements: When you bind disableProperty() to an expression derived from the model state, the button automatically re-enables itself as soon as the condition changes — no manual listener juggling required.

CheckBox

A CheckBox represents a boolean choice. Its key property is selectedProperty(), a BooleanProperty. By default a check box has two states: selected and unselected. If you call setAllowIndeterminate(true) it gains a third, indeterminate state (the dash shown for "some items selected" in file trees).

import javafx.scene.control.CheckBox; CheckBox agreeBox = new CheckBox("I agree to the terms and conditions"); // Read state on demand boolean agreed = agreeBox.isSelected(); // React to changes agreeBox.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> { System.out.println("Agreed: " + isNowSelected); }); // Bind button to require agreement submitBtn.disableProperty().bind( nameField.textProperty().isEmpty().or(agreeBox.selectedProperty().not()) );

Putting It Together: A Simple Registration Form

The following complete start() method wires all four controls into a working mini-form:

@Override public void start(Stage stage) { Label nameLabel = new Label("Full name:"); TextField nameField = new TextField(); nameField.setPromptText("Enter your name"); Label statusLabel = new Label(); statusLabel.textProperty().bind( nameField.textProperty().length().asString("Characters: %d") ); CheckBox agreeBox = new CheckBox("Accept terms"); Button submitBtn = new Button("Register"); submitBtn.setDefaultButton(true); submitBtn.disableProperty().bind( nameField.textProperty().isEmpty().or(agreeBox.selectedProperty().not()) ); submitBtn.setOnAction(e -> { System.out.println("Registered: " + nameField.getText()); nameField.clear(); agreeBox.setSelected(false); }); VBox root = new VBox(10, nameLabel, nameField, statusLabel, agreeBox, submitBtn); root.setPadding(new javafx.geometry.Insets(20)); stage.setScene(new Scene(root, 360, 240)); stage.setTitle("Registration"); stage.show(); }

Common Pitfalls

Never create or modify nodes off the JavaFX Application Thread. Constructing a Label inside a new Thread(() -> { ... }).start() block will silently misbehave or throw IllegalStateException. Always offload only the computation to the background thread and push the UI update back with Platform.runLater().
  • Do not call getText() in a property listener to read the new value — use the newVal parameter already provided to the listener.
  • Avoid inline CSS for anything shared — put recurring styles in an external .css file and load it with scene.getStylesheets().add(...).
  • Prefer setPromptText() over placeholder Labels — prompt text is built into TextField and disappears cleanly when the user starts typing.

Summary

The four controls covered here form the backbone of nearly every JavaFX form. Label displays text and binds to other properties. TextField captures user input and exposes a reactive textProperty(). Button triggers actions and can be enabled or disabled via binding. CheckBox models a boolean choice with an optional indeterminate state. In the next lesson you will handle richer data with ListView, TableView, and ComboBox.