Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Multiple windows

The main window is not the only one: an application can open as many Stage windows as it needs. For the notebook we will make an edit window. A double click on a note opens it, and after "Save" the changed note returns to the table. From this lesson on, the main window form is only for new notes.

The window markup

<?xml version="1.0" encoding="UTF-8"?>
<!-- src/main/resources/com/example/notes/edit-view.fxml -->
 
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
 
<VBox spacing="8" prefWidth="400" stylesheets="@styles.css"
      xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
      fx:controller="com.example.notes.EditController">
    <padding>
        <Insets top="16" right="16" bottom="16" left="16"/>
    </padding>
    <TextField fx:id="titleField"/>
    <TextArea fx:id="textArea" prefRowCount="6" wrapText="true"/>
    <CheckBox fx:id="importantBox" text="Important"/>
    <HBox spacing="8" alignment="CENTER_RIGHT">
        <Button text="Save" onAction="#save" defaultButton="true"/>
        <Button text="Cancel" onAction="#cancel" cancelButton="true"/>
    </HBox>
</VBox>

stylesheets="@styles.css" attaches the same styles: a new scene has none of its own. defaultButton fires on Enter and cancelButton on Esc.

The window controller

// src/main/java/com/example/notes/EditController.java
package com.example.notes;
 
import java.util.Optional;
import javafx.fxml.FXML;
import javafx.scene.control.CheckBox;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
 
public class EditController {
 
    @FXML
    private TextField titleField;
    @FXML
    private TextArea textArea;
    @FXML
    private CheckBox importantBox;
 
    private Note note;
    private Note result;
 
    public void setNote(Note note) {
        this.note = note;
        titleField.setText(note.title());
        textArea.setText(note.text());
        importantBox.setSelected(note.important());
    }
 
    public Optional<Note> getResult() {
        return Optional.ofNullable(result);
    }
 
    @FXML
    private void save() {
        result = new Note(
                titleField.getText().strip(),
                textArea.getText(),
                note.category(),
                importantBox.isSelected());
        close();
    }
 
    @FXML
    private void cancel() {
        close();
    }
 
    private void close() {
        Stage stage = (Stage) titleField.getScene().getWindow();
        stage.close();
    }
}

Data comes in through an ordinary public method, setNote, and goes out through getResult. If "Cancel" was clicked, result stays null and the result is empty. The controller finds its window through any node: getScene().getWindow().

Opening the window

In the main controller we load the markup, get the controller and show the window:

// src/main/java/com/example/notes/MainController.java — the new editNote() method
private void editNote(int index) {
    try {
        FXMLLoader loader = new FXMLLoader(App.class.getResource("edit-view.fxml"));
        Parent root = loader.load();
        EditController controller = loader.getController();
        controller.setNote(notes.get(index));
 
        Stage stage = new Stage();
        stage.setTitle("Edit note");
        stage.initOwner(table.getScene().getWindow());
        stage.initModality(Modality.WINDOW_MODAL);
        stage.setScene(new Scene(root));
        stage.showAndWait();
 
        controller.getResult().ifPresent(note -> notes.set(index, note));
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

loader.getController() returns the object the loader created from fx:controller. WINDOW_MODAL blocks the owner window while this one is open. showAndWait() waits for the window to close, so the next line already sees the result. notes.set replaces the note, and the table updates.

ModalityWhat it blocks
NONENothing, the windows are independent (the default)
WINDOW_MODALThe owner window from initOwner
APPLICATION_MODALAll application windows

A double click on a row

Clicks are caught on table rows, so in initialize() we set a row factory instead of the old selection listener:

// src/main/java/com/example/notes/MainController.java — part of initialize()
table.setRowFactory(view -> {
    TableRow<Note> row = new TableRow<>();
    row.setOnMouseClicked(event -> {
        if (event.getClickCount() == 2 && !row.isEmpty()) {
            editNote(row.getIndex());
        }
    });
    return row;
});

The showNote method is no longer needed. Instead of selecting the new row, addNote now calls a new clearForm() method: it clears the title and the text and unticks the check box.

Trying it out

Double-click "Buy milk". An "Edit note" window opens with the title, the text "2 liters" and a ticked check box, with blue "Save" and "Cancel" buttons at the bottom right. While it is open, the main window does not respond. The title in the field is fully selected: type "Buy milk and bread" and press Enter. The window closes and the table shows the new title.

Switching scenes

Sometimes a second window is not needed, and replacing the content of the current one is enough, for example to show a settings screen. For that, call stage.setScene(otherScene) or change the root: scene.setRoot(newRoot). The window stays the same; only its content changes.

Common mistake

Modality is set after the window is shown: stage.show() followed by stage.initModality(...). The program crashes with java.lang.IllegalStateException: Cannot set modality once stage has been set visible. Call initOwner and initModality before show() or showAndWait().

Thanks for reading!

Donate to the site — we work hard for you.