ListView и ObservableList
Пора хранить заметки по-настоящему. Каждая заметка станет объектом записи Note, а все они — элементами наблюдаемого списка ObservableList. Это обычный List, который сообщает подписчикам о каждом добавлении и удалении. ListView подписывается на такой список и перерисовывается сам: добавили элемент в список — он появился на экране.
Запись Note
Создайте файл для модели:
// src/main/java/com/example/notes/Note.java
package com.example.notes;
public record Note(String title, String text, String category, boolean important) {
}Запись неизменяема: чтобы изменить заметку, создают новую и заменяют ею старую в списке.
Список и ListView
// src/main/java/com/example/notes/App.java — поля класса
private final ObservableList<Note> notes = FXCollections.observableArrayList();
private final ListView<Note> listView = new ListView<>(notes);FXCollections.observableArrayList() создаёт пустой наблюдаемый список. ListView получает его в конструкторе и показывает его элементы. Для начала положим в список две заметки:
// src/main/java/com/example/notes/App.java — фрагмент start()
notes.addAll(
new Note("Купить молоко", "2 литра", "Дом", true),
new Note("Позвонить Олегу", "Обсудить отпуск", "Работа", false));Как показать элемент
По умолчанию ListView выводит toString() элемента, у записи это Note[title=Купить молоко, text=2 литра, ...]. Чтобы показать только заголовок, задают фабрику ячеек — объект, который создаёт строки списка:
// src/main/java/com/example/notes/App.java — фрагмент start()
listView.setCellFactory(view -> new ListCell<>() {
@Override
protected void updateItem(Note note, boolean empty) {
super.updateItem(note, empty);
if (empty || note == null) {
setText(null);
} else {
setText(note.important() ? "★ " + note.title() : note.title());
}
}
});ListView создаёт всего несколько ячеек на видимую часть и переиспользует их при прокрутке. Метод updateItem вызывается, когда ячейке достаётся другой элемент или она становится пустой, — поэтому пустой случай обязательно обрабатывают.
Выбор, добавление и удаление
Выбранный элемент хранится в модели выбора getSelectionModel(). Подпишемся на него, чтобы показывать заметку в форме:
// src/main/java/com/example/notes/App.java — фрагмент
listView.getSelectionModel().selectedItemProperty().addListener(
(observable, oldNote, note) -> {
if (note != null) {
showNote(note);
}
});
// ...
private void addNote(ActionEvent event) {
Note note = new Note(
titleField.getText().strip(),
textArea.getText(),
categoryBox.getValue(),
importantBox.isSelected());
notes.add(note);
listView.getSelectionModel().select(note);
}
private void deleteNote(ActionEvent event) {
int index = listView.getSelectionModel().getSelectedIndex();
if (index >= 0) {
notes.remove(index);
}
}Мы меняем только список notes, а ListView обновляется сам. Метод showNote заполняет поля формы значениями заметки.
Привязки к списку
Счётчик и кнопку «Удалить» привяжем так же, как в прошлом уроке:
// src/main/java/com/example/notes/App.java — фрагмент start()
deleteButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull());
Label countLabel = new Label();
countLabel.textProperty().bind(Bindings.size(notes).asString("Заметок: %d"));Bindings.size(notes) — число элементов списка в виде привязки. Своё свойство count больше не нужно.
Класс целиком
// src/main/java/com/example/notes/App.java
package com.example.notes;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class App extends Application {
private final ObservableList<Note> notes = FXCollections.observableArrayList();
private final ListView<Note> listView = new ListView<>(notes);
private final TextField titleField = new TextField();
private final TextArea textArea = new TextArea();
private final ComboBox<String> categoryBox = new ComboBox<>();
private final CheckBox importantBox = new CheckBox("Важная");
@Override
public void start(Stage stage) {
notes.addAll(
new Note("Купить молоко", "2 литра", "Дом", true),
new Note("Позвонить Олегу", "Обсудить отпуск", "Работа", false));
listView.setCellFactory(view -> new ListCell<>() {
@Override
protected void updateItem(Note note, boolean empty) {
super.updateItem(note, empty);
if (empty || note == null) {
setText(null);
} else {
setText(note.important() ? "★ " + note.title() : note.title());
}
}
});
listView.getSelectionModel().selectedItemProperty().addListener(
(observable, oldNote, note) -> {
if (note != null) {
showNote(note);
}
});
textArea.setPrefRowCount(5);
textArea.setWrapText(true);
categoryBox.getItems().addAll("Работа", "Дом", "Идеи");
categoryBox.setValue("Идеи");
Button addButton = new Button("Добавить");
addButton.setOnAction(this::addNote);
addButton.disableProperty().bind(titleField.textProperty().isEmpty());
Button deleteButton = new Button("Удалить");
deleteButton.setOnAction(this::deleteNote);
deleteButton.disableProperty().bind(
listView.getSelectionModel().selectedItemProperty().isNull());
Label countLabel = new Label();
countLabel.textProperty().bind(Bindings.size(notes).asString("Заметок: %d"));
GridPane form = new GridPane();
form.setHgap(8);
form.setVgap(8);
form.setPadding(new Insets(0, 0, 0, 16));
form.add(new Label("Заголовок:"), 0, 0);
form.add(titleField, 1, 0);
form.add(new Label("Текст:"), 0, 1);
form.add(textArea, 1, 1);
form.add(new Label("Категория:"), 0, 2);
form.add(categoryBox, 1, 2);
form.add(importantBox, 1, 3);
ColumnConstraints labels = new ColumnConstraints();
labels.setMinWidth(Region.USE_PREF_SIZE);
ColumnConstraints fields = new ColumnConstraints();
fields.setHgrow(Priority.ALWAYS);
form.getColumnConstraints().addAll(labels, fields);
VBox listPane = new VBox(8, listView, countLabel);
listPane.setPrefWidth(200);
VBox.setVgrow(listView, Priority.ALWAYS);
HBox buttons = new HBox(8, addButton, deleteButton);
buttons.setPadding(new Insets(12, 0, 0, 0));
Label header = new Label("Мои заметки");
BorderPane root = new BorderPane(form, header, null, buttons, listPane);
root.setPadding(new Insets(16));
BorderPane.setMargin(header, new Insets(0, 0, 12, 0));
stage.setTitle("Заметки");
stage.setScene(new Scene(root, 640, 480));
stage.show();
}
private void addNote(ActionEvent event) {
Note note = new Note(
titleField.getText().strip(),
textArea.getText(),
categoryBox.getValue(),
importantBox.isSelected());
notes.add(note);
listView.getSelectionModel().select(note);
}
private void deleteNote(ActionEvent event) {
int index = listView.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());
}
public static void main(String[] args) {
launch(args);
}
}Слева — список из «★ Купить молоко» и «Позвонить Олегу», под ним «Заметок: 2». Щёлкните по заметке — её данные появятся в форме, а кнопка «Удалить» станет доступной. Измените заголовок на «Прочитать книгу» и нажмите «Добавить» — в списке станет три строки, новая будет выделена.
Частая ошибка
Данные держат в обычном ArrayList, а в ListView передают FXCollections.observableArrayList(list). Этот метод копирует элементы в новый список, поэтому последующие list.add(...) на экране не появляются — и ошибки при этом нет. Храните заметки в одном ObservableList и меняйте именно его.