Every JavaFX app is built the same way a building is: a window, a floor plan, and the fixtures inside it. Ten sheets, each one explained properly — the "what," the "why," and the mistake beginners actually make.
Every JavaFX app opens a window and then decides what to put in it. That's the whole idea of lesson one — everything else in this guide is just filling that window in.
import javafx.application.Application; import javafx.stage.Stage; public class MyApp extends Application { @Override public void start(Stage stage) { stage.setTitle("My First App"); stage.show(); } public static void main(String[] args) { launch(args); } }
JavaFX apps don't run like a normal Java program that executes top to bottom and finishes. They run inside a lifecycle managed by the Application class, which calls three methods for you in order:
| Method | When it runs | What you use it for |
|---|---|---|
init() | Before the window appears | Optional setup — loading data, no UI allowed here yet |
start(Stage) | Right after init() | Build and show your UI — this is where almost all your code lives |
stop() | When the window closes | Optional cleanup — saving files, closing connections |
You never call start() yourself, and you never write new MyApp() either. Calling launch(args) is what tells the JavaFX runtime "set up the toolkit, create an instance of this class, and call its lifecycle methods for me." Trying to run a JavaFX app by directly instantiating it or calling start() by hand will fail with a toolkit-not-initialized error — launch() is doing real setup work behind the scenes, not just being polite ceremony.
start()stage.show(). The program compiles, runs, and appears to do nothing — no error, no window. If your app "isn't working" and there's no error message, check for a missing show() first.show() and re-running, just to see what "silently broken" looks like.Because launch() blocks and hands control over to the JavaFX event system until the window is closed. Code placed after launch(args) in main() won't run until the whole app has already finished.
Yes — you can create additional new Stage() windows yourself for dialogs or secondary windows. The one passed into start() is just the first, "primary" one.
One chain to memorize, and most of JavaFX's structure falls into place:
Stage → Scene → Root Node → Other Nodes
The Stage is the operating-system window: the frame, the minimize/close buttons, the title bar. It knows almost nothing about what's inside it.
The Scene is the entire content area inside that frame — one rectangle of pixels. A Stage can only display one Scene at a time, but you can swap that Scene out for a different one whenever you want, which is exactly how multi-screen apps work: a "settings screen" is just a second Scene you build once and call stage.setScene(settingsScene) on later.
Every Scene has exactly one root node — usually a layout like VBox or BorderPane — and everything else hangs off that root as children, and their children as grandchildren, and so on. This whole tree is called the scene graph, and it's not just a mental model: JavaFX really does walk this tree every time it needs to lay things out, repaint the screen, or figure out which node a click landed on.
Label label = new Label("Hello!"); Scene scene = new Scene(label, 400, 300); stage.setScene(scene); stage.show();
The two numbers, 400 and 300, are the Scene's starting width and height in pixels. They're a suggestion, not a lock — the user can still resize the window afterward unless you explicitly call stage.setResizable(false).
A node can only appear once in a scene graph. If you try to add the same Label to two different containers, JavaFX throws an IllegalArgumentException at runtime — it doesn't silently clone it for you. If you want the same text in two places, create two separate Label objects.
new Scene(label, 400, 300) only works because Label can act as a root on its own. Try passing something that isn't a valid root — like a bare String — and it won't compile; the Scene constructor specifically expects a Parent (which layouts and most containers are).Layouts arrange your controls automatically. You should almost never place a button by hand-typed pixel coordinates — let one of these do it instead.
VBox box = new VBox(10); // vertical stack, 10px gap between children box.setPadding(new Insets(16)); // 16px of breathing room inside the box's own edges box.setAlignment(Pos.CENTER); // centers children instead of hugging the top-left
That constructor argument, 10, is the spacing between children — not padding around the outside. Padding and spacing solve different problems and beginners mix them up constantly: spacing is the gap between items, padding is the gap between the items and the container's own border.
BorderPane pane = new BorderPane(); pane.setTop(new Label("Menu bar")); pane.setCenter(new Label("Main content")); pane.setBottom(new Label("Status bar"));
Each slot holds exactly one node — but that node is very often itself a VBox or HBox holding several more. Nesting layouts inside layouts is completely normal; it's how every non-trivial JavaFX screen is actually built. The center region automatically stretches to fill whatever space top/bottom/left/right don't use, which is why it's almost always where the "main" content of a screen goes.
GridPane grid = new GridPane(); grid.setHgap(8); grid.setVgap(8); grid.add(new Label("Name:"), 0, 0); // column 0, row 0 grid.add(new TextField(), 1, 0); // column 1, row 0
The add() arguments are column, then row — the opposite order from how you'd say "row 0, column 1" out loud, and a very common source of "why is my grid backwards" bugs.
setPrefWidth only when you specifically need to override that.Controls are the nodes people actually interact with. Knowing their names is step one — knowing their two or three most useful properties is what actually lets you build something.
| Control | Purpose | Key methods |
|---|---|---|
Label | Displays text, non-editable | setText(), getText() |
Button | Can be clicked | setOnAction(), setDisable(true) |
TextField | One line of typed input | getText(), setPromptText() |
TextArea | Multiple lines of typed input | setWrapText(true) |
PasswordField | Like TextField, but masks characters | getText() |
CheckBox | An on/off choice | isSelected() |
RadioButton | Pick one from a group | grouped via ToggleGroup |
ComboBox<T> | A dropdown picker | getValue(), getItems().add() |
Slider | Drag to pick a numeric value | getValue() |
Label name = new Label("Name:"); TextField input = new TextField(); input.setPromptText("Type here..."); // greyed-out hint, disappears once typed Button button = new Button("Click Me"); button.setDisable(true); // greys it out and blocks clicks
Almost every control shares a small set of properties inherited from Node: setVisible(false) hides it entirely (and removes its space), setDisable(true) greys it out but keeps its space, and setPrefWidth() / setPrefHeight() suggest a size without forcing one.
Radio buttons only exclude each other if you explicitly put them in the same ToggleGroup — otherwise every RadioButton on screen behaves independently, which is a very common "why can I select both?!" bug.
ToggleGroup group = new ToggleGroup(); RadioButton small = new RadioButton("Small"); RadioButton large = new RadioButton("Large"); small.setToggleGroup(group); large.setToggleGroup(group);
getText() on a TextField and expecting a number. It always returns a String — even if the user typed "42" — which is exactly the problem sheet 08 walks through solving.An event is something that happens — a click, a keystroke, a mouse move. You attach a small block of code, called a handler, that runs when it does.
Button button = new Button("Click Me"); button.setOnAction(e -> { System.out.println("Button clicked!"); });
e -> { ... } is a lambda expression — read it as plain English: "when this happens, do this." The e is the event object itself; you often won't need to use it at all, which is exactly why so many JavaFX examples name it something short and forgettable.
| Handler | Fires on |
|---|---|
setOnAction() | Buttons, MenuItems — a simple "activated" event |
setOnMouseClicked() | Any node, gives you click position and count (single/double click) |
setOnMousePressed() / setOnMouseReleased() | The press and the release, separately — needed for dragging |
setOnKeyPressed() | A key going down while the node has focus |
setOnMouseEntered() / setOnMouseExited() | The cursor entering or leaving a node's bounds |
The genuinely useful pattern is one control's handler updating a different control:
Label label = new Label("Not clicked"); Button button = new Button("Click"); button.setOnAction(e -> label.setText("You clicked the button!")); VBox root = new VBox(10, label, button); Scene scene = new Scene(root, 400, 250);
This works because the lambda can "reach out" and use label, a variable declared outside of it — as long as that variable is never reassigned after it's declared, Java lets a lambda capture it. This rule is called effectively final, and it's why you'll see beginners get a compile error the moment they try to reassign a captured variable from inside a lambda.
button.setOnAction(e -> doSomething()); more than once on the same button expecting both to run. Each call to setOnAction replaces the previous handler — it doesn't add a second one. If you genuinely need multiple independent handlers, use addEventHandler() instead.JavaFX styling reads almost exactly like CSS, just with an -fx- prefix on every property.
button.setStyle( "-fx-background-color: #3a7bd5; " + "-fx-text-fill: white; " + "-fx-font-size: 16px;" );
setStyle() is fine for a one-off test, but it hardcodes appearance directly into your logic code and has to be repeated on every control. The moment you have more than a couple of styled controls, move styling into a real .css file instead:
/* style.css */
.button {
-fx-background-color: #3a7bd5;
-fx-text-fill: white;
}
.button:hover {
-fx-background-color: #2c5fa8;
}
scene.getStylesheets().add("style.css");
Every JavaFX control already has a default style class matching its type in lowercase — Button → .button, Label → .label — so the CSS above styles every button in the scene at once. Add your own class with node.getStyleClass().add("danger") to target specific controls without touching every button in the app. Pseudo-classes like :hover and :pressed only work in an actual stylesheet — setStyle() can't express them.
A text field, a button, and a label that responds — every idea from sheets 01–06 in one runnable program.
import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class HelloApp extends Application { @Override public void start(Stage stage) { Label label = new Label("Type your name:"); TextField input = new TextField(); Button button = new Button("Say Hello"); Label output = new Label(); button.setOnAction(e -> output.setText("Hello, " + input.getText() + "!")); VBox root = new VBox(10, label, input, button, output); Scene scene = new Scene(root, 400, 250); stage.setTitle("Hello App"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { launch(args); } }
launch(args) boots JavaFX and calls start(stage) for you.setOnAction attaches a handler to button, but the lambda's code doesn't run yet — it's stored, waiting for a click.VBox, which becomes the Scene's root — this is the moment they actually join the scene graph.show() finally paints all of it to the screen.input at that moment, not whatever was there when the handler was written.+ "!" to see the string concatenation actually change. Small deliberate breakages like this teach you more than reading ever will.The whole trick of a calculator app: text fields hold text, even when it looks like a number, so you have to convert it first — and handle it politely when the user types something that isn't a number at all.
double a = Double.parseDouble(first.getText()); double b = Double.parseDouble(second.getText()); double answer = a + b; result.setText("Answer = " + answer);
plusButton.setOnAction(e -> calculate("+")); minusButton.setOnAction(e -> calculate("-")); timesButton.setOnAction(e -> calculate("*")); divideButton.setOnAction(e -> calculate("/")); private void calculate(String op) { try { double a = Double.parseDouble(first.getText()); double b = Double.parseDouble(second.getText()); double answer = switch (op) { case "+" -> a + b; case "-" -> a - b; case "*" -> a * b; case "/" -> a / b; default -> 0; }; result.setText("Answer = " + answer); } catch (NumberFormatException ex) { result.setText("Please enter valid numbers"); } }
The try / catch matters more than it looks like it should. Without it, typing letters into either field crashes the calculation with an unhandled NumberFormatException — catching it and showing a friendly message is the difference between a toy and something that survives real user input. Dividing by zero, by contrast, won't crash at all: double division by zero produces Infinity rather than throwing, which is worth testing for on purpose.
double to int parsing and see which of your test cases break.Yes — JavaFX can genuinely power something in the spirit of Scratch: a free canvas of objects you can grab, drag, and command with buttons. It needs a different kind of container than everything so far.
Every layout in sheet 03 actively arranges its children for you — that's the opposite of what a drag-and-drop workspace needs. A plain Pane does no automatic layout at all: children stay exactly where you put them, which is precisely why it's the right container for freely positioned, draggable objects.
Rectangle sprite = new Rectangle(60, 60); double[] offset = new double[2]; sprite.setOnMousePressed(e -> { offset[0] = e.getX() - sprite.getLayoutX(); offset[1] = e.getY() - sprite.getLayoutY(); }); sprite.setOnMouseDragged(e -> { sprite.setLayoutX(e.getX() - offset[0]); sprite.setLayoutY(e.getY() - offset[1]); }); Pane workspace = new Pane(sprite);
The version from the original guide moved the rectangle straight to the cursor position, which makes it snap so its top-left corner sits under your mouse the instant you grab it anywhere. Capturing the offset on press — the gap between where you clicked and the shape's current corner — and subtracting it during drag keeps the shape glued to wherever you actually grabbed it. This is the single most common polish fix beginners discover when their first drag implementation feels "off."
getX() / getY() on a mouse event are relative to the node the handler is attached to. setLayoutX() / setLayoutY() position a node relative to its parent. Mixing these up — using a coordinate meant for one system in the other — is the usual cause of a sprite that drifts or jumps when dragged.
A realistic order to actually get good at this — each step leans on the one before it. Rushing ahead to step 8 without steps 1–4 solid is the most common reason beginners stall out.
1. Stage, Scene, Node — the container chain from sheet 02, until it's automatic
2. VBox, HBox, GridPane — get comfortable arranging things without fighting the layout
3. Label, Button, TextField — the everyday controls, plus their common properties
4. Button events & lambdas — make things respond, including invalid input
5. Pane & mouse events — free-form movement, offsets, and coordinate systems
6. Build a calculator — your first real logic, with error handling that doesn't crash
7. Build a small drawing app — combine shapes, mouse events, and a Canvas
8. Build a drag-and-drop block editor — the Scratch-like project, block snapping included
9. Add your own blocks and commands — make it yours, and start reading the official JavaFX docs directly
Once you're past these basics, the two resources worth bookmarking are the official OpenJFX documentation and the JavaFX CSS Reference Guide — both are dense, but they're the ground truth for exactly which properties and methods exist on which class, which no beginner guide (including this one) can fully replace.
Tap each one off as you build it. Nothing saves when you leave — so if you're serious, keep the code, not the checkmarks.