JavaFX文本框自动建议

10

我希望这个文本框能像Lucene一样具有建议功能。我已经搜索了整个网络,但只找到了ComboBox的实现方法。

TextField instNameTxtFld = instNameTxtFld();

private TextField instNameTxtFld() {
    TextField txtFld = new TextField();
    txtFld.setPrefSize(600, 75);
    return txtFld;
}

我不能使用ComboBox的方法是因为如果我使用ComboBox,无法将值输入到数据库中。

private void goNext() {

    if (nameTxtFld.getText() == null || nameTxtFld.getText().trim().isEmpty()
            || instNameTxtFld.getText()== null || instNameTxtFld.getText().trim().isEmpty()
            || addTxtArea.getText() == null || addTxtArea.getText().trim().isEmpty()) {
        alertDialog.showAndWait();
    } else {
        String satu = idNumTxtFld.getText();
        String dua = nameTxtFld.getText();
        String tiga = addTxtArea.getText();
        String empat = instNameTxtFld.getText();
        int delapan = idType.getSelectionModel().getSelectedIndex();
        String sembilan = timeStamp.getText();
        try {
            KonekDB.createConnection();
            Statement st = KonekDB.conn.createStatement();
            String sql = "INSERT INTO privateguest"
                    + "(idNumber, name, address, institution, idType, startTime) "
                    + "VALUES "
                    + "('" + satu + "','" + dua + "','" + tiga + "','" + empat + "','" + delapan + "','" + sembilan + "')";

            System.out.println(sql);
            st.executeUpdate(sql);

        } catch (SQLException ex) {

            System.out.println(satu + " " + dua + " " + tiga + " " + empat + " " + delapan + " " + sembilan);
            System.out.println("SQL Exception (next)");
            ex.printStackTrace();
        }
        Frame3Private frame3 = new Frame3Private(english);
        this.getScene().setRoot(frame3);
    }

}

请帮我制作最简单的代码来实现文本框提示/自动完成功能。


1
你考虑过使用来自ControlsFXAutoComplete吗? - Itai
8个回答

21

这是基于这个的解决方案。

我的解决方案基于这个链接提供的内容。
public class AutocompletionlTextField extends TextFieldWithLengthLimit {
    //Local variables
    //entries to autocomplete
    private final SortedSet<String> entries;      
    //popup GUI
    private ContextMenu entriesPopup;


    public AutocompletionlTextField() {
        super();
        this.entries = new TreeSet<>();
        this.entriesPopup = new ContextMenu();

        setListner();
    }  


    /**
     * wrapper for default constructor with setting of "TextFieldWithLengthLimit" LengthLimit
     * 
     * @param lengthLimit 
     */
    public AutocompletionlTextField(int lengthLimit) {        
        this();
        super.setLengthLimit(lengthLimit);                
    }


    /**
     * "Suggestion" specific listners
     */
    private void setListner() {     
        //Add "suggestions" by changing text
        textProperty().addListener((observable, oldValue, newValue) -> {
            String enteredText = getText();
            //always hide suggestion if nothing has been entered (only "spacebars" are dissalowed in TextFieldWithLengthLimit)
            if (enteredText == null || enteredText.isEmpty()) {
                entriesPopup.hide();
            } else {
                //filter all possible suggestions depends on "Text", case insensitive
                List<String> filteredEntries = entries.stream()
                        .filter(e -> e.toLowerCase().contains(enteredText.toLowerCase()))
                        .collect(Collectors.toList());
                //some suggestions are found
                if (!filteredEntries.isEmpty()) {
                    //build popup - list of "CustomMenuItem"
                    populatePopup(filteredEntries, enteredText);
                    if (!entriesPopup.isShowing()) { //optional
                        entriesPopup.show(AutocompletionlTextField.this, Side.BOTTOM, 0, 0); //position of popup
                    }
                //no suggestions -> hide
                } else {
                    entriesPopup.hide();
                }
            }
        });

        //Hide always by focus-in (optional) and out
        focusedProperty().addListener((observableValue, oldValue, newValue) -> {
            entriesPopup.hide();
        });
    }             


    /**
    * Populate the entry set with the given search results. Display is limited to 10 entries, for performance.
    * 
    * @param searchResult The set of matching strings.
    */
    private void populatePopup(List<String> searchResult, String searchReauest) {
        //List of "suggestions"
        List<CustomMenuItem> menuItems = new LinkedList<>();
        //List size - 10 or founded suggestions count
        int maxEntries = 10;
        int count = Math.min(searchResult.size(), maxEntries);
        //Build list as set of labels
        for (int i = 0; i < count; i++) {
          final String result = searchResult.get(i);
          //label with graphic (text flow) to highlight founded subtext in suggestions
          Label entryLabel = new Label();
          entryLabel.setGraphic(Styles.buildTextFlow(result, searchReauest));  
          entryLabel.setPrefHeight(10);  //don't sure why it's changed with "graphic"
          CustomMenuItem item = new CustomMenuItem(entryLabel, true);
          menuItems.add(item);

          //if any suggestion is select set it into text and close popup
          item.setOnAction(actionEvent -> {
              setText(result);
              positionCaret(result.length());
              entriesPopup.hide();
          });
        }

        //"Refresh" context menu
        entriesPopup.getItems().clear();
        entriesPopup.getItems().addAll(menuItems);
    }


    /**
    * Get the existing set of autocomplete entries.
    * 
    * @return The existing autocomplete entries.
    */
    public SortedSet<String> getEntries() { return entries; }
}

你必须继承自"TextField"而不是"TextFieldWithLengthLimit",并删除带有“长度限制”的构造函数。

我使用静态方法来处理样式。这里用于在建议结果中“突出显示”输入的文本。以下是此类的方法代码:

/**
 * Build TextFlow with selected text. Return "case" dependent.
 * 
 * @param text - string with text
 * @param filter - string to select in text
 * @return - TextFlow
 */
public static TextFlow buildTextFlow(String text, String filter) {        
    int filterIndex = text.toLowerCase().indexOf(filter.toLowerCase());
    Text textBefore = new Text(text.substring(0, filterIndex));
    Text textAfter = new Text(text.substring(filterIndex + filter.length()));
    Text textFilter = new Text(text.substring(filterIndex,  filterIndex + filter.length())); //instead of "filter" to keep all "case sensitive"
    textFilter.setFill(Color.ORANGE);
    textFilter.setFont(Font.font("Helvetica", FontWeight.BOLD, 12));  
    return new TextFlow(textBefore, textFilter, textAfter);
}    

您可以在FXML中添加此“AutocompletionlTextField”(不要忘记“导入”),也可以在构造函数中添加。要设置“建议”列表,请使用“entries” getter:
AutocompletionlTextField field = new AutocompletionlTextField();
field.getEntries().addAll(YOUR_ARRAY_OF_STRINGS);

在我的应用程序中,看起来像这样:enter image description here 希望能对您有所帮助。

TextFieldWithLengthLimit 位于哪里? - firephil
这是我的自定义组件 - 你需要使用TextField代替。就像我在评论中写的那样:“你必须从“TextField”继承,而不是从“TextFieldWithLengthLimit”继承,并删除带有“长度限制”的构造函数”。 - Ruslan Gabbazov
好的,谢谢。下次请尽量发布可运行的代码,并且也请发布TextFieldWithLengthLimit的代码... - firephil
我想在从自动完成文本字段中选择值时触发函数。我们该如何实现这一点? - Satish Pahuja
抱歉,我不再拥有这段代码,并且目前也不使用JavaFX,所以很遗憾我无法提供帮助。 - Ruslan Gabbazov
1
@SatishPahuja 我的答案基于这个答案,并允许您使用所选对象。 https://dev59.com/Vpbfa4cB1Zd3GeqPt3W3#56173327 - trilogy

12

你可以使用ControlsFX --> maven

解决方案:

TextFields.bindAutoCompletion(textfield,"text to suggest", "another text to suggest");

6
快速解决方案,但是弹出窗口非常缓慢,我可以在弹出窗口出现之前输入完整个单词。 - Wesos de Queso
查看ControlsFX自动完成代码,您可以调整默认延迟时间350毫秒,这个时间有点长... - firephil

8

在这个 示例 中,只允许使用字符串。

我编辑了它,使其能够接受任何对象,并仅使用该对象的 toString 方法。这样可以让你使用所选对象来做其他事情,而不仅仅是填充文本字段。同时修复了一个错误,当你删除一个不属于条目的 StringTextField 中时,会抛出异常。

使用示例:

SortedSet<Address> entries = new TreeSet<>((Address o1, Address o2) -> o1.toString().compareTo(o2.toString()));

entries.add(new Address("50", "Main Street", "Oakville", "Ontario", "T6P4K9"));
entries.add(new Address("3", "Fuller Road", "Toronto", "Ontario", "B6S4T9"));

AutoCompleteTextField<Address> text = new AutoCompleteTextField(entries);

text.getEntryMenu().setOnAction(e ->
{
    ((MenuItem) e.getTarget()).addEventHandler(Event.ANY, event ->
    {
         if (text.getLastSelectedObject() != null)
         {
            text.setText(text.getLastSelectedObject().toString());
            System.out.println(text.getLastSelectedObject().getProvince());
         }
    });
});

enter image description here

AutoCompleteTextField.java

import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;

import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;

/**
 * This class is a TextField which implements an "autocomplete" functionality,
 * based on a supplied list of entries.<p>
 *
 * If the entered text matches a part of any of the supplied entries these are
 * going to be displayed in a popup. Further the matching part of the entry is
 * going to be displayed in a special style, defined by
 * {@link #textOccurenceStyle textOccurenceStyle}. The maximum number of
 * displayed entries in the popup is defined by
 * {@link #maxEntries maxEntries}.<br>
 * By default the pattern matching is not case-sensitive. This behaviour is
 * defined by the {@link #caseSensitive caseSensitive}
 * .<p>
 *
 * The AutoCompleteTextField also has a List of
 * {@link #filteredEntries filteredEntries} that is equal to the search results
 * if search results are not empty, or {@link #filteredEntries filteredEntries}
 * is equal to {@link #entries entries} otherwise. If
 * {@link #popupHidden popupHidden} is set to true no popup is going to be
 * shown. This list can be used to bind all entries to another node (a ListView
 * for example) in the following way:
 * <pre>
 * <code>
 * AutoCompleteTextField auto = new AutoCompleteTextField(entries);
 * auto.setPopupHidden(true);
 * SimpleListProperty filteredEntries = new SimpleListProperty(auto.getFilteredEntries());
 * listView.itemsProperty().bind(filteredEntries);
 * </code>
 * </pre>
 *
 * @author Caleb Brinkman
 * @author Fabian Ochmann
 * @param <S>
 */
public class AutoCompleteTextField<S> extends TextField
{

    private final ObjectProperty<S> lastSelectedItem = new SimpleObjectProperty<>();

    /**
     * The existing autocomplete entries.
     */
    private final SortedSet<S> entries;

    /**
     * The set of filtered entries:<br>
     * Equal to the search results if search results are not empty, equal to
     * {@link #entries entries} otherwise.
     */
    private ObservableList<S> filteredEntries
            = FXCollections.observableArrayList();

    /**
     * The popup used to select an entry.
     */
    private ContextMenu entriesPopup;

    /**
     * Indicates whether the search is case sensitive or not. <br>
     * Default: false
     */
    private boolean caseSensitive = false;

    /**
     * Indicates whether the Popup should be hidden or displayed. Use this if
     * you want to filter an existing list/set (for example values of a
     * {@link javafx.scene.control.ListView ListView}). Do this by binding
     * {@link #getFilteredEntries() getFilteredEntries()} to the list/set.
     */
    private boolean popupHidden = false;

    /**
     * The CSS style that should be applied on the parts in the popup that match
     * the entered text. <br>
     * Default: "-fx-font-weight: bold; -fx-fill: red;"
     * <p>
     * Note: This style is going to be applied on an
     * {@link javafx.scene.text.Text Text} instance. See the <i>JavaFX CSS
     * Reference Guide</i> for available CSS Propeties.
     */
    private String textOccurenceStyle = "-fx-font-weight: bold; "
            + "-fx-fill: red;";

    /**
     * The maximum Number of entries displayed in the popup.<br>
     * Default: 10
     */
    private int maxEntries = 10;

    /**
     * Construct a new AutoCompleteTextField.
     *
     * @param entrySet
     */
    public AutoCompleteTextField(SortedSet<S> entrySet)
    {
        super();
        this.entries = (entrySet == null ? new TreeSet<>() : entrySet);
        this.filteredEntries.addAll(entries);

        entriesPopup = new ContextMenu();

        textProperty().addListener((ObservableValue<? extends String> observableValue, String s, String s2) ->
        {

            if (getText() == null || getText().length() == 0)
            {
                filteredEntries.clear();
                filteredEntries.addAll(entries);
                entriesPopup.hide();
            } else
            {
                LinkedList<S> searchResult = new LinkedList<>();
                //Check if the entered Text is part of some entry
                String text1 = getText();
                Pattern pattern;
                if (isCaseSensitive())
                {
                    pattern = Pattern.compile(".*" + text1 + ".*");
                } else
                {
                    pattern = Pattern.compile(".*" + text1 + ".*", Pattern.CASE_INSENSITIVE);
                }
                for (S entry : entries)
                {
                    Matcher matcher = pattern.matcher(entry.toString());
                    if (matcher.matches())
                    {
                        searchResult.add(entry);
                    }
                }
                if (!entries.isEmpty())
                {
                    filteredEntries.clear();
                    filteredEntries.addAll(searchResult);
                    //Only show popup if not in filter mode
                    if (!isPopupHidden())
                    {
                        populatePopup(searchResult, text1);
                        if (!entriesPopup.isShowing())
                        {
                            entriesPopup.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
                        }
                    }
                } else
                {
                    entriesPopup.hide();
                }
            }
        });

        focusedProperty().addListener((ObservableValue<? extends Boolean> observableValue, Boolean aBoolean, Boolean aBoolean2) ->
        {
            entriesPopup.hide();
        });

    }

    /**
     * Get the existing set of autocomplete entries.
     *
     * @return The existing autocomplete entries.
     */
    public SortedSet<S> getEntries()
    {
        return entries;
    }

    /**
     * Populate the entry set with the given search results. Display is limited
     * to 10 entries, for performance.
     *
     * @param searchResult The set of matching strings.
     */
    private void populatePopup(List<S> searchResult, String text)
    {
        List<CustomMenuItem> menuItems = new LinkedList<>();
        int count = Math.min(searchResult.size(), getMaxEntries());
        for (int i = 0; i < count; i++)
        {
            final String result = searchResult.get(i).toString();
            final S itemObject = searchResult.get(i);
            int occurence;

            if (isCaseSensitive())
            {
                occurence = result.indexOf(text);
            } else
            {
                occurence = result.toLowerCase().indexOf(text.toLowerCase());
            }
            if (occurence < 0)
            {
                continue;
            }
            //Part before occurence (might be empty)
            Text pre = new Text(result.substring(0, occurence));
            //Part of (first) occurence
            Text in = new Text(result.substring(occurence, occurence + text.length()));
            in.setStyle(getTextOccurenceStyle());
            //Part after occurence
            Text post = new Text(result.substring(occurence + text.length(), result.length()));

            TextFlow entryFlow = new TextFlow(pre, in, post);

            CustomMenuItem item = new CustomMenuItem(entryFlow, true);
            item.setOnAction((ActionEvent actionEvent) ->
            {
                lastSelectedItem.set(itemObject);
                entriesPopup.hide();
            });
            menuItems.add(item);
        }
        entriesPopup.getItems().clear();
        entriesPopup.getItems().addAll(menuItems);

    }

    public S getLastSelectedObject()
    {
        return lastSelectedItem.get();
    }

    public ContextMenu getEntryMenu()
    {
        return entriesPopup;
    }

    public boolean isCaseSensitive()
    {
        return caseSensitive;
    }

    public String getTextOccurenceStyle()
    {
        return textOccurenceStyle;
    }

    public void setCaseSensitive(boolean caseSensitive)
    {
        this.caseSensitive = caseSensitive;
    }

    public void setTextOccurenceStyle(String textOccurenceStyle)
    {
        this.textOccurenceStyle = textOccurenceStyle;
    }

    public boolean isPopupHidden()
    {
        return popupHidden;
    }

    public void setPopupHidden(boolean popupHidden)
    {
        this.popupHidden = popupHidden;
    }

    public ObservableList<S> getFilteredEntries()
    {
        return filteredEntries;
    }

    public int getMaxEntries()
    {
        return maxEntries;
    }

    public void setMaxEntries(int maxEntries)
    {
        this.maxEntries = maxEntries;
    }

}

7

使用JFoenix有另一种解决方案。自2018年2月以来,他们添加了自动完成类。这是它的实现。

// when initializing the window or in some other method
void initialize() {
    JFXAutoCompletePopup<String> autoCompletePopup = new JFXAutoCompletePopup<>();
    autoCompletePopup.getSuggestions().addAll("option1", "option2", "...");

    autoCompletePopup.setSelectionHandler(event -> {
        textField.setText(event.getObject());

        // you can do other actions here when text completed
    });

    // filtering options
    textField.textProperty().addListener(observable -> {
        autoCompletePopup.filter(string -> string.toLowerCase().contains(textField.getText().toLowerCase()));
        if (autoCompletePopup.getFilteredSuggestions().isEmpty() || textField.getText().isEmpty()) {
            autoCompletePopup.hide();
            // if you remove textField.getText.isEmpty() when text field is empty it suggests all options
            // so you can choose
        } else {
            autoCompletePopup.show(textField);
        }
    });
}

这是一种新的方法,对我来说效果很好。希望它能有所帮助,感谢JFoenix开发人员。


你能够使用JFoenix或ControlsFx对自动完成功能中显示的下拉列表进行CSS样式设置吗? - iCoder
这是最好的方法。谢谢。 - Mayur Patel
@iCoder您可以使用.autocomplete-list和.autocomplete-pane样式类来设置样式。.autocomplete-list { -fx-padding: 5; -fx-font-family: "Roboto"; -fx-font-size: 16; -fx-font-smoothing-type: gray; }.autocomplete-pane { -fx-padding: 5; -fx-background-color: lightskyblue; } - Igor Delac

1
这是一个相当老的话题,但由于我刚遇到这个问题,我将已经提到的部分组装成了我的解决方案。 就像@RicherdK已经说过的那样,您可以使用controlsfx,最简单的绑定方式就像他所说的那样:
TextFields.bindAutoCompletion(textfield,"text to suggest", "another text to suggest");

在他的帖子评论中提到,这种自动完成速度相当慢。这是真的,因为它需要250毫秒才能显示建议,即使没有键盘输入。因此,您可以将延迟设置为任何您想要的时间:
TextFields.bindAutoCompletion(textfield,"text to suggest", "another text to suggest").setDelay(50);

除了这个小细节,我在使用这个库时遇到了问题,我不得不添加:

--add-exports=javafx.base/com.sun.javafx.event=ALL-UNNAMED

针对我的VM选项进行设置。有些人也遇到了类似的问题,你可以在这里找到相关信息,并确保你的JavaFx版本匹配。


1
这是我的解决方案-只需要一个ComboBox参数的完整方法:
 /**
 * My own autocomplete combobox
 *
 * @param categoryComboBox
 */
public static void bindAutoCompleteToComboBox(ComboBox<String> categoryComboBox) {

    /**
     * backup the original list
     */
    List<String> categoryComboBoxItemsList = new ArrayList<String>(categoryComboBox.getItems());

    /**
     * if mouse pressed: select all of the text field
     */
    categoryComboBox.getEditor().setOnMousePressed(new EventHandler<MouseEvent>() {

        @Override
        public void handle(MouseEvent event) {
            Platform.runLater(new Runnable() {
                @Override
                public void run() {
                    if (categoryComboBox.getEditor().isFocused() && !categoryComboBox.getEditor().getText().isEmpty()) {
                        categoryComboBox.getEditor().selectAll();
                    }
                }
            });
        }
    });

    /**
     * events on text input
     */
    categoryComboBox.setOnKeyReleased(new EventHandler<KeyEvent>() {

        private List<String> reducedList = new ArrayList<String>();

        @Override
        public void handle(KeyEvent event) {

            if (event.getCode().isLetterKey() || event.getCode().isDigitKey() || event.getCode().equals(KeyCode.BACK_SPACE)) {

                /**
                 * Open comboBox if letter, number or backspace
                 */
                categoryComboBox.show();

                String temp = categoryComboBox.getEditor().getText();
                reducedList = new ArrayList<String>();

                /**
                 * If backspace pressed, selection refers to the basic list again
                 */
                if (event.getCode().equals(KeyCode.BACK_SPACE)) {
                    categoryComboBox.getItems().clear();
                    categoryComboBox.getItems().addAll(categoryComboBoxItemsList);

                    // java fx workaround to restore the default list height of 10
                    categoryComboBox.hide();
                    categoryComboBox.setVisibleRowCount(10);
                    categoryComboBox.show();
                }

                /**
                 * loop through all entrys and look whether input contains this text.
                 *
                 * after that, entry will be added to the reduced list
                 */
                for (String element : categoryComboBox.getItems()) {
                    if (StringUtils.containsIgnoreCase(element, temp)) {
                        reducedList.add(element);
                    }
                }

                /**
                 * all elements are cleared, the reduced list will be added. First element is selected
                 */
                categoryComboBox.getItems().clear();
                categoryComboBox.getItems().addAll(reducedList);
                categoryComboBox.getSelectionModel().select(0);
                categoryComboBox.getEditor().setText(temp);

            } else if (event.getCode().equals(KeyCode.ENTER)) {

                /**
                 * if enter, the element which is selected will be applied to the text field and the dropdown will be closed
                 */
                if (categoryComboBox.getSelectionModel().getSelectedIndex() != -1) {
                    categoryComboBox.getEditor().setText(categoryComboBox.getItems().get((categoryComboBox.getSelectionModel().getSelectedIndex())));
                } else {
                    categoryComboBox.getEditor().setText(categoryComboBox.getItems().get(0));
                }

            } else if (event.getCode().equals(KeyCode.DOWN)) {

                /**
                 * arrow down shows the dropdown
                 */
                categoryComboBox.show();
            }

            /**
             * Tab marks everything (when tabbing into the field
             */
            if (event.getCode().equals(KeyCode.TAB)) {

                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        if (categoryComboBox.getEditor().isFocused() && !categoryComboBox.getEditor().getText().isEmpty()) {
                            categoryComboBox.getEditor().selectAll();
                        }
                    }
                });

            } else {
                /**
                 * all entries except for tab put the caret on the last character
                 */
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        categoryComboBox.getEditor().positionCaret(categoryComboBox.getEditor().getText().length());
                    }
                });
            }

        }
    });

    /**
     * focus lost
     */
    categoryComboBox.focusedProperty().addListener(new ChangeListener<Boolean>() {

        /**
         * if focus lost: refill the category combo box with the original items
         */
        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

            if (oldValue) {

                /**
                 * saves whether textfield was empty before reset the comboBox
                 */
                boolean emptyTextField = categoryComboBox.getEditor().getText().isEmpty();

                if (categoryComboBox.getSelectionModel().getSelectedIndex() != -1) {
                    categoryComboBox.getEditor().setText(categoryComboBox.getItems().get(categoryComboBox.getSelectionModel().getSelectedIndex()));
                }

                String temp = categoryComboBox.getEditor().getText();

                categoryComboBox.getItems().clear();
                categoryComboBox.getItems().addAll(categoryComboBoxItemsList);

                if (!emptyTextField) {
                    categoryComboBox.getSelectionModel().select(temp);
                } else {
                    categoryComboBox.getEditor().setText("");
                }
            }
        }
    });
}

0
我使用了 Trilogy 的答案,并使其能够在 TableCell 内工作。
TableCell 骨架
import javafx.scene.control.Control;
import javafx.scene.control.TableCell;

public abstract class ObjectTableCell<C extends Control,T,S> extends TableCell<T, S>{

    private C component;
    
    public ObjectTableCell(C component) {
        this.component = component;
    }
    
    protected abstract void onEditStart(C component);
    protected abstract void onEditCommit();
    
    @Override
    public void startEdit() {
        super.startEdit();
        setGraphic(component);
        onEditStart(component);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setGraphic(null);
    }

    @Override
    public void commitEdit(S newValue) {
        super.commitEdit(newValue);
        setGraphic(null);
        onEditCommit();
    }

    @Override
    protected void updateItem(S item, boolean empty) {
        super.updateItem(item, empty);
        if(empty || item == null) {
            setText(null);
        }else {
            setText(item.toString());
        }
    }
    
}

实现示例:

return new ObjectTableCell<AutoCompleteTextField<S>, T, S>(new AutoCompleteTextField<>()) {

    @SuppressWarnings("unchecked")
    @Override
    protected void onEditStart(AutoCompleteTextField<S> component) {
        SortedSet<S> entries = new TreeSet<>((S o1, S o2) -> o1.toString().compareTo(o2.toString()));
        entries.addAll((Collection<? extends S>) crud.getLastSearchItems());
        component.setEntries(entries);
        component.getEntryMenu().setOnAction(e -> {
            ((MenuItem) e.getTarget()).addEventHandler(Event.ANY, event -> {
                var lastSelected = component.getSelectedObject() ;
                if (lastSelected != null) {
                    if(!crud.sameId(getItem(), lastSelected)) {
                        component.setText(lastSelected.toString());
                        setItem(lastSelected);
                        commitEdit(lastSelected);
                    }else {
                        cancelEdit();
                    }
                }
            });
        });
        component.setSelectedObject(getItem());
        var obj = component.getSelectedObject();
        component.setText(obj==null? null:obj.toString());
        component.requestFocus();
        component.selectAll();
    }

    @Override
    protected void onEditCommit() {
        //whatever happens after the edit is commited
        TableUI.this.onEditCommit(getSelectedItem());//this triggers database updates in my project
    }
};

这是Trilogy答案的稍作修改版本。

import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.TextField;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;

public class AutoCompleteTextField<S> extends TextField {

    private final ObjectProperty<S> selectedObject = new SimpleObjectProperty<>();
    private SortedSet<S> sortedEntries;
    private ObservableList<S> filteredEntries;
    private ContextMenu entriesPopup = new ContextMenu();
    private String textOccurenceStyle = "-fx-font-weight: bold; " + "-fx-fill: black;";

    public AutoCompleteTextField() {
        super();
        textProperty().addListener((obs, old, niu) -> {
            if (getText() == null || getText().length() == 0) {
                getFilteredEntries().clear();
                getFilteredEntries().addAll(getSortedEntries());
                entriesPopup.hide();
            } else {
                LinkedList<S> searchResult = new LinkedList<>();
                Pattern pattern = Pattern.compile(".*" + niu + ".*", Pattern.CASE_INSENSITIVE);
                for (S entry : getSortedEntries()) {
                    Matcher matcher = pattern.matcher(entry.toString());
                    if (matcher.matches()) {
                        searchResult.add(entry);
                    }
                }
                if (!getSortedEntries().isEmpty()) {
                    getFilteredEntries().clear();
                    getFilteredEntries().addAll(searchResult);
                        populatePopup(searchResult, niu);
                        if (!entriesPopup.isShowing()) {
                            entriesPopup.show(AutoCompleteTextField.this, Side.BOTTOM, 0, 0);
                        }
                } else {
                    entriesPopup.hide();
                }
            }
        });
        focusedProperty().addListener((obs, old, niu) -> {
            entriesPopup.hide();
        });
    }

    private void populatePopup(List<S> searchResult, String text) {
        List<CustomMenuItem> menuItems = new LinkedList<>();
        int count = Math.min(searchResult.size(), 8);
        for (int i = 0; i < count; i++) {
            final String result = searchResult.get(i).toString();
            final S itemObject = searchResult.get(i);
            int occurence = result.toLowerCase().indexOf(text.toLowerCase());
            if (occurence < 0) {
                continue;
            }
            // Part before occurence (might be empty)
            Text pre = new Text(result.substring(0, occurence));
            // Part of (first) occurence
            Text in = new Text(result.substring(occurence, occurence + text.length()));
            in.setStyle(getTextOccurenceStyle());
            // Part after occurence
            Text post = new Text(result.substring(occurence + text.length(), result.length()));

            TextFlow entryFlow = new TextFlow(pre, in, post);

            CustomMenuItem item = new CustomMenuItem(entryFlow, true);
            item.setOnAction((ActionEvent actionEvent) -> {
                selectedObject.set(itemObject);
                entriesPopup.hide();
            });
            menuItems.add(item);
        }
        entriesPopup.getItems().clear();
        entriesPopup.getItems().addAll(menuItems);

    }

    public void setEntries(SortedSet<S> entrySet) {
        this.sortedEntries = (entrySet == null ? new TreeSet<>() : entrySet);
        setFilteredEntries(FXCollections.observableArrayList(sortedEntries));
    }
    
    public ObservableList<S> getFilteredEntries() {
        return filteredEntries;
    }

    public void setFilteredEntries(ObservableList<S> filteredEntries) {
        this.filteredEntries = filteredEntries;
    }

    public SortedSet<S> getSortedEntries() {
        return this.sortedEntries;
    }

    public ObjectProperty<S> selectedObject() {
        return selectedObject;
    }

    public void setSelectedObject(S object) {
        selectedObject.set(object);
        Platform.runLater(() -> {
            entriesPopup.hide();
        });
    }

    public S getSelectedObject() {
        return selectedObject.get();
    }

    public ContextMenu getEntryMenu() {
        return entriesPopup;
    }

    public String getTextOccurenceStyle() {
        return textOccurenceStyle;
    }

    public void setTextOccurenceStyle(String textOccurenceStyle) {
        this.textOccurenceStyle = textOccurenceStyle;
    }

}

enter image description here


-1
如果您不想使用任何外部库(如controlsfx),这是我的解决方案:
1. 创建一个文本字段以供用户输入文本。 2. 创建一个ListView来显示建议。 3. 将TextField的text属性绑定到JavaFX控制器类中的字符串属性。 4. 为文本属性创建一个事件处理程序,根据文本字段的当前值更新建议。 5. 使用建议填充ListView。 6. 处理用户与ListView的交互,以选择一个建议,然后在TextField中显示它。
以下是您的JavaFX控制器类中代码可能看起来像的示例:
public class AutocompleteController {

    @FXML
    private TextField textField;

    @FXML
    private ListView<String> suggestionsList;

    private final ObservableList<String> suggestions = FXCollections.observableArrayList();

    private final StringProperty text = new SimpleStringProperty();

    public void initialize() {
        text.bind(textField.textProperty());
        text.addListener((observable, oldValue, newValue) -> updateSuggestions(newValue));
        suggestionsList.setItems(suggestions);
        suggestionsList.setOnMouseClicked(event -> {
            String selectedSuggestion = suggestionsList.getSelectionModel().getSelectedItem();
            if (selectedSuggestion != null) {
                textField.setText(selectedSuggestion);
                suggestions.clear();
            }
        });
    }

    private void updateSuggestions(String enteredText) {
        // Clear the previous suggestions
        suggestions.clear();

        // Get the new suggestions based on enteredText and add them to the suggestions list
        List<String> newSuggestions = getSuggestions(enteredText);
        suggestions.addAll(newSuggestions);
    }

    private List<String> getSuggestions(String enteredText) {
        // Implement your logic to retrieve suggestions based on enteredText
        // ...

        return suggestions;
    }
}

如果您希望建立一个动态的建议列表视图,使其高度取决于列表视图中的项目,则可以创建一个DoubleBinding来计算ListView的高度,该高度基于列表中的项目数量和固定单元格大小。然后将绑定应用于ListView的prefHeight属性,以使高度动态变化。
以下是一个示例:
suggestionsList.setFixedCellSize(24.0); // Set a fixed height for each cell

DoubleBinding heightBinding = new DoubleBinding() {
    {
        super.bind(suggestionsList.getItems());
    }
    @Override
    protected double computeValue() {
        return suggestionsList.getFixedCellSize() * suggestionsList.getItems().size();
    }
};

suggestionsList.prefHeightProperty().bind(heightBinding);

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