【问题标题】:Bug with JavaFX ComboBox?JavaFX 组合框的错误?
【发布时间】:2016-06-27 16:22:14
【问题描述】:

我的一位开发人员尝试将 ComboBox 扩展为根据用户类型进行自动过滤:

public class AutoCompleteComboBox<T> extends ComboBox<T> {

    private FilteredList<T> filteredItems;
    private SortedList<T> sortedItems;

    public AutoCompleteComboBox() {
        setEditable(true);
        setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
        setOnMouseClicked(e -> handleOnMouseClicked(e));
    }

    private void handleOnMouseClicked(MouseEvent e) {
        getItems().stream()
                .filter(item -> item.toString().equals(getEditor().getText()))
                .forEach(item -> getSelectionModel().select(item));
        setCaretPositionToEnd();
    }

    private void handleOnKeyReleasedEvent(KeyEvent e) {
        if (e.getCode() == KeyCode.UP || e.getCode() == KeyCode.DOWN) {
            getItems().stream()
                .filter(item -> item.toString().equals(getEditor().getText()))
                .forEach(item -> getSelectionModel().select(item));
            show();
            setCaretPositionToEnd();
        } else if (e.getCode() == KeyCode.ENTER || e.getCode() == KeyCode.TAB) {
                String editorStr = getEditor().getText();
                getSelectionModel().clearSelection();
                getEditor().setText(editorStr);
                setItems(this.sortedItems);
                getItems().stream()
                    .filter(item -> item.toString().equals(editorStr))
                    .forEach(item -> getSelectionModel().select(item));

                getEditor().selectEnd();
                if (e.getCode() == KeyCode.ENTER) {
                    getEditor().deselect();
                }
                hide();
        } else if (e.getText().length() == 1) {
            getSelectionModel().clearSelection();
            if (getEditor().getText().length() == 0) {
                getEditor().setText(e.getText());
            }
            filterSelectionList();
            show();
        } else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() > 0) {
            String editorStr = getEditor().getText();
            getSelectionModel().clearSelection();
            getEditor().setText(editorStr);
            int beforeFilter = getItems().size();
            filterSelectionList();
            int afterFilter = getItems().size();
            if (afterFilter > beforeFilter) {
                hide();
            }
            show();
        } else if (e.getCode() == KeyCode.BACK_SPACE && getEditor().getText().length() == 0) {
            clearSelection();
            hide();
            show();
        }
    }

    private void filterSelectionList() {
        setFilteredItems();
        setCaretPositionToEnd();
    }

    private void setFilteredItems() {
        filteredItems.setPredicate(item -> 
                item.toString().toLowerCase().startsWith(getEditor().getText().toLowerCase()));
    }

    private void setCaretPositionToEnd() {
        getEditor().selectEnd();
        getEditor().deselect();
    }

    public void setInitItems(ObservableList<T> values) {
        filteredItems = new FilteredList<>(values);
        sortedItems = new SortedList<>(filteredItems);
        setItems(this.sortedItems);
    }

    public void clearSelection() {
        getSelectionModel().clearSelection();
        getEditor().clear();
        if (this.filteredItems != null) {
            this.filteredItems.setPredicate(item -> true);
        }
    }

    public T getSelectedItem() {
        T selectedItem = null;
        if (getSelectionModel().getSelectedIndex() > -1) {
            selectedItem = getItems().get(getSelectionModel().getSelectedIndex());
        }
        return selectedItem;
    }

    public void select(String value) {
        if (!value.isEmpty()) {
            getItems().stream()
                .filter(item -> value.equals(item.toString()))
                .findFirst()
                .ifPresent(item -> getSelectionModel().select(item));
        }
    }
}

控件工作得很好,直到我将某些东西绑定到 Selected 或 Value 属性。一旦发生这种情况,一旦我尝试离开该字段(或按 Enter 键导致该操作发生),我将收到以下异常。 (只需用鼠标选择值就不会出现问题)我在此控件中绑定的是 CodeTableValue 的简单对象 - 它有两个属性 - 字符串、代码和值。 toString 返回 Value 属性。

如果控件只是在没有任何侦听器的情况下设置,它就可以正常工作。但是,一旦我听到其中一个属性的值,它就会失败。

当我在课堂上使用AutoCompleteComboBox 控件时:

@FXML private AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox;

myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
              System.out.println(nv);
});

抛出的异常:

Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: java.lang.String cannot be cast to cache.CodeTableValue
       at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
       at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
       at javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
       at javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
       at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
       at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
       at javafx.scene.control.SelectionModel.setSelectedItem(SelectionModel.java:102)
       at javafx.scene.control.ComboBox.lambda$new$152(ComboBox.java:249)
       at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
       at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
       at javafx.beans.property.ObjectPropertyBase.fireValueChangedEvent(ObjectPropertyBase.java:105)
       at javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
       at javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:146)
       at javafx.scene.control.ComboBoxBase.setValue(ComboBoxBase.java:150)
       at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.setTextFromTextFieldIntoComboBoxValue(ComboBoxPopupControl.java:405)
       at com.sun.javafx.scene.control.skin.ComboBoxPopupControl.lambda$new$291(ComboBoxPopupControl.java:82)
       at com.sun.javafx.binding.ExpressionHelper$Generic.fireValueChangedEvent(ExpressionHelper.java:361)
       at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
       at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
       at javafx.scene.Node$FocusedProperty.notifyListeners(Node.java:7718)
       at javafx.scene.Node.setFocused(Node.java:7771)
       at javafx.scene.Scene$KeyHandler.setWindowFocused(Scene.java:3932)
       at javafx.scene.Scene$KeyHandler.lambda$new$11(Scene.java:3954)
       at com.sun.javafx.binding.ExpressionHelper$SingleInvalidation.fireValueChangedEvent(ExpressionHelper.java:137)
       at com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:81)
       at javafx.beans.property.ReadOnlyBooleanPropertyBase.fireValueChangedEvent(ReadOnlyBooleanPropertyBase.java:72)
       at javafx.beans.property.ReadOnlyBooleanWrapper.fireValueChangedEvent(ReadOnlyBooleanWrapper.java:103)
       at javafx.beans.property.BooleanPropertyBase.markInvalid(BooleanPropertyBase.java:110)
       at javafx.beans.property.BooleanPropertyBase.set(BooleanPropertyBase.java:144)
       at javafx.stage.Window.setFocused(Window.java:439)
       at com.sun.javafx.stage.WindowPeerListener.changedFocused(WindowPeerListener.java:59)
       at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:100)
       at com.sun.javafx.tk.quantum.GlassWindowEventHandler.run(GlassWindowEventHandler.java:40)
       at java.security.AccessController.doPrivileged(Native Method)
       at com.sun.javafx.tk.quantum.GlassWindowEventHandler.lambda$handleWindowEvent$423(GlassWindowEventHandler.java:150)
       at com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:389)
       at com.sun.javafx.tk.quantum.GlassWindowEventHandler.handleWindowEvent(GlassWindowEventHandler.java:148)
       at com.sun.glass.ui.Window.handleWindowEvent(Window.java:1266)
       at com.sun.glass.ui.Window.notifyFocus(Window.java:1245)
       at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
       at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
       at java.lang.Thread.run(Thread.java:745)

如果 editableFlag 为 false,则此方法有效。当我们允许用户在组合框中输入内容来过滤内容时,就会发生这种情况。如果没有绑定,我们不会出现异常,并且值设置正确。

我一直在深入研究这个异常,它被抛出在这里:

com.sun.javafx.binding.ExpressionHelper.Generic.fireValueChangedEvent()

                if (curChangeSize > 0) {
                    final T oldValue = currentValue;
                    currentValue = observable.getValue();
                    final boolean changed = (currentValue == null)? (oldValue != null) : !currentValue.equals(oldValue);
                    if (changed) {
                        for (int i = 0; i < curChangeSize; i++) {
                            try {
                                curChangeList[i].changed(observable, oldValue, currentValue);
                            } catch (Exception e) {
                                Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                            }
                        }
                    }
                }

只有在绑定了 valueProperty 或 selectedItemProperty 时才从这里来。否则我们不会得到这个问题。

javafx.beans.property.ObjectPropertyBase.set(T)    @Override
    public void set(T newValue) {
        if (isBound()) {
            throw new java.lang.RuntimeException((getBean() != null && getName() != null ?
                    getBean().getClass().getSimpleName() + "." + getName() + " : ": "") + "A bound value cannot be set.");
        }
        if (value != newValue) {
            value = newValue;
            markInvalid();
        }
    }

我已尝试将 StringConverter 添加到编辑器中,以查看是否可以解决问题,但它再次抛出此异常。也许我们正试图错误地处理这一切?

基本上,我们希望在用户输入字段时过滤组合框选择项。如果我们应该以不同的方式处理这个问题,请告诉我,但目前,我认为这可能是 JDK 中的一个错误?如果该字段未绑定,那么我们没有问题,但是一旦绑定,当该字段失去焦点或按下回车键时会发生此异常。

【问题讨论】:

    标签: combobox java-8 javafx-8


    【解决方案1】:

    我使用您的控件创建了一个快速示例:

    @Override
    public void start(Stage primaryStage) {
    
        AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox = 
                new AutoCompleteComboBox<>();
        myAutoCompleteBox.setInitItems(FXCollections.observableArrayList(new CodeTableValue("One", "1"), 
                new CodeTableValue("Two", "2"), new CodeTableValue("Three", "4")));
    
        StackPane root = new StackPane(myAutoCompleteBox);
        myAutoCompleteBox.getSelectionModel().selectedItemProperty().addListener((obs, ov, nv) -> {
                        System.out.println(nv);
          });
    
        Scene scene = new Scene(root, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
    

    基于一个简单的模型类:

        public class CodeTableValue {
    
        private String code;
        private String value;
    
        public CodeTableValue(String code, String value) {
            this.code = code;
            this.value = value;
        }
    
        public String getCode() {
            return code;
        }
    
        public void setCode(String code) {
            this.code = code;
        }
    
        public String getValue() {
            return value;
        }
    
        public void setValue(String value) {
            this.value = value;
        }
    
        @Override
        public String toString() {
            return "code=" + code + ", value=" + value;
        }
    }
    

    我可以在我完成输入并单击 Enter 提交值并离开控件后重现您的异常:

    java.lang.ClassCastException: java.lang.String cannot be cast to CodeTableValue
    

    例外来自在编辑模式下从 ComboBox 使用的 TextField 将始终返回 String,但您强制自定义 ComboBox 使用 CodeTableValue 类。

    解决方案只是提供一种在 String 和 CodeTableValue 之间转换的方法,使用 StringConverter

    所以我修改了你的控件:

    public AutoCompleteComboBox(StringConverter<T> converter) {
        setEditable(true);
        setOnKeyReleased(e -> handleOnKeyReleasedEvent(e));
        setOnMouseClicked(e -> handleOnMouseClicked(e));
    
        super.setConverter(converter);
    }
    

    现在在示例中:

    AutoCompleteComboBox<CodeTableValue> myAutoCompleteBox = 
                new AutoCompleteComboBox<>(new StringConverter<CodeTableValue>() {
    
            @Override
            public String toString(CodeTableValue object) {
                if (object != null) {
                    return object.getValue();
                } 
                return null;
            }
    
            @Override
            public CodeTableValue fromString(String string) {
                return new CodeTableValue(string, string);
            }
    
        });
    

    现在可以使用,没有ClassCastException

    显然,您必须提供一种输入字符串(codevalue)并从中检索另一个字符串的方法。

    【讨论】:

    • 谢谢!这行得通。我将转换器放在编辑器上,而不是控件上!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-06-16
    • 2019-08-23
    • 2017-02-08
    • 2015-03-06
    • 1970-01-01
    • 1970-01-01
    • 2017-01-07
    相关资源
    最近更新 更多