Swing中JTabbedPane自定义选中标签样式实现问题
Swing中JTabbedPane自定义选中标签样式实现问题
我太懂你这种折腾Swing组件样式的痛苦了😅,之前我也在JTabbedPane的自定义样式上踩过好几个坑。你之前遇到的UIManager不生效、自定义Tab填充不全的问题,其实都是Swing默认UI渲染机制的锅,下面给你一套能完美实现需求的方案:
问题根源分析
- UIManager方法失效:很多系统默认的LookAndFeel(比如Windows原生LAF)会忽略
TabbedPane.selected这类属性,因为它们用原生组件渲染,兼容性极差,自定义TabComponent才是通用方案。 - 自定义Tab填充不全:你之前的
CustomTab用fillRect(0,0,getWidth(),getHeight())绘制背景,但JPanel默认有内边距(Insets),导致实际绘制区域没覆盖整个Tab的可点击范围,就会出现留白。
完整解决方案
第一步:实现正确的自定义Tab组件
这个类会处理选中/未选中状态的背景、底部下划线,并且完美适配Tab的绘制区域:
import javax.swing.*; import java.awt.*; import java.awt.event.ChangeListener; public class CustomTab extends JPanel { private final JTabbedPane tabbedPane; private final String tabTitle; // 样式常量,方便统一修改 private static final int UNDERLINE_HEIGHT = 3; private static final Color SELECTED_BG = new Color(51,51,51); private static final Color UNSELECTED_BG = new Color(35,35,35); private static final Color UNDERLINE_COLOR = Color.WHITE; private static final Color TEXT_COLOR = Color.WHITE; public CustomTab(JTabbedPane tabbedPane, String tabTitle) { this.tabbedPane = tabbedPane; this.tabTitle = tabTitle; setOpaque(false); setLayout(new BorderLayout()); setPreferredSize(new Dimension(140, 30)); // 调整合适的Tab大小,之前15px高度太矮会截断文字 setFont(new Font("FS Sinclair", Font.BOLD, 14)); // 监听Tab选中状态变化,自动重绘 ChangeListener listener = e -> repaint(); tabbedPane.addChangeListener(listener); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g.create(); // 开启文字抗锯齿 g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); int tabIndex = tabbedPane.indexOfTab(tabTitle); boolean isSelected = tabbedPane.getSelectedIndex() == tabIndex; // 1. 绘制Tab背景(考虑组件内边距,解决填充不全问题) Insets insets = getInsets(); int drawX = insets.left; int drawY = insets.top; int drawWidth = getWidth() - insets.left - insets.right; int drawHeight = getHeight() - insets.top - insets.bottom; g2.setColor(isSelected ? SELECTED_BG : UNSELECTED_BG); g2.fillRect(drawX, drawY, drawWidth, drawHeight); // 2. 绘制选中状态的白色下划线 if (isSelected) { g2.setColor(UNDERLINE_COLOR); g2.fillRect(drawX, drawY + drawHeight - UNDERLINE_HEIGHT, drawWidth, UNDERLINE_HEIGHT); } // 3. 绘制Tab文字 g2.setColor(TEXT_COLOR); FontMetrics fm = g2.getFontMetrics(); int textX = drawX + (drawWidth - fm.stringWidth(tabTitle)) / 2; int textY = drawY + (drawHeight + fm.getAscent()) / 2 - fm.getDescent() / 2; g2.drawString(tabTitle, textX, textY); g2.dispose(); } }
第二步:修改HDApp的组件初始化逻辑
需要禁用JTabbedPane的默认UI渲染,并且给每个Tab绑定我们的自定义组件:
public class HDApp { private final JFrame frame = new JFrame(); private final JTabbedPane tabPanel = new JTabbedPane(); private final MainPanel mainPanel = new MainPanel(); private final GearPanel gearPanel = new GearPanel(); private final RulesPanel rulesPanel = new RulesPanel(); public HDApp() { initComponents(); } private void initComponents() { frame.getContentPane().setBackground(new Color(51,51,51)); frame.setTitle("Helldivers 2 Randomizer"); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(1042, 540); tabPanel.setBackground(new Color(51,51,51)); tabPanel.setForeground(Color.WHITE); tabPanel.setBorder(null); // 关键:替换默认TabbedPaneUI,禁用原生背景绘制 tabPanel.setUI(new BasicTabbedPaneUI() { @Override protected void installDefaults() { super.installDefaults(); tabInsets = new Insets(2, 4, 2, 4); // 调整Tab之间的间距 contentBorderInsets = new Insets(0,0,0,0); // 去掉内容区域的默认边框 } @Override protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) { // 空实现:不让默认UI绘制Tab背景,完全用我们的CustomTab渲染 } @Override protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) { return 140; // 强制所有Tab宽度一致,避免文字长短不一导致的布局混乱 } }); // 添加Tab并绑定自定义TabComponent JPanel randomizerPage = mainPanel.getMainPanel(); int randomizerIdx = tabPanel.addTab("Randomizer", randomizerPage); tabPanel.setTabComponentAt(randomizerIdx, new CustomTab(tabPanel, "Randomizer")); JPanel gearPage = gearPanel.getPanel(); int gearIdx = tabPanel.addTab("Gear Information", gearPage); tabPanel.setTabComponentAt(gearIdx, new CustomTab(tabPanel, "Gear Information")); JScrollPane rulePage = rulesPanel.getjScrollPane(); int rulesIdx = tabPanel.addTab("Randomzier Rules", rulePage); tabPanel.setTabComponentAt(rulesIdx, new CustomTab(tabPanel, "Randomzier Rules")); frame.add(tabPanel); frame.setVisible(true); } }
最终效果说明
- 选中的Tab背景为你想要的深灰色
(51,51,51) - 选中Tab底部有3px高的白色下划线
- 未选中Tab使用稍浅的灰色(可以根据需求修改
UNSELECTED_BG常量) - 完全解决之前的填充不全问题,Tab背景会覆盖整个可点击区域
额外注意点
如果你的系统没有FS Sinclair字体,Swing会自动 fallback 到默认字体,要是想强制使用该字体,可以把字体文件打包到项目中,用Font.createFont注册后再使用。




