JavaFX中的自动完成ComboBox

58

我正在寻找一种方法,可以为JavaFX ComboBox 添加自动补全功能。

已知有一个名为AutoFillBox的控件,但不符合我的需求。 我需要的是可编辑的ComboBox,当输入时,应该对列表进行筛选。但我还希望能够在没有输入的情况下打开列表,并查看所有项。

有任何想法吗?

11个回答

73
首先,您需要在项目中创建这个类:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class FxUtilTest {

    public interface AutoCompleteComparator<T> {
        boolean matches(String typedText, T objectToCompare);
    }

    public static<T> void autoCompleteComboBoxPlus(ComboBox<T> comboBox, AutoCompleteComparator<T> comparatorMethod) {
        ObservableList<T> data = comboBox.getItems();

        comboBox.setEditable(true);
        comboBox.getEditor().focusedProperty().addListener(observable -> {
            if (comboBox.getSelectionModel().getSelectedIndex() < 0) {
                comboBox.getEditor().setText(null);
            }
        });
        comboBox.addEventHandler(KeyEvent.KEY_PRESSED, t -> comboBox.hide());
        comboBox.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {

            private boolean moveCaretToPos = false;
            private int caretPos;

            @Override
            public void handle(KeyEvent event) {
                if (event.getCode() == KeyCode.UP) {
                    caretPos = -1;
                    if (comboBox.getEditor().getText() != null) {
                        moveCaret(comboBox.getEditor().getText().length());
                    }
                    return;
                } else if (event.getCode() == KeyCode.DOWN) {
                    if (!comboBox.isShowing()) {
                        comboBox.show();
                    }
                    caretPos = -1;
                    if (comboBox.getEditor().getText() != null) {
                        moveCaret(comboBox.getEditor().getText().length());
                    }
                    return;
                } else if (event.getCode() == KeyCode.BACK_SPACE) {
                    if (comboBox.getEditor().getText() != null) {
                        moveCaretToPos = true;
                        caretPos = comboBox.getEditor().getCaretPosition();
                    }
                } else if (event.getCode() == KeyCode.DELETE) {
                    if (comboBox.getEditor().getText() != null) {
                        moveCaretToPos = true;
                        caretPos = comboBox.getEditor().getCaretPosition();
                    }
                } else if (event.getCode() == KeyCode.ENTER) {
                    return;
                }

                if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.LEFT || event.getCode().equals(KeyCode.SHIFT) || event.getCode().equals(KeyCode.CONTROL)
                        || event.isControlDown() || event.getCode() == KeyCode.HOME
                        || event.getCode() == KeyCode.END || event.getCode() == KeyCode.TAB) {
                    return;
                }

                ObservableList<T> list = FXCollections.observableArrayList();
                for (T aData : data) {
                    if (aData != null && comboBox.getEditor().getText() != null && comparatorMethod.matches(comboBox.getEditor().getText(), aData)) {
                        list.add(aData);
                    }
                }
                String t = "";
                if (comboBox.getEditor().getText() != null) {
                    t = comboBox.getEditor().getText();
                }

                comboBox.setItems(list);
                comboBox.getEditor().setText(t);
                if (!moveCaretToPos) {
                    caretPos = -1;
                }
                moveCaret(t.length());
                if (!list.isEmpty()) {
                    comboBox.show();
                }
            }

            private void moveCaret(int textLength) {
                if (caretPos == -1) {
                    comboBox.getEditor().positionCaret(textLength);
                } else {
                    comboBox.getEditor().positionCaret(caretPos);
                }
                moveCaretToPos = false;
            }
        });
    }

    public static<T> T getComboBoxValue(ComboBox<T> comboBox){
        if (comboBox.getSelectionModel().getSelectedIndex() < 0) {
            return null;
        } else {
            return comboBox.getItems().get(comboBox.getSelectionModel().getSelectedIndex());
        }
    }

}

为了让你的ComboBox自动完成,可以按照以下方式使用它:
FxUtilTest.autoCompleteComboBoxPlus(myComboBox, (typedText, itemToCompare) -> itemToCompare.getName().toLowerCase().contains(typedText.toLowerCase()) || itemToCompare.getAge().toString().equals(typedText));

接下来,添加类似以下示例的StringConverter(因为ComboBox值将返回一个String,需要将其转换为您的对象):

myComboBox.setConverter(new StringConverter<>() {

    @Override
    public String toString(YourObject object) {
        return object != null ? object.getName() : "";
    }

    @Override
    public YourObject fromString(String string) {
        return myComboBox.getItems().stream().filter(object ->
                object.getName().equals(string)).findFirst().orElse(null);
    }

});

当您需要从组合框中获取所选值时,请务必使用此方法,否则您可能会遇到一些异常,例如“类转换异常”:

FxUtilTest.getComboBoxValue(myComboBox);

附注:在JRE 8.51和8.65版本之间,该方法存在一些问题,导致出现了一些奇怪的行为,现在这些问题似乎不再发生。如果您遇到了问题,您可以查看对此答案所做的修改,并获取解决该问题时的旧版本。

如果您遇到任何问题,请告诉我,这个方法应该可以正常工作。


太好了!我曾经花了很长时间来解决这个问题,一直以为是我的错!现在当客户打电话抱怨时,我就不必再责怪微软Windows了。 - Mateus Viccari
2
我很惊讶这个功能没有被包含在像ControlsFX这样的库中。 - Stevoisiak
3
我认为我在这里做了一点改进。我的做法是修改了代码的这部分: } else if (event.getCode() == KeyCode.ENTER) { int selectedIndex = comboBox.getSelectionModel().getSelectedIndex(); if(selectedIndex > 0) comboBox.getSelectionModel().select(selectedIndex); else comboBox.getSelectionModel().selectFirst(); return; } 它的作用是,如果你点击回车并且没有选择任何内容,它会选择第一个。 - JFValdes
1
这个实现有很多方面是有问题的。另外,getComboBoxValue() 是用来做什么的?在这段代码中没有任何地方调用它。 - Zephyr
1
啊,我不知道怎么错过了那个。那就忽略那个评论吧。但是仍然无法让这个解决方案在“ComboBox”失去焦点时停止清除选择。似乎不喜欢从此处以外的代码更改选择。 - Zephyr
显示剩余8条评论

40
我找到了一个适合我的解决方案:
public class AutoCompleteComboBoxListener<T> implements EventHandler<KeyEvent> {

    private ComboBox comboBox;
    private StringBuilder sb;
    private ObservableList<T> data;
    private boolean moveCaretToPos = false;
    private int caretPos;

    public AutoCompleteComboBoxListener(final ComboBox comboBox) {
        this.comboBox = comboBox;
        sb = new StringBuilder();
        data = comboBox.getItems();

        this.comboBox.setEditable(true);
        this.comboBox.setOnKeyPressed(new EventHandler<KeyEvent>() {

            @Override
            public void handle(KeyEvent t) {
                comboBox.hide();
            }
        });
        this.comboBox.setOnKeyReleased(AutoCompleteComboBoxListener.this);
    }

    @Override
    public void handle(KeyEvent event) {

        if(event.getCode() == KeyCode.UP) {
            caretPos = -1;
            moveCaret(comboBox.getEditor().getText().length());
            return;
        } else if(event.getCode() == KeyCode.DOWN) {
            if(!comboBox.isShowing()) {
                comboBox.show();
            }
            caretPos = -1;
            moveCaret(comboBox.getEditor().getText().length());
            return;
        } else if(event.getCode() == KeyCode.BACK_SPACE) {
            moveCaretToPos = true;
            caretPos = comboBox.getEditor().getCaretPosition();
        } else if(event.getCode() == KeyCode.DELETE) {
            moveCaretToPos = true;
            caretPos = comboBox.getEditor().getCaretPosition();
        }

        if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.LEFT
                || event.isControlDown() || event.getCode() == KeyCode.HOME
                || event.getCode() == KeyCode.END || event.getCode() == KeyCode.TAB) {
            return;
        }

        ObservableList list = FXCollections.observableArrayList();
        for (int i=0; i<data.size(); i++) {
            if(data.get(i).toString().toLowerCase().startsWith(
                AutoCompleteComboBoxListener.this.comboBox
                .getEditor().getText().toLowerCase())) {
                list.add(data.get(i));
            }
        }
        String t = comboBox.getEditor().getText();

        comboBox.setItems(list);
        comboBox.getEditor().setText(t);
        if(!moveCaretToPos) {
            caretPos = -1;
        }
        moveCaret(t.length());
        if(!list.isEmpty()) {
            comboBox.show();
        }
    }

    private void moveCaret(int textLength) {
        if(caretPos == -1) {
            comboBox.getEditor().positionCaret(textLength);
        } else {
            comboBox.getEditor().positionCaret(caretPos);
        }
        moveCaretToPos = false;
    }

}

你可以使用以下方式调用:
new AutoCompleteComboBoxListener<>(comboBox);

这是基于这个实现的,我对其进行了自定义以满足我的需求。

欢迎使用,如果有人能够改进它,请告诉我。


很高兴能够帮助! 你可以发布你删除的那一行代码吗?也许它对其他人也有帮助。 - JulianG
1
@JulianG 您的回答已经被编辑 - 被删除的部分是 ListView lv = ((ComboBoxListViewSkin) comboBox.getSkin()).getListView();. - assylias
哦,我没看到。谢谢。 - JulianG
2
为什么当我对ComboBox<Car>执行campoOperacaoEstoque.getValue()时,它会抛出异常java.lang.ClassCastException: java.lang.String cannot be cast to model.Car - Mateus Viccari
1
JulianG,我稍微改进了一下,以适应我的需求,请参见我对问题的回答。 - Mateus Viccari
显示剩余3条评论

20

使用ControlsFX库,您只需两行代码即可完成:

comboBox.setEditable(true);
TextFields.bindAutoCompletion(comboBox.getEditor(), comboBox.getItems());

4
如果用户使用鼠标点击组合框箭头并选择一个值,此解决方案会触发自动完成弹出窗口。 - Coren
@ConquerorsHaki,可以向AutoComplete添加节点,我的意思是通过添加图形来更新项目。 - Menai Ala Eddine - Aladdin
@ConquerorsHaki,可以向AutoComplete添加节点,我的意思是通过添加图形来更新项目。 - Menai Ala Eddine - Aladdin
@MenaiAlaEddine 应该是可以的。 - Ahmed Hasn.
2
ControlsFX是一个每年更新两次的错误合集。自己制作的解决方案更加可靠。 - Evan
显示剩余5条评论

8

基于Jonatan的回答,我成功地构建了以下解决方案:

输入图像描述

输入图像描述

输入图像描述

输入图像描述

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;

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

    public void start(Stage stage)
    {
        List<String> countries = new ArrayList<>();

        countries.add("Afghanistan");
        countries.add("Albania");
        countries.add("Algeria");
        countries.add("Andorra");
        countries.add("Angola");
        countries.add("Antigua and Barbuda");
        countries.add("Argentina");
        countries.add("Armenia");
        countries.add("Australia");
        countries.add("Austria");
        countries.add("Azerbaijan");
        countries.add("Bahamas");
        countries.add("Bahrain");
        countries.add("Bangladesh");
        countries.add("Barbados");
        countries.add("Belarus");
        countries.add("Belgium");
        countries.add("Belize");
        countries.add("Benin");
        countries.add("Bhutan");
        countries.add("Bolivia");
        countries.add("Bosnia and Herzegovina");
        countries.add("Botswana");
        countries.add("Brazil");
        countries.add("Brunei");
        countries.add("Bulgaria");
        countries.add("Burkina Faso");
        countries.add("Burundi");
        countries.add("Cabo Verde");
        countries.add("Cambodia");
        countries.add("Cameroon");
        countries.add("Canada");
        countries.add("Central African Republic (CAR)");
        countries.add("Chad");
        countries.add("Chile");
        countries.add("China");
        countries.add("Colombia");
        countries.add("Comoros");
        countries.add("Democratic Republic of the Congo");
        countries.add("Republic of the Congo");
        countries.add("Costa Rica");
        countries.add("Cote d'Ivoire");
        countries.add("Croatia");
        countries.add("Cuba");
        countries.add("Cyprus");
        countries.add("Czech Republic");
        countries.add("Denmark");
        countries.add("Djibouti");
        countries.add("Dominica");
        countries.add("Dominican Republic");
        countries.add("Ecuador");
        countries.add("Egypt");
        countries.add("El Salvador");
        countries.add("Equatorial Guinea");
        countries.add("Eritrea");
        countries.add("Estonia");
        countries.add("Ethiopia");
        countries.add("Fiji");
        countries.add("Finland");
        countries.add("France");
        countries.add("Gabon");
        countries.add("Gambia");
        countries.add("Georgia");
        countries.add("Germany");
        countries.add("Ghana");
        countries.add("Greece");
        countries.add("Grenada");
        countries.add("Guatemala");
        countries.add("Guinea");
        countries.add("Guinea-Bissau");
        countries.add("Guyana");
        countries.add("Haiti");
        countries.add("Honduras");
        countries.add("Hungary");
        countries.add("Iceland");
        countries.add("India");
        countries.add("Indonesia");
        countries.add("Iran");
        countries.add("Iraq");
        countries.add("Ireland");
        countries.add("Israel");
        countries.add("Italy");
        countries.add("Jamaica");
        countries.add("Japan");
        countries.add("Jordan");
        countries.add("Kazakhstan");
        countries.add("Kenya");
        countries.add("Kiribati");
        countries.add("Kosovo");
        countries.add("Kuwait");
        countries.add("Kyrgyzstan");
        countries.add("Laos");
        countries.add("Latvia");
        countries.add("Lebanon");
        countries.add("Lesotho");
        countries.add("Liberia");
        countries.add("Libya");
        countries.add("Liechtenstein");
        countries.add("Lithuania");
        countries.add("Luxembourg");
        countries.add("Macedonia (FYROM)");
        countries.add("Madagascar");
        countries.add("Malawi");
        countries.add("Malaysia");
        countries.add("Maldives");
        countries.add("Mali");
        countries.add("Malta");
        countries.add("Marshall Islands");
        countries.add("Mauritania");
        countries.add("Mauritius");
        countries.add("Mexico");
        countries.add("Micronesia");
        countries.add("Moldova");
        countries.add("Monaco");
        countries.add("Mongolia");
        countries.add("Montenegro");
        countries.add("Morocco");
        countries.add("Mozambique");
        countries.add("Myanmar (Burma)");
        countries.add("Namibia");
        countries.add("Nauru");
        countries.add("Nepal");
        countries.add("Netherlands");
        countries.add("New Zealand");
        countries.add("Nicaragua");
        countries.add("Niger");
        countries.add("Nigeria");
        countries.add("North Korea");
        countries.add("Norway");
        countries.add("Oman");
        countries.add("Pakistan");
        countries.add("Palau");
        countries.add("Palestine");
        countries.add("Panama");
        countries.add("Papua New Guinea");
        countries.add("Paraguay");
        countries.add("Peru");
        countries.add("Philippines");
        countries.add("Poland");
        countries.add("Portugal");
        countries.add("Qatar");
        countries.add("Romania");
        countries.add("Russia");
        countries.add("Rwanda");
        countries.add("Saint Kitts and Nevis");
        countries.add("Saint Lucia");
        countries.add("Saint Vincent and the Grenadines");
        countries.add("Samoa");
        countries.add("San Marino");
        countries.add("Sao Tome and Principe");
        countries.add("Saudi Arabia");
        countries.add("Senegal");
        countries.add("Serbia");
        countries.add("Seychelles");
        countries.add("Sierra Leone");
        countries.add("Singapore");
        countries.add("Slovakia");
        countries.add("Slovenia");
        countries.add("Solomon Islands");
        countries.add("Somalia");
        countries.add("South Africa");
        countries.add("South Korea");
        countries.add("South Sudan");
        countries.add("Spain");
        countries.add("Sri Lanka");
        countries.add("Sudan");
        countries.add("Suriname");
        countries.add("Swaziland");
        countries.add("Sweden");
        countries.add("Switzerland");
        countries.add("Syria");
        countries.add("Taiwan");
        countries.add("Tajikistan");
        countries.add("Tanzania");
        countries.add("Thailand");
        countries.add("Timor-Leste");
        countries.add("Togo");
        countries.add("Tonga");
        countries.add("Trinidad and Tobago");
        countries.add("Tunisia");
        countries.add("Turkey");
        countries.add("Turkmenistan");
        countries.add("Tuvalu");
        countries.add("Uganda");
        countries.add("Ukraine");
        countries.add("United Arab Emirates (UAE)");
        countries.add("United Kingdom (UK)");
        countries.add("United States of America (USA)");
        countries.add("Uruguay");
        countries.add("Uzbekistan");
        countries.add("Vanuatu");
        countries.add("Vatican City (Holy See)");
        countries.add("Venezuela");
        countries.add("Vietnam");
        countries.add("Yemen");
        countries.add("Zambia");
        countries.add("Zimbabwe");

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

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

更新:

在Java 9+中,您可以像这样访问ListView

ListView<ComboBoxItem> lv = (ListView<ComboBoxItem>) ((ComboBoxListViewSkin<?>) comboBox.getSkin()).getPopupContent();

同时,您需要修复组合框在按下“SPACE”时关闭的问题。 https://dev59.com/Harka4cB1Zd3GeqPkeLj - trilogy
@Evan...我认为你正在寻找一个自动完成文本框,而不是一个自动完成下拉框。这个解决方案是一个自动完成下拉框。 - trilogy
@Evan,下拉菜单确实支持可编辑文本,但允许输入字符意味着它允许输入可能不在下拉列表中的内容。根据定义,这将是一个自动完成文本字段。此解决方案加倍强调了必须选择列表中的某些内容。如果您有兴趣,还有一个很好的自动完成文本字段解决方案。 - trilogy
@trilogy 不,从用户体验的角度来看,这并不好。用户怎么知道它是可编辑的或支持输入的?它看起来就像普通的组合框,直到你开始输入才会发现。这就是这种解决方案的主要问题。 - Evan
@Evan 无论如何,不要为了小节而忘记了大局。我已经将它放入了真实的生产环境,并且只得到了积极的反馈。很多人自己意识到了它的存在。无论如何,使用它并不是强制性的。如果必须使用它,我会理解你的观点。他们可以像普通下拉菜单一样自由地使用它。 - trilogy
显示剩余5条评论

7

我觉得Eng Fouad的答案是最好的(即使有10,000项),但我必须修复3个错误:

  1. 当你点击SHIFT键时,编辑器消失了

  2. 当你输入SPACE时,ComboBox会关闭

  3. 当你“清除选择”然后打开组合框并在没有选择任何内容的情况下关闭它时,它会重新选择上一个项目。

我还添加了一个StringConverter的传递,使用Apache StringUtils进行比较,并将Streams移动到常规for循环以提高性能,如:https://blog.jooq.org/2015/12/08/3-reasons-why-you-shouldnt-replace-your-for-loops-by-stream-foreach/

另外根据Eng Fouad的答案:

在Java 9+中,您可以像这样访问ListView

ListView<ComboBoxItem> lv = (ListView<ComboBoxItem>) ((ComboBoxListViewSkin<?>) comboBox.getSkin()).getPopupContent();

示例用法:

List<Locale> locales = Arrays.asList(Locale.getAvailableLocales());
ComboBox<HideableItem<Locale>> dropDown = AutoCompleteComboBox.createComboBoxWithAutoCompletionSupport(locales, new StringConverter<HideableItem<Locale>>()
{
                        
    @Override
    public String toString(HideableItem<Locale> object)
    {
        if(object!=null)
        {                    
            return object.getObject().getDisplayName();
        }else
        {
           return null;
        }
    }

    @Override
    public HideableItem<Locale> fromString(String string)
    {
        Locale foundLocale = locales.stream().filter((Locale i)
                        -> (i.getDisplayName()).equals(string)).findFirst().orElse(null);
        return new HideableItem(foundLocale, this);
    }
    
});

AutoCompleteComboBox.java

import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
import java.util.List;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.Event;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Control;
import javafx.scene.control.ListView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.StringConverter;
import org.apache.commons.lang3.StringUtils;

public class AutoCompleteComboBox
{

    public static class HideableItem<T>
    {

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

        public HideableItem(T object, StringConverter converter)
        {
            setConverter(converter);
            setObject(object);

        }

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

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

        private void setConverter(StringConverter converter)
        {
            this.converter = converter;
        }

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

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

        for (T item : items)
        {
            HideableItem<T> hideableItem = new HideableItem<>(item, converter);
            hideableHideableItems.add(hideableItem);
        }

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

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

        ComboBoxListViewSkin<HideableItem<T>> comboBoxListViewSkin = new ComboBoxListViewSkin<HideableItem<T>>(comboBox);
        comboBoxListViewSkin.getPopupContent().addEventFilter(KeyEvent.ANY, (KeyEvent event) ->
        {
            if (event.getCode() == KeyCode.SPACE)
            {
                event.consume();
            }
        });
        comboBox.setSkin(comboBoxListViewSkin);

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

        comboBox.addEventHandler(KeyEvent.KEY_PRESSED, event ->
        {
            if (!comboBox.isShowing() || event.isShiftDown() || event.isControlDown())
            {
                return;
            }
            comboBox.setEditable(true);
            comboBox.getEditor().clear();
        });

        comboBox.showingProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean 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);
                if (value != null)
                {
                    Platform.runLater(() ->
                    {
                        comboBox.getSelectionModel().select(selectedItem[0]);
                        comboBox.setValue(selectedItem[0]);
                    });
                }

            }
        });

        comboBox.setOnHidden((Event event) ->
        {
            for (HideableItem item : hideableHideableItems)
            {
                item.setHidden(false);
            }
        });
        comboBox.valueProperty().addListener((ObservableValue<? extends HideableItem<T>> obs, HideableItem<T> oldValue, HideableItem<T> newValue) ->
        {
            if (newValue == null)
            {
                for (HideableItem item : hideableHideableItems)
                {
                    item.setHidden(false);
                }
            }
        });

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

            Platform.runLater(() ->
            {
                if (comboBox.getSelectionModel().getSelectedItem() == null)
                {
                    for (HideableItem item : hideableHideableItems)
                    {
                        item.setHidden(!StringUtils.containsIgnoreCase(item.toString(), newValue));
                    }
                } else
                {

                    boolean validText = false;

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

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

        return comboBox;
    }
}

4
有点晚了,但当我遇到这个问题时,我找到了这个帖子,喜欢答案和方法,所以感谢所有贡献者。
但是,我不知道你们在 ComboBox 中放的是什么,每当我没有做出具体选择就离开框时,就会抛出一个 ClassCastException 异常。所以我猜你们主要使用 ComboBox 来选择字符串,所以我必须想出一个 StringConverter,因为我正在使用 ComboBox 来处理对象(FilterCriteria)。
所以这就是转换器,希望对某人有所帮助。
private final StringConverter<FilterCriteria> filterCriteriaStringConverter = new StringConverter<FilterCriteria>()
{
    @Override public String toString(FilterCriteria filterCriteria)
    {
        if (filterCriteria == null)
        {
            return "";
        } else
        {
            return filterCriteria.getName();
        }
    }

    @Override public FilterCriteria fromString(String string)
    {
        Optional<FilterCriteria> optionalFilterCriteria = availableTypesComboBox.getItems().stream()
          .filter(filterCriteria -> filterCriteria.getName().contains(string))
          .findFirst();
        return optionalFilterCriteria.orElseGet(() -> availableTypesComboBox.getItems().get(0));
    }
};

4

我四处寻找并尝试着一些东西。 这看起来不错:

public void handle( KeyEvent event ) {
        if( event.getCode() == KeyCode.BACK_SPACE)
            s = s.substring( 0, s.length() - 1 );
        else s += event.getText();
        for( String item: items ) {
            if( item.startsWith( s ) ) sm.select( item );
        }
    }

一个键句柄用于选择具有匹配起始字符的项目。

我希望这能帮到您。


1
如果您使用ControlsFx,那么SearchableComboBox是一个简单的ComboBox扩展,它在弹出窗口显示时显示搜索字段。用户可以在此搜索字段中键入任何文本以过滤弹出列表。

enter image description here


你能帮我解决一下如何在我的项目中使用SearchableBox吗? - undefined
你在项目中使用ControlsFx库吗?@AdnanArch - undefined
问题已经解决,谢谢。 - undefined

0
补充Mateus的代码,以下内容将为自动完成创建提示文本。例如,如果您键入“s”,则以“s”开头的ObservableArray中的项目(该数组填充了ComboBox)将用作提示文本。显然,使用“CONTAINING”参数不太合适。
public class ACComboBox1 {
static String some;
static String typedText;
static StringBuilder sb = new StringBuilder();
public enum AutoCompleteMode {
    STARTS_WITH,CONTAINING,;
}


public static<T> void autoCompleteComboBox(ComboBox<T> comboBox, AutoCompleteMode mode) {

    ObservableList<T> data = comboBox.getItems();

    comboBox.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
        public void handle(KeyEvent event){

        comboBox.hide();
        }
            });
    comboBox.addEventHandler(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
        private boolean moveCaretToPos = false;
        private int caretPos;
        public void handle(KeyEvent event) {

            String keyPressed = event.getCode().toString().toLowerCase();



            if ("space".equals(keyPressed) ){
                typedText= " ";
            } else if ("shift".equals(keyPressed ) || "command".equals(keyPressed)
                   || "alt".equals(keyPressed) ) {

                return;
            } else  {
                typedText = event.getCode().toString().toLowerCase();
            }


            if (event.getCode() == KeyCode.UP) {
                caretPos = -1;
                moveCaret(comboBox.getEditor().getText().length());
                return;
            } else if (event.getCode() == KeyCode.DOWN) {
                if (!comboBox.isShowing()) {
                    comboBox.show();
                }
                caretPos = -1;
                moveCaret(comboBox.getEditor().getText().length());
                return;
            } else if (event.getCode() == KeyCode.BACK_SPACE) {
                moveCaretToPos = true;

                caretPos = comboBox.getEditor().getCaretPosition();
                typedText=null;
                sb.delete(0, sb.length());
                comboBox.getEditor().setText(null);
                return;

            } else if (event.getCode() == KeyCode.DELETE) {
                moveCaretToPos = true;
                caretPos = comboBox.getEditor().getCaretPosition();
                return;
            } else if (event.getCode().equals(KeyCode.TAB)) {

                some = null;
                typedText = null;
                sb.delete(0, sb.length());
                return;
            } else if (event.getCode() == KeyCode.LEFT
                    || event.isControlDown() || event.getCode() == KeyCode.HOME
                    || event.getCode() == KeyCode.END || event.getCode() == KeyCode.RIGHT) {

                return;
            }


            if (typedText==null){
            typedText = comboBox.getEditor().getText().toLowerCase();
            sb.append(typedText);

            }else{
                System.out.println("sb:"+sb);
                System.out.println("tt:"+typedText);

                sb.append(typedText);


            } 

            ObservableList<T> list = FXCollections.observableArrayList();
            for (T aData : data) {

                  if (mode.equals(AutoCompleteMode.STARTS_WITH) && aData.toString().toLowerCase().startsWith(sb.toString())) {
                    list.add(aData);
                    some = aData.toString();
                } else if (mode.equals(AutoCompleteMode.CONTAINING) && aData.toString().toLowerCase().contains(comboBox.getEditor().getText().toLowerCase())) {
                    list.add(aData);
                }
            }


            comboBox.setItems(list);

            comboBox.getEditor().setText(some);



            comboBox.getEditor().positionCaret(sb.length());
            comboBox.getEditor().selectEnd();
            if (!moveCaretToPos) {
                caretPos = -1;
            }

            if (!list.isEmpty()) {
                comboBox.show();
            }
        }

        private void moveCaret(int textLength) {
            if (caretPos == -1) {
                comboBox.getEditor().positionCaret(textLength);
            } else {
                comboBox.getEditor().positionCaret(caretPos);
            }
            moveCaretToPos = false;
        }
    });
}

public static<T> T getComboBoxValue(ComboBox<T> comboBox){
    if (comboBox.getSelectionModel().getSelectedIndex() < 0) {
        return null;
    } else {
        return comboBox.getItems().get(comboBox.getSelectionModel().getSelectedIndex());
    }
}

}


嗨,我尝试了你的代码,如果我将焦点移出组件并再次将其聚焦以搜索另一个项目,则不会清除已键入的文本...而且我无法使用退格键删除已键入的字符... - Mateus Viccari

0

我建议尝试来自小型实用程序库 jalvafx的解决方案

List<String> items = Arrays.asList("Mercury", 
                                   "Venus", 
                                   "Earth", 
                                   "Mars", 
                                   "Jupiter", 
                                   "Saturn", 
                                   "Neptune");

ComboBoxCustomizer.create(comboBox)
                  .autocompleted(items)
                  .customize();

默认情况下,双击以清除值。还有一些其他有用的功能。您可以添加额外的列或字形,单独选择特定项目,更改项目的默认toString表示...

ComboBoxCustomizer.create(comboBox)
                  .autocompleted(items)
                  .overrideToString(o -> "planet: " + o)
                  .multyColumn(o -> Arrays.asList("column 2", "column 3"))
                  .emphasized(o -> o.endsWith("s"))
                  .customize();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接