Lesson 77Elective: Desktop apps with JavaFX
Creating and running a JavaFX application
The Application class, the three steps inside start(), the lifecycle, and two complete programs compiled and run from the command line and VS Code.
By BasithUpdated 65 min
- Application
- start()
- launch()
- Group
- ObservableList
- Scene
The last lesson described a JavaFX program from the outside: a Stage holding a Scene holding a tree of nodes. This lesson writes one. You will build two programs, a brown window 600 pixels wide and a window that says “Welcome to Ooty”, then compile and run them yourself from a terminal, knowing what every word of the long javac and java commands does.
- Write the skeleton every JavaFX program shares: a class that extends
Application, overridesstart, and callslaunchfrommain. - Build a scene graph in a
Group, wrap it in aSceneof a chosen size, and show it on theStage. - Trace the lifecycle (constructor,
init,start,stop), say which thread runs each, and end a program the right way. - Compile and run a JavaFX program from the command line, explain every flag, and fix the error each missing flag causes.
- Place text in a window at a chosen point, and explain why that point is the text's baseline.
javac and running with java are two separate steps (setup and the JVM), and today each one has to be told where JavaFX lives. Your program class extends a library class and overrides one of its methods. main can call launch without an object because launch is static. The classpath from packages and imports is where java looks for your own classes, and the lifecycle runs on more than one thread.Creating a JavaFX application#
Every JavaFX program is built around one library class, Application in the package javafx.application. It is the entry point of a JavaFX application, and your program is a class that inherits from it.
Application is an abstract class: its method start(Stage) has no body, so new Application()does not compile (“Application is abstract; cannot be instantiated”). You write a subclass that supplies start, and the JavaFX launcher creates the object for you by calling your class's public no-argument constructor.Three pieces turn a class into a JavaFX program:
extends Applicationmakes your class a JavaFX application.start(Stage primaryStage)is the abstract method you must override. JavaFX calls it once, handing it the primary stage it created, and all the code that builds your window goes inside it. The template declaresthrows ExceptionbecauseApplicationdeclares it that way. Ifstartdoes throw, JavaFX prints “Exception in Application start method” with the stack trace, and the program ends.maincallslaunch(args), a static methodApplicationprovides. It starts the JavaFX runtime, creates an instance of your class and calls itsstartmethod. It does not return until the application has finished.
public class JavafxSample extends Application { @Override public void start(Stage primaryStage) throws Exception { /* Code for JavaFX application. (Stage, scene, scene graph) */ } public static void main(String args[]){ launch(args); }}This is a template: it compiles once the imports javafx.application.Application and javafx.stage.Stage are added. Run as it stands, it shows nothing. start() never calls show(), so no window appears, and with no window to close, the program waits until you press Ctrl+C.
Inside start, a typical JavaFX application follows three steps:
- 1Prepare a scene graph with the required nodes.
- 2Prepare a Scene with the required dimensions, and add the scene graph to it by passing in its root node.
- 3Prepare a Stage, add the scene to it, and display the contents of the stage.
Key ideaEvery start method does the same three things in the same order: build the tree of nodes, wrap it in a Scene, then put the Scene on the Stage and show it.
Step 1: preparing the scene graph#
A scene graph starts with its root node, since every other node hangs below it. There are three kinds of root to choose from: a Group, a Region (in practice one of its subclasses, a layout pane such as StackPane or GridPane) or a WebView. The controls and layouts lesson goes through the Region family. The examples in this lesson use a Group.
A Group (package javafx.scene) is a node whose only job is to hold other nodes. Creating one is a plain constructor call, Group root = new Group();, and an empty group shows nothing. To put nodes in it, ask for its list of children: getChildren() returns an ObservableList (package javafx.collections) holding the group's nodes, and whatever you add to that list becomes a child of the group.
//Retrieving the observable list objectObservableList list = root.getChildren(); //Setting the text object as a nodelist.add(NodeObject);NodeObject stands for any node you have already created: a Text, a Rectangle, a Button. After add(), it is a child of root, and it appears once the scene is shown.
ObservableList is an interface that extends java.util.List, so it has the methods you would expect: add, addAll, remove, get, size. Observable means the group is notified the instant the list changes. That is why adding a node to the list is enough to put it in the window, with no refresh call, and why removing it later makes it disappear.
Two shorter forms reach the same result. The first passes the node to the constructor: Group root = new Group(NodeObject);. That constructor accepts any number of nodes, so new Group(title, photo, button) works too. And you can skip the variable: root.getChildren().add(NodeObject);.
Step 2: preparing the Scene#
A JavaFX scene is an object of the class Scene in the package javafx.scene. Passing the root to the constructor is mandatory; the size is optional.
// size worked out from the contentScene scene = new Scene(root); // 600 pixels wide, 300 pixels tallScene scene = new Scene(root, 600, 300);Two alternatives: a program uses one or the other, since both declare a variable named scene.
With no size, the scene makes itself just big enough for the root's content, which for an empty group means a tiny window. With a size, the scene is exactly that big, whatever the content.
Scene(Parent root, double width, double height): width first, then height. new Scene(root, 600, 300)is 600 pixels wide and 300 tall, a wide, short window. Some tutorials call the two numbers “height and width”; the window itself shows which is right. The parameters are double; the int values 600 and 300 are widened to double automatically, as in operators and casting.A scene also has a fill, the colour behind everything in it, white unless you change it. scene.setFill(Color.BROWN) paints it brown. Color lives in javafx.scene.paint. It has a constant for every named web colour (Color.BROWN is #A52A2A, Color.NAVY is #000080), and it builds any other colour with Color.rgb(165, 42, 42) or Color.web("#A52A2A"). One more constructor takes the fill directly: new Scene(root, 600, 300, Color.BROWN).
Step 3: preparing the Stage#
The stage is the container of a JavaFX application: it provides the window. It is an object of the class Stage in the package javafx.stage, and JavaFX passes one to start() as its parameter. With that object you set the window's title with setTitle(), attach the scene with setScene(), and display the stage with show().
//Setting the title to Stage.primaryStage.setTitle("Sample application"); //Setting the scene to StageprimaryStage.setScene(scene); //Displaying the stageprimaryStage.show();A window titled "Sample application" opens, showing the scene.
Two things about show(). Nothing is on screen before it runs; until then the stage is an object in memory. And it does not wait: show() returns at once, start reaches its closing brace, and the window stays open because the JavaFX runtime is still running the event loop on its own thread. A loop added at the end of startto “keep the window open” does the opposite: it blocks that thread and freezes the window.
The lifecycle of a JavaFX application#
The Application class has three lifecycle methods:
start(): the entry point, where the JavaFX graphics code is written. It is abstract, so you must override it.stop(): empty by default. Override it to write the logic that runs when the application stops, such as saving settings or closing a database connection.init(): empty by default. Override it for setup that should happen before any window exists, such as reading a configuration file. You cannot create a stage or a scene in it.
Beside them is the static method launch(). Because it is static, you call it from a static context, normally main. Whenever a JavaFX application is launched, these actions happen in this order:
- 1An instance of your application class is created: its constructor runs.
- 2
init()is called. - 3
start()is called, with the primary stage. - 4The launcher waits for the application to finish, which happens when the last window is closed or the program calls
Platform.exit(), and then callsstop(). Only after that doeslaunchreturn tomain.
Which thread runs what#
The lifecycle is spread over three threads. main runs on the main thread, which then sits inside launch until the very end. The constructor, start, stop and every event handler run on the JavaFX Application Thread. init is the odd one out: it runs on a separate launcher thread, named JavaFX-Launcher.
That is the reason behind the rule for init. A Stage and a Scene belong to the JavaFX Application Thread, and the JavaFX documentation states plainly that an application must not construct either one in init. Try to create a Stage there and it fails with IllegalStateException: Not on FX application thread; currentThread = JavaFX-Launcher. Use init for work that does not touch the screen, and build the window in start. This program prints the name of the thread at every stage.
import javafx.application.Application;import javafx.scene.Group;import javafx.scene.Scene;import javafx.stage.Stage; public class Lifecycle extends Application { public Lifecycle() { System.out.println("constructor on " + Thread.currentThread().getName()); } @Override public void init() { System.out.println("init on " + Thread.currentThread().getName()); } @Override public void start(Stage stage) { System.out.println("start on " + Thread.currentThread().getName()); stage.setTitle("Lifecycle"); stage.setScene(new Scene(new Group(), 300, 150)); stage.show(); } @Override public void stop() { System.out.println("stop on " + Thread.currentThread().getName()); } public static void main(String[] args) { System.out.println("main on " + Thread.currentThread().getName()); launch(args); System.out.println("launch returned, main ends"); }}main on main constructor on JavaFX Application Thread init on JavaFX-Launcher start on JavaFX Application Thread (a 300 by 150 window titled "Lifecycle" is open; close it) stop on JavaFX Application Thread launch returned, main ends
Read the output against the steps. main prints first, on the main thread. The constructor and start share the JavaFX Application Thread, while init runs between them on the launcher thread. Nothing more happens until you close the window. Then stop runs, and only once it has finished does launch return and let main print its last line.
Terminating a JavaFX application#
By default a JavaFX application ends implicitly: when its last window is closed, the launcher calls stop() and the program finishes. You can switch this off by passing false to setImplicitExit().
Platform.setImplicitExit(false), a static method of the class Platform in javafx.application. Being static means you call it through the class name. It does not have to be called from a static method, and the documentation says it may be called from any thread, so start or init is a natural place for it. With implicit exit off, closing the last window leaves the program running with no window at all until it calls Platform.exit() itself, which suits a program that hides its window and shows it again later.To end the application explicitly, there are two methods, Platform.exit() and System.exit(int). They do not do the same thing:
stop() method runs first.stop() method never runs.import javafx.application.Platform; // in start(): closing the last window no longer ends the programPlatform.setImplicitExit(false); // later, in a Quit button's handler: a proper shutdown, stop() runsPlatform.exit(); // or end the JVM at once with exit status 0; stop() is skippedSystem.exit(0);Button handlers arrive in the event handling session. Until then, read Platform.exit() as the call a Quit button should make.
Example: creating an empty window#
Here is one complete program, JavafxSample, built in seven steps. The result is a window titled “Sample Application”, 600 pixels wide and 300 tall, filled brown and otherwise empty.
- 1Create a class that inherits the
Applicationclass of the packagejavafx.applicationand implements itsstart()method:public class JavafxSample extends Application, containing an emptypublic void start(Stage primaryStage) throws Exceptionmarked@Override. - 2Create a Group object inside
start(), by instantiating the classGroupof the packagejavafx.scene:Group root = new Group(); - 3Create a Scene object, passing the group as the root, along with the size:
Scene scene = new Scene(root, 600, 300);The two numbers are the width and height of the scene, in that order. - 4Set the title of the stage through the
primaryStageparameter:primaryStage.setTitle("Sample Application");JavaFX passes the stage tostartin yourApplicationsubclass. - 5Add the scene to the stage:
primaryStage.setScene(scene); - 6Display the contents of the stage:
primaryStage.show(); - 7Launch the application by calling the static method
launch()frommain:public static void main(String args[]){ launch(args); }. The formString args[]is an older way of writingString[] args; both mean the same.
Put together with its imports and one extra line that colours the scene, this is the whole program. The group variable is called group here rather than root; any name works.
import javafx.application.Application;import javafx.scene.Group;import javafx.scene.Scene;import javafx.scene.paint.Color;import javafx.stage.Stage; public class JavafxSample extends Application { @Override public void start(Stage primaryStage) throws Exception { //creating a Group object Group group = new Group(); //Creating a Scene by passing the group object, width and height Scene scene = new Scene(group ,600, 300); //setting color to the scene scene.setFill(Color.BROWN); //Setting the title to Stage. primaryStage.setTitle("Sample Application"); //Adding the scene to Stage primaryStage.setScene(scene); //Displaying the contents of the stage primaryStage.show(); } public static void main(String args[]){ launch(args); }}A window titled "Sample Application" whose 600 by 300 content area is filled brown (Color.BROWN, #A52A2A). Nothing is printed by the program itself. Close the window and the program ends.
Compiling and running from the command line#
Since Java 11, JavaFX is not part of the JDK, so javac and java cannot find it on their own. You download the JavaFX SDK from openjfx.io, choosing version 27 and your own operating system and processor, and unzip it, for example to C:\Program Files\javafx-sdk-27. Its lib folder holds JavaFX's modules as .jar files (javafx.base.jar, javafx.graphics.jar, javafx.controls.jar and the rest), and every command below points at that folder.
java -version and javac -version; both should report 25 or higher. Each JavaFX release states a minimum JDK (JavaFX 25, for instance, needs JDK 23 or newer), and an older JDK refuses to load newer JavaFX classes, with an error that mentions a class file version. On an Apple Silicon Mac, download the aarch64 SDK rather than the x64 one.PATH_TO_FX and the Command Prompt commands#
The first pair of commands uses %PATH_TO_FX%. PATH_TO_FX is an environment variable, a named value the terminal keeps, which you create so the long path to the lib folder is typed only once. In the Windows Command Prompt, set creates it and %PATH_TO_FX% stands for its value:
set PATH_TO_FX="C:\Program Files\javafx-sdk-27\lib"javac --module-path %PATH_TO_FX% --add-modules javafx.controls JavafxSample.javajava --module-path %PATH_TO_FX% --add-modules javafx.controls JavafxSamplejavac prints nothing and leaves JavafxSample.class beside the .java file. java opens the brown window. On JDK 24 or newer it also prints a few WARNING lines about a restricted method; the VS Code commands below add the flag that removes them.
The quotes in the set line become part of the value. When %PATH_TO_FX%is expanded, the path arrives with its quotes, so the space in “Program Files” does not split it in two. A variable made with set lasts only as long as that Command Prompt window; open a new window and you must set it again.
The VS Code commands#
In the terminal inside VS Code the commands look different. On Windows that terminal runs PowerShell by default, and PowerShell does not understand %PATH_TO_FX%, so these write the path out in full:
javac --module-path "C:\Program Files\javafx-sdk-27\lib" --add-modules javafx.controls -d out JavafxSample.javajava --enable-native-access=javafx.graphics --module-path "C:\Program Files\javafx-sdk-27\lib" --add-modules javafx.controls -cp out JavafxSamplejavac creates a folder named out and puts JavafxSample.class in it. java loads the class from out and opens the brown window, with no warnings.
Every flag in those two commands, and what it is for:
| Flag | Used with | What it does |
|---|---|---|
--module-path | javac and java | Where to look for modules: here, JavaFX's lib folder. On its own it only makes them findable. |
--add-modules javafx.controls | javac and java | Which modules to load. Your class is not part of any module, so nothing asks for JavaFX until you name it here. javafx.controls brings javafx.graphics and javafx.base with it. Add more, separated by commas, when you use them: javafx.controls,javafx.media. |
-d out | javac | Writes the compiled .class files into the folder out, creating it if needed, instead of beside the source file. |
-cp out | java | The classpath: where to find your own classes. It must name the folder -d wrote to, or java cannot find JavafxSample. |
--enable-native-access=javafx.graphics | java | Allows JavaFX's graphics module to load its native libraries, the compiled code that talks to the GPU and the operating system. From JDK 24 the JVM warns about any library that does this without permission. If you use media or WebView, list those modules too: --enable-native-access=javafx.graphics,javafx.media. |
Key ideaBoth commands need to know where JavaFX is (
--module-path) and which parts of it to load (--add-modules). Give the flags tojavacbut not tojava, and the program compiles and then refuses to run.
On macOS and Linux#
The same commands work with three changes: export instead of set, $PATH_TO_FX instead of %PATH_TO_FX%, and forward slashes in the path.
export PATH_TO_FX=$HOME/javafx-sdk-27/libjavac --module-path $PATH_TO_FX --add-modules javafx.controls -d out JavafxSample.javajava --enable-native-access=javafx.graphics --module-path $PATH_TO_FX --add-modules javafx.controls -cp out JavafxSampleAssumes the SDK was unzipped into your home folder; change the path if not. The window opens just as it does on Windows.
What you see when a flag is missing#
Each of these catches someone the first time. The message tells you which command, and which flag, to look at.
| What you see | Why | Fix |
|---|---|---|
error: package javafx.application does not exist, from javac, followed by an error for each JavaFX class you use | javac cannot see JavaFX: the module flags are missing from the javac command. | Add --module-path and --add-modules javafx.controls to javac. |
Error: Could not find or load main class JavafxSample with Caused by: java.lang.NoClassDefFoundError: javafx/application/Application, from java | The class compiled with the flags but was run without them, so java cannot find JavaFX at all. | Add the same two flags to the java command. Compiling with them is not enough. |
Error: JavaFX runtime components are missing, and are required to run this application, from java | JavaFX's jar files are on the class path instead of the module path, as happens when an IDE or a build file adds them as ordinary libraries. | Load JavaFX with --module-path and --add-modules. The guide to this error covers Maven, Gradle and each IDE. |
WARNING: A restricted method in java.lang.System has been called, plus three more WARNING lines; the window still opens | JDK 24 and newer warn whenever a library loads native code without permission. | Add --enable-native-access=javafx.graphics to java, as one of the warning lines itself suggests. |
Error occurred during initialization of boot layer and java.lang.module.FindException: Module javafx.controls not found (from javac: error: module not found: javafx.controls) | The module path points at the SDK folder (javafx-sdk-27) instead of its lib folder, or the path is mistyped, or PATH_TO_FX was set in a different window. | Point --module-path at ...\javafx-sdk-27\lib, and check the variable with echo %PATH_TO_FX%. |
Error: Could not find or load main class JavafxSample with Caused by: java.lang.ClassNotFoundException: JavafxSample | Compiled with -d out but run without -cp out, so java looked in the current folder. | Add -cp out, naming the same folder as -d. |
In an IDE#
An IDE needs the same two pieces of information, entered once in its settings instead of typed every time.
- VS Code. For compiling, add the jars from the SDK's
libfolder underReferenced Librariesin the Java Projects view, which clears the red underlines on the imports. For running, put the flags in.vscode/launch.jsonasvmArgs, as below, and run with F5. - IntelliJ IDEA. Add the
libfolder as a library under File, Project Structure, Libraries. Then open Run, Edit Configurations, choose Modify options, Add VM options, and paste the flags:--module-path "C:\Program Files\javafx-sdk-27\lib" --add-modules javafx.controls --enable-native-access=javafx.graphics. Alternatively, start from IntelliJ's New Project wizard and pick its JavaFX generator. It creates a Maven project that downloads JavaFX by itself, and you run it with thejavafx:rungoal in the Maven tool window, ormvn javafx:runin a terminal, with no module path to manage.
{ "version": "0.2.0", "configurations": [ { "type": "java", "name": "JavafxSample", "request": "launch", "mainClass": "JavafxSample", "vmArgs": "--module-path \"C:/Program Files/javafx-sdk-27/lib\" --add-modules javafx.controls --enable-native-access=javafx.graphics" } ]}Forward slashes work in Java paths on Windows and avoid escaping backslashes in JSON. The \" marks around the path keep its space from splitting it.
Example: displaying text#
The second example embeds text in the scene. Save it in a file named DisplayingText.java. It follows the same three steps as the empty window, with one node in the group: a Text.
import javafx.application.Application;import javafx.collections.ObservableList;import javafx.scene.Group;import javafx.scene.Scene;import javafx.stage.Stage;import javafx.scene.text.Font;import javafx.scene.text.Text; public class DisplayingText extends Application { @Override public void start(Stage stage) { //Creating a Text object Text text = new Text(); //Setting font to the text text.setFont(new Font(45)); //setting the position of the text text.setX(50); text.setY(150); //Setting the text to be added. text.setText("Welcome to Ooty"); //Creating a Group object Group root = new Group(); //Retrieving the observable list object ObservableList list = root.getChildren(); //Setting the text object as a node to the group object list.add(text); //Creating a scene object Scene scene = new Scene(root, 600, 300); //Setting title to the Stage stage.setTitle("Sample Application"); //Adding scene to the stage stage.setScene(scene); //Displaying the contents of the stage stage.show(); } public static void main(String args[]){ launch(args); }}javac compiles it and prints two notes (explained below): Note: DisplayingText.java uses unchecked or unsafe operations. Note: Recompile with -Xlint:unchecked for details. java opens a 600 by 300 window titled "Sample Application" showing "Welcome to Ooty" in large letters that start 50 pixels from the left and stand on a line 150 pixels from the top.
Line by line: Text (package javafx.scene.text) is a node that draws a string. It is a shape, a cousin of Rectangle and Circle, so it is placed by coordinates. new Font(45) asks for the default system font at size 45, headline letters in a 300-pixel-tall window. setText supplies the words. The rest is the familiar pattern: the text goes into the group through its list of children, the group into a 600 by 300 scene, and the scene onto the stage.
Why y is the baseline#
Letters stand on an invisible line called the baseline. Most of each letter sits above it, and the tails of letters such as y, g and p hang below. A Text node's x is where its first letter starts, and its y is the height of its baseline, measured down from the top of the scene. So setY(150) stands the words on a line half way down the 300-pixel window, as in the sketch above.
Now leave out the setX and setYlines and run it again. Without them, both values are 0, so the baseline lies along the window's top edge. Every letter is drawn above it, outside the window, and a group does nothing to bring it back into view. All you see is the tail of the y.
Key ideaA Text node's y is its baseline, the line its letters stand on, not its top edge.
Text or Label?#
JavaFX has a second node for words, Label, and the two suit different jobs. Text is a shape: you place it at exact coordinates, fill and outline it like any shape, and it has no padding, background or border of its own. Label (package javafx.scene.control) is a control: a layout pane decides where it goes, CSS can give it padding, a background and a border, it can show an icon beside its words, and it shortens its text with “...” when space runs out. Use Text for words drawn at a chosen point in a Group, as here, and Label for the captions in a form, as the registration form does.
The note javac prints#
Compiling DisplayingText prints two lines that begin with Note:. They come from ObservableList list = root.getChildren();. getChildren() really returns an ObservableList<Node>, a list of nodes, but the variable is declared as a plain ObservableList without the <Node>, which is called a raw type. With a raw type the compiler cannot check that only nodes go into the list, so it flags list.add(text) as unchecked. A note does not stop the build: the class file is written and the program runs. To silence it, give the list its type, ObservableList<Node> list = root.getChildren(); with import javafx.scene.Node;, or skip the variable: root.getChildren().add(text);.
Compile and run it the same way as the first example:
javac --module-path %PATH_TO_FX% --add-modules javafx.controls DisplayingText.javajava --module-path %PATH_TO_FX% --add-modules javafx.controls DisplayingTextThe two Note lines from javac, then the window. In VS Code's terminal use the full-path form with -d out, -cp out and --enable-native-access=javafx.graphics, exactly as for JavafxSample.
Try it
Answer each one in your head or on paper first, then open the answer.
JavafxSample so the window is 400 pixels wide, 250 tall and dark blue (Color.NAVY). Which lines change? Write them.new Scene(root, 600, 300) makes a scene 600 pixels tall. Is 600 the height? How could you check without reading the documentation?Error: Could not find or load main class DisplayingText and Caused by: java.lang.NoClassDefFoundError: javafx/application/Application. A friend's fails earlier, at javac, with error: package javafx.application does not exist. What is missing in each case? Write the corrected Command Prompt commands.stage.show()?Group with a StackPane. Write the changed lines. What happens to setX and setY, and what happens when you resize the window?stop() method to DisplayingText that prints “Goodbye from Ooty”. When does the line appear? Would it appear if a Quit button called System.exit(0)? And if it called Platform.exit()?Practice
Work through these on your own. Each one builds on this page.
Text to DisplayingText: “Queen of Hill Stations” at size 30, placed at (50, 220). Sketch on paper where you expect it before you run the program, then compare.Platform.setImplicitExit(false); as the first line of start, run it again and close the window. What does the terminal show now, and how do you end the program? Explain why stop did not run, and say what would have to call Platform.exit().JavafxSample five times, each time with one thing wrong: no --module-path; no --add-modules; no -cp out; no --enable-native-access; and the module path pointing at the SDK folder instead of its lib folder. Copy the first two lines of each message into a table next to its cause. Keep the table for the next time something fails.Label instead of a Text, inside a StackPane root. Which lines and imports change? Then add label.setStyle("-fx-background-color: lightyellow; -fx-padding: 10;") and describe what the Label can show that the Text could not.Short guides on this topic
Quick answers to what people search for about it, with code to run.
Spotted a mistake, or a part that did not make sense? Report it by email