展开菜单
首页 精品内容 本月促销 装机必备 Windows macOS软件 IOS软件 Android AI PDF教程 专题
全部分类

当前位置:

首页 > 编程开发 > JavaSwing实现自定义可搜索下拉框(附完整代码)

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

一种基于JavaSwing的可搜索下拉框CusComboBox,通过继承JComboBox并启用可编辑模式,实现输入关键字动态过滤选项、键盘导航(上下键、回车、ESC)及无匹配回调,同时加入200ms防抖延迟和中文输入状态处理,优化搜索性能与用户体验。

一、背景

在实际的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桌面应用,需要给用户一个顺手又高效的下拉搜索体验,这个组件可以直接拿过去用,省去自己从头踩坑的时间。

本文内容来源于互联网,如有侵权请联系删除。
作者最新文章
编程开发
相关文章 更多
精品专题 更多
本月促销

正软商城本月促销专区,汇集办公、设计、安全、影音、系统工具及AI软件等正版软件优惠活动,提供限时折扣、特价授权和优惠购买信息,活动库存及价格以页面实时展示为准。

装机必备

正软商城装机必备专区,精选办公、浏览器、安全防护、影音播放、压缩解压、设计创作和系统工具等电脑常用正版软件,帮助用户快速完成新电脑软件配置。

Windows

正软商城Windows软件专区,汇集适用于Windows电脑的办公、设计、安全防护、影音播放、开发工具和系统优化软件,提供软件介绍、系统要求、正版授权及购买下载服务。

macOS软件

正软商城macOS软件专区,精选适用于Mac电脑的办公、设计、影音、效率、开发和系统工具,提供软件功能介绍、macOS兼容版本、正版授权及购买下载服务。

IOS软件

正软商城iOS软件专区,精选适用于iPhone和iPad的办公、学习、影音、设计、效率及AI应用,提供功能介绍、适用设备、系统要求和正版获取方式等信息。

AI

正软商城AI软件专区,汇集AI写作、AI绘画、AI视频、AI办公、AI编程、AI翻译、智能客服和数据分析等人工智能工具,提供功能介绍、适用平台、收费方式及正版购买信息。

PDF教程

正软商城PDF教程频道提供PDF编辑、转换、合并、拆分、压缩及格式处理方法,同时介绍常用PDF软件和工具的使用技巧。

Mac软件 更多
灵活计算器
灵活计算器

灵活计算器是一款笔记式算数应用,支持实时计算、动态关联和云端同步功能。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

赤友清理大师
赤友清理大师

赤友清理大师是一款为 Mac 设计的智能清理优化工具,可精准扫描垃圾、大文件、重复文件等,释放磁盘空间。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

图几
图几

图几是一款适用于 macOS 的截图、标注与美化工具,支持离线操作保障隐私。界面整理和高频系统操作被放到一起考虑,桌面或窗口内容一多时,管理起来会更省心。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

Mountain Duck
Mountain Duck

Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

Menuist
Menuist

Menuist 是一款面向 macOS 的 Finder 右键菜单增强工具,主要用来补充新建文件、快捷导航等常用操作,让日常文件管理和访问路径时更高效、更顺手。

Mole
Mole

Mole 是一款专为 Mac 设计的深度清理优化工具,涵盖缓存清理、应用管理及实时状态监控等功能。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

WINDOWS 更多
Windows 10
Windows 10

Windows 10 是一款微软推出的经典操作系统,拥有硬件兼容性与多任务处理能力。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

极度公式
极度公式

极度公式是一款跨平台专业LaTeX公式识别编辑软件,支持OCR公式识别和多平台编辑。和使用说明,避免使用,享受完整功能与稳定支持。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

密码键盘
密码键盘

密码键盘是一款兼具安全性与便捷性的高效密码管理器。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

思源笔记
思源笔记

思源笔记是一款本地笔记软件,提供所见即所得的编辑方式,为长文写作带来顺滑的体验。记录、整理和输出之间的过渡会更自然,适合长期写作、做笔记或持续沉淀个人内容。

傲梅轻松备份
傲梅轻松备份

傲梅轻松备份是一款专业易用的数据备份软件,为重要数据提供安全保障。日常使用里的持续防护和信息管理会更突出,适合把安全控制放进长期使用流程中的场景。

Office 365 简体中文
Office 365 简体中文

一款文字处理软件,一种订阅式的跨平台办公软件,基于云平台提供多种服务,通过将 Excel 和 Outlook 等应用与 OneDrive 和 Microsoft Teams 等强大的云服务相结合,Office 365 可让任何人使用任何设备随时随地创建和共享内容。

Wise Folder Hider Pro
Wise Folder Hider Pro

Wise Folder Hider Pro 是一款专业级文件和文件夹隐藏加密软件,为私密数据添加多重保护。高频操作更强调就近处理,浏览、整理和跨目录移动文件时,来回切换和重复点击都会少很多。

WALTR PRO
WALTR PRO

WALTR是一款电脑至iOS文件传输转换工具,操作简单,快速实现文件识别与传送。做扫描整理、文字提取和表格转换时,它能把识别后的处理步骤接得更顺,资料录入这类场景会省下不少时间。

CodeExpander
CodeExpander

CodeExpander 是一款快捷短语输入增强工具,通过键入缩写自动展开为自定义文段,提升工作效率。任务管理和过程控制会更完整,持续下载、批量同步或需要稳定传输流程的场景会更适合它。

PinStack
PinStack

PinStack是一款轻量级的Windows平台剪贴板管理工具,优化您的剪贴板使用体验。它更偏向把系统状态查看和常用调节动作放在一起,适合需要持续观察和微调设备状态的场景。

Mountain Duck
Mountain Duck

Mountain Duck 是一款能将多个网盘挂载到本地的工具,像本地磁盘一样使用网盘。清理链路的完整性会更好一些,做应用卸载、残留处理和空间整理时,通常能少走很多手动排查步骤。

Seer
Seer

Seer是一款在Win平台下的空格键功能增强效率工具,只需轻敲空格键,就能预览几乎任何格式的文件。它更适合把零散的小功能集中起来使用,处理高频琐碎任务时会更省事。