Skip to main content

    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 Scenestage.setScene(scene)stage.show().

    Worked Example: Hello, JavaFX!
    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
        }
    }
    Output
    A 320x200 desktop window titled "My First Window"
    opens with the text "Hello, JavaFX!" centred in the middle.
    This opens a real desktop window, so it can't run in an online sandbox. Run it with a local JDK plus the JavaFX SDK (add 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:

    PaneArranges children…Use it for
    VBoxIn a vertical columnStacked forms, menus
    HBoxIn a horizontal rowButton bars, toolbars
    GridPaneIn rows and columnsLabelled input forms
    BorderPaneInto top/bottom/left/right/centreWhole-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.

    Worked Example: A Login Form with Nested Panes
    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);
        }
    }
    Output
    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.
    Opens a real window — run it with a local JDK + JavaFX SDK. Try resizing the window to watch the panes reflow.

    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.

    Worked Example: A Click Counter (event + binding)
    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);
        }
    }
    Output
    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.
    Opens a real window — run it with a local JDK + JavaFX SDK. Notice no code ever calls 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.

    Your Turn: Build the Greeter
    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!
    Output
    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!".
    Fill in the blanks, then run it with a local JDK + JavaFX SDK. Compare what you see with the Output panel above.

    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.

    Your Turn: Connect fx:id and onAction
    <!-- 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, ...
    Output
    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, ...
    This is an FXML view plus its controller. Load the FXML with 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(...).

    Worked Example: style.css
    /* 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;
    }
    Output
    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.
    Save as 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.

    Mini-Challenge: Celsius to Fahrenheit
    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
        }
    }
    Output
    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".
    Write your solution, then run it with a local JDK + JavaFX SDK. Hint: 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) in main, so the runtime never started and start() was never called. Also check you called stage.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 Task or Service and push results back with Platform.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 (and javafx-fxml for FXML) via Maven/Gradle and run on the module-path.
    • "javafx.fxml.LoadException" / NullPointerException on an @FXML field — an fx:id in the FXML doesn't match the field name, or fx:controller points at the wrong class. Make the names line up exactly.

    📋 Quick Reference

    TaskCodeNotes
    Start the applaunch(args);In main(); calls start()
    Show the windowstage.setScene(s); stage.show();Stage = window, Scene = content
    Vertical stacknew VBox(10, a, b)HBox for a row
    Grid cellgrid.add(node, col, row)GridPane: column then row
    Click handlerbtn.setOnAction(e -> ...)Lambda runs on each click
    Bind a valuelabel.textProperty().bind(p)Auto-updates the UI
    Load FXMLFXMLLoader.load(url)Wires @FXML / fx:id
    Add CSSscene.getStylesheets().add(...)Properties use -fx- prefix
    UI from a threadPlatform.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.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service