一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

Java Swing自定义组件库系列分享第十期

时间:2026-06-02 13:50:01 编辑:袖梨 来源:一聚教程网

在Swing开发中,原生JComboBox存在无法搜索过滤的痛点。本文将详细介绍如何通过CusComboBox组件实现智能搜索下拉框功能。

可搜索下拉框 — CusComboBox

一、背景

虽然Swing提供的JComboBox支持下拉选择功能,但存在两个主要缺陷:

Java Swing 自定义组件库分享(十)

  1. 用户无法通过输入内容筛选选项
  2. 数据量大时需要手动滚动查找,操作不便

CusComboBox组件在JComboBox基础上新增了搜索功能,支持关键字过滤、键盘导航操作,并提供了无匹配结果时的回调处理机制。

二、核心设计

该组件继承自JComboBox,通过setEditable(true)启用编辑模式后实现以下功能:

  1. 使用JTextField作为自定义编辑器
  2. 实时输入内容变化并动态过滤选项
  3. 支持上下键导航、回车确认和ESC取消操作
  4. 兼容中文输入法的组合状态处理
  5. 200ms延迟搜索机制避免频繁刷新

三、类源码

import cn.hutool.core.collection.CollectionUtil;import javax.swing.*;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;
import javax.swing.plaf.basic.BasicComboBoxEditor;
import java.awt.*;
import java.awt.event.*;
import java.text.AttributedCharacterIterator;
import java.util.Collection;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
import java.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 Collectionextends T> dataList;
    /** 是否正在输入(中文输入法组合状态) */
    private static boolean isComposing = false;
    /** 是否正在搜索,用于抑制输入筛选前的选中事件 */
    private boolean suppressActionEvents = false;
    /** 键盘导航标志 */
    private boolean keyboardNavigating = 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) {}
        });        setupKeyboardNavigation(searchField);
    }    /**
     * 设置键盘导航
     * @param searchField 搜索输入框
     */
    private void setupKeyboardNavigation(JTextField searchField) {
        searchField.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.getKeyCode() == KeyEvent.VK_ENTER) {
                    // 回车确认选择
                    if (keyboardNavigating && pendingSelection != null) {
                        suppressActionEvents = false;
                        setSelectedItem(pendingSelection);
                        searchField.setText(field.apply(pendingSelection));
                        keyboardNavigating = false;
                        pendingSelection = null;
                        setPopupVisible(false);
                        e.consume();
                    }
                } else if (e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
                    // 上下键开始键盘导航
                    if (!keyboardNavigating) {
                        keyboardNavigating = true;
                        pendingSelection = getItemAt(0);
                        suppressActionEvents = true;
                    }
                } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
                    // ESC取消导航
                    keyboardNavigating = false;
                    pendingSelection = null;
                    suppressActionEvents = false;
                    setPopupVisible(false);
                    e.consume();
                }
            }
        });        addItemListener(e -> {
            if (keyboardNavigating && 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;
                    keyboardNavigating = 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);
    }
}

四、核心功能说明

可搜索过滤:

  1. 支持关键字自动过滤选项(不区分大小写)
  2. 200ms延迟搜索机制避免性能损耗
  3. 通过InputMethodListener处理中文输入法状态

键盘导航:

  1. 上下箭头键:切换选中项
  2. 回车键:确认当前选择
  3. ESC键:取消操作并关闭下拉面板

数据源适配:

  1. 兼容基本数据类型集合
  2. 支持泛型对象通过函数提取显示字段
  3. 可配置保留null项及其占位文本

其他特性:

  1. 无匹配结果回调处理
  2. 下拉面板宽度智能调整
  3. 搜索过程中抑制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. 中文输入法兼容:已通过器处理输入法组合状态
  3. 数据源保留:原始dataList会作为永久数据源
  4. null项保护:配置keepNull后相关删除操作会被保护
  5. 事件抑制:搜索过程中会临时阻止ActionEvent触发

七、小结

CusComboBox通过自定义编辑器、动态过滤和键盘导航等机制,有效解决了原生JComboBox的搜索痛点,为Swing应用提供了更智能的下拉选择体验。

热门栏目