Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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 и меняйте именно его.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.