Нет изменений в ListView при вводе текста в фильтр TextField

EDIT --- Показаны классы объектов, добавлен код внутри метода инициализации, удален прежний метод фильтрации и удалено OnAction для TextField из FXML. ---

Мои первые попытки разобраться в этом привели к множеству проблем с Android, так что я здесь.

Я сделал простой графический интерфейс, используя JavaFX, Scenebuilder и FXML. Этот графический интерфейс имитирует инвентарь заводского цеха и может добавлять/удалять из/отображать/сохранять в файл из ObservableList. Также предполагается фильтровать объекты на основе частичных String, введенных пользователем в TextField.

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

Каждый объект печатается с псевдослучайным идентификационным номером с заданным пользователем префиксом (F для цветов, W для сорняков, H для трав, Fn для грибов), именем, цветом и логическими атрибутами, такими как «колючий», «ароматный». , "съедобный" и т. д. Они определяются радиокнопками. Например, если пользователь вводит все значения нового цветка, он печатается как: «ID: F-21, имя: роза, цвет: красный, колючий? Ложь, душистый? Истина».

Вот мои исходные ObservableList и FilteredList:

ObservableList<Plant> observablePlantList = FXCollections.observableArrayList();
FilteredList<Plant> filteredList = new FilteredList<Plant>(observablePlantList);

Вот мой класс Plant и один подкласс (есть 4 абсолютно одинаковых), чтобы показать, как они отображаются в списке (логические значения — это переключатели):

    public class Plant {
public String ID;
public String name;
public String color;
public boolean smell;
public boolean thorns;
public boolean edible;
public boolean poisonous;
public boolean flavor;
public boolean medicine;
public boolean seasonal;
public int idNum;

public Plant(String ID, int idNum, String name, String color, boolean smell, boolean thorns, boolean edible, boolean poisonous, boolean flavor, boolean medicine, boolean seasonal) {
    this.ID = ID;
    this.idNum = randomID(idNum);
    this.name = name;
    this.color = color;
    this.smell = false;
    this.thorns = false;
    this.edible = false;
    this.poisonous = false;
    this.flavor = false;
    this.medicine = false;
    this.seasonal = false;
}

public void setColor(String color) {this.color = color;}
public void setID(String ID) {this.ID = ID;}
public void setName(String name) {this.name = name;}

public String getID() {return ID;}
public String getName() {return name;}

public int randomID(int idNum) {
    Random randomNum = new Random();
    idNum = randomNum.nextInt(50);
    return idNum;
}

public String toString() {

    return "ID: " + this.ID + "-" + this.idNum + ", Name: " + this.name + ", Color: " + this.color;
}

}

Подкласс

    public class Flower extends Plant {

public Flower(String ID, int idNum, String name, String color, boolean smell, boolean thorns, boolean edible, boolean poisonous, boolean flavor, boolean medicine, boolean seasonal) {

    super(ID, idNum, name, color, smell, thorns, edible, poisonous, flavor, medicine, seasonal);
}
public void setSmell(boolean smell) {
    this.smell = smell;
}
public void setThorns(boolean thorns) {
    this.thorns = thorns;
}
public String toString() {

    return super.toString() + ", Scent? " + this.smell + ", Thorns? " + this.thorns;
}

}

Это то, что у меня есть в моем методе инициализации благодаря предложению в ответе ниже. Проблема в том, что ничего не меняется:

    @Override
public void initialize(URL fxmlFileLocation, ResourceBundle rb) {

    plantList.setItems(filteredList);

    //binding filterInput text entries
    filteredList.predicateProperty().bind(Bindings.createObjectBinding(() -> {
        String filterText = filterInput.getText();
        if(filterText == null || filterText.isEmpty()) {
            return null;
        }
        else {
            final String uppercase = filterText.toUpperCase();
            return (plant) -> plant.getName().toUpperCase().contains(uppercase);
        }
    }, filterInput.textProperty()));

Моя цель — фильтровать ObservableArrayList в режиме реального времени, а не использовать кнопку, которая фильтрует и показывает элементы в новой сцене. Я видел, как это реализовано, и мне очень нравится функциональность.

Спасибо за любую предложенную помощь.


person IRGeekSauce    schedule 24.11.2015    source источник
comment
Вы не можете изменить FilteredList, так как он неизменяем. Целью FilteredList является предоставление представления другого списка, ограниченного теми элементами, которые соответствуют Predicate. Кстати: проверка внутри Predicate, является ли новая строка пустой или null, и преобразование этой строки в верхний регистр неоптимально, поскольку это нужно делать только один раз для каждого изменения текста, а не один раз для вызова Predicate.test. То, что вы пытаетесь сделать, почти то же самое, что и я в своем ответе; Разница в том, что я использую привязку для замены предиката, а вы делаете это из слушателя.   -  person fabian    schedule 25.11.2015
comment
Значит, в этом случае предикаты устраняют необходимость в слушателе? Кстати, я новичок в предикатах.   -  person IRGeekSauce    schedule 25.11.2015
comment
Используются слушатели. Это просто скрыто внутри методов API в моем ответе: bind регистрирует прослушиватель, Bindings.createObjectBinding тоже регистрирует прослушиватель, а FilteredList прослушивает изменения в исходном списке. Predicate — это просто функциональный интерфейс, предоставляющий метод, который получает один аргумент и возвращает boolean. Он часто используется для принятия решения на основе входных данных. То, что делает FilteredList, очень похоже на то, что filter делает с Stream, если вы более знакомы с этим.   -  person fabian    schedule 26.11.2015
comment
Нет, я с ними не знаком... пока.   -  person IRGeekSauce    schedule 26.11.2015


Ответы (2)


Что не так с вашим кодом:

  • вы регистрируете прослушиватель в onAction обработчике событий TextField вместо Initializable.initialize метод контроллера. Это означает, что прослушиватель будет зарегистрирован, когда произойдет событие Action (по умолчанию, когда вы нажмете ENTER), и новый будет зарегистрирован каждый раз, когда произойдет событие Action.
  • Вы присваиваете filterItems свойству items вашего ListView, но если текст становится короче, вы не назначаете новое значение перед повторной фильтрацией. Это означает, что plantList.getItems() (список, который вы повторяете) может быть тем же списком, что и тот, в который вы добавляете элементы => ConcurrentModificationException.
  • вы никогда не очищаете отфильтрованный список

Более элегантный способ

Используйте FilteredList. Это класс, который предназначен для фильтрации за вас.

@FXML
private ListView<Plant> plantList;
@FXML
private TextField filterInput;

ObservableList<Plant> observablePlantList = FXCollections.observableArrayList();
FilteredList<Plant> filterItems = new FilteredList<>(observablePlantList);

@Override
public void initialize(URL url, ResourceBundle rb) {
    plantList.setItems(filterItems);
    
    // bind predicate to text filterInput text
    filterItems.predicateProperty().bind(javafx.beans.binding.Bindings.createObjectBinding(() -> {
        String text = filterInput.getText();
        if (text == null || text.isEmpty()) {
            return null;
        } else {
            final String uppercase = text.toUpperCase();
            return (plant) -> plant.getName().toUpperCase().contains(uppercase);
        }
    }, filterInput.textProperty()));
}

Конечно, в этом случае вы удалите атрибут onAction из TextField в файле fxml.

person fabian    schedule 24.11.2015
comment
Я отметил это как приемлемый ответ, но меня нет дома, чтобы проверить его. Я понимаю, что я сделал неправильно, поэтому я верю, что это сработает, как только я попробую. Спасибо, что помогли мне понять, как это работает. - person IRGeekSauce; 24.11.2015
comment
Теперь, когда у меня было время сесть и посмотреть на это, я не понимаю, куда вы поместили слушателя. Похоже, у вас есть все внутри метода инициализации. Больше нет фильтра или метода поиска? Потому что в моем слушателе я вызвал search((String) oldPlant, (String) newPlant); - person IRGeekSauce; 25.11.2015
comment
@csheridan: Теперь, когда я присматриваюсь, ваш метод search кажется еще более странным ... (особенно break;) В вопросе говорится, что вы хотите найти элементы, содержащие текст TextField в качестве подстроки в имени. Я связал предикат, который фильтрует list на значение, созданное Callable (первый параметр createObjectBinding) и зависящее от текста в TextField; если текст в filterInput изменяется, предикат FilteredList обновляется до нового предиката, который ограничивает элементы теми элементами исходный список, содержащий новый текст в имени. - person fabian; 25.11.2015
comment
Да, я хочу, чтобы в списке отображались только те растения, которые соответствуют моей строке, и отфильтровывался до более точного совпадения с большим количеством введенных символов. Например, если у меня есть Дейзи, Роза, Фиалка и Одуванчик, и я наберу D, он покажет Дейзи и Одуванчик. Если я добавлю a, он все равно покажет эти два. Затем, если я наберу n, он покажет только Одуванчик. Нужно сделать обратное, когда я нажимаю клавишу Backspace, пока исходный список не отобразится, когда поле пусто. - person IRGeekSauce; 25.11.2015
comment
Если я не получу ответов, что мне делать, задать вопрос еще раз в надежде, что кто-нибудь его увидит? - person IRGeekSauce; 25.11.2015
comment
Отредактированный пост, чтобы показать обновление того, что происходит сейчас. - person IRGeekSauce; 25.11.2015
comment
Я почти хочу, чтобы вы увидели всю программу, потому что то, что вы видите выше, ничего не делает. Я удалил свой слушатель и добавил все, что показано выше, только к методу инициализации. По крайней мере, он не падает, потому что это все, что я получал раньше. - person IRGeekSauce; 26.11.2015

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

Вот рабочий код, все еще с ChangeListener в методе инициализации, а OnAction все еще удален из файла FXML. Теперь, когда я набираю символ, появляется соответствующее растение и показывает оригинал при нажатии Backspace.

Я искренне извиняюсь за то, что не использовал то, что вы предложили. У меня еще недостаточно опыта, чтобы полностью понять предикаты и привязки, но мне определенно есть что исследовать!

     public void filterPlantList(String oldValue, String newValue) {

    ObservableList<Plant> filteredList = FXCollections.observableArrayList();
    if(filterInput == null || (newValue.length() < oldValue.length()) || newValue == null) {
        plantList.setItems(observablePlantList);
    }
    else {
        newValue = newValue.toUpperCase();
        for(Plant plants : plantList.getItems()) {
            String filterText = plants.getName();
            if(filterText.toUpperCase().contains(newValue)) {
                filteredList.add(plants);
            }
        }
        plantList.setItems(filteredList);
    }
}
@Override
public void initialize(URL fxmlFileLocation, ResourceBundle rb) {

    //add Listener to filterInput TextField
    filterInput.textProperty().addListener(new ChangeListener() {
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            filterPlantList((String) oldValue, (String) newValue);
        }
    });
person IRGeekSauce    schedule 26.11.2015