Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

The FXML controller

FXML describes how the window looks, and the controller describes what it does. A controller is an ordinary Java class that FXMLLoader creates itself while loading the markup. The loader puts the nodes with matching fx:id values into the controller's fields and connects buttons to handler methods. All the logic that used to be in App moves into the controller.

Linking the markup and the class

The root tag names the controller class, and the buttons name a method after #:

<!-- src/main/resources/com/example/notes/main-view.fxml — changed lines -->
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
            fx:controller="com.example.notes.MainController">
 
<Button fx:id="addButton" text="Add" onAction="#addNote"/>
<Button fx:id="deleteButton" text="Delete" onAction="#deleteNote"/>

The rest of the file stays the same.

The controller class

Create a file next to App:

// src/main/java/com/example/notes/MainController.java
package com.example.notes;
 
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
 
public class MainController {
 
    private final ObservableList<Note> notes = FXCollections.observableArrayList(
            new Note("Buy milk", "2 liters", "Home", true),
            new Note("Call Oleg", "Discuss the vacation", "Work", false));
 
    @FXML
    private TableView<Note> table;
    @FXML
    private TableColumn<Note, String> titleColumn;
    @FXML
    private TableColumn<Note, String> categoryColumn;
    @FXML
    private TableColumn<Note, String> importantColumn;
    @FXML
    private Label countLabel;
    @FXML
    private TextField titleField;
    @FXML
    private TextArea textArea;
    @FXML
    private ComboBox<String> categoryBox;
    @FXML
    private CheckBox importantBox;
    @FXML
    private Button addButton;
    @FXML
    private Button deleteButton;
 
    @FXML
    private void initialize() {
        titleColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().title()));
        categoryColumn.setCellValueFactory(cell -> new SimpleStringProperty(cell.getValue().category()));
        importantColumn.setCellValueFactory(
                cell -> new SimpleStringProperty(cell.getValue().important() ? "★" : ""));
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY_FLEX_LAST_COLUMN);
        table.setItems(notes);
 
        table.getSelectionModel().selectedItemProperty().addListener(
                (observable, oldNote, note) -> {
                    if (note != null) {
                        showNote(note);
                    }
                });
 
        categoryBox.getItems().addAll("Work", "Home", "Ideas");
        categoryBox.setValue("Ideas");
 
        countLabel.textProperty().bind(Bindings.size(notes).asString("Notes: %d"));
        addButton.disableProperty().bind(titleField.textProperty().isEmpty());
        deleteButton.disableProperty().bind(
                table.getSelectionModel().selectedItemProperty().isNull());
    }
 
    @FXML
    private void addNote() {
        Note note = new Note(
                titleField.getText().strip(),
                textArea.getText(),
                categoryBox.getValue(),
                importantBox.isSelected());
        notes.add(note);
        table.getSelectionModel().select(note);
    }
 
    @FXML
    private void deleteNote() {
        int index = table.getSelectionModel().getSelectedIndex();
        if (index >= 0) {
            notes.remove(index);
        }
    }
 
    private void showNote(Note note) {
        titleField.setText(note.title());
        textArea.setText(note.text());
        categoryBox.setValue(note.category());
        importantBox.setSelected(note.important());
    }
}

App stays as in the previous lesson: it only loads the FXML.

How it works

  1. FXMLLoader reads fx:controller and creates a MainController object with the no-argument constructor.
  2. It builds the nodes from the markup and writes each node with an fx:id into the field with the same name.
  3. It connects onAction="#addNote" to the addNote method.
  4. It calls initialize(); by then all the fields are filled in.
ElementWhy it is needed
@FXML on a fieldLets the loader fill a private field
@FXML on a methodMakes a private method available to onAction
initialize()Setup after loading: columns, bindings, starting data

A handler does not have to declare an ActionEvent parameter if it does not need the event. In the controller's constructor the fields are still empty, so all setup goes into initialize.

Trying it out

Run mvn javafx:run. The window is the same as in the TableView lesson: two notes in the table, "Notes: 2", and the "Add" button is disabled while the title is empty. Adding, selecting and deleting work, but all the code is now in the controller and the layout is in FXML.

Common mistake

The name in onAction must match the method name exactly. If the markup says #addNotes and the method is called addNote, the window does not open: javafx.fxml.LoadException: Error resolving onAction='#addNotes', either the event handler is not in the Namespace or there is an error in the script. The message includes the file path and line number. And if you forget opens com.example.notes to javafx.fxml in module-info.java, the error is module com.example.notes does not "opens com.example.notes" to module javafx.fxml.

Thanks for reading!

Donate to the site — we work hard for you.