子组件隐藏后触发父组件尺寸重计算的JComboBox问题
问题描述
自定义JComboBox实现了搜索功能:当选项数量超过最大显示行数时显示搜索文本框,否则隐藏搜索框。功能正常,但首次点击下拉框时底部会出现多余空白——这是因为垂直滚动条隐藏后,弹窗尺寸未及时更新;再次点击则尺寸恢复正常。尝试在componentHidden()中调用popupMenu.getPreferredSize(),并配合revalidate()、repaint()、doLayout()均无效。
解决方案
问题根源在于弹窗显示时,滚动条的可见性判断滞后,导致搜索框的显示状态调整晚于弹窗布局,从而出现空白。以下两种方式可解决该问题:
方式一:提前判断滚动条可见性,弹窗显示前调整搜索框状态
修改SearchableComboPopup中的show()方法,先让滚动条完成布局,提前确定其可见性,再设置搜索框的显示状态,最后显示弹窗:
@Override public void show() { comboBox.firePopupMenuWillBecomeVisible(); int itemListVisibleRowCount = Math.min(comboBox.getItemCount(), comboBox.getMaximumRowCount()); itemList.setVisibleRowCount(itemListVisibleRowCount); // 先让滚动条完成布局,准确获取可见性 scrollPane.setSize(comboBox.getWidth(), itemList.getPreferredSize().height); scrollPane.doLayout(); boolean scrollBarVisible = scrollPane.getVerticalScrollBar().isVisible(); searchBox.setVisible(scrollBarVisible); popupMenu.show(comboBox, 0, comboBox.getHeight()); searchTextField.requestFocusInWindow(); }
方式二:监听滚动条隐藏事件,触发弹窗重新布局
在创建滚动条时添加ComponentListener,当滚动条隐藏时,强制弹窗重新计算大小并更新位置:
- 修改
createScrollPane()方法,添加滚动条监听器:
private JScrollPane createScrollPane() { JScrollPane scrollPane = new JScrollPane(itemList); // 添加滚动条隐藏监听器,触发弹窗重新布局 scrollPane.getVerticalScrollBar().addComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { SwingUtilities.invokeLater(() -> { popupMenu.pack(); // 重新定位弹窗到ComboBox下方 popupMenu.setLocation(comboBox.getLocationOnScreen().x, comboBox.getLocationOnScreen().y + comboBox.getHeight()); }); } }); return scrollPane; }
- 保留原
show()方法中的搜索框可见性判断,确保初始状态正确:
@Override public void show() { comboBox.firePopupMenuWillBecomeVisible(); int itemListVisibleRowCount = Math.min(comboBox.getItemCount(), comboBox.getMaximumRowCount()); itemList.setVisibleRowCount(itemListVisibleRowCount); popupMenu.show(comboBox, 0, comboBox.getHeight()); searchBox.setVisible(scrollPane.getVerticalScrollBar().isVisible()); searchTextField.requestFocusInWindow(); }
修改后的完整代码
SearchableComboPopup.java
package demos.combo; import di.Actions; import org.apache.commons.lang3.StringUtils; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.plaf.basic.ComboPopup; import java.awt.*; import java.awt.event.*; import java.util.ArrayList; public class SearchableComboPopup<T> implements ComboPopup { private final JComboBox<T> comboBox; private final JPopupMenu popupMenu; private final JTextField searchTextField; private final Box searchBox; private final JList<T> itemList; private final JScrollPane scrollPane; public SearchableComboPopup(JComboBox<T> comboBox) { this.comboBox = comboBox; this.searchTextField = createSearchTextField(); this.searchBox = createSearchBox(); this.itemList = createList(comboBox.getModel()); this.scrollPane = createScrollPane(); this.popupMenu = createPopupMenu(); } private JTextField createSearchTextField() { JTextField textField = new JTextField(); textField.addKeyListener(createSearchFieldKeyListener()); textField.getDocument().addDocumentListener(createSearchTextFieldDocumentListener()); return textField; } private KeyListener createSearchFieldKeyListener() { return new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_DOWN) { ensureSelection(); itemList.requestFocusInWindow(); } } private void ensureSelection() { if (itemList.isSelectionEmpty()) itemList.setSelectedIndex(0); } }; } private DocumentListener createSearchTextFieldDocumentListener() { return new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { updateModel(); } @Override public void removeUpdate(DocumentEvent e) { updateModel(); } private void updateModel() { String enteredText = searchTextField.getText(); ArrayList<T> matchingElements = findMatchingElements(enteredText); DefaultListModel<T> modelWithOnlyMatchingElements = new DefaultListModel<>(); matchingElements.forEach(modelWithOnlyMatchingElements::addElement); itemList.setModel(modelWithOnlyMatchingElements); } private ArrayList<T> findMatchingElements(String enteredText) { ArrayList<T> matchingElements = new ArrayList<>(); ComboBoxModel<T> comboModel = comboBox.getModel(); for (int i = 0; i < comboModel.getSize(); i++) { T modelElement = comboModel.getElementAt(i); if (StringUtils.containsIgnoreCase(modelElement.toString(), enteredText)) matchingElements.add(modelElement); } return matchingElements; } @Override public void changedUpdate(DocumentEvent e) { } }; } private Box createSearchBox() { Box searchBox = new Box(BoxLayout.Y_AXIS); searchBox.add(searchTextField); searchBox.add(Box.createRigidArea(new Dimension(0, 3))); return searchBox; } private JList<T> createList(ListModel<T> searchListModel) { JList<T> list = new JList<>(searchListModel); list.addMouseListener(createListMouseListener()); list.addKeyListener(createListKeyListener()); configureInputMap(list); return list; } private MouseListener createListMouseListener() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { selectAndHide(); } }; } private void selectAndHide() { T selectedValue = itemList.getSelectedValue(); if (selectedValue == null) return; comboBox.setSelectedItem(selectedValue); hide(); } private KeyListener createListKeyListener() { return new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { switchFocusToTextFieldIfNecessary(e); submitSelectionIfNecessary(e); } private void switchFocusToTextFieldIfNecessary(KeyEvent e) { boolean isUpmostItemSelected = itemList.getSelectedIndex() == 0; if (e.getKeyCode() == KeyEvent.VK_UP && isUpmostItemSelected) searchTextField.requestFocusInWindow(); } private void submitSelectionIfNecessary(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) selectAndHide(); } }; } private void configureInputMap(JList<T> list) { String switchFocusToTextFieldKey = "switchFocusToTextField"; list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, KeyEvent.CTRL_DOWN_MASK), switchFocusToTextFieldKey); list.getActionMap().put(switchFocusToTextFieldKey, Actions.requestFocusInWindow(searchTextField)); } private JScrollPane createScrollPane() { JScrollPane scrollPane = new JScrollPane(itemList); // 添加滚动条隐藏监听器,触发弹窗重新布局 scrollPane.getVerticalScrollBar().addComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { SwingUtilities.invokeLater(() -> { popupMenu.pack(); popupMenu.setLocation(comboBox.getLocationOnScreen().x, comboBox.getLocationOnScreen().y + comboBox.getHeight()); }); } }); return scrollPane; } private JPopupMenu createPopupMenu() { JPopupMenu popupMenu = new ComboBoxPopupMenu<>(comboBox); popupMenu.add(searchBox); popupMenu.add(scrollPane); return popupMenu; } @Override public void show() { comboBox.firePopupMenuWillBecomeVisible(); int itemListVisibleRowCount = Math.min(comboBox.getItemCount(), comboBox.getMaximumRowCount()); itemList.setVisibleRowCount(itemListVisibleRowCount); popupMenu.show(comboBox, 0, comboBox.getHeight()); searchBox.setVisible(scrollPane.getVerticalScrollBar().isVisible()); searchTextField.requestFocusInWindow(); } @Override public void hide() { comboBox.firePopupMenuWillBecomeInvisible(); searchTextField.setText(""); popupMenu.setVisible(false); comboBox.firePopupMenuCanceled(); } public void toggle() { if (isVisible()) hide(); else show(); } @Override public boolean isVisible() { return popupMenu.isVisible(); } @Override @SuppressWarnings("unchecked") public JList<Object> getList() { return (JList<Object>) itemList; } @Override public MouseListener getMouseListener() { return createMouseListener(); } private MouseListener createMouseListener() { return new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { toggle(); } }; } @Override public MouseMotionListener getMouseMotionListener() { return null; } @Override public KeyListener getKeyListener() { return null; } @Override public void uninstallingUI() { } private static class ComboBoxPopupMenu<T> extends JPopupMenu { private final JComboBox<T> comboBox; public ComboBoxPopupMenu(JComboBox<T> comboBox) { this.comboBox = comboBox; } @Override public Dimension getPreferredSize() { Dimension preferredSize = super.getPreferredSize(); preferredSize.width = comboBox.getWidth(); return preferredSize; } } }
说明
- 方式一通过提前布局滚动条,避免弹窗显示后再调整组件状态,执行效率更高。
- 方式二通过监听滚动条事件动态调整弹窗大小,适配搜索后选项数量变化导致滚动条显示/隐藏的复杂场景。
内容的提问来源于stack exchange,提问作者Wacage
相关产品推荐
相关产品推荐

