Windows L&F下JComboBox灰色背景实现原理及无受限依赖方案
问题描述
我们需要自定义JComboBox弹出框,通过匿名扩展实现自定义ComboPopup,但遇到以下矛盾:
- 继承
WindowsComboBoxUI时,JComboBox当前选中项背景为浅灰色(RGB约225,225,225),符合用户使用习惯; - 继承
BasicComboBoxUI时,当前选中项背景为白色,与原生组件风格不一致; - Java 9之后
WindowsComboBoxUI属于受限模块,现有Java 8项目升级会遭遇阻碍,因此必须避免引用该类及其所在包。
我们需要探究WindowsComboBoxUI实现选中项灰色背景的原理,并在自定义扩展中模拟该行为。
经调试发现:
- 绘制用的
ListCellRenderer是透明的(opaque属性设为false); - 其所在的
CellRendererPane同样是透明的; - 承载该面板的JComboBox不透明,背景色为白色(RGB255,255,255),按逻辑应显示白色背景,但实际并非如此。
在javax.swing.JComponent#paint方法中添加条件断点getClass().getName().contains("Renderer")后观察到:com.sun.java.swing.plaf.windows.WindowsComboBoxUI$WindowsComboBoxRenderer渲染时为透明状态,其父组件(渲染面板)也透明,祖父组件(JComboBox)不透明且背景为白色,但测试1中并未显示白色背景。
测试案例
测试1:效果符合预期,但引用了受限的com.sun包
package demos.combo; import com.sun.java.swing.plaf.windows.WindowsComboBoxUI; import javax.swing.*; import javax.swing.plaf.ComboBoxUI; import java.util.ArrayList; import java.util.List; public class SimpleComboDemo { public static void main(String[] args) throws UnsupportedLookAndFeelException, ClassNotFoundException, InstantiationException, IllegalAccessException { UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); JFrame frame = new JFrame("Simple combo demo"); JPanel mainPanel = new JPanel(); ComboBoxModel<String> model = new DefaultComboBoxModel<>(); JComboBox<String> combo = new JComboBox<>(model); combo.setUI(comboUi()); comboItems().forEach(combo::addItem); mainPanel.add(combo); frame.setContentPane(mainPanel); frame.pack(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); } private static ComboBoxUI comboUi() { return new WindowsComboBoxUI() { // 假设此处需要重写方法,例如createPopup() }; } private static List<String> comboItems() { List<String> items = new ArrayList<>(); items.add("Pen"); items.add("Pencil"); items.add("Eraser"); return items; } }
[选中项为浅灰色背景的JComboBox]
测试2:未引用com.sun包,但背景为白色(前景色也为白色)
package demos.combo; import di.ListCellRenderers; import javax.swing.*; import javax.swing.plaf.ComboBoxUI; import javax.swing.plaf.basic.BasicComboBoxUI; import java.util.ArrayList; import java.util.List; public class SimpleComboDemo { public static void main(String[] args) throws UnsupportedLookAndFeelException, ClassNotFoundException, InstantiationException, IllegalAccessException { UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel"); JFrame frame = new JFrame("Simple combo demo"); JPanel mainPanel = new JPanel(); ComboBoxModel<String> model = new DefaultComboBoxModel<>(); JComboBox<String> combo = new JComboBox<>(model); // 注意此行:必须设置自定义渲染器,因为BasicComboBoxRenderer的opaque=true,会被绘制成蓝色(List.selectionBackground) combo.setRenderer(ListCellRenderers.renderer(Object::toString)); combo.setUI(comboUi()); comboItems().forEach(combo::addItem); mainPanel.add(combo); frame.setContentPane(mainPanel); frame.pack(); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); } private static ComboBoxUI comboUi() { return new BasicComboBoxUI() { // 假设此处需要重写方法,例如createPopup() // 另外,若认为重写createRenderer()有用,实际并无效果 }; } private static List<String> comboItems() { List<String> items = new ArrayList<>(); items.add("Pen"); items.add("Pencil"); items.add("Eraser"); return items; } }
package di; import common.BaseListCellRenderer; import javax.swing.*; import java.util.function.Function; public class ListCellRenderers { private ListCellRenderers() { } public static <T> ListCellRenderer<T> renderer(Function<T, String> stringFunction) { return new BaseListCellRenderer<T>() { @Override protected String getLabelText(T element) { return stringFunction.apply(element); } }; } }
package common; import javax.swing.*; import java.awt.*; import java.util.Optional; public abstract class BaseListCellRenderer<E> extends JLabel implements ListCellRenderer<E> { @Override public Component getListCellRendererComponent(JList<? extends E> list, E value, int index, boolean isSelected, boolean cellHasFocus) { setColors(list, isSelected); setFont(list.getFont()); setContent(value); determineTransparency(list, index); return this; } private void setColors(JList<? extends E> list, boolean isSelected) { if (isSelected) setSelectionColors(list); else setRegularColors(list); } private void setSelectionColors(JList<? extends E> list) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } private void setRegularColors(JList<? extends E> list) { setBackground(list.getBackground()); setForeground(list.getForeground()); } private void setContent(E value) { if (value instanceof Icon) setIcon((Icon) value); else { String nullableLabelText = getLabelText(value); String labelText = Optional.ofNullable(nullableLabelText).orElse(""); setText(labelText); } } private void determineTransparency(JList<? extends E> list, int index) { boolean shouldBeTransparent = index == -1; if (shouldBeTransparent) { // 注意:此处标签不能设为opaque=true setOpaque(false); setForeground(list.getForeground()); } else setOpaque(true); } protected abstract String getLabelText(E element); @Override public void addNotify() { super.addNotify(); } }
[选中项为白色背景的JComboBox]
测试3:与测试2相同,但未设置Windows外观
[选中项为透明背景的JComboBox]
目前尚未追踪到new Color(225, 225, 225)的创建或设置逻辑,不清楚该灰色背景的来源。
内容的提问来源于stack exchange,提问作者Wacage
相关产品推荐
相关产品推荐

