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
FXMLLoaderreadsfx:controllerand creates aMainControllerobject with the no-argument constructor.- It builds the nodes from the markup and writes each node with an
fx:idinto the field with the same name. - It connects
onAction="#addNote"to theaddNotemethod. - It calls
initialize(); by then all the fields are filled in.
| Element | Why it is needed |
|---|---|
@FXML on a field | Lets the loader fill a private field |
@FXML on a method | Makes 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.