在Swing开发中,原生JComboBox存在无法搜索过滤的痛点。本文将详细介绍如何通过CusComboBox组件实现智能搜索下拉框功能。
虽然Swing提供的JComboBox支持下拉选择功能,但存在两个主要缺陷:

CusComboBox组件在JComboBox基础上新增了搜索功能,支持关键字过滤、键盘导航操作,并提供了无匹配结果时的回调处理机制。
该组件继承自JComboBox,通过setEditable(true)启用编辑模式后实现以下功能:
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 Collection<? extends 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<? 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) {}
}); 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<? 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);
}
}
可搜索过滤:
键盘导航:
数据源适配:
其他特性:
List fruits = Arrays.asList("苹果", "香蕉", "橙子", "葡萄", "西瓜");
CusComboBox comboBox = new CusComboBox<>(fruits);
comboBox.setEditable(true);
panel.add(comboBox);
List userList = getUserList();
CusComboBox comboBox = new CusComboBox<>(userList, User::getName);
comboBox.setEditable(true);
panel.add(comboBox);
CusComboBox comboBox = new CusComboBox<>(list, "请选择");
comboBox.setEditable(true);
comboBox.setOnNoMatch(keyword -> {
System.out.println("未找到匹配项:" + keyword);
});
User selected = comboBox.getSelectedItem();
String text = comboBox.getEditorText();
CusComboBox comboBox = CusComboBox.create(list, "请选择");
comboBox.setEditable(true);
CusComboBox通过自定义编辑器、动态过滤和键盘导航等机制,有效解决了原生JComboBox的搜索痛点,为Swing应用提供了更智能的下拉选择体验。