You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Swing中JTabbedPane自定义选中标签样式实现问题

Swing中JTabbedPane自定义选中标签样式实现问题

我太懂你这种折腾Swing组件样式的痛苦了😅,之前我也在JTabbedPane的自定义样式上踩过好几个坑。你之前遇到的UIManager不生效、自定义Tab填充不全的问题,其实都是Swing默认UI渲染机制的锅,下面给你一套能完美实现需求的方案:

问题根源分析

  1. UIManager方法失效:很多系统默认的LookAndFeel(比如Windows原生LAF)会忽略TabbedPane.selected这类属性,因为它们用原生组件渲染,兼容性极差,自定义TabComponent才是通用方案。
  2. 自定义Tab填充不全:你之前的CustomTabfillRect(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注册后再使用。

火山引擎 最新活动