Контроллер FXML
FXML описывает, как окно выглядит, а контроллер — что оно делает. Контроллер — обычный Java-класс, который FXMLLoader создаёт сам при загрузке разметки. Загрузчик подставляет в поля контроллера узлы с совпадающими fx:id и связывает кнопки с методами-обработчиками. В контроллер переедет вся логика, которая раньше была в App.
Связь разметки и класса
В корневом теге указывают класс контроллера, а у кнопок — имя метода после #:
<!-- src/main/resources/com/example/notes/main-view.fxml — изменённые строки -->
<BorderPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.notes.MainController">
<Button fx:id="addButton" text="Добавить" onAction="#addNote"/>
<Button fx:id="deleteButton" text="Удалить" onAction="#deleteNote"/>Остальной файл не меняется.
Класс контроллера
Создайте файл рядом с 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("Купить молоко", "2 литра", "Дом", true),
new Note("Позвонить Олегу", "Обсудить отпуск", "Работа", 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("Работа", "Дом", "Идеи");
categoryBox.setValue("Идеи");
countLabel.textProperty().bind(Bindings.size(notes).asString("Заметок: %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 остаётся таким, как в прошлом уроке: он только загружает FXML.
Как это работает
FXMLLoaderчитаетfx:controllerи создаёт объектMainControllerконструктором без параметров.- Строит узлы из разметки и записывает каждый узел с
fx:idв поле с тем же именем. - Связывает
onAction="#addNote"с методомaddNote. - Вызывает
initialize()— к этому моменту все поля уже заполнены.
| Элемент | Зачем нужен |
|---|---|
@FXML на поле | Разрешает загрузчику заполнить закрытое поле |
@FXML на методе | Делает закрытый метод доступным для onAction |
initialize() | Настройка после загрузки: колонки, привязки, начальные данные |
Обработчику можно не объявлять параметр ActionEvent, если событие не нужно. В конструкторе контроллера поля ещё пусты, поэтому вся настройка — в initialize.
Проверка
Запустите mvn javafx:run. Окно такое же, как в уроке о TableView: две заметки в таблице, «Заметок: 2», кнопка «Добавить» неактивна, пока заголовок пуст. Добавление, выбор и удаление работают, но весь код теперь в контроллере, а разметка — в FXML.
Частая ошибка
Имя в onAction должно точно совпадать с именем метода. Если в разметке написано #addNotes, а метод называется addNote, окно не откроется: javafx.fxml.LoadException: Error resolving onAction='#addNotes', either the event handler is not in the Namespace or there is an error in the script. В сообщении есть путь к файлу и номер строки. Если же забыть opens com.example.notes to javafx.fxml в module-info.java, ошибка будет module com.example.notes does not "opens com.example.notes" to module javafx.fxml.