在 ComboBox 中为 FilteredList 设置谓词会影响输入

Posted

技术标签:

【中文标题】在 ComboBox 中为 FilteredList 设置谓词会影响输入【英文标题】:Setting predicate for FilteredList in ComboBox affects input 【发布时间】:2016-05-15 02:54:45 【问题描述】:

我已经实现了一个ComboBox,它的列表由ComboBoxTextField 中的输入过滤。它的工作方式与您可能期望此类控件的过滤器工作一样。列表中以输入文本开头的每个项目都会显示在列表中。

我只有一个小问题。如果我从列表中选择一个项目,然后尝试删除文本字段中的最后一个字符,则没有任何反应。如果我从列表中选择一个项目,然后尝试删除除最后一个字符之外的任何其他字符,则整个字符串都会被删除。只有当这是我在ComboBox 中做的第一件事时,才会出现这两个问题。如果我先在组合框中写一些东西,或者如果我第二次选择一个项目,则不会出现所描述的问题。

对我来说真正奇怪的是,这些问题似乎是由设置的谓词引起的(如果我注释掉 setPredicate 的调用,一切正常)。这很奇怪,因为我认为这只会影响为谓词设置的列表。它不应该影响ComboBox 的其余部分。

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
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.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class TestInputFilter extends Application 
    public void start(Stage stage) 
        VBox root = new VBox();

        ComboBox<ComboBoxItem> cb = new ComboBox<ComboBoxItem>();
        cb.setEditable(true);

        cb.setConverter(new StringConverter<ComboBoxItem>() 

            @Override
            // To convert the ComboBoxItem to a String we just call its
            // toString() method.
            public String toString(ComboBoxItem object) 
                return object == null ? null : object.toString();
            

            @Override
            // To convert the String to a ComboBoxItem we loop through all of
            // the items in the combobox dropdown and select anyone that starts
            // with the String. If we don't find a match we create our own
            // ComboBoxItem.
            public ComboBoxItem fromString(String string) 
                return cb.getItems().stream().filter(item -> item.getText().startsWith(string)).findFirst()
                        .orElse(new ComboBoxItem(string));
            
        );

        ObservableList<ComboBoxItem> options = FXCollections.observableArrayList(new ComboBoxItem("One is a number"),
                new ComboBoxItem("Two is a number"), new ComboBoxItem("Three is a number"),
                new ComboBoxItem("Four is a number"), new ComboBoxItem("Five is a number"),
                new ComboBoxItem("Six is a number"), new ComboBoxItem("Seven is a number"));
        FilteredList<ComboBoxItem> filteredOptions = new FilteredList<ComboBoxItem>(options, p -> true);
        cb.setItems(filteredOptions);

        InputFilter inputFilter = new InputFilter(cb, filteredOptions);
        cb.getEditor().textProperty().addListener(inputFilter);

        root.getChildren().add(cb);

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

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

    class ComboBoxItem 

        private String text;

        public ComboBoxItem(String text) 
            this.text = text;
        

        public String getText() 
            return text;
        

        @Override
        public String toString() 
            return text;
        
    

    class InputFilter implements ChangeListener<String> 

        private ComboBox<ComboBoxItem> box;
        private FilteredList<ComboBoxItem> items;

        public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) 
            this.box = box;
            this.items = items;
        

        @Override
        public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) 
            String value = newValue;
            // If any item is selected we get the first word of that item.
            String selected = box.getSelectionModel().getSelectedItem() != null
                    ? box.getSelectionModel().getSelectedItem().getText() : null;

            // If an item is selected and the value of in the editor is the same
            // as the selected item we don't filter the list.
            if (selected != null && value.equals(selected)) 
                items.setPredicate(item -> 
                    return true;
                );
             else 
                items.setPredicate(item -> 
                    if (item.getText().toUpperCase().startsWith(value.toUpperCase())) 
                        return true;
                     else 
                        return false;
                    
                );
            
        
    

编辑:我试图覆盖关键侦听器以拼命尝试解决问题:

cb.getEditor().addEventFilter(KeyEvent.KEY_PRESSED, e -> 
    TextField editor = cb.getEditor();
    int caretPos = cb.getEditor().getCaretPosition();
    StringBuilder text = new StringBuilder(cb.getEditor().getText());

    // If BACKSPACE is pressed we remove the character at the index
    // before the caret position.
    if (e.getCode().equals(KeyCode.BACK_SPACE)) 
        // BACKSPACE should only remove a character if the caret
        // position isn't zero.
        if (caretPos > 0) 
            text.deleteCharAt(--caretPos);
        
        e.consume();
    
    // If DELETE is pressed we remove the character at the caret
    // position.
    else if (e.getCode().equals(KeyCode.DELETE)) 
        // DELETE should only remove a character if the caret isn't
        // positioned after that last character in the text.
        if (caretPos < text.length()) 
            text.deleteCharAt(caretPos);
        
    
    // If LEFT key is pressed we move the caret one step to the left.
    else if (e.getCode().equals(KeyCode.LEFT)) 
        caretPos--;
    
    // If RIGHT key is pressed we move the caret one step to the right.
    else if (e.getCode().equals(KeyCode.RIGHT)) 
        caretPos++;
    
    // Otherwise we just add the key text to the text.
    // TODO We are currently not handling UP/DOWN keys (should move
    // caret to the end/beginning of the text).
    // TODO We are currently not handling keys that doesn't represent
    // any symbol, like ALT. Since they don't have a text, they will
    // just move the caret one step to the right. In this case, that
    // caret should just hold its current position.
    else 
        text.insert(caretPos++, e.getText());
        e.consume();
    

    final int finalPos = caretPos;

    // We set the editor text to the new text and finally we move the
    // caret to its new position.
    editor.setText(text.toString());
    Platform.runLater(() -> editor.positionCaret(finalPos));
);

// We just consume KEY_RELEASED and KEY_TYPED since we don't want to
// have duplicated input.
cb.getEditor().addEventFilter(KeyEvent.KEY_RELEASED, e -> 
    e.consume();
);
cb.getEditor().addEventFilter(KeyEvent.KEY_TYPED, e -> 
    e.consume();
);

遗憾的是,这也不能解决问题。如果我例如选择“三是数字”项,然后尝试删除“三”中的最后一个“e”,这是文本属性将在其中切换的值:

TextProperty: Three is a number
TextPropery: Thre is a number
TextPropery: 

所以它首先删除了正确的字符,但后来由于某种原因它删除了整个String。如前所述,这只是因为已经设置了谓词,并且仅在我第一次选择项目后进行第一次输入时才会发生。

【问题讨论】:

【参考方案1】:

乔纳坦,

正如 Manuel 所说,一个问题是 setPredicate() 将触发您的 changed() 方法两次,因为您正在更改组合框模型,但真正的问题是组合框会用任何看起来合适的值覆盖编辑器值。以下是对您的症状的解释:

如果我从列表中选择一个项目,然后尝试删除最后一个 文本字段中的字符,没有任何反应。

在这种情况下,最后一个字符的删除实际上正在发生,但是第一次调用 setPredicate() 匹配一个可能的项目(与您删除最后一个字符完全相同的项目)并将组合框内容更改为仅一个项目.这会导致一个调用,其中组合框使用当前的 combobox.getValue() 字符串恢复编辑器值,给人一种什么都没有发生的错觉。它还会导致对您的 changed() 方法进行第二次调用,但此时编辑器文本已更改。

为什么这只是第一次发生,但以后再也不会发生?

好问题!这只会发生一次,因为您修改了组合框的整个底层模型一次(如前所述,这会触发对 changed() 方法的第二次调用)。

所以在上一个场景发生后,如果您单击下拉按钮(向右箭头),您将看到只剩下一个项目,如果您再次尝试删除一个字符,您仍然会留下相同的项目,也就是说,模型(组合框的内容)没有改变,因为 setPredicate() 仍将匹配相同的内容,因此不会在 TextInputControl 类中引起 markInvalid() 调用,因为内容实际上并没有改变,这意味着不会恢复再次显示项目字符串(如果您想查看第一次实际恢复文本字段的位置,请参阅带有 JavaFX 源的 ComboBoxPopupControl.updateDisplayNode() 方法)。

如果我从列表中选择一个项目,然后尝试删除任何其他项目 比最后一个字符,整个字符串被删除。

在您的第二个场景中,没有任何内容与第一个 setPredicate() 调用匹配(没有项目与您的 startsWith 条件匹配),这会删​​除组合框中的所有项目,同时删除您当前的选择和编辑器字符串。

提示:尝试自己理解这一点,在 changed() 方法中切换断点以查看其进入的次数以及原因(如果您想遵循 ComboBox 及其组件行为,则需要 JavaFX 源代码)

解决方案: 如果您想继续使用您的 ChangeListener,您可以通过在过滤后恢复编辑器中的文本来简单地解决您的主要问题(即在 setPredicate 调用后替换编辑器内容):

class InputFilter implements ChangeListener<String> 
    private ComboBox<ComboBoxItem> box;
    private FilteredList<ComboBoxItem> items;

    public InputFilter(ComboBox<ComboBoxItem> box, FilteredList<ComboBoxItem> items) 
        this.box = box;
        this.items = items;
    

    @Override
    public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) 
        String value = newValue;
        // If any item is selected we get the first word of that item.
        String selected = box.getSelectionModel().getSelectedItem() != null
                ? box.getSelectionModel().getSelectedItem().getText() : null;

        // If an item is selected and the value of in the editor is the same
        // as the selected item we don't filter the list.
        if (selected != null && value.equals(selected)) 
            items.setPredicate(item -> 
                return true;
            );
         else 
            // This will most likely change the box editor contents
            items.setPredicate(item -> 
                if (item.getText().toUpperCase().startsWith(value.toUpperCase())) 
                    return true;
                 else 
                    return false;
                
            );

            // Restore the original search text since it was changed
            box.getEditor().setText(value);
        

        //box.show(); // <-- Uncomment this line for a neat look
    

我以前曾使用 KeyEvent 处理程序完成此操作(以避免在 changed() 事件中多次调用我的代码),但是您始终可以使用 Semaphore 或 java.util.concurrent 中您最喜欢的类类以避免任何不必要的重新进入你的方法,如果你觉得你开始需要它。现在,getEditor().setText() 将始终恢复正确的值,即使相同的方法冒泡了两到三遍。

希望这会有所帮助!

【讨论】:

解释得很好。谢谢!在我接受这个作为正确答案之前,我只需要对我认为尚未解释的部分问题的答案:为什么这只会发生第一次,但以后再也不会发生?您的解释(完全有道理)暗示这应该每次都发生。 已添加对您为什么只发生一次的问题的答案。我还建议您在有疑问时调试代码,或者至少添加一些 System.out.println(newValue) 以进行快速诊断。就我个人而言,我在调试代码和 Java 源代码的过程中学到了很多东西,起初它可能会让人不知所措,但后来你意识到这太棒了。 我在答案中添加了一些额外的细节,例如 ComboBoxPopupControl 类中 updateDisplayNode() 方法的调用,这是使用 ComboBoxBase.getValue() 结果恢复字符串的方法,以及也清除它。那里的行为似乎很有趣,直到可以认为是错误的地步,因为即使 getSelectionModel().getSelectedIndex() 返回 -1,getValue() 也会返回完整的字符串,但是 API 声明它也可以返回最后一个选定的项目(如在您的第一个场景中)。如果所有这些都回答了您的问题或至少有用,则将此问题标记为正确。【参考方案2】:

设置谓词将触发您的 ChangeListener,因为您正在更改 ComboBox-Items 并因此更改 cb-editor 的文本值。删除侦听器并重新添加它会阻止那些意外的操作。

我在您的 change(...) - 方法中添加了三行。 试试看,如果这能解决您的问题。

信息:我只使用了你的第一段代码

@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) 
    String value = newValue;
    // If any item is selected we get the first word of that item.
    String selected = box.getSelectionModel().getSelectedItem() != null
            ? box.getSelectionModel().getSelectedItem().getText() : null;

    box.getEditor().textProperty().removeListener(this); // new line #1

    // If an item is selected and the value of in the editor is the same
    // as the selected item we don't filter the list.
    if (selected != null && value.equals(selected)) 
        items.setPredicate(item -> 
            return true;
        );
     else 
        items.setPredicate(item -> 
            if (item.getText().toUpperCase().startsWith(value.toUpperCase())) 
                return true;
             else 
                return false;
            
        );
        box.getEditor().setText(newValue); // new line #2
    

    box.getEditor().textProperty().addListener(this); // new line #3

【讨论】:

以上是关于在 ComboBox 中为 FilteredList 设置谓词会影响输入的主要内容,如果未能解决你的问题,请参考以下文章

C#中为DataGrid添加下拉列表框(转载)

ComboBox绑定Dictionary做为数据源

ComboBox在WPF中的绑定示例:绑定项集合转换,及其源代码

COMBOBOX绑定DICTIONARY做为数据源

这是 Qt Quick ComboBox 中的错误吗?

easyui datagrid combobox 里面获取焦点事件怎么写