积微成著 不积跬步,无以至千里

首页   >   web开发   >   Swing界面优化JTabbedPane详细图文教程

Swing界面优化JTabbedPane详细图文教程

在使用一些聊天软件的时候,如果打开了多个好友窗口,就会被整合到一起,有一个tab页签用来切换,这个功能在swing中对应的就是JTabbedPane组件,沿袭了swing一贯的风格,这个组件的原生UI简直丑的不能看,今天就来教大家如何定制自己的UI。

Swing界面优化JTabbedPane教程

没错,上面这个图就是原生UI,不知道大家能不能忍,反正我是忍不了。

图片中,我已经做了标注,它大体就分为两块儿,上面的tab,加上下面的一个pane,内容区域每次只能展示一个,点击哪个tab就展示对应的pane,目测是一个卡片布局,所以就算是自己来实现一个,也不是不可能的事情,只不过要花费很多的时间来做细节,毕竟一个成熟的组件是要考虑到任何场景的,我们就简单点,直接优化一下它的UI。

经过之前的几篇文章,大家应该都已经看到,基本上swing的任何组件,都是有对应的paint方法来负责绘制UI的,内容区域就是一个矩形,可以不用优化,自己根据需要调整一下背景和边框颜色,不过在使用的时候,放入了其他组件,背景就被盖住了,也就无所谓背景色了,所以这次优化的主要着力点就是在tab页签上面

还是依照“扁平化”的理念来优化UI,思路如下:

1、改变tab的形状、大小、颜色、边框;

2、改变pane区域的边框颜色;

查看BasicTabbedPaneUI的代码,利用工具的快捷键搜一下,直接就能看到关于tab绘制的相关方法,分别是paintTabArea、paintTab、paintTabBackground、paintTabBorder这四个

第一个方法是总入口,会被paintComponent方法调用

public void paintComponent(Graphics g) {
	super.paintComponent(g);
	BasicTabbedPaneUI.this.paintTabArea(g, tabPane.getTabPlacement(), tabPane.getSelectedIndex());
	if (tabScroller.croppedEdge.isParamsSet() && tabContainer == null) {
		Rectangle croppedRect = rects[tabScroller.croppedEdge.getTabIndex()];
		g.translate(croppedRect.x, croppedRect.y);
		tabScroller.croppedEdge.paintComponent(g);
		g.translate(-croppedRect.x, -croppedRect.y);
	}
}

再来看一下paintTabArea方法的代码

protected void paintTabArea(Graphics g, int tabPlacement, int selectedIndex) {
	int tabCount = tabPane.getTabCount();

	Rectangle iconRect = new Rectangle(),
			  textRect = new Rectangle();
	Rectangle clipRect = g.getClipBounds();

	// Paint tabRuns of tabs from back to front
	for (int i = runCount - 1; i >= 0; i--) {
		int start = tabRuns[i];
		int next = tabRuns[(i == runCount - 1)? 0 : i + 1];
		int end = (next != 0? next - 1: tabCount - 1);
		for (int j = start; j <= end; j++) {
			if (j != selectedIndex && rects[j].intersects(clipRect)) {
				paintTab(g, tabPlacement, rects, j, iconRect, textRect);
			}
		}
	}

	// Paint selected tab if its in the front run
	// since it may overlap other tabs
	if (selectedIndex >= 0 && rects[selectedIndex].intersects(clipRect)) {
		paintTab(g, tabPlacement, rects, selectedIndex, iconRect, textRect);
	}
}

它又去调用了paintTab方法

protected void paintTab(Graphics g, int tabPlacement,
						Rectangle[] rects, int tabIndex,
						Rectangle iconRect, Rectangle textRect) {
	Rectangle tabRect = rects[tabIndex];
	int selectedIndex = tabPane.getSelectedIndex();
	boolean isSelected = selectedIndex == tabIndex;

	if (tabsOpaque || tabPane.isOpaque()) {
		paintTabBackground(g, tabPlacement, tabIndex, tabRect.x, tabRect.y,
				tabRect.width, tabRect.height, isSelected);
	}

	paintTabBorder(g, tabPlacement, tabIndex, tabRect.x, tabRect.y,
				   tabRect.width, tabRect.height, isSelected);

	...... // 此处代码省略,太占地方
	
	paintFocusIndicator(g, tabPlacement, rects, tabIndex,
			  iconRect, textRect, isSelected);
}

我们可以看到,paintTab方法,分别去调用了border和background的绘制方法。

与此次优化无关的代码,我都以省略号代替了,大家可能会好奇,为什么最后那个paintFocusIndicator方法么有去掉呢?起初我也是去掉了的,可是后来发现,这个方法也需要优化一下,否则会在tab获得焦点的出现一条虚线边框。

最后再来看一下tab大小的调整,方法内容我们可以直接不管,因为最终的宽高只是一个数字,我们只需要灵活改变这个数字即可,我们可以从super中拿到这个值,然后在此基础上加加减减。

// 计算tab宽度
protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) {
	......
}
// 计算tab高度
protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight) {
	......
}

优化的时候,我们只需要重写paintTabBorder、paintTabBackground、calculateTabWidth、calculateTabHeight,先上一张图片看看成果(代码在后面会统一贴出来)

Swing界面优化JTabbedPane教程

下面我们再来看一下关于pane内容区域的相关代码,官方也只是提供了一个绘制边框的方法paintContentBorder,这个方法很有特色,他并不是跟我们一贯的思维一样,直接去绘制一个矩形的四条边,而是分别去绘制,top、left、bottom、right

protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
    
	...... // 省略的代码,是用来计算坐标和宽高的,也就是x、y、w、h的值

	paintContentBorderTopEdge(g, tabPlacement, selectedIndex, x, y, w, h);
	paintContentBorderLeftEdge(g, tabPlacement, selectedIndex, x, y, w, h);
	paintContentBorderBottomEdge(g, tabPlacement, selectedIndex, x, y, w, h);
	paintContentBorderRightEdge(g, tabPlacement, selectedIndex, x, y, w, h);

}

四条边的具体绘制方法,我们就不去看了,边框就是两个点之间的一条线段,在这里可以理解为两个坐标之间的一条线段,肯定是一个drawLine方法来搞定。

好了,优化思路中提到的点,已经分别找到对应的优化方法了,下面直接给出重写的UI代码:

package com.wolffy.ui;

import javax.swing.UIManager;
import javax.swing.plaf.basic.BasicTabbedPaneUI;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;

/**
 * Created by SongFei on 2017/11/8.
 */
public class MyTabbedPaneUI extends BasicTabbedPaneUI {

    // 不知道是个什么东西,绘制pane时,计算坐标和宽高的方法需要用到,直接从父类拷贝过来的
    private boolean tabsOverlapBorder = UIManager.getBoolean("TabbedPane.tabsOverlapBorder");

    // 边框和背景的颜色
    private Color SELECT_COLOR = new Color(57, 181, 215);

    /*// 绘制整个选项卡区域
    @Override
    protected void paintTabArea(Graphics g, int tabPlacement, int selectedIndex) {
        // 如果没有特殊要求,就使用默认的绘制方案
        super.paintTabArea(g, tabPlacement, selectedIndex);
    }*/

    // 绘制tab页签的边框
    @Override
    protected void paintTabBorder(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
        // 测试了一下,发现没有边框更好看一点儿
        /*g.setColor(SELECT_COLOR);
        switch (tabPlacement) {
            case LEFT:
                g.drawLine(x, y, x, y + h - 1);
                g.drawLine(x, y, x + w - 1, y);
                g.drawLine(x, y + h - 1, x + w - 1, y + h - 1);
                break;
            case RIGHT:
                g.drawLine(x, y, x + w - 1, y);
                g.drawLine(x, y + h - 1, x + w - 1, y + h - 1);
                g.drawLine(x + w - 1, y, x + w - 1, y + h - 1);
                break;
            case BOTTOM:
                g.drawLine(x, y, x, y + h - 1);
                g.drawLine(x + w - 1, y, x + w - 1, y + h - 1);
                g.drawLine(x, y + h - 1, x + w - 1, y + h - 1);
                break;
            case TOP:
            default:
                g.drawLine(x, y, x, y + h - 1);
                g.drawLine(x, y, x + w - 1, y);
                g.drawLine(x + w - 1, y, x + w - 1, y + h - 1);
        }*/
    }

    // 绘制选中的选项卡背景色
    @Override
    protected void paintTabBackground(Graphics g, int tabPlacement, int tabIndex, int x, int y, int w, int h, boolean isSelected) {
        Graphics2D g2d = (Graphics2D) g;
        GradientPaint gradient;
        switch (tabPlacement) {
            case LEFT:
                if (isSelected) {
                    gradient = new GradientPaint(x + 1, y, SELECT_COLOR, x + w, y, Color.WHITE, true);
                } else {
                    gradient = new GradientPaint(x + 1, y, Color.LIGHT_GRAY, x + w, y, Color.WHITE, true);
                }
                g2d.setPaint(gradient);
                g.fillRect(x + 1, y + 1, w - 1, h - 2);
                break;
            case RIGHT:
                if (isSelected) {
                    gradient = new GradientPaint(x + w, y, SELECT_COLOR, x + 1, y, Color.WHITE, true);
                } else {
                    gradient = new GradientPaint(x + w, y, Color.LIGHT_GRAY, x + 1, y, Color.WHITE, true);
                }
                g2d.setPaint(gradient);
                g.fillRect(x, y + 1, w - 1, h - 2);
                break;
            case BOTTOM:
                if (isSelected) {
                    gradient = new GradientPaint(x + 1, y + h, SELECT_COLOR, x + 1, y, Color.WHITE, true);
                } else {
                    gradient = new GradientPaint(x + 1, y + h, Color.LIGHT_GRAY, x + 1, y, Color.WHITE, true);
                }
                g2d.setPaint(gradient);
                g.fillRect(x + 1, y, w - 2, h - 1);
                break;
            case TOP:
            default:
                if (isSelected) {
                    gradient = new GradientPaint(x + 1, y, SELECT_COLOR, x + 1, y + h, Color.WHITE, true);
                } else {
                    gradient = new GradientPaint(x + 1, y, Color.LIGHT_GRAY, x + 1, y + h, Color.WHITE, true);
                }
                g2d.setPaint(gradient);
                g2d.fillRect(x + 1, y + 1, w - 2, h - 1);
        }

    }

    // 绘制TabbedPane容器的四周边框样式
    @Override
    protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
        // 如果不想要边框,直接重写一个空方法
        // super.paintContentBorder(g, tabPlacement, selectedIndex);

        // 这些计算坐标和宽高的代码,直接从父类拷贝出来重用即可
        int width = tabPane.getWidth();
        int height = tabPane.getHeight();
        Insets insets = tabPane.getInsets();
        Insets tabAreaInsets = getTabAreaInsets(tabPlacement);

        int x = insets.left;
        int y = insets.top;
        int w = width - insets.right - insets.left;
        int h = height - insets.top - insets.bottom;

        switch (tabPlacement) {
            case LEFT:
                x += calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
                if (tabsOverlapBorder) {
                    x -= tabAreaInsets.right;
                }
                w -= (x - insets.left);
                break;
            case RIGHT:
                w -= calculateTabAreaWidth(tabPlacement, runCount, maxTabWidth);
                if (tabsOverlapBorder) {
                    w += tabAreaInsets.left;
                }
                break;
            case BOTTOM:
                h -= calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
                if (tabsOverlapBorder) {
                    h += tabAreaInsets.top;
                }
                break;
            case TOP:
            default:
                y += calculateTabAreaHeight(tabPlacement, runCount, maxTabHeight);
                if (tabsOverlapBorder) {
                    y -= tabAreaInsets.bottom;
                }
                h -= (y - insets.top);
        }

        // 四个边框的绘制方法,都自己重写一遍,方便控制颜色和一些特效
        paintContentBorderTopEdge(g, tabPlacement, selectedIndex, x, y, w, h);
        paintContentBorderLeftEdge(g, tabPlacement, selectedIndex, x, y, w, h);
        paintContentBorderBottomEdge(g, tabPlacement, selectedIndex, x, y, w, h);
        paintContentBorderRightEdge(g, tabPlacement, selectedIndex, x, y, w, h);
    }

    // 绘制容器左侧边框,不是tab,是pane
    // 上下左右,都可以重写方法来绘制,相应的方法:paintContentBorder*Edge(),由paintContentBorder()方法统一调用
    @Override
    protected void paintContentBorderLeftEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) {
        g.setColor(SELECT_COLOR);
        g.drawLine(x, y, x, y + h - 2);
    }

    @Override
    protected void paintContentBorderTopEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) {
        g.setColor(SELECT_COLOR);
        g.drawLine(x, y, x + w - 2, y);
    }

    // 右边和下边,这两个需要注意,x + w 和 y + h 已达到边框临界,必须减掉几个数值,否则边框会显示不出来
    @Override
    protected void paintContentBorderRightEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) {
        g.setColor(SELECT_COLOR);
        g.drawLine(x + w - 1, y, x + w - 1, y + h - 1);
    }

    @Override
    protected void paintContentBorderBottomEdge(Graphics g, int tabPlacement, int selectedIndex, int x, int y, int w, int h) {
        g.setColor(SELECT_COLOR);
        g.drawLine(x, y + h - 1, x + w - 1, y + h - 1);
    }

    // 绘制选中某个Tab后,获得焦点的样式
    @Override
    protected void paintFocusIndicator(Graphics g, int tabPlacement, Rectangle[] rects, int tabIndex, Rectangle iconRect, Rectangle textRect, boolean isSelected) {
        // 重写空方法,主要用来去掉虚线
    }

    // 计算tab页签的宽度
    @Override
    protected int calculateTabWidth(int tabPlacement, int tabIndex, FontMetrics metrics) {
        // 可根据placement来做不同的调整
        return super.calculateTabWidth(tabPlacement, tabIndex, metrics) + 20;
    }

    // 计算tab页签的高度
    @Override
    protected int calculateTabHeight(int tabPlacement, int tabIndex, int fontHeight) {
        // 可根据placement来做不同的调整
        return super.calculateTabHeight(tabPlacement, tabIndex, fontHeight) + 20;
    }

}

写一个测试类

package com.wolffy.frame;

import com.wolffy.ui.MyTabbedPaneUI;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTabbedPane;
import javax.swing.WindowConstants;
import java.awt.BorderLayout;


/**
 * Created by SongFei on 2017/11/8.
 */
public class TabPaneFrame extends JFrame {

    private static final long serialVersionUID = 474825076033661007L;

    private JPanel jPanel;

    public TabPaneFrame() {
        initGUI();
    }

    private void initGUI() {
        setSize(700, 400);
        setUndecorated(true);// 把边框去了,优化的效果能看的更明显
        setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

        jPanel = new JPanel();
        //jPanel.setBackground(Color.ORANGE);
        jPanel.setLayout(new BorderLayout());
        getContentPane().add(jPanel, BorderLayout.CENTER);

        JTabbedPane tab = new JTabbedPane(JTabbedPane.TOP);
        tab.setUI(new MyTabbedPaneUI());

        JPanel jPanel1 = new JPanel();
        jPanel1.add(new JButton("AAA111"));
        JPanel jPanel2 = new JPanel();
        jPanel2.add(new JButton("AAA222"));
        JPanel jPanel3 = new JPanel();
        jPanel3.add(new JButton("AAA333"));
        JPanel jPanel4 = new JPanel();
        jPanel4.add(new JButton("AAA444"));
        JPanel jPanel5 = new JPanel();
        jPanel5.add(new JButton("AAA555"));

        tab.add(jPanel1, "AAA111");
        tab.add(jPanel2, "AAA222");
        tab.add(jPanel3, "AAA333");
        tab.add(jPanel4, "AAA444");
        tab.add(jPanel5, "AAA555");

        jPanel.add(tab, BorderLayout.CENTER);
    }

    public static void main(String[] args) {
        TabPaneFrame paneFrame = new TabPaneFrame();
        paneFrame.setVisible(true);
        //loginFrame.pack();
        paneFrame.setLocationRelativeTo(null);
    }

}

案例中,我们将最外层的边框去了,这样优化的效果能看得更加明显,不废话了,上图:

Swing界面优化JTabbedPane教程

再来一张tab放到左侧的效果图:

Swing界面优化JTabbedPane教程

好了,到这里为止,整个UI的优化就完毕了!

QQ群:积微成著官方群(686430774),验证消息:积微成著

站长Q:1347384268(加好友请注明来意)

分享到:

欢迎分享本文,转载请注明出处!

作者:不忘初心

发布时间:2017-11-09

永久地址:http://www.jiweichengzhu.com/article/c70d07a6a8034940a5a19fb61c24a65a