Swing自定义ComboBox弹窗无法稳定获取焦点问题求助
因为JComboBox部分行为不符合需求,且需要重写的方法为私有,我基于Swing实现了自定义ComboBox:继承JComponent,内置JTextField,并重写JPopupMenu作为下拉弹窗。测试时通过JTextField的上下键触发弹窗,但弹窗内的JList无法稳定获取焦点——有时选中项呈灰色(无实际焦点)。我尝试了多种show()、setVisible()、requestFocus()等组合及SwingUtilities.invokeLater(),当前代码为最优尝试,但仍有问题。测试用例(含main方法)运行正常,但在复杂的主应用(添加到工具栏、使用其他Look and Feel)中问题复现,现寻求解决思路。
package com.sdi.cmsanalyzer.gui; import javax.swing.*; import java.awt.*; import java.awt.event.*; public class CustomComboBox extends JComponent { private final JTextField textField; private final DropdownMenu dropdown; public CustomComboBox() { setLayout(new BorderLayout()); textField = new JTextField(); textField.addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { int keyCode = e.getKeyCode(); if (keyCode == KeyEvent.VK_DOWN) { System.out.println("DOWN"); showDropdown(); dropdown.selectFirstItem(); } else if (keyCode == KeyEvent.VK_UP) { System.out.println("UP"); showDropdown(); dropdown.selectLastItem(); } else if (keyCode == KeyEvent.VK_ENTER) { System.out.println("ENTER"); } } }); add(textField, BorderLayout.CENTER); dropdown = new DropdownMenu(); } public JTextField getEditor() { return this.textField; } private void showDropdown() { dropdown.updateItems(getDropdownItems()); dropdown.setSize(new Dimension(textField.getWidth(), dropdown.getPreferredSize().height)); int x = 0; int y = textField.getHeight(); Container parent = textField.getParent(); if (parent != null) { Point textFieldLocation = textField.getLocation(); SwingUtilities.convertPointToScreen(textFieldLocation, textField); SwingUtilities.convertPointFromScreen(textFieldLocation, parent); x = textFieldLocation.x; y = textFieldLocation.y + textField.getHeight(); } dropdown.show(textField, x, y); } private String[] getDropdownItems() { // Replace with your logic to retrieve the dropdown items return new String[]{"Option 1", "Option 2", "Option 3", "Option 4", "Option 5", "Option 6", "Option 7", "Option 8", "Option 9", "Option 10"}; } private static class DropdownMenu extends JPopupMenu { DefaultListModel<String> listModel = new DefaultListModel<>(); JScrollPane scrollPane; JList<String> list; public DropdownMenu() { // Create a JList with the list model list = new JList<>(listModel); // Create a scroll pane and add the list to it scrollPane = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); add(scrollPane); pack(); } public void updateItems(String[] items) { listModel.clear(); for (String item : items) { listModel.addElement(item); } } protected void selectFirstItem() { SwingUtilities.invokeLater(() -> { list.setSelectedIndex(0); list.ensureIndexIsVisible(0); list.requestFocus(); }); } protected void selectLastItem() { SwingUtilities.invokeLater(() -> { list.setSelectedIndex(listModel.getSize() - 1); list.ensureIndexIsVisible(listModel.getSize() - 1); list.requestFocus(); }); } } public static void main(String[] args) { SwingUtilities.invokeLater(() -> createAndShowGUI()); } private static void createAndShowGUI() { // Create the main frame JFrame frame = new JFrame("Custom ComboBox Example"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // Create a toolbar JToolBar toolBar = new JToolBar(); // Create an instance of your CustomComboBox CustomComboBox customComboBox = new CustomComboBox(); // Add the CustomComboBox to the toolbar toolBar.add(customComboBox); // Add the toolbar to the frame frame.add(toolBar, BorderLayout.NORTH); // Create a center panel JPanel centerPanel = new JPanel(); frame.add(centerPanel, BorderLayout.CENTER); // Set the frame size and make it visible frame.setSize(400, 200); frame.setVisible(true); } }
替换
requestFocus()为requestFocusInWindow()
直接调用requestFocus()在复杂界面或不同LAF下兼容性差,改用requestFocusInWindow()更符合Swing焦点规则,成功率更高。修改selectFirstItem()和selectLastItem()中的焦点请求代码:list.requestFocusInWindow();在弹窗显示完成后再请求焦点
弹窗的show()方法可能存在异步执行逻辑,可重写DropdownMenu的show()方法,确保弹窗完全显示后再触发焦点请求:@Override public void show(Component invoker, int x, int y) { super.show(invoker, x, y); SwingUtilities.invokeLater(() -> { if (list.getSelectedIndex() != -1) { list.requestFocusInWindow(); } }); }消费上下键事件,避免焦点被抢占
在JTextField的按键处理中,调用e.consume()阻止事件继续传递,防止工具栏或其他组件的焦点逻辑干扰JList:if (keyCode == KeyEvent.VK_DOWN) { System.out.println("DOWN"); showDropdown(); dropdown.selectFirstItem(); e.consume(); } else if (keyCode == KeyEvent.VK_UP) { System.out.println("UP"); showDropdown(); dropdown.selectLastItem(); e.consume(); }确保JList的焦点可访问性
显式设置JList的焦点属性,避免LAF或默认配置禁用焦点:public DropdownMenu() { list = new JList<>(listModel); list.setFocusable(true); list.setRequestFocusEnabled(true); // 其他初始化代码... }简化弹窗位置计算
手动计算位置容易出错,直接以textField为调用者,设置相对位置即可:private void showDropdown() { dropdown.updateItems(getDropdownItems()); dropdown.setPreferredSize(new Dimension(textField.getWidth(), dropdown.getPreferredSize().height)); dropdown.show(textField, 0, textField.getHeight()); }处理Look and Feel的影响
不同LAF可能修改组件行为,可在弹窗显示前同步UI:SwingUtilities.updateComponentTreeUI(dropdown);
内容的提问来源于stack exchange,提问作者UglyBob

