【问题标题】:ComboBox ChangeListener getting called 3 times in AutoCompleteDropDownComboBox ChangeListener 在 AutoCompleteDropDown 中被调用 3 次
【发布时间】:2019-04-15 20:44:35
【问题描述】:

在这里使用这个例子:

https://stackoverflow.com/a/47933342

我能够创建一个自动完成的下拉搜索,但是当我添加一个更改侦听器以刷新数据库中的数据时,即使我只选择了一次值,它也会被调用 3 次。我输入一个国家,然后点击国家,输出是:

连接到数据库

连接到数据库

连接到数据库

预期输出是:

连接到数据库

这是我的代码:

package autocomplete;

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class Main extends Application
{

    public static class HideableItem<T>
    {

        private final ObjectProperty<T> object = new SimpleObjectProperty<>();
        private final BooleanProperty hidden = new SimpleBooleanProperty();

        private HideableItem(T object)
        {
            setObject(object);
        }

        private ObjectProperty<T> objectProperty()
        {
            return this.object;
        }

        private T getObject()
        {
            return this.objectProperty().get();
        }

        private void setObject(T object)
        {
            this.objectProperty().set(object);
        }

        private BooleanProperty hiddenProperty()
        {
            return this.hidden;
        }

        private boolean isHidden()
        {
            return this.hiddenProperty().get();
        }

        private void setHidden(boolean hidden)
        {
            this.hiddenProperty().set(hidden);
        }

        @Override
        public String toString()
        {
            return getObject() == null ? null : getObject().toString();
        }
    }

    @Override
    public void start(Stage stage)
    {
        List<String> countries = new ArrayList<>();
        for (String countryCode : Locale.getISOCountries())
        {

            Locale obj = new Locale("", countryCode);
            countries.add(obj.getDisplayCountry());

        }

        ComboBox<HideableItem<String>> comboBox = createComboBoxWithAutoCompletionSupport(countries);
        comboBox.setMaxWidth(Double.MAX_VALUE);

        comboBox.valueProperty().addListener(new ChangeListener()
        {
            @Override
            public void changed(ObservableValue observable, Object oldValue, Object newValue)
            {
                System.out.println("CONNECT TO DATABASE");
            }

        });

        HBox root = new HBox();
        root.getChildren().add(comboBox);

        Scene scene = new Scene(root);
        stage.setScene(scene);
        stage.show();

        comboBox.setMinWidth(comboBox.getWidth());
        comboBox.setPrefWidth(comboBox.getWidth());
    }

    public static void main(String[] args)
    {
        launch();
    }

    private static <T> ComboBox<HideableItem<T>> createComboBoxWithAutoCompletionSupport(List<T> items)
    {
        ObservableList<HideableItem<T>> hideableHideableItems = FXCollections.observableArrayList(hideableItem -> new Observable[]
        {
            hideableItem.hiddenProperty()
        });

        items.forEach(item ->
        {
            HideableItem<T> hideableItem = new HideableItem<>(item);
            hideableHideableItems.add(hideableItem);
        });

        FilteredList<HideableItem<T>> filteredHideableItems = new FilteredList<>(hideableHideableItems, t -> !t.isHidden());

        ComboBox<HideableItem<T>> comboBox = new ComboBox<>();
        comboBox.setItems(filteredHideableItems);

        @SuppressWarnings("unchecked")
        HideableItem<T>[] selectedItem = (HideableItem<T>[]) new HideableItem[1];

        comboBox.addEventHandler(KeyEvent.KEY_PRESSED, event ->
        {
            if (!comboBox.isShowing())
            {
                return;
            }

            comboBox.setEditable(true);
            comboBox.getEditor().clear();
        });

        comboBox.showingProperty().addListener((observable, oldValue, newValue) ->
        {
            if (newValue)
            {
                @SuppressWarnings("unchecked")
                ListView<HideableItem> lv = ((ComboBoxListViewSkin<HideableItem>) comboBox.getSkin()).getListView();

                Platform.runLater(() ->
                {
                    if (selectedItem[0] == null) // first use
                    {
                        double cellHeight = ((Control) lv.lookup(".list-cell")).getHeight();
                        lv.setFixedCellSize(cellHeight);
                    }
                });

                lv.scrollTo(comboBox.getValue());
            } else
            {
                HideableItem<T> value = comboBox.getValue();
                if (value != null)
                {
                    selectedItem[0] = value;
                }

                comboBox.setEditable(false);

                Platform.runLater(() ->
                {
                    comboBox.getSelectionModel().select(selectedItem[0]);
                    comboBox.setValue(selectedItem[0]);
                });
            }
        });

        comboBox.setOnHidden(event -> hideableHideableItems.forEach(item -> item.setHidden(false)));

        comboBox.getEditor().textProperty().addListener((obs, oldValue, newValue) ->
        {
            if (!comboBox.isShowing())
            {
                return;
            }

            Platform.runLater(() ->
            {
                if (comboBox.getSelectionModel().getSelectedItem() == null)
                {
                    hideableHideableItems.forEach(item -> item.setHidden(!item.getObject().toString().toLowerCase().contains(newValue.toLowerCase())));
                } else
                {
                    boolean validText = false;

                    for (HideableItem hideableItem : hideableHideableItems)
                    {
                        if (hideableItem.getObject().toString().equals(newValue))
                        {
                            validText = true;
                            break;
                        }
                    }

                    if (!validText)
                    {
                        comboBox.getSelectionModel().select(null);
                    }
                }
            });
        });

        return comboBox;
    }
}

编辑:

似乎没有真正的解决方案......所以我最终只是听了ObjectProperty,而不是听下拉菜单的变化。如果值不为空,我然后更新属性。

【问题讨论】:

  • 每次调用时oldValue和newValue的值是多少?
  • @Slaw 他们像oldValue = null, newValue = Canada --- 然后是oldValue = Canada, newValue = null, 然后回到oldValue = null, newValue = Canada
  • 嗯...就像Simou's answer 我无法使用您的代码重现问题。每次更改选择时,我只会得到一个“连接到数据库”。
  • @Slaw 你用鼠标点击了那个项目吗?
  • 啊,我做得不对。我错过了我必须输入并选择过滤项目的事实。我可以重现该问题。

标签: java javafx


【解决方案1】:

正在阅读并看到这个,非常有趣,您看到额外条目的原因如下:

  • comboBox.setEditable(true) 使组合框创建一个 TextField 并因此将值设置为 null(文本字段的值)
  • 在框中键入过滤列表,然后单击选择一个项目
  • comboBox.setEditable(false) 导致组合框将 TextField 设为 null,因此将值设为 null
  • comboBox.getSelectionModel().select(selectedItem[0]) 最终将其设置回不为空

看ComboBox/ComboBoxBase,editableProperty上的cmets是

Note that when the editable property changes, the value property is reset, along with any other relevant state.

我不确定在输入和关闭 ComboBox 时是否有特定原因关闭和打开可编辑标志,但可能是您实际上不需要做的事情。

最后,就我自己的知识而言,使用 HideableItem 并且不重置谓词优于仅使用字符串并在文本字段值更改更好时更新谓词?我从未见过以这种方式实现的 FX 中的自动完成功能,我很好奇。

【讨论】:

  • 有趣...这将解决它,但自动完成下拉菜单旨在用作具有可搜索字段的下拉菜单。例如,我可以选择一个值,然后再次开始在该字段中输入(这是违反直觉的)。那么它似乎是带有建议的TextField,而不是带有可搜索字段的下拉列表。
  • 我知道你从哪里来,我们从来没有真正遇到过问题,也没有提出用建议的方式在文本字段中进行处理。为了得到你想要的,不测试它,我认为选择是 a) 跟踪原始值并在更改可编辑标志时将其设置回来 b) 创建一个新组件,在顶部堆叠一个单独的文本字段允许输入的组合框,并将值结合在一起。
【解决方案2】:

我刚刚复制并运行了您的代码,它工作正常,正如您所期望的那样,它只显示一个输出。

在您发表评论后,我尝试使用键盘,因为目的是自动完成,并且它碰巧打印了 3 次,但不是一次。 让我告诉你,这不是问题,这就是 on change 监听器的工作方式,每次你指向一个新值时,监听器都会触发它包含的代码块。 HERE IS THE ORACLE DOCUMENTATION OF IT

但是,这取决于你打算在听众中做什么,现在我建议你慢慢地小心地工作。 如果您需要任何帮助,我随时待命。

【讨论】:

  • 你用鼠标点击了吗?
  • 不,我最初也错过了的问题是当您键入以过滤下拉列表并然后选择一个值时。 trilogy's comment中提到,value变成了新选中的item,变成null,然后又变成了新选中的item。
  • 是的,当您输入内容然后单击过滤值时会发生这种情况。有没有办法让听众开火一次?它只被选中一次。当数据库只需要 1 次时,我不想要 3 次调用。
【解决方案3】:

您可以通过使用简单的 if-else 逻辑过滤值来实现对数据库的 1 次调用而不是 3 次调用。

   boolean changeStat = true;
    String oldVal;

    cmbState.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        try {
            if (!oldValue.getStateName().equals(newValue.getStateName())) {
                if (newValue.getStateName() != null) {
                    System.out.println("new : " + cmbState.getSelectionModel().getSelectedItem().getStateName());
                    oldVal = newValue.getStateName();
                }
            }
        } catch (NullPointerException ne) {
            if (oldValue == null && newValue != null && (oldValue != cmbState.getSelectionModel().getSelectedItem()) && changeStat) {
                System.out.println("new : " + cmbState.getSelectionModel().getSelectedItem().getStateName());
                oldVal = newValue.getStateName();
                changeStat = false;
            } else if (oldValue == null && newValue != null && (!oldVal.equals(cmbState.getSelectionModel().getSelectedItem().getStateName()))) {
                System.out.println("new1 : " + cmbState.getSelectionModel().getSelectedItem().getStateName());
                oldVal = newValue.getStateName();
            }
        }
    });

【讨论】:

    猜你喜欢
    • 2018-09-15
    • 2016-07-02
    • 1970-01-01
    • 2018-02-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多