9

我偶然发现(在我眼中)一个愚蠢的问题。但是我没有找到解决方案(可能是因为没有使用正确的搜索关键字,或者在很容易的时候让它变得太难了..)场景:

我有一个包含 500 个客户的组合框。我必须选择一个客户。

在 Swing 中,当列表关闭并且您开始输入时,它会自动跳转到输入的字母。例如:

项目:

  • 亚当
  • 短剑
  • 弗雷迪
  • ...
  • 罗杰
  • 史蒂文
  • Z人

当组合框列表打开时,我只需输入“R”,然后在摇摆中,它会跳转到以“R”开头的第一个客户。在 javafx 2 中,它似乎没有这种行为......是否有一些我必须启用的选项,或者我应该做一些事情,比如使用可编辑的组合框,并制作一个filter()在每次按键时触发的方法?

编辑:基于 Bhupendra 的答案的解决方案:

public class FilterComboBox<T> extends ComboBox<T> {
private final FilterComboBox<T> fcbo = this;

//private FilterComboBox fcbo = this;
private ObservableList<T> items;
private ObservableList<T> filter;
private String s;
private Object selection;

private class KeyHandler implements EventHandler< KeyEvent> {

    private SingleSelectionModel<T> sm;

    public KeyHandler() {
        sm = getSelectionModel();
        s = "";
    }

    @Override
    public void handle(KeyEvent event) {
        filter.clear();
        // handle non alphanumeric keys like backspace, delete etc
        if (event.getCode() == KeyCode.BACK_SPACE && s.length() > 0) {
            s = s.substring(0, s.length() - 1);
        } else {
            s += event.getText();
        }

        if (s.length() == 0) {
            fcbo.setItems(items);
            sm.selectFirst();
            return;
        }
        //System.out.println(s);
        if (event.getCode().isLetterKey()) {
            for (T item : items) {
                if (item.toString().toUpperCase().startsWith(s.toUpperCase())) {

                    filter.add(item);
                    //System.out.println(item);

                    fcbo.setItems(filter);

                    //sm.clearSelection();
                    //sm.select(item);

                }
            }
            sm.select(0);
        }

    }
}

public FilterComboBox(final ObservableList<T> items) {
    super(items);
    this.items = items;
    this.filter = FXCollections.observableArrayList();

    setOnKeyReleased(new KeyHandler());

    this.focusedProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            if (newValue == false) {
                s = "";
                fcbo.setItems(items);
                fcbo.getSelectionModel().select((T)selection);
            }

        }
    });

    this.getSelectionModel().selectedItemProperty().addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {
            if (newValue != null) {
                selection = (Object) newValue;
            }

        }
    });
}

}

4

4 回答 4

7

过滤器组合框的最简单形式如下面的代码所示。但它需要更多的工作来完善它。此外,如果列表很大,如您的情况,则可能存在性能问题,因为我们在每次按键时都循环遍历整个集合。

public class FilterComboBox extends ComboBox< String > {
    private ObservableList< String >    items;

    private class KeyHandler implements EventHandler< KeyEvent > {

        private SingleSelectionModel< String >  sm;
        private String                          s;

        public KeyHandler() {
            sm = getSelectionModel();
            s = "";
        }

        @Override
        public void handle( KeyEvent event ) {
            // handle non alphanumeric keys like backspace, delete etc
            if( event.getCode() == KeyCode.BACK_SPACE && s.length()>0)
                s = s.substring( 0, s.length() - 1 );
            else s += event.getText();

            if( s.length() == 0 ) {
                sm.selectFirst();
                return;
            }
            System.out.println( s );
            for( String item: items ) {
                if( item.startsWith( s ) ) sm.select( item );
            }
        }

    }

    public FilterComboBox( ObservableList< String > items ) {
        super( items );
        this.items = items;

        setOnKeyReleased( new KeyHandler() );
    }
}
于 2012-11-14T02:47:07.127 回答
3

这样的代码还不够吗?

    comboBox.setOnKeyReleased(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {
            String s = jumpTo(event.getText(), comboBox.getValue(), comboBox.getItems());
            if (s != null) {
                comboBox.setValue(s);
            }
        }
    });

...

static String jumpTo(String keyPressed, String currentlySelected, List<String> items) {
    String key = keyPressed.toUpperCase();
    if (key.matches("^[A-Z]$")) {
        // Only act on letters so that navigating with cursor keys does not
        // try to jump somewhere.
        boolean letterFound = false;
        boolean foundCurrent = currentlySelected == null;
        for (String s : items) {
            if (s.toUpperCase().startsWith(key)) {
                letterFound = true;
                if (foundCurrent) {
                    return s;
                }
                foundCurrent = s.equals(currentlySelected);
            }
        }
        if (letterFound) {
            return jumpTo(keyPressed, null, items);
        }
    }
    return null;
}

当您按下一个字母时,这将跳转到第一个项目。如果您再次按下该字母,它将跳转到以该字母开头的下一个项目,如果没有更多以该字母开头的项目,则返回第一个项目。

于 2014-01-12T21:17:00.240 回答
1

我无法真正得到 Perneel 的解决方案来满足我的需要。Bhupendra's 很好,但有一个细节:它选择了最后一个匹配的项目。如果您有从 0 到 20 的数字(作为字符串),如果键入“1”,它将返回 19 而不是 1...

下面的代码添加了解决此问题所需的行。

import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.SingleSelectionModel;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

// TODO: Auto-generated Javadoc
/**
 * The Class FilterComboBox.
 */
public class FilterComboBox extends ComboBox< String > 
{

    /** The items. */
    private ObservableList< String >    items;

    /**
     * The Class KeyHandler.
     */
    private class KeyHandler implements EventHandler< KeyEvent > 
    {

        /** The sm. */
        private SingleSelectionModel< String >  sm;

        /** The s. */
        private String                          s;

        /**
         * Instantiates a new key handler.
         */
        public KeyHandler() 
        {
            sm = getSelectionModel();
            s = "";
        }

        /* (non-Javadoc)
         * @see javafx.event.EventHandler#handle(javafx.event.Event)
         */
        @Override
        public void handle( KeyEvent event ) 
        {
            // handle non alphanumeric keys like backspace, delete etc
            if( event.getCode() == KeyCode.BACK_SPACE && s.length()>0)
            {
                s = s.substring( 0, s.length() - 1 );
            }
            else if(event.getCode() != KeyCode.TAB )
            {
                s += event.getText();
            }

            if( s.length() == 0 ) 
            {
                sm.selectFirst();
                return;
            }
            System.out.println( s );
            for( String item: items ) 
            {
                if( item.startsWith( s ) ) 
                {
                    sm.select( item );
                    return;
                }
            }
        }

    }

    /**
     * Instantiates a new filter combo box.
     *
     * @param items the items
     */
    public FilterComboBox( ObservableList< String > items ) 
    {
        super( items );
        this.items = items;

        setOnKeyReleased( new KeyHandler() );
    }
}

这个组件是一个 ComboBox,它只接受 String 作为输入并且可以通过输入一些字符来过滤。所有功劳归于 Bhupendra,我只发布此代码是为了防止其他人不得不过多考虑这个常见问题。最后编辑:添加了一个测试以防止 TAB 被视为字符(允许在表单中导航而不破坏组件)

于 2013-12-11T15:06:55.463 回答
1

这是另一种选择。以网站https://tech.chitgoks.com
中 的示例为基础。

它具有优雅的解决方案,如果需要,也可以在前面的示例中使用。
打字时,列表自动滚动,非常方便。

    import com.sun.javafx.scene.control.skin.ComboBoxListViewSkin;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.control.ComboBox;
    import javafx.scene.control.ListView;
    import javafx.scene.input.KeyCode;
    import javafx.scene.input.KeyEvent;

    import java.time.Duration;
    import java.time.Instant;
    import java.util.Collection;

    class SearchComboBox<T> extends ComboBox<T> {

        private static final int IDLE_INTERVAL_MILLIS = 1000;

        private Instant instant = Instant.now();
        private StringBuilder sb = new StringBuilder();

        public SearchComboBox(Collection<T> choices) {
            this(FXCollections.observableArrayList(choices));
        }

        public SearchComboBox(final ObservableList<T> items) {
            this();
            setItems(items);
            getSelectionModel().selectFirst();
        }

        public SearchComboBox() {
            super();

            this.addEventFilter(KeyEvent.KEY_RELEASED, event -> {
                if (event.getCode() == KeyCode.ESCAPE && sb.length() > 0) {
                    resetSearch();
                }
            });

            this.setOnKeyReleased(event -> {

                        if (Duration.between(instant, Instant.now()).toMillis() > IDLE_INTERVAL_MILLIS) {
                            resetSearch();
                        }

                        instant = Instant.now();

                        if (event.getCode() == KeyCode.DOWN || event.getCode() == KeyCode.UP || event.getCode() == KeyCode.TAB) {
                            return;
                        } else if (event.getCode() == KeyCode.BACK_SPACE && sb.length() > 0) {
                            sb.deleteCharAt(sb.length() - 1);
                        } else {
                            sb.append(event.getText().toLowerCase());
                        }

                        if (sb.length() == 0) {
                            return;
                        }

                        boolean found = false;
                        for (int i = 0; i < getItems().size(); i++) {
                            if (event.getCode() != KeyCode.BACK_SPACE && getItems().get(i).toString().toLowerCase().startsWith(sb.toString())) {
                                ListView listView = getListView();
                                listView.getSelectionModel().clearAndSelect(i);
                                scroll();
                                found = true;
                                break;
                            }
                        }

                        if (!found && sb.length() > 0)
                            sb.deleteCharAt(sb.length() - 1);
                    }
            );

            // add a focus listener such that if not in focus, reset the search process
            this.focusedProperty().addListener((observable, oldValue, newValue) -> {
                if (!newValue) {
                    resetSearch();
                } else {
                    scroll();
                }
            });
        }

        private void resetSearch() {
            sb.setLength(0);
            instant = Instant.now();
        }

        private void scroll() {
            ListView listView = getListView();
            int selectedIndex = listView.getSelectionModel().getSelectedIndex();
            listView.scrollTo(selectedIndex == 0 ? selectedIndex : selectedIndex - 1);
        }

        private ListView getListView() {
            return ((ComboBoxListViewSkin) this.getSkin()).getListView();
        }
    }

我以两种方式改进了这个例子。

  1. 所有代码都封装在一个类中——不需要从外部连接任何东西。
  2. 如果用户有一段时间没有显示活动,则搜索字符串将被重置。另一种解决方案是如何重置搜索:按 Backspace 键,或使 ComboBox 失去焦点。
于 2019-11-15T11:34:12.287 回答