JavaFX ListView показывает старые ячейки, не кликабельные - PullRequest
0 голосов
/ 10 июля 2020

Я в настоящее время кодирую приложение todo.

Часть его показывает заметки и задачи в ListView, где пользователь может взаимодействовать с ними. Однако у меня есть переключатель, чтобы определить между заархивированными заметками и активными. При переключении ObservableList обновляется, а также ListView Cells, но каким-то образом остаются некоторые старые заметки, с которыми больше нельзя взаимодействовать.

Две верхние ноты справа, нижние - слева над

Две верхние заметки справа, нижние - слева и не доступны для кликов.

Я расширяю свой NoteCell, который отображается в ListView из ListCell <>


import javafx.scene.control.*;

public class NoteCell extends ListCell<Note> {

    @FXML
    void initialize() { //Event Handlers}


    @Override
    protected void updateItem(Note note, boolean empty) {
        super.updateItem(note,empty);

        if (empty || note == null) {
            setText(null);
            setGraphic(null);
        } else {
            if (fxmlLoader == null) {
                fxmlLoader = new FXMLLoader(getClass().getResource("/view/NoteCells.fxml"));
                fxmlLoader.setController(this);
                try {
                    fxmlLoader.load();
                } catch (IOException e) {
                    e.printStackTrace();
                    logger.error("IOException: " + e);
                }
            }

            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    cellNoteTitle.setText(note.getTitle());
                    cellNoteDescription.setText(note.getContent());
                    cellNoteDescription.setWrapText(true);
                    cellNoteDescription.maxWidth(394);
                    cellNoteDescription.minWidth(394);
                    cellNoteDate.setText(String.valueOf(note.getCreationDate()));

                    setText(null);
                    setGraphic(rootPane);
                }
            });
        }
    }

Я думал о проблеме с потоками, но не понял. При переходе в заархивированное представление я получаю параллельный поток заметок, поэтому я решил, что они должны синхронизироваться, но я не знаю, как этого добиться. Итак, я попробовал обычный поток, но все еще та же проблема.

Любая помощь приветствуется

Код моего контроллера обзора, в котором есть ListView. В основном интересным должен быть метод toggleArchive.


package mainpackage.controller;

import com.jfoenix.controls.*;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.text.Font;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import mainpackage.ListManager;
import mainpackage.Main;
import mainpackage.animation.FadeIn;
import mainpackage.exceptions.UnsupportedCellType;
import mainpackage.model.Note;
import mainpackage.model.Task;
import mainpackage.threads.ClockThread;
import mainpackage.threads.SaveThread;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

/**
 * Main view after log in. Shows three different views of the created tasks.
 */

public class Overview {

    @FXML
    private ResourceBundle resources;
    @FXML
    private URL location;
    @FXML
    private AnchorPane rootPane;
    @FXML
    private Label dateLabel;
    @FXML
    private Label timeLabel;
    @FXML
    private ImageView overviewAddItemImage;
    @FXML
    private ImageView overviewAddNoteImage;
    @FXML
    private ImageView overviewCalendarImage;
    @FXML
    private ListView<Note> noteListView;
    @FXML
    private ImageView overviewExport;
    @FXML
    private JFXTextField noteListSearchField;
    @FXML
    private JFXComboBox<String> sortNoteListDropdown;
    @FXML
    private JFXToggleButton toggleArchiveButton;
    @FXML
    private ListView<Task> taskListView;
    @FXML
    private JFXTextField taskListSearchField;
    @FXML
    private JFXComboBox<String> sortTaskListDropdown;

    private static final Logger logger = LogManager.getLogger(Main.class.getName());
    private final ListManager listManager = new ListManager();
    private final ObservableList<Task> usersTasks = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    private final ObservableList<Task> usersTasksSearch = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    private final ObservableList<Note> usersNotes = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    private final ObservableList<Note> usersNotesSearch = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
    private final ClockThread clock = new ClockThread();
//    private static final Logger log = LogManager.getLogger(Overview.class);

    @FXML
    synchronized void initialize() {


        logger.info("Overview initializing");
        //listManager.getNoteList().forEach(usersNotes::add);
        //listManager.getTaskList().forEach(usersTasks::add);
        //setLists();

        overviewCalendarImage.setOnMouseClicked(mouseEvent -> loadCalendar());
        overviewAddItemImage.setOnMouseClicked(mouseEvent -> loadAddTask());
        overviewAddNoteImage.setOnMouseClicked(mouseEvent -> loadAddNote());
        overviewExport.setOnMouseClicked(mouseEvent -> export());

        toggleArchiveButton.selectedProperty().addListener((arg0, arg1, arg2) -> {
            if(toggleArchiveButton.isSelected()) {
                noteListSearchField.clear();
                toggleArchive();
            }
            else {
                noteListSearchField.clear();
                toggleActive();
            }
        });

        noteListSearchField.textProperty().addListener((observable, oldValue, newValue) -> {
            //debugLogger.debug("Value Changed from: " + oldValue + " to " + newValue);

            if (!newValue.trim().isEmpty() && usersNotes.size() > 0) {
                usersNotesSearch.setAll(search(noteListSearchField.getText(), usersNotes));
                noteListView.setItems(usersNotesSearch);
            } else {
                noteListView.setItems(usersNotes);
            }

            //debugLogger.debug("Search");
            //debugLogger.info("TaskListView Size: " + todolistTaskList.getItems().size());
            //debugLogger.info("TaskList Size: " + taskListView.size());
            //debugLogger.info("tasks Arraylist Size: " + tasks.getTasks().size());
        });

        sortNoteListDropdown.setOnAction(event -> sortNotes(sortNoteListDropdown.getValue()));

        sortNoteListDropdown.setValue("Sort by date (newest to oldest)");

        //Initializing clock
        clock.setLabels(timeLabel, dateLabel);
        clock.setDaemon(true);
        clock.start();

        ExecutorService ex = Executors.newCachedThreadPool();
        ex.execute(this::setNotes);
        ex.execute(this::setTasks);
        ex.shutdown();

        sortNotes(sortNoteListDropdown.getValue());
    }

    /**
     * Sorting notes depending on selected String in sortNoteListDropdown (dropdown menu to sort notes in overview)
     * @param choice selected String in DropDown
     */
    private void sortNotes(String choice) {
        switch (choice) {
            case "Sort by date (newest to oldest)":
                sortDateDesc(usersNotes);
                break;
            case "Sort by date (oldest to newest)":
                sortDateAsc(usersNotes);
                break;
            case "Sort alphabetically (A-Z)":
                sortTitleAsc(usersNotes);
                break;
            case "Sort alphabetically (Z-A)":
                sortTitleDesc(usersNotes);
                break;
        }
    }


    /**
     * Clearing list of user's note and adding only archived notes.
     * Result: only archived notes are shown when toggleArchiveButton is selected
     */
    private synchronized void toggleArchive() {
        usersNotes.clear();
        listManager.getNoteList().filter(n -> n.getState()==2).forEach(usersNotes::add);
        sortNotes(sortNoteListDropdown.getValue());
    }

    /**
     * Clearing list of user's note and adding only archived notes.
     * Result: only active notes are shown when toggleArchiveButton is not selected
     */
    private void toggleActive() {
        usersNotes.clear();
        listManager.getNoteList().forEach((n) -> {
            if (n.getState() == 0) {
                usersNotes.add(n);
            }
        });
        sortNotes(sortNoteListDropdown.getValue());
    }

    /**
     * Sorting list of user's notes by date (descending)
     * @param usersNotes list of user's notes
     */
    private void sortDateDesc(ObservableList<Note> usersNotes) {
            usersNotes.sort((t1, t2) -> t2.getCreationDate().compareTo(t1.getCreationDate()));
            //debugLogger.info("List " + list.toString() + "  sorted by takdates in descending order.");
    }

    /**
     * Sorting list of user's notes by date (ascending)
     * @param usersNotes list of user's notes
     */
    private void sortDateAsc(ObservableList<Note> usersNotes) {
        usersNotes.sort(Comparator.comparing(Note::getCreationDate));
        //debugLogger.info("List " + list.toString() + "  sorted by takdates in descending order.");

    }

    /**
     * Sorting list of user's notes alphabetically (ascending)
     * @param usersNotes list of user's notes
     */
    private void sortTitleAsc(ObservableList<Note> usersNotes) {
        usersNotes.sort(Comparator.comparing(n -> n.getTitle().toUpperCase()));
        //debugLogger.info("List " + list.toString() + "  sorted by title in ascending order.");
    }

    /**
     * Sorting list of user's notes alphabetically (descending)
     * @param usersNotes list of user's notes
     */
    private void sortTitleDesc(ObservableList<Note> usersNotes) {
        usersNotes.sort((n1, n2) -> n2.getTitle().toUpperCase().compareTo(n1.getTitle().toUpperCase()));
        //debugLogger.info("List " + list.toString() + "  sorted by title in descending order.");
    }

    /**
     * Exporting notes and tasks into a .txt file on user's computer
     */
    private void export() {
        FileChooser fileChooser = new FileChooser();
        FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("TXT files (*.txt)", "*.txt");
        fileChooser.getExtensionFilters().add(extFilter);
        fileChooser.setInitialFileName("Orga-Exports.txt");
        File file = fileChooser.showSaveDialog(rootPane.getScene().getWindow());
        if (file != null) {
            SaveThread save = new SaveThread(file);
            save.setDaemon(true);
            save.start();
        }
    }

    public synchronized void setNotes() {

        // Placeholder if user has no notes
        Label noNotes = new Label("No notes yet!");
        noNotes.setFont(new Font(20));
        noteListView.setPlaceholder(noNotes);

        usersNotes.clear();
        listManager.getNoteList().filter(t->t.getState()==0).forEach(usersNotes::add);
        CellFactory cellFactory = new CellFactory();
        noteListView.setCellFactory(NoteCell -> {
            try {
                return cellFactory.createCell("note");
            } catch (UnsupportedCellType unsupportedCellType) {
                unsupportedCellType.printStackTrace();
                return new JFXListCell<>();
            }
        });
        noteListView.setItems(usersNotes);
        logger.info("Notes loaded to overview listview");
    }

    public synchronized void setTasks() {

        // Placeholder if user has no tasks
        Label noTasks = new Label("No tasks yet!");
        noTasks.setFont(new Font(20));
        taskListView.setPlaceholder(noTasks);

        usersTasks.clear();
        listManager.getTaskList().filter(t->t.getState()==0||t.getState()==1).forEach(usersTasks::add);
        CellFactory cellFactory = new CellFactory();
        taskListView.setCellFactory(TaskCell -> {
           try {
               return cellFactory.createCell("task");
           } catch (UnsupportedCellType unsupportedCellType) {
               unsupportedCellType.printStackTrace();
               return new JFXListCell<>();
           }
        });
        taskListView.setItems(usersTasks);

        logger.info("Tasks loaded to overview listview");

    }

    private void loadAddNote() {

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("/view/CreateNotes.fxml"));

        try {
            loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Parent root = loader.getRoot();
        Stage stage = new Stage();
        stage.setScene(new Scene(root));
        stage.setResizable(false);
        stage.getIcons().add(new Image("icon/Logo organizingTool 75x75 blue.png"));
        overviewAddNoteImage.setDisable(true);
        stage.showAndWait();
        if (!toggleArchiveButton.isSelected()) {
            usersNotes.add(listManager.getLatestNote());
        }
        sortNotes(sortNoteListDropdown.getValue());
        overviewAddNoteImage.setDisable(false);

    }

    private void loadAddTask() {

        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("/view/CreateTask.fxml"));
        loader.setController(new CreateTask());

        try {
            loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Parent root = loader.getRoot();
        Stage stage = new Stage();
        stage.setScene(new Scene(root));
        stage.setResizable(false);
        stage.getIcons().add(new Image("icon/Logo organizingTool 75x75 blue.png"));
        overviewAddItemImage.setDisable(true);
        stage.showAndWait();
        overviewAddItemImage.setDisable(false);

    }

    private void loadCalendar() {

        Stage stage = (Stage) rootPane.getScene().getWindow();
        stage.setTitle("Calendar");

        AnchorPane calendar = null;
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("/view/Calendar.fxml"));
        try {
            calendar = loader.load();
        } catch (IOException e) {
            e.printStackTrace();
        }
        new FadeIn(rootPane).play();
        rootPane.getChildren().clear();
        rootPane.getChildren().setAll(calendar);

    }

    private ArrayList<Note> search(String filter, ObservableList<Note> list) {

        //debugLogger.info("Searching for the filter : " + filter + "in list " + list.toString());
        ArrayList<Note> searchResult = new ArrayList<>();
            if (!filter.isEmpty() && !filter.trim().equals("")) {
                //debugLogger.info("Searching for a task containing the filter: '" + filter + "'.");
                for (Note t : list) {
                    if (t.getTitle().toLowerCase().contains(filter.toLowerCase()) || t.getContent().toLowerCase().contains(filter.toLowerCase()) || t.getCreationDate().toString().contains(filter.toLowerCase())) {
                        searchResult.add(t);
                    }
                }
                return searchResult;
            } else if (searchResult.isEmpty()) {
                // debugLogger.info("No task found containing the filter: '" + filter + "'.");
            } else {
                searchResult.addAll(list);
            }
            return searchResult;

    }

    @FXML
    void logout(ActionEvent event) {

        ListManager.wipe();

        AnchorPane login = null;
        try {
            login = FXMLLoader.load(getClass().getResource("/view/Login.fxml"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        rootPane.getChildren().setAll(login);
        new FadeIn(login).play();

    }
}



1 Ответ

3 голосов
/ 10 июля 2020

Существуют значительные проблемы с использованием потоков в опубликованном вами коде, которые я не буду здесь рассматривать, поскольку они не являются главной c вопросом. Однако метод updateItem() в подклассе ячейки всегда вызывается в потоке приложения FX, поэтому любое использование Platform.runLater() в лучшем случае избыточно.

Вызов Platform.runLater() из потока приложения FX приведет к предоставленный Runnable в очередь для запуска в том же потоке в более позднее время (по сути, когда все ожидающие обработки потока приложения FX были завершены).

Метод updateItem() может вызываться довольно часто , особенно когда впервые отображается ListView и когда пользователь выполняет прокрутку. Не существует (преднамеренно) определенного порядка, в котором для определенных c ячеек вызываются их методы updateItem() и с какими параметрами. Таким образом, ячейка может становиться пустой или непустой практически в произвольное время.

Если реализация ListView решит временно сделать ячейку непустой, а затем немедленно сделать ее пустой, ваш метод updateItem() будет вызывается дважды в быстрой последовательности в потоке приложения FX. Первый вызов будет планировать запуск исполняемого файла, который будет запущен позже, который устанавливает graphi c в содержимое файла F XML. Второй вызов установит для графа c значение null. Если второй вызов происходит до того, как запускаемый объект, помещенный в очередь, будет выполнен, в ячейке, которая должна быть пустой, будет отображаться содержимое, потому что вызовы setGraphic() происходят в неправильном порядке.

Просто удалите Platform.runLater(...) от updateItem().

@Override
protected void updateItem(Note note, boolean empty) {
    super.updateItem(note,empty);

    if (empty || note == null) {
        setText(null);
        setGraphic(null);
    } else {
        if (fxmlLoader == null) {
            fxmlLoader = new FXMLLoader(getClass().getResource("/view/NoteCells.fxml"));
            fxmlLoader.setController(this);
            try {
                fxmlLoader.load();
            } catch (IOException e) {
                e.printStackTrace();
                logger.error("IOException: " + e);
            }
        }

        cellNoteTitle.setText(note.getTitle());
        cellNoteDescription.setText(note.getContent());
        cellNoteDescription.setWrapText(true);
        cellNoteDescription.maxWidth(394);
        cellNoteDescription.minWidth(394);
        cellNoteDate.setText(String.valueOf(note.getCreationDate()));

        setText(null);
        setGraphic(rootPane);

    }
} 

Текущая потоковая передача просто не работает: вы получаете доступ к общим данным из нескольких потоков и обновляете элементы пользовательского интерфейса из фоновых потоков. Я бы рекомендовал удалить все фоновые потоки; если действительно есть задачи, которые необходимо запускать в фоновом потоке, вам необходимо изучить некоторые материалы по параллелизму JavaFX. Прочтите этот пост и этот учебник для начала.

Вкратце, асинхронная реализация вашего метода toggleArchive() может выглядеть примерно так:

// move this to an instance field:
private ExecutorService ex = Executors.newCachedThreadPool();

private void toggleArchive() {
    final String choice = sortNoteListDropdown.getValue();
    Task<List<Note>> getNotesTask = new Task<>() {
        @Override
        public List<Note> call() {
            List<Note> notes = listManager.getNoteList()
                .filter(n -> n.getState()==2)
                .collect(Collectors.toList());
            // sort notes here based on choice
            // (note you could sort the stream after the filter
            // operation instead)
            return notes ;
        }
    };
    getNotesTask.setOnSucceeded(e -> userNotes.setAll(getNotesTask.getValue()));
    ex.submit(getNotesTask);
}

Здесь потенциально длительная задача извлечения и сортировки заметок выполняется в фоновом потоке и работает исключительно с отдельным списком, не затрагивая пользовательский интерфейс или какие-либо данные, на которые он опирается. Когда задача завершается, обработчик onSucceeded, который вызывается в потоке приложения FX, обновляет элементы ListView новыми данными.

Вам необходимо провести аналогичный рефакторинг для всех асинхронных вызовов . Также удалите все ключевые слова низкого уровня synchronized, которые, по крайней мере, после этого рефакторинга, станут ненужными.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...