Any application more complicated than a calculator is going to need multiple windows. From login screens to dashboard editors and warnings, windows often control the flow of an application’s behaviour or state.

A new window can be created in JavaFX by creating and opening a Stage. Before opening, each Stage will need a Scene, which can be constructed from FXML or using Java code. Window “modality” can be used to control the state of an application in which multiple windows are open and can be controlled by invoking initModality().

Alerts and Dialogs can be used to create pre-formatted windows, and can be customised significantly. They’re usually used to facilitate quick interactions with the user, like displaying a warning, or asking a question, rather than prolonged user interaction.

What you’ll get from this article

Trying to create a window can be achieved in two main ways (click the links to jump to that section):

  1. Create and open a new Stage using FXML
  2. Create and open a new Stage using only Java code

There are some specific use cases where creating views directly from Java code will lead to a better user experience, like where you’re running in a really resource-constrained environment. Check out the second section for more details on that one.

Next, because they’re so important in controlling the flow of information in your application, I’ll also cover how to:

  1. Block input to some or all windows when opening a new one
  2. Close the current Window when opening a new one
  3. Open a new Scene in the current window

Finally, I want to touch on which other options are available to JavaFX developers when they’re opening new windows. It doesn’t always have to be a completely custom Stage object.

  1. Default Window types available in JavaFX

In fact, I think a really important part of using JavaFX effectively is being aware of the tools that are already available when you want to do something like create a Window.

Create a new window with FXML

One of the main benefits of creating a Stage rather than an Alert or Dialog is the complete control it gives you on the layout and styling of the view. This can facilitate incredibly complex user interactions, which is why it’s ideal for the main windows in your application.

I think the most effective way to create and style a view is by splitting the responsibility between Java code, FXML and CSS. It’s how JavaFX was designed, and you can generate some really beautiful looking windows with very little code.

ElemetnResponsibility
JavaCreate the Stage, and Scene objects.
Show the window.
FXMLDefine the layout of the view
CSSStyle the view

OK, here’s a quick example of how you’d go about doing that.

//Create Stage
Stage newWindow = new Stage();
newWindow.setTitle("New Scene");

//Create view from FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("mainWindow.fxml"));

//Set view in window
newWindow.setScene(new Scene(loader.load()));

//Launch
newWindow.show();
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox alignment="CENTER" spacing="20.0" styleClass="container" stylesheets="@styles.css"
      xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
   <Label styleClass="app-title" text="Fancy App Title"/>
   <TextField maxWidth="200.0" promptText="Enter your name"/>
   <Button text="OK"/>
</VBox>
@font-face{
  font-family: 'GloriousFree';
  font-style: normal;
  font-weight: 400;
  src: url('https://edencoding.com/wp-content/uploads/2021/02/GloriousFree-dBR6.ttf') format('truetype');
}

.root{
    -fx-background-color: linear-gradient(to bottom right, #2c3e50, #3498db);
}

.label{
    -fx-text-fill: white;
}

.app-title{
    -fx-font: 36px 'Glorious Free';
}

.text-field {
  -fx-border-color: grey;
  -fx-border-width: 0 0 1 0;
  -fx-background-color: transparent;
}

.text-field:focused {
    -fx-border-color: -fx-focus-color;
}
Result:

This is far and away my preferred way of creating windows and views. You absolutely don’t have to include CSS when you’re defining views this way, but the user experience you get by including some styling is just immeasurably deeper.

That being said, if it’s a really simple view, you might want to create it from Java code. That has some performance benefits, because the view won’t be created be reflectively populating your controller and creating the scene graph.

Create a new window with only Java code

Loading views from FXML won’t cause an issue in most situations. However, if you’re running in a resource-constrained environment, loading your views from Java code may improve lead times considerably. If you’ve tried running JavaFX on a Raspberry Pi, that’s the sort of thing I’m talking about.

Loading a view from FXML involves reading the FXML file from disk, parsing the XML structure, and creating the view using Java reflection. That’s always going to be slower than reading pre-compiled Java byte code.

If you’re planning on running the application on something like a Pi, you may want to test load times for the FXML approach against the Java one.

//Create Stage
Stage newWindow = new Stage();
newWindow.setTitle("New Scene");

//Create view in Java
Label title = new Label("This is a pretty simple view!");
TextField textField = new TextField("Enter your name here");
Button button = new Button("OK");
button.setOnAction(event -> {
    //handle button press
});
VBox container = new VBox(title, textField, button);

//Style container
container.setSpacing(15);
container.setPadding(new Insets(25));
container.setAlignment(Pos.CENTER);

//Set view in window
newWindow.setScene(new Scene(container));

//Launch
newWindow.show();
Result:
Simple new windows can be created entirely from Java code

Here, the view may not be quite as ‘sexy’, but with a little tweaking, you can still create a very functional window.

Blocking input to the current window

Depending on how you want to use your window, it may be important to block user input to other windows in the application.

This behaviour is controlled using window ‘modality‘, which is set individually on each window by invoking initModality(Modality modality).

Behaviour can be controlled in one of two ways: “window modal” and “application modal”. Let’s take a look at each one:

Modality.WINDOW_MODAL:

This behaviour blocks user input to the window that launched this new window (and any windows ‘above’ those). BUT, allow input to other windows that are open

When setting the modality to WINDOW_MODAL, you will also need to invoke initOwner(Stage stage) on the new Stage. By passing in the current stage, we lock input to just that Stage

newStage.initOwner(currentStage);

If no owner is specified, JavaFX will treat the window as it it has no owner, and no blocking behaviour will be applied.

Modality.APPLICATION_MODAL
  • This behaviour will block input to all other windows in the application, regardless of how they were produced.

These behaviours can be really useful in controlling the user’s attention and interaction with the software. Ensuring the user provides information before returning to a window can be useful.

Closing the current Window when opening a new one

Another way to control the user’s ability to interact with our application is to close the current window as the new window opens. This ensures that the underlying state of the application changes at the same time that we change the view presented to the user.

To close any Stage in JavaFX, simply invoke hide() or close() on the Stage object to be closed. These method invocations are equivalent and both internally set the showing property of the window to false. Both of these methods must be executed from the JavaFX Application thread.

//close the stage
stage.close();

//equivalent
stage.hide();

Because of JavaFX’s automated garbage collection, once any retained references to the closed stage are lost, it will become eligible for collection.

Once the last window in an application has exited, JavaFX will automatically attempt to close the application. In most cases, this is the desired behaviour. It can be overridden by setting Platform.setImplicitExit(false).

Lastly, there are a few occasions where long-running background tasks might keep an application running even after the last window has closed down. The safest way to ensure this doesn’t happen is to use the concurrency options that are included with JavaFX, rather than using Java-only multi-threading approaches.

Open a new Scene in the current window

Opening a new Scene in the current window can be done in two simple steps:

  1. Identify the current Stage (window) in code
  2. Create a new Scene and invoke setScene() on the Stage.

1. Identifying the current Stage

The current window can be navigated to from anywhere in the scene graph. To get the current stage, simply identify an object that is currently displayed, and invoke getScene().getWindow(). This returns a Window object, which we can cast to Stage.

Both the stage and scene can be accessed from anywhere in the scene

2. Creating and setting a new Scene

Creating and setting a new Scene can be accomplished using either the FXML or Java-only methods above. For the sake of this example, we can do it really easily using the FXML file we defined above.

//Get the current Stage
Stage currentStage = (Stage) button.getScene().getWindow()

//Create view from FXML
FXMLLoader loader = new FXMLLoader(getClass().getResource("secondWindow.fxml"));

//Set new Scene in current window
currentStage.setScene(new Scene(loader.load()));

Default Window types available in JavaFX

Often, you won’t need a fully-fledged, customised Stage to interact with your user. Especially if your new window will mediate a temporary interaction, like displaying information.

In those cases, it might be more convenient to use an Alert of Dialog, which are pre-formatted windows for just that purpose.

Deciding which window you need is mostly based on the type of interaction you need to have with your user. The number of options can seem overwhelming, but I usually break them down into three general types of interaction:

  1. Information only
  2. A quick question you need to ask your user
  3. A completely custom approach

Here they all are, viewed in those three ways:

Alerts, Dialogs and Stage objects can all be used to launch new windows

How to choose which window to use

With that in mind, you probably already have a pretty good idea of how to use most of these, but I find choosing which window to use often comes down to the flow I want to achieve in your application.

In fact, I’ll often draw out the flow I want for an application when I’m designing it.

> Alerts

For simple windows, where I’m displaying information to the user, an Alert is usually enough. They’re simple to create, and are relatively easy to customise, and simple user interaction can be registered using customised buttons.

Code:
//Create an Alert with predefined warning image
Alert alert = new Alert(AlertType.WARNING);

//Set text in conveinently pre-defined layout
alert.setTitle("Warning");
alert.setHeaderText("Are you sure?");
alert.setContentText("Do you want to close the application?");

//Set custom buttons
ButtonType okButton= new ButtonType("Yes, exit", ButtonType.OK);
ButtonType cancelButton= new ButtonType("No, do not exit", ButtonData.CANCEL_CLOSE);
alert.getButtonTypes().setAll(okButton, cancelButton);

//Prevent all interaction with application until resolved.
alert.initModality(Modality.APPLICATION_MODAL);

//Launch
alert.showAndWait();
Result:

> Dialogs

Dialogs allow you to have a slightly richer user interaction. Their feedback isn’t limited to button clicks, and results can still be returned using the convenient Optional API.

Dialogs are amazingly extensible, meaning you can rig up a LoginDialog object in the background and launch it straight from the place you need to in code. You can even load custom content into the layout using FXML and CSS (as can be Alert, to be fair).

Code:
        // Create the login dialog.
        LoginDialog dialog = new LoginDialog();

        //Get result with lambda syntax
        dialog.showAndWait()
                .ifPresent(result -> login(result));
package com.edencoding;

import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.util.Pair;

import java.io.IOException;

public class LoginDialog extends Dialog<Pair<String, String>> {

    @FXML
    private TextField usernameField;
    @FXML
    private PasswordField passwordField;

    public static ButtonType loginButtonType = new ButtonType("Login", ButtonBar.ButtonData.OK_DONE);

    public LoginDialog() {
        setDefaultInformation();
        setDefaultButtons();
        setContent();
        setBehaviour();
        setStyleSheets();
    }

    private void setDefaultInformation() {
        //Default login prompt information
        setTitle("Login");
        setHeaderText("Login");

        // Default login image
        setGraphic(
                new ImageView(
                        new Image(getClass().getResourceAsStream("/images/padlock.png"))
                )
        );
    }

    private void setDefaultButtons() {
        getDialogPane().getButtonTypes().setAll(loginButtonType, ButtonType.CANCEL);
    }

    private void setContent() {
        //Create the content using FXML
        FXMLLoader loader = new FXMLLoader(getClass().getResource("loginView.fxml"));
        loader.setController(this);
        try {
            getDialogPane().setContent(loader.load());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void setBehaviour() {
        // If username and password have both been set, return values, else return null
        setResultConverter(dialogButton ->
                !usernameField.getText().isBlank() && !passwordField.getText().isBlank()
                ? new Pair<>(usernameField.getText(), passwordField.getText())
                : null
        );

        //Only enable login button when username and password have both been entered
        Node loginButton = getDialogPane().lookupButton(LoginDialog.loginButtonType);
        loginButton.disableProperty()
                .bind(Bindings.or(
                        usernameField.textProperty().isEmpty(),
                        passwordField.textProperty().isEmpty()
                ));
    }

    private void setStyleSheets() {
        getDialogPane().getStylesheets().add(
                getClass().getResource("styles.css").toExternalForm()
        );

    }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<GridPane hgap="15.0" vgap="15.0" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
    <padding>
        <Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
    </padding>
    <Label text="Username" />
    <Label text="Password" GridPane.rowIndex="1" />
    <TextField fx:id="usernameField" GridPane.columnIndex="1" />
    <PasswordField fx:id="passwordField" GridPane.columnIndex="1" GridPane.rowIndex="1" />
</GridPane>
.button{
    -fx-background-color: #375fcb;
}

.button:hover{
    -fx-background-color: #fcc200;
}

.button:pressed{
    -fx-background-color: #FEEBAA;
}

.text-field {
  -fx-border-color: grey;
  -fx-border-width: 0 0 1 0;
  -fx-background-color: transparent;
}

.text-field:focused {
    -fx-border-color: -fx-focus-color;
}
Result:

> Stages

For custom, or complex interactions, a Stage is usually appropriate. Good signs you’ll need a Stage are:

  1. Custom behaviour requiring a Controller
  2. Custom layouts not achievable with the DialogPane system
  3. It’s a main window in your application

Conclusions

Launching a new window in JavaFX requires a Stage and Scene, populated with at least one root Node. The normal way to achieve this is by loading a View through JavaFX’s FXMLLoader utility class. However, it’s just as possible in Java code.

To control the application’s state while opening another window, developers can halt input, close the current window, or simply open the new content directly into the current Stage. Blocking input, for instance, might be important for displaying an important warning to the user.

Finally, JavaFX comes with default pre-formatted windows called Alerts and Dialogs, which can be useful in passing interactions with users. I’ve definitely used them in prototyping, even if I come back and create a custom styled window later.