Lesson 43 • Advanced
JavaFX – Desktop GUIs
Move from console output to real windows. By the end of this lesson you'll be able to open a JavaFX window, lay out controls, respond to clicks, keep your UI in sync with property bindings, and style it all with CSS.
What You'll Learn in This Lesson
- ✓Extend Application and open a window via start(), Stage, and Scene
- ✓Arrange controls with VBox, HBox, BorderPane, and GridPane
- ✓Add Button, Label, and TextField controls to a screen
- ✓Run code on clicks with setOnAction event handlers
- ✓Keep the UI in sync automatically with properties and bindings
- ✓Separate UI from logic with FXML + controllers, and style with CSS
Before You Start
You should be comfortable with classes and objects (OOP) — JavaFX is built from classes you instantiate and extend — and with lambda expressions, which you'll use for event handlers. To run these examples you'll add the JavaFX libraries with Maven or Gradle.
A Real-World Analogy: A Theatre Production
A JavaFX app is like staging a play. The Stage is the physical window — the building the audience walks into. The Scene is the set for the current act: everything currently on display. The Nodes are the actors and props — your buttons, labels, and text fields.
Just as a theatre swaps the set between acts while the building stays put, you can swap a new Scene onto the same Stage to move from a login screen to a dashboard. The layout panes are the stage directions that tell each actor where to stand.
1️⃣ Your First Window: Application, start, Stage, Scene
Every JavaFX app is a class that extends Application. You override one method, start(Stage stage) — JavaFX calls it for you once the runtime is ready, handing you the primary Stage (the window).
Inside start you build a tree of Nodes (the visible controls), put them in a Scene (which sets the window size), give the Scene to the Stage, and call stage.show(). In main you call launch(args), which boots the JavaFX runtime and then calls your start.
💡 The chain to remember: launch() → start(Stage) → build Nodes → wrap in a Scene → stage.setScene(scene) → stage.show().
import javafx.application.Application; // every JavaFX app extends this
import javafx.scene.Scene; // the content inside a window
import javafx.scene.control.Label; // a piece of read-only text
import javafx.scene.layout.StackPane; // a layout that centres its child
import javafx.stage.Stage; // the window itself
public class Main extends Application {
@Override
public void start(Stage stage) { // JavaFX calls this for you on startup
// 1) A control (Node) — the thing the user sees
Label hello = new Label("Hello, JavaFX!");
// 2) A layout pane holds Nodes; StackPane centres its single child
StackPane root = new StackPane(hello);
// 3) A Scene wraps the root and sets the window size (width x height)
Scene scene = new Scene(root, 320, 200); // 320 wide, 200 tall
// 4) The Stage IS the window — give it the scene, a title, show it
stage.setScene(scene);
stage.setTitle("My First Window");
stage.show(); // nothing appears until you call show()
}
public static void main(String[] args) {
launch(args); // boots the JavaFX runtime, then calls start() above
}
}A 320x200 desktop window titled "My First Window"
opens with the text "Hello, JavaFX!" centred in the middle.org.openjfx:javafx-controls via Maven or Gradle).2️⃣ Layout Panes — Arranging Your Controls
You rarely position controls by pixel coordinates. Instead you drop them into a layout pane that arranges them for you and reflows when the window resizes. Four panes cover almost everything:
| Pane | Arranges children… | Use it for |
|---|---|---|
| VBox | In a vertical column | Stacked forms, menus |
| HBox | In a horizontal row | Button bars, toolbars |
| GridPane | In rows and columns | Labelled input forms |
| BorderPane | Into top/bottom/left/right/centre | Whole-app shells |
Panes nest freely: a BorderPane can hold a VBox in its centre, which holds a GridPane and an HBox. That's how real layouts are built — small panes inside bigger ones.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
// GridPane = rows & columns. add(node, column, row) places each cell.
GridPane form = new GridPane();
form.setHgap(8); // horizontal gap between columns
form.setVgap(8); // vertical gap between rows
form.add(new Label("Email:"), 0, 0); // column 0, row 0
form.add(new TextField(), 1, 0); // column 1, row 0
form.add(new Label("Password:"), 0, 1); // column 0, row 1
form.add(new TextField(), 1, 1); // column 1, row 1
// HBox = one horizontal row of nodes (spacing of 10px between them).
HBox buttons = new HBox(10, new Button("Sign In"), new Button("Cancel"));
buttons.setAlignment(Pos.CENTER);
// VBox = a vertical stack. This becomes the centre of a BorderPane.
VBox centre = new VBox(12, form, buttons);
centre.setAlignment(Pos.CENTER);
centre.setPadding(new Insets(20)); // 20px of breathing room all round
// BorderPane has 5 regions: top, bottom, left, right, centre.
BorderPane root = new BorderPane();
root.setTop(new Label(" Welcome Back")); // header strip along the top
root.setCenter(centre); // form sits in the middle
stage.setScene(new Scene(root, 380, 260));
stage.setTitle("Login");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}A 380x260 "Login" window. "Welcome Back" sits along the top.
In the centre, a 2x2 grid lines up the Email and Password labels
beside their text fields, with "Sign In" and "Cancel" buttons
centred in a row underneath.3️⃣ Controls, Events, and Property Bindings
Controls are the interactive Nodes: a Button the user clicks, a Label that shows text, a TextField they type into. You make code run on a click with an event handler — pass a lambda to setOnAction.
A property is a value the UI can watch — like SimpleIntegerProperty. Binding ties one property to another: when the source changes, the target updates by itself. Bind a Label's textProperty() to a counter and you never touch the label again — it follows the number for you. That's reactive UI: describe the relationship once, and JavaFX maintains it.
💡 Analogy: A binding is a linked spreadsheet cell. Write =A1 in cell B1 and B1 mirrors A1 forever — you don't re-copy it every time A1 changes. label.textProperty().bind(count.asString()) is the same idea.
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty; // an observable int
import javafx.geometry.Insets;
import javafx.geometry.Pos;
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 Main extends Application {
@Override
public void start(Stage stage) {
// A property is a value the UI can WATCH. When it changes, anything
// bound to it updates automatically — no manual UI refresh needed.
SimpleIntegerProperty count = new SimpleIntegerProperty(0);
Label display = new Label();
// One-way binding: the label's text always follows the count value.
display.textProperty().bind(count.asString("Clicks: %d"));
Button button = new Button("Click me");
// Event handler: a lambda that runs every time the button is pressed.
button.setOnAction(e -> count.set(count.get() + 1)); // count++ -> label updates
VBox root = new VBox(12, display, button);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
stage.setScene(new Scene(root, 260, 160));
stage.setTitle("Counter");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}A 260x160 "Counter" window shows "Clicks: 0" above a
"Click me" button. Each press bumps the label — "Clicks: 1",
"Clicks: 2", ... — with no code touching the label directly.
The binding keeps it in sync.display.setText(); the binding does it.🎯 Your Turn #1 — A Name Greeter
Fill in the two blanks marked ___. You'll create a Button and set a label's text from a handler. The hints (👉) tell you exactly what goes in each gap.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
// 🎯 YOUR TURN — build a tiny "greeter": type a name, click, get a hello.
TextField nameField = new TextField();
nameField.setPromptText("Type your name");
Label greeting = new Label("Waiting...");
// 1) Make a button labelled "Greet"
Button greet = ___; // 👉 new Button("Greet")
// 2) When clicked, set the greeting label to "Hello, <name>!"
greet.setOnAction(e -> {
String name = nameField.getText();
greeting.setText(___); // 👉 "Hello, " + name + "!"
});
VBox root = new VBox(10, nameField, greet, greeting);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(20));
stage.setScene(new Scene(root, 280, 180));
stage.setTitle("Greeter");
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
// ✅ Expected: type "Sam", click Greet, the label reads: Hello, Sam!A 280x180 "Greeter" window: a text field, a "Greet" button,
and a label. Type "Sam", click Greet, and the label changes
from "Waiting..." to "Hello, Sam!".4️⃣ FXML and Controllers — Separating UI from Logic
Building the whole UI in Java works, but it mixes layout with behaviour. FXML is an XML file that describes the UI on its own — which panes and controls exist, and how they nest. A separate Controller class holds the behaviour.
Two attributes connect the FXML to the controller: fx:id="emailField" in the FXML matches an @FXML field of the same name, and onAction="#handleLogin" matches an @FXML method. FXMLLoader.load(...) reads the file, builds the Nodes, and injects them into the controller. The free Scene Builder tool lets you edit FXML by dragging.
🎯 Your Turn #2 — Wire FXML to a Controller
Connect the FXML to the controller below by filling the two blanks: give the Label an fx:id and point the Button's onAction at the controller method.
<!-- counter.fxml — describe the UI; the controller supplies the behaviour -->
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml"
fx:controller="com.app.CounterController"
spacing="12" alignment="CENTER">
<!-- 🎯 YOUR TURN — wire this FXML to the controller below -->
<!-- 1) Give this Label an fx:id so the controller can reach it -->
<Label ___="display" text="Clicks: 0"/> <!-- 👉 fx:id -->
<!-- 2) Point the button's onAction at the controller's method -->
<Button text="Click me" onAction="___"/> <!-- 👉 #handleClick -->
</VBox>
/* ── CounterController.java ── */
public class CounterController {
@FXML private Label display; // matches fx:id="display" above
private int count = 0;
@FXML
private void handleClick(ActionEvent e) { // matches onAction="#handleClick"
count++;
display.setText("Clicks: " + count);
}
}
// ✅ Expected: FXMLLoader injects "display", clicks call handleClick,
// and the label counts up: Clicks: 1, Clicks: 2, ...Once fx:id="display" and onAction="#handleClick" are filled in,
FXMLLoader connects the @FXML Label and method. Each button press
runs handleClick and updates the label: Clicks: 1, Clicks: 2, ...FXMLLoader.load(getClass().getResource("counter.fxml")) and run with a local JDK + JavaFX SDK.5️⃣ Styling with CSS
JavaFX is styled with CSS that looks like the web's — selectors, pseudo-classes like :hover and :focused — but every property name starts with -fx- (so background-color becomes -fx-background-color).
You target nodes by type (.button), by style class you assign with getStyleClass().add("btn-primary") or styleClass in FXML, or by id (#title). Attach the stylesheet with scene.getStylesheets().add(...).
/* style.css — JavaFX CSS looks like web CSS but every property is -fx- */
.root {
-fx-font-family: "Segoe UI";
-fx-font-size: 14px;
-fx-background-color: #f8fafc;
}
#title { /* targets a node with id "title" (setId/fx:id) */
-fx-font-size: 24px;
-fx-font-weight: bold;
}
.btn-primary { /* targets nodes with styleClass "btn-primary" */
-fx-background-color: #3b82f6;
-fx-text-fill: white;
-fx-background-radius: 6;
-fx-padding: 8 16;
}
.btn-primary:hover { /* pseudo-classes work just like the web */
-fx-background-color: #2563eb;
}
.text-field:focused {
-fx-border-color: #3b82f6;
}Applied with scene.getStylesheets().add(...): the title becomes
24px bold, primary buttons turn blue with rounded corners and
darken on hover, and a focused text field gets a blue border.style.css next to your code and load it with scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm()).🧩 Mini-Challenge — Temperature Converter
Now with the scaffolding removed. The starter has only a comment outline — you write the controls, the layout, and the click handler yourself. Use the worked examples above as a reference, and check your result against the expected output.
import javafx.application.Application;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage stage) {
// 🎯 MINI-CHALLENGE: Temperature converter (Celsius -> Fahrenheit)
// 1. Add a TextField for Celsius and a "Convert" Button
// 2. Add a Label to show the result
// 3. On click: read the field, parse it to a double, compute
// fahrenheit = celsius * 9 / 5 + 32, and show it on the label
// 4. Arrange everything in a VBox and put it in a Scene/Stage
//
// ✅ Expected (enter 100, click Convert): "212.0 °F"
// your code here
}
public static void main(String[] args) {
launch(args); // don't forget this — without launch() nothing starts
}
}A small window with a Celsius field, a "Convert" button, and a
result label. Enter 100, click Convert -> the label shows "212.0 °F".
Enter 0 -> "32.0 °F".Double.parseDouble(field.getText()) turns the typed text into a number.Common Errors (and the Fix)
- ❌ "Not on FX application thread; currentThread = …" — you changed a control from a background thread. The UI may only be touched on the JavaFX Application Thread. Wrap the update:
Platform.runLater(() -> label.setText(result)). - ❌ Program runs but no window appears — you forgot
launch(args)inmain, so the runtime never started andstart()was never called. Also check you calledstage.show(). - ❌ UI freezes after a click — you did slow work (network/file/loop) inside a handler, blocking the FX thread that paints the screen. Move it to a background
TaskorServiceand push results back withPlatform.runLater(). - ❌ "Error: JavaFX runtime components are missing" — JavaFX isn't bundled with the JDK any more, so you forgot the modules. Add
org.openjfx:javafx-controls(andjavafx-fxmlfor FXML) via Maven/Gradle and run on the module-path. - ❌ "javafx.fxml.LoadException" / NullPointerException on an @FXML field — an
fx:idin the FXML doesn't match the field name, orfx:controllerpoints at the wrong class. Make the names line up exactly.
📋 Quick Reference
| Task | Code | Notes |
|---|---|---|
| Start the app | launch(args); | In main(); calls start() |
| Show the window | stage.setScene(s); stage.show(); | Stage = window, Scene = content |
| Vertical stack | new VBox(10, a, b) | HBox for a row |
| Grid cell | grid.add(node, col, row) | GridPane: column then row |
| Click handler | btn.setOnAction(e -> ...) | Lambda runs on each click |
| Bind a value | label.textProperty().bind(p) | Auto-updates the UI |
| Load FXML | FXMLLoader.load(url) | Wires @FXML / fx:id |
| Add CSS | scene.getStylesheets().add(...) | Properties use -fx- prefix |
| UI from a thread | Platform.runLater(() -> ...) | Required off the FX thread |
❓ Frequently Asked Questions
What is the difference between a Stage and a Scene in JavaFX?
A Stage is the actual window (the title bar, borders, and OS frame). A Scene is the content shown inside that window — a tree of Nodes rooted at one layout pane. One Stage shows one Scene at a time, but you can swap Scenes on the same Stage to switch screens, like changing the set on a theatre stage.
Why doesn't my JavaFX window open?
The three most common causes are: forgetting launch(args) in main() (so the runtime never starts); forgetting stage.show() (the window is built but never displayed); or missing JavaFX modules on the classpath/module-path. JavaFX is no longer bundled with the JDK, so you must add org.openjfx:javafx-controls (and javafx-fxml for FXML) via Maven or Gradle.
What is property binding and why use it?
A property is an observable value the UI can watch. Binding links one property to another so that when the source changes, the target updates automatically — like a spreadsheet cell that references another cell. Bind a Label's textProperty to a count or a Slider's valueProperty and the label refreshes itself; you never write code to manually update the UI.
What is FXML and do I have to use it?
FXML is an XML file that describes your UI declaratively — which layouts and controls exist and how they nest — separate from your Java logic. A Controller class holds @FXML fields (matched by fx:id) and @FXML methods (matched by onAction). It is optional: you can build the whole UI in Java instead, but FXML keeps layout and logic apart and lets you edit the UI visually in Scene Builder.
Why does my UI freeze when I do work in a button handler?
Event handlers run on the JavaFX Application Thread, which is also the thread that draws the screen. If you do slow work (a network call, a big file read) inside a handler, the UI can't repaint until it finishes, so the window looks frozen. Move slow work to a background Task or Service, and update the UI from it with Platform.runLater().
🎉 Lesson Complete!
You can now build real desktop apps. You know the launch → start → Stage → Scene → show chain, how to arrange controls with VBox, HBox, BorderPane, and GridPane, how to respond to clicks with event handlers, how to keep the UI in sync with property bindings, and how to split UI from logic with FXML, controllers, and CSS.
Next up: Networking — sockets, HTTP clients, and talking to the web from your Java programs.
Sign up for free to track which lessons you've completed and get learning reminders.