发布于2026-06-30 阅读(0)
扫一扫,手机访问
在实际的Ja va桌面应用开发中,Swing自带的JComboBox虽然能完成基本的下拉选择,但用起来总有两个让人头疼的地方:

而CusComboBox要解决的就是这个问题——在原生JComboBox的基础上,给它加上搜索过滤的能力。用户输入关键字时,下拉列表自动匹配并过滤;同时支持键盘上下键选择、回车确认、ESC取消这些快捷键操作;如果输入的内容完全没匹配到,还能通过回调做额外处理。这样一来,既保留了原生的下拉交互,又弥补了搜索上的缺口。
这个组件的实现思路并不复杂——CusComboBox直接继承JComboBox,然后通过setEditable(true)开启可编辑模式。在此基础上,做了几件事:
BasicComboBoxEditor),用JTextField来接管输入框;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. 基本类型数据: * Listlist = 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 extends T> 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 extends T> 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 extends T> 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 extends T> 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 extends T> items, Function field) { return new CusComboBox<>(items, field); } public static CusComboBox create(Collection extends T> items, Function field, String nullText) { return new CusComboBox<>(items, field, nullText); } }
可搜索过滤:
InputMethodListener处理中文输入法的组合状态,不会在打拼音时乱跳。键盘导航:
↑/↓:开始键盘导航模式,移动高亮项;Enter:确认当前高亮的选择;ESC:取消导航并关闭下拉面板。数据源适配:
Collection等基本类型,开箱即用;Function指定显示的字段;null项,并用占位文本显示(比如“请选择”)。其他特性:
ActionEvent事件,避免误触发。Listfruits = Arrays.asList("苹果", "香蕉", "橙子", "葡萄", "西瓜"); CusComboBox comboBox = new CusComboBox<>(fruits); comboBox.setEditable(true); // 开启可搜索 panel.add(comboBox);
ListuserList = getUserList(); CusComboBox comboBox = new CusComboBox<>(userList, User::getName); comboBox.setEditable(true); panel.add(comboBox);
CusComboBoxcomboBox = new CusComboBox<>(list, "请选择"); comboBox.setEditable(true);
comboBox.setOnNoMatch(keyword -> {
System.out.println("未找到匹配项:" + keyword);
// 可弹窗提示或动态添加选项
});
// 获取选中对象 User selected = comboBox.getSelectedItem(); // 获取输入框文本 String text = comboBox.getEditorText();
CusComboBoxcomboBox = CusComboBox.create(list, "请选择"); comboBox.setEditable(true);
InputMethodListener做了处理,输入中文时不会触发搜索,可以放心使用。dataList会被保留作为数据源,不会丢失。keepNull = true时,removeAllItems()和removeItemAt(0)都会被保护,不会误删。ActionEvent,避免选中的回调被反复触发,算是一个细节上的打磨。CusComboBox在原生JComboBox基础上增加了可搜索能力,回顾一下核心实现要点:
setEditable(true)开启可编辑模式;BasicComboBoxEditor,用JTextField接管输入;如果正好在写Ja va桌面应用,需要给用户一个顺手又高效的下拉搜索体验,这个组件可以直接拿过去用,省去自己从头踩坑的时间。
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8