商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > JavaSwing实现自定义可搜索下拉框(附完整代码)

JavaSwing实现自定义可搜索下拉框(附完整代码)

  发布于2026-06-30 阅读(0)

扫一扫,手机访问

一、背景

在实际的Ja va桌面应用开发中,Swing自带的JComboBox虽然能完成基本的下拉选择,但用起来总有两个让人头疼的地方:

Ja vaSwing实现自定义可搜索下拉框(附完整代码)

  • 输入框只能机械地从列表里选,没法自己输入文字来筛选;
  • 一旦数据量上来,用户就得上下滚动半天才能找到目标,体验实在算不上好。

CusComboBox要解决的就是这个问题——在原生JComboBox的基础上,给它加上搜索过滤的能力。用户输入关键字时,下拉列表自动匹配并过滤;同时支持键盘上下键选择、回车确认、ESC取消这些快捷键操作;如果输入的内容完全没匹配到,还能通过回调做额外处理。这样一来,既保留了原生的下拉交互,又弥补了搜索上的缺口。

二、核心设计

这个组件的实现思路并不复杂——CusComboBox直接继承JComboBox,然后通过setEditable(true)开启可编辑模式。在此基础上,做了几件事:

  • 自定义编辑器(基于BasicComboBoxEditor),用JTextField来接管输入框;
  • 监听输入框的文本变化,动态过滤下拉列表项;
  • 实现键盘导航(上下键、回车、ESC);
  • 处理中文输入法的组合状态,避免打字过程中触发无意义的搜索;
  • 加入200ms的防抖延迟,防止每次击键都去重新过滤,浪费性能。

三、类源码

import cn.hutool.core.collection.CollectionUtil;

import ja vax.swing.*;
import ja vax.swing.event.PopupMenuEvent;
import ja vax.swing.event.PopupMenuListener;
import ja vax.swing.plaf.basic.BasicComboBoxEditor;
import ja va.awt.*;
import ja va.awt.event.*;
import ja va.text.AttributedCharacterIterator;
import ja va.util.Collection;
import ja va.util.concurrent.atomic.AtomicBoolean;
import ja va.util.function.Consumer;
import ja va.util.function.Function;

/**
 * 自定义下拉选择器(可搜索)
 * 支持输入关键字过滤下拉选项、键盘导航、无匹配回调
 *
 * 使用示例:
 * 1. 基本类型数据:
 *    List list = Arrays.asList("苹果", "香蕉", "橙子");
 *    CusComboBox comboBox = new CusComboBox<>(list);
 *
 * 2. 对象数据,指定显示字段:
 *    CusComboBox comboBox = new CusComboBox<>(userList, User::getName);
 *
 * 3. 设置无匹配回调:
 *    comboBox.setOnNoMatch(text -> System.out.println("无匹配:" + text));
 */
public class CusComboBox extends JComboBox {
    /** 显示的字段 */
    private Function field;
    /** null项的显示文本 */
    private String nullText;
    /** 是否保留null项 */
    private boolean keepNull = false;
    /** 数据集 */
    private Collection dataList;
    /** 是否正在输入(中文输入法组合状态) */
    private static boolean isComposing = false;
    /** 是否正在搜索,用于抑制输入筛选前的选中事件 */
    private boolean suppressActionEvents = false;
    /** 键盘导航标志 */
    private boolean keyboardNa vigating = false;
    /** 待确认的选择项 */
    private T pendingSelection = null;
    /** 输入无匹配项时的回调 */
    private Consumer onNoMatchCallback;

    public CusComboBox() {
        super();
    }

    /**
     * 基本数据类型包装类和String的便利构造方法
     * @param items 数据源
     */
    @SuppressWarnings("unchecked")
    public CusComboBox(Collection items) {
        this.field = Object::toString;
        this.keepNull = false;
        this.dataList = (Collection) items;
        initRenderer();
        addAllItems(this.dataList);
    }

    /**
     * 基本数据类型包装类和String的便利构造方法(包含null项)
     * @param items 数据源
     * @param nullText null项的显示文本
     */
    @SuppressWarnings("unchecked")
    public CusComboBox(Collection items, String nullText) {
        this.field = Object::toString;
        this.nullText = nullText;
        this.keepNull = true;
        this.dataList = (Collection) items;
        initRenderer();
        addAllItems(this.dataList);
    }

    /**
     * 泛型构造方法
     * @param items 数据源
     * @param field 显示字段提取函数
     */
    public CusComboBox(Collection items, Function field) {
        super();
        this.field = field;
        this.keepNull = false;
        this.dataList = items;
        initRenderer();
        addAllItems(items);
    }

    /**
     * 泛型构造方法(包含null项)
     * @param items 数据源
     * @param field 显示字段提取函数
     * @param nullText null项的显示文本
     */
    public CusComboBox(Collection items, Function field, String nullText) {
        super();
        this.field = field;
        this.nullText = nullText;
        this.keepNull = true;
        this.dataList = items;
        initRenderer();
        addAllItems(items);
    }

    /**
     * 初始化渲染器
     */
    @SuppressWarnings("unchecked")
    private void initRenderer() {
        setRenderer(new DefaultListCellRenderer() {
            @Override
            public Component getListCellRendererComponent(JList list, Object value, int index,
                                                          boolean isSelected, boolean cellHasFocus) {
                super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
                setText(value == null ? nullText : field.apply((T) value));
                return this;
            }
        });
    }

    /**
     * 设置显示字段
     * @param field 显示字段提取函数
     */
    public void setDisplayField(Function field) {
        this.field = field;
        this.updateUI();
    }

    /**
     * 添加所有数据项
     * @param items 数据源
     */
    private void addAllItems(Collection items) {
        if (keepNull) {
            addItem(null);
        }
        items.forEach(this::addItem);
    }

    /**
     * 移除所有项(保护null项)
     */
    @Override
    public void removeAllItems() {
        if (keepNull) {
            super.removeAllItems();
            addItem(null);
        } else {
            super.removeAllItems();
        }
    }

    /**
     * 移除指定索引的项(保护null项)
     * @param index 索引
     */
    @Override
    public void removeItemAt(int index) {
        if (keepNull && index == 0) {
            return;
        }
        super.removeItemAt(index);
    }

    /**
     * 设置是否可搜索
     * @param editable true=可搜索,false=不可搜索
     */
    @Override
    public void setEditable(boolean editable) {
        super.setEditable(editable);
        if (editable) {
            initEditor();
            handleTextChange();
        }
    }

    /**
     * 初始化可搜索时的编辑器
     */
    private void initEditor() {
        JTextField searchField = new JTextField();
        JTextField originalField = (JTextField) getEditor().getEditorComponent();
        searchField.setBorder(originalField.getBorder());
        searchField.setBackground(originalField.getBackground());
        searchField.setForeground(originalField.getForeground());
        searchField.setFont(originalField.getFont());
        searchField.setPreferredSize(originalField.getPreferredSize());

        setEditor(new BasicComboBoxEditor() {
            @Override
            public void setItem(Object item) {
                if (item == null) {
                    searchField.setText("");
                } else {
                    @SuppressWarnings("unchecked")
                    T typedItem = (T) item;
                    String text = field.apply(typedItem);
                    searchField.setText(text);
                    searchField.setCaretPosition(text.length());
                }
            }

            @Override
            public Object getItem() {
                return getSelectedItem();
            }

            @Override
            public Component getEditorComponent() {
                return searchField;
            }
        });

        // 下拉面板宽度控制
        addPopupMenuListener(new PopupMenuListener() {
            @Override
            public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
                JPopupMenu popup = (JPopupMenu) getUI().getAccessibleChild(CusComboBox.this, 0);
                if (null != popup) {
                    int dataHeight = 60;
                    if (CollectionUtil.isNotEmpty(dataList)) {
                        int height = Math.min(dataList.size() * 25, 380);
                        dataHeight = Math.max(dataHeight, height);
                    }
                    popup.setPreferredSize(new Dimension(getWidth(), dataHeight));
                }
            }

            @Override
            public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {}

            @Override
            public void popupMenuCanceled(PopupMenuEvent e) {}
        });

        setupKeyboardNa vigation(searchField);
    }

    /**
     * 设置键盘导航
     * @param searchField 搜索输入框
     */
    private void setupKeyboardNa vigation(JTextField searchField) {
        searchField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                    // 回车确认选择
                    if (keyboardNa vigating && pendingSelection != null) {
                        suppressActionEvents = false;
                        setSelectedItem(pendingSelection);
                        searchField.setText(field.apply(pendingSelection));
                        keyboardNa vigating = false;
                        pendingSelection = null;
                        setPopupVisible(false);
                        e.consume();
                    }
                } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
                    // 上下键开始键盘导航
                    if (!keyboardNa vigating) {
                        keyboardNa vigating = true;
                        pendingSelection = getItemAt(0);
                        suppressActionEvents = true;
                    }
                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    // ESC取消导航
                    keyboardNa vigating = false;
                    pendingSelection = null;
                    suppressActionEvents = false;
                    setPopupVisible(false);
                    e.consume();
                }
            }
        });

        addItemListener(e -> {
            if (keyboardNa vigating && e.getStateChange() == ItemEvent.SELECTED) {
                @SuppressWarnings("unchecked")
                T selected = (T) e.getItem();
                pendingSelection = selected;
            }
        });
    }

    /**
     * 重写fireActionEvent,在搜索时抑制事件
     */
    @Override
    protected void fireActionEvent() {
        if (!suppressActionEvents) {
            super.fireActionEvent();
        }
    }

    /**
     * 处理输入框文本变化(搜索过滤)
     */
    private void handleTextChange() {
        JTextField textField = (JTextField) getEditor().getEditorComponent();

        // 处理中文输入法组合状态
        textField.addInputMethodListener(new InputMethodListener() {
            @Override
            public void inputMethodTextChanged(InputMethodEvent event) {
                AttributedCharacterIterator text = event.getText();
                isComposing = text != null && text.getEndIndex() - text.getBeginIndex() > 0;
            }

            @Override
            public void caretPositionChanged(InputMethodEvent event) {}
        });

        textField.addKeyListener(new KeyAdapter() {
            private Timer searchTimer;

            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_SPACE) {
                    isComposing = false;
                }
            }

            @Override
            public void keyReleased(KeyEvent e) {
                if (isComposing) return;

                // 导航键不触发搜索
                if (e.getKeyCode() == KeyEvent.VK_UP ||
                    e.getKeyCode() == KeyEvent.VK_DOWN ||
                    e.getKeyCode() == KeyEvent.VK_ENTER ||
                    e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    return;
                }

                if (null != searchTimer) {
                    searchTimer.stop();
                }

                searchTimer = new Timer(200, evt -> {
                    suppressActionEvents = true;
                    keyboardNa vigating = false;
                    pendingSelection = null;

                    String text = textField.getText();
                    Object previouslySelectedItem = getSelectedItem();
                    AtomicBoolean hasMatches = new AtomicBoolean(false);

                    removeAllItems();
                    dataList.forEach(item -> {
                        String fieldValue = field.apply(item);
                        if (fieldValue.toLowerCase().contains(text.toLowerCase())) {
                            addItem(item);
                            hasMatches.set(true);
                        }
                    });

                    if (!hasMatches.get() && !text.isEmpty()) {
                        CallbackProcessor.accept(onNoMatchCallback, text);
                    }

                    setPopupVisible(hasMatches.get() && !text.isEmpty());
                    setSelectedItem(previouslySelectedItem);
                    textField.setText(text);
                    suppressActionEvents = false;
                });
                searchTimer.setRepeats(false);
                searchTimer.start();
            }
        });
    }

    /**
     * 获取输入框的文本
     * @return 输入框文本
     */
    public String getEditorText() {
        if (isEditable() && null != getEditor()) {
            Component editorComponent = getEditor().getEditorComponent();
            if (editorComponent instanceof JTextField) {
                return ((JTextField) editorComponent).getText();
            }
        }
        return null;
    }

    /**
     * 设置无匹配结果的回调
     * @param callback 回调函数,参数为输入的关键字
     */
    public void setOnNoMatch(Consumer callback) {
        this.onNoMatchCallback = callback;
    }

    /**
     * 静态工厂方法
     */
    public static  CusComboBox create(Collection items) {
        return new CusComboBox<>(items);
    }

    public static  CusComboBox create(Collection items, String nullText) {
        return new CusComboBox<>(items, nullText);
    }

    public static  CusComboBox create(Collection items, Function field) {
        return new CusComboBox<>(items, field);
    }

    public static  CusComboBox create(Collection items, Function field, String nullText) {
        return new CusComboBox<>(items, field, nullText);
    }
}

四、核心功能说明

可搜索过滤:

  • 输入关键字时自动过滤下拉选项,大小写不敏感;
  • 200ms延迟搜索,避免频繁重绘;
  • 通过InputMethodListener处理中文输入法的组合状态,不会在打拼音时乱跳。

键盘导航:

  • /:开始键盘导航模式,移动高亮项;
  • Enter:确认当前高亮的选择;
  • ESC:取消导航并关闭下拉面板。

数据源适配:

  • 支持Collection等基本类型,开箱即用;
  • 也支持任意泛型对象,通过Function指定显示的字段;
  • 可以保留null项,并用占位文本显示(比如“请选择”)。

其他特性:

  • 无匹配结果时可以通过回调做额外操作;
  • 下拉面板宽度自适应内容;
  • 搜索过程中会临时抑制ActionEvent事件,避免误触发。

五、使用示例

5.1 基本类型数据

List fruits = Arrays.asList("苹果", "香蕉", "橙子", "葡萄", "西瓜");
CusComboBox comboBox = new CusComboBox<>(fruits);
comboBox.setEditable(true);  // 开启可搜索
panel.add(comboBox);

5.2 对象数据,指定显示字段

List userList = getUserList();
CusComboBox comboBox = new CusComboBox<>(userList, User::getName);
comboBox.setEditable(true);
panel.add(comboBox);

5.3 包含 null 项

CusComboBox comboBox = new CusComboBox<>(list, "请选择");
comboBox.setEditable(true);

5.4 设置无匹配回调

comboBox.setOnNoMatch(keyword -> {
    System.out.println("未找到匹配项:" + keyword);
    // 可弹窗提示或动态添加选项
});

5.5 获取选中值和输入文本

// 获取选中对象
User selected = comboBox.getSelectedItem();
// 获取输入框文本
String text = comboBox.getEditorText();

5.6 静态工厂方法

CusComboBox comboBox = CusComboBox.create(list, "请选择");
comboBox.setEditable(true);

六、注意事项

  1. 必须调用 setEditable(true):只有开启了可编辑模式,搜索功能才会生效,这一点很容易忽略。
  2. 中文输入法:代码已经通过InputMethodListener做了处理,输入中文时不会触发搜索,可以放心使用。
  3. 数据源引用:搜索过滤时会重新构建下拉列表,但原始的dataList会被保留作为数据源,不会丢失。
  4. null项保护:当keepNull = true时,removeAllItems()removeItemAt(0)都会被保护,不会误删。
  5. ActionEvent 抑制:搜索过程中会临时抑制ActionEvent,避免选中的回调被反复触发,算是一个细节上的打磨。

七、小结

CusComboBox在原生JComboBox基础上增加了可搜索能力,回顾一下核心实现要点:

  • 通过setEditable(true)开启可编辑模式;
  • 自定义BasicComboBoxEditor,用JTextField接管输入;
  • 监听输入框文本变化,动态过滤下拉选项;
  • 加入200ms延迟搜索和中文输入法处理;
  • 实现键盘导航(上下键、回车、ESC)。

如果正好在写Ja va桌面应用,需要给用户一个顺手又高效的下拉搜索体验,这个组件可以直接拿过去用,省去自己从头踩坑的时间。

本文转载于:https://www.jb51.net/program/3649861mu.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注