Мне нужно реализовать функцию поиска JComboBox
: которая фильтрует отображаемые элементы по мере ввода.
Вдохновленный этим ответом, я написал эту реализацию.
package di;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Vector;
public class ComboBoxes {
private ComboBoxes() {
}
public static <T> JComboBox<T> searchableComboBox() {
return searchableComboBox(new ArrayList<>());
}
public static <T> JComboBox<T> searchableComboBox(Collection<T> items) {
ComboBoxModel<T> model = new DefaultComboBoxModel<>(new Vector<>(items));
JComboBox<T> comboBox = new JComboBox<>(model);
comboBox.setEditable(true);
ComboBoxEditor comboBoxEditor = comboBox.getEditor();
JTextField editorTextField = (JTextField) comboBoxEditor.getEditorComponent();
KeyAdapter searchableComboBoxKeyListener = createSearchableComboBoxKeyListener(comboBox, model);
editorTextField.addKeyListener(searchableComboBoxKeyListener);
return comboBox;
}
private static <T> KeyAdapter createSearchableComboBoxKeyListener(JComboBox<T> comboBox, ComboBoxModel<T> initialModel) {
return new KeyAdapter() {
@Override
public void keyReleased(KeyEvent e) {
JTextField editorTextField = (JTextField) comboBox.getEditor().getEditorComponent();
String enteredText = editorTextField.getText();
ArrayList<T> matchingElements = findMatchingElements(enteredText);
DefaultComboBoxModel<T> modelWithOnlyMatchingElements = new DefaultComboBoxModel<>(new Vector<>(matchingElements));
comboBox.setModel(modelWithOnlyMatchingElements);
comboBox.setSelectedItem(enteredText);
comboBox.showPopup();
}
private ArrayList<T> findMatchingElements(String enteredText) {
ArrayList<T> matchingElements = new ArrayList<>();
for (int i = 0; i < initialModel.getSize(); i++) {
T modelElement = initialModel.getElementAt(i);
if (StringUtils.containsIgnoreCase(modelElement.toString(), enteredText))
matchingElements.add(modelElement);
}
return matchingElements;
}
};
}
}
Он отлично работает со строками. С пользовательскими объектами все работает нормально, но есть небольшая проблема.
Ключевой прослушиватель устанавливает введенную строку как новый выбранный элемент. Это может сойти с рук, поскольку setSelectedItem()
принимает Object
, хотя JComboBox
является параметризованным типом (поскольку дженерики были введены намного позже, чем Swing, JComboBox
на самом деле не является универсальным типом)
Поэтому, если вы прослушиваете новые выборки и ожидаете, что выбранные элементы будут иметь аргумент типа JComboBox
, вы получите ClassCastException
, поскольку строки не могут быть приведены к этому типу. Вот демо
package demos.combo;
import di.ComboBoxes;
import javax.swing.*;
import java.awt.event.ItemEvent;
import java.util.ArrayList;
public class SearchableComboBoxDemo {
public static void main(String[] args) {
JFrame frame = new JFrame("Searchable Combo Box Demo");
JPanel mainPanel = new JPanel();
JComboBox<Plant> searchableCombo = ComboBoxes.searchableComboBox();
searchableCombo.addItemListener(SearchableComboBoxDemo::onItemSelection);
comboBoxItems().forEach(searchableCombo::addItem);
mainPanel.add(searchableCombo);
frame.setContentPane(mainPanel);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private static void onItemSelection(ItemEvent event) {
if (event.getStateChange() != ItemEvent.SELECTED) return;
Plant selectedItem = (Plant) event.getItem(); // throws
System.out.printf("Selected item: " + selectedItem + "\n");
}
private static ArrayList<Plant> comboBoxItems() {
ArrayList<Plant> items = new ArrayList<>();
items.add(new Plant("Potato"));
items.add(new Plant("Peach"));
items.add(new Plant("Banana"));
items.add(new Plant("Orange"));
items.add(new Plant("Carrot"));
items.add(new Plant("Cabbage"));
return items;
}
}
package demos.combo;
public class Plant {
private final String name;
public Plant(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
Звонок setSelectedItem()
важен. Если вы удалите его, текстовое поле восстановит допустимое значение после того, как вы попытаетесь удалить символ. Это побочный эффект установки новой модели
Можно было бы предположить, что установка текста текстового поля напрямую исправит это.
comboBox.setModel(modelWithOnlyMatchingElements);
editorTextField.setText(enteredText); // like this
comboBox.showPopup();
но тогда JComboBox
не срабатывает ItemEvent
, как ожидалось. Попробуйте ввести «о», затем нажать «Оранжевый», затем выделить и удалить весь текст, затем выбрать «Картошка». Вы не получите событие «Картошка выбрана». Это потому, что когда поле очищается и устанавливается новая модель, выбранный элемент новой модели (в данном случае первый элемент) автоматически назначается полю JComboBox
selectedItemReminder
.
/*
the invoked DefaultComboBoxModel constructor automatically selects the first element,
there's nothing we can do about it
*/
public DefaultComboBoxModel(Vector<E> v) {
objects = v;
if ( getSize() > 0 ) {
selectedObject = getElementAt( 0 );
}
}
Если выбор пользователя соответствует этому (а это так), то IventItem
никогда не запускается (см. javax.swing.JComboBox#setSelectedItem
).
Итак, вкратце, как мне реализовать возможность поиска JComboBox
:
ItemListener
может слушать?
Один из обходных путей — установить selectedItem
на null
.
comboBox.setModel(modelWithOnlyMatchingElements);
comboBox.setSelectedItem(null);
editorTextField.setText(enteredText);
comboBox.showPopup();