You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

子组件隐藏后触发父组件尺寸重计算的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,当滚动条隐藏时,强制弹窗重新计算大小并更新位置:

  1. 修改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;
}
  1. 保留原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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 18:34:54