简体   繁体   English

JavaFX ListView 显示旧单元格,不可点击

[英]JavaFX ListView showing old Cells, not clickable

im currently coding a todo app.我目前正在编写一个待办事项应用程序。

Part of it is showing the notes and todos in a ListView, where the user can interact with them.其中一部分是在 ListView 中显示注释和待办事项,用户可以在其中与它们进行交互。 However I have a toggle to determine between archived notes and active ones.但是,我有一个切换来确定存档笔记和活动笔记之间的关系。 When toggling the ObservableList gets updated and the ListView Cells as well, but somehow there end up some old notes which are not interactable anymore.当切换 ObservableList 和 ListView 单元格时也会更新,但不知何故,最终会出现一些不再可交互的旧注释。

前两个音符是正确的,底部是剩下的

The top two notes are right, the bottom ones are left overs and not clickable.前两个音符是正确的,底部是剩下的,不可点击。

I extend my NoteCell that gets displayed in the ListView from ListCell<>我扩展了从 ListCell<> 显示在 ListView 中的 NoteCell


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);
                }
            });
        }
    }

I was thinking about a threading problem, but I did not get a clue.我正在考虑线程问题,但我没有得到任何线索。 When changing to the archived view, I am fetching a parallel stream of Notes, so I figured they should get synchronized, but I don't know how I can achieve this.当更改为存档视图时,我正在获取笔记的并行 stream,所以我认为它们应该同步,但我不知道如何实现这一点。 So I tried a normal stream but still the same issue.所以我尝试了一个普通的 stream 但仍然是同样的问题。

Any help would be appreciated任何帮助,将不胜感激

The code of my overview controller, which has the ListView.我的概述 controller 的代码,其中有 ListView。 Mainly interesting should be the toggleArchive method.主要有趣的应该是 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();

    }
}



There are significant issues with your use of threads in the code you posted, which I won't address here since they're not the topic of the question.您在发布的代码中使用线程存在重大问题,我不会在这里解决,因为它们不是问题的主题。 However, the updateItem() method in a cell subclass is always called on the FX Application Thread, so any use of Platform.runLater() there is redundant at best.但是,单元子类中的updateItem()方法始终在 FX 应用程序线程上调用,因此任何使用Platform.runLater()充其量都是多余的。

Calling Platform.runLater() from the FX Application Thread will place the supplied Runnable into a queue to be run on the same thread at a later time (essentially when all pending things the FX Application Thread have been completed).从 FX 应用程序线程调用Platform.runLater()会将提供的Runnable放入队列中,以便稍后在同一线程上运行(基本上是当 FX 应用程序线程的所有待处理的事情都已完成时)。

The updateItem() method can be called quite frequently, especially when the ListView is first displayed, and when the user is scrolling. updateItem()方法可以非常频繁地调用,尤其是当ListView第一次显示时,以及用户正在滚动时。 There is (deliberately) no defined order in which specific cells have their updateItem() methods invoked, and with which parameters. (故意)没有定义特定单元格调用其updateItem()方法的顺序以及使用哪些参数。 Thus a cell may become empty or non-empty at essentially arbitrary times.因此,一个单元格可能在基本上任意时间变为空或非空。

If the ListView implementation chooses to temporarily make a cell non-empty and then immediately make it empty, your updateItem() method will be called twice in rapid succession on the FX Application Thread.如果ListView实现选择暂时将单元格设为非空,然后立即将其设为空,则您的updateItem()方法将在 FX 应用程序线程上快速连续调用两次。 The first call will schedule a runnable to be run later that sets the graphic to the content of the FXML file.第一次调用将安排一个稍后运行的 runnable,它将图形设置为 FXML 文件的内容。 The second call will set the graphic to null.第二次调用会将图形设置为 null。 If the second call happens before the runnable posted to the queue is executed, the cell which is supposed to be empty will display the content, because the calls to setGraphic() happen in the wrong order.如果第二次调用发生在发布到队列的可运行对象执行之前,则应该为空的单元格将显示内容,因为对setGraphic()的调用以错误的顺序发生。

Simply remove the Platform.runLater(...) from updateItem() .只需从updateItem()中删除Platform.runLater(...) ) 。

@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);

    }
} 

Your current threading simply doesn't work: you are accessing shared data from multiple threads and updating UI elements from background threads.您当前的线程根本不起作用:您正在从多个线程访问共享数据并从后台线程更新 UI 元素。 I would recommend removing all the background threads;我建议删除所有后台线程; if there really are tasks that need to be run in a background thread, you need to learn some of the JavaFX concurrency material.如果真的有任务需要在后台线程中运行,就需要学习一些JavaFX并发资料。 Read this post and this tutorial as a start.阅读这篇文章本教程作为开始。

Briefly, though, an asynchronous implementation of your toggleArchive() method might look something like this:不过,简而言之, 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);
}

Here the potentially long-running task of retrieving and sorting the notes is done in a background thread, and operates solely on a separate list, without affecting the UI or any of the data on which it relies.在这里,检索和排序笔记的潜在长时间运行任务在后台线程中完成,并且仅在单独的列表上运行,不会影响 UI 或它所依赖的任何数据。 When the task completes, the onSucceeded handler, which is called on the FX Application Thread, updates the ListView 's items with the new data.任务完成后,在 FX 应用程序线程上调用的onSucceeded处理程序使用新数据更新ListView的项目。

You need to make similar refactoring for all the asynchronous calls.您需要对所有异步调用进行类似的重构。 Also remove all the low-level synchronized keywords, which, at least after this refactoring, will be unneccessary.还要删除所有低级synchronized关键字,至少在此重构之后,这些关键字将是不必要的。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM