Swing:LookAndFeel 教程第一篇——手把手教你写出自己的 LookAndFeel

本文是 LookAndFeel 系列教程的第一篇。

是我在对 Swing 学习摸索中的一些微薄经验。

我相信,仔细看完全系列之后,你就能写出自己的 LookAndFeel。

你会发现 Swing 原来可以这样美。

--------------------------------------------------------------------------------


引言:


我第一次接触 Java 要追溯到很多年前做毕业设计的时候。

那天我和同学来到了一个微型软件公司(三程序员、一会计、一老总),

第一次接触到了面向对象,第一次接触到了 Java,

很神奇的,在公司某技术大牛的帮助下完成了一个 HelloWorld 之后,我便开始接触 Swing。

这位技术大牛可以说是我在 Java 之路上的领路人,我之后的代码风格很大程度上就是受他的影响。

不得不说,从那时起,我就和 Swing 结下了不解之缘。

虽然毕设做完之后许久没有再碰 Java……


直到两年前,工作转型,从工程实施转为软件开发,才又拾起了 Java,

第一个大项目就是 Swing 的项目,不得不说这就是缘分。

虽然阔别 Swing 多年,但是当年领路人的一句话我一直牢记于心:

“Swing 真正的精髓就是感观(LookAndFeel),如果能开发感观了,那才是真正的进入到 Swing 的核心领域。”

关于这一点,可能仁者见仁,智者见智。但是,它正是我指引我前进的航标。

所以,当公司的 Swing 项目提出了开发定制皮肤的需求时,我毫不犹豫的应下了这个差事。

这是何其巧妙与神奇的缘分啊……


随着项目的展开,随着对 Swing 源码的深入解读,LookAndFeel 的大门在我的面前渐渐清晰,并已在隐隐开启。

最终,历时两个月,我成功的完成了人生的第一套 LookAndFeel,

虽然外观依然不够美观,虽然功能还有不少缺陷,虽然代码还是略显幼稚,

但是门已打开,前方的道路一片光明……


所以今天写这篇文,就是为了告诉大家一些我的经验,告诉大家一个真正的 Swing。

我在开发这套 LookAndFeel 之前,虽然浅浅的接触过一些 Swing,但是也谈不上多深的基础。

在不断摸索中,走了不少歪路,但依然还是在两个月内完成了开发,所以……

如果你有良好的 Swing 基础,那你理解 LookAndFeel 的速度一定飞快;

如果你没有哪怕一点点的 Swing 基础,没有关系,我带领你从另一个角度踏入 Swing 的领域;

如果你是不 Java 爱好者,那也无妨,Swing/LookAndFeel 优美的 MVC 模式会给你一个良好的编程思想。

就像这些年不断冒出的修真小说上描述的那样——

无论是修仙,修禅,还是修魔——都是修真。

道虽不同,但大道相通。


--------------------------------------------------------------------------------


题外话:在看本文之前,你最好已经安装了一个 JDK,

因为安装了 JDK 你才能看到 Java 的源码,你才能更好的理解本文。

JDK 安装之后,在其安装目录中有个 src.zip 压缩包,里面就放着 Java 源码。


作为 LookAndFeel 教程的第一篇,本文要说些什么?

先列个提纲吧:

1、为什么要用 Swing 而不用 AWT

2、什么是 LookAndFeel

3、一个 Swing 控件是怎么由 MVC 结构组成的

4、一个 Swing 控件是如何绘制——准确说是如何通过 UI 类来绘制的

5、一个 Swing 控件是如何获得对应于自己的那个 UI 类对象的

这是第一篇的主要内容,也是 LookAndFeel 的核心部分,

看完这篇,你至少会知道什么是 LookAndFeel。

虽然你可能暂时还不能写出一个自己的 LookAndFeel,

但是,这就是开始,沿着这条路走下去,你肯定会成功。

下面开始正文:


--------------------------------------------------------------------------------


一、为什么要用 Swing 而不用 AWT


Java 官方的 GUI 有两套:AWT 以及 Swing。

在这里,我们略微探讨一下这两者之间的历史关系,

但是不讨论官方的 GUI 和一些非官方的 GUI (例如 SWT)之间孰优孰劣。

我们今天要说的重点是——Swing。


为什么是 Swing 而不是 AWT 呢?

因为早在十多年前,Java 官方就发觉了 AWT 控件的缺陷——这种重量级方案想做到平台一致性,难度太大。

不但界面难以美观,为了保证平台一致性,官方开发人员不断的去除 AWT 控件中可能会引起平台差异性的特性,最后导致 AWT 控件的功能也异常薄弱。

在这个时候,官方大胆的抛弃了 AWT 控件,在 AWT 事件机制的基础上推出了 Swing。

从此之后,官方只对 Swing 进行更新维护,而停止了对 AWT 控件的更新维护。

也就是说,如果你现在还在学习 AWT 控件,那就等于是在学习一种被官方抛弃,并停止更新维护长达十年之久的技术。


Swing 是什么?

Swing 是 Java 官方推出的,绝大部分控件都由 Graphics2D 绘制的一种轻量级 GUI 方案。其所有的轻量级控件都继承自 JComponent 类。

需要注意的是,Swing 中依然有三个重量级控件:JFrame,JDialog,JWindow。

不过它们都是窗体,他们都继承自 Window 类。

而无论是 JComponent 还是 Window 它们都继承自 Container 类,这其实也就意味着:所有的 Swing 控件,都可以做控件容器。


--------------------------------------------------------------------------------


二、什么是 LookAndFeel


现在,我们直接切入重点:Swing 是如何通过 Graphics2D 绘制这些控件的呢?

答案就是 LookAndFeel 机制。

那什么是 LookAndFeel 呢?

通俗的说,这就是皮肤;

从功能上说,这是一种批量管理 Swing 控件外观的机制;

从根源来说,这是 Swing 的核心。

官方正式推出的 LookAndFeel 在 Java 7 版本中已经增加到了 4 套。

现在,让我们简单的预览一下它们的效果吧——


1、(官方)MetalLookAndFeel(Swing 默认的 LookAndFeel):

类路径:javax.swing.plaf.metal.MetalLookAndFeel

跨平台性:可跨平台


2、(官方)WindowsLookAndFeel:

类路径:com.sun.java.swing.plaf.windows.WindowsLookAndFeel

跨平台性:限于 Windows 平台


3、(官方)MotifLookAndFeel:

类路径:com.sun.java.swing.plaf.motif.MotifLookAndFeel

跨平台性:可跨平台


4、(官方)NimbusLookAndFeel:

类路径:javax.swing.plaf.nimbus.NimbusLookAndFeel

跨平台性:可跨平台


5、(本人随意之作)UltimaLookAndFeel:

跨平台性:可跨平台


6、最后看一下这个……

这是什么?这就是 AWT……

其实不是我不想给它加上 Tab 页,实在是在 AWT 里找不到 Tab 页控件……

其实也不是我不想给它加个标题边框,实在是找不到设置 AWT 控件边框的方法……

呵呵,看出为什么 AWT 会被抛弃了么?


话说 MetalLookAndFeel 作为 Swing 的默认 LookAndFeel 实在有些寒碜。

而官方在 Java 7 中正式推出的 NimbusLookAndFeel 则要美观的多。

而我的 UltimaLookAndFeel 是以 MetalLookAndFeel 为基础的一个优化版,

目前有蓝色、绿色、黑色三种风格。

各位看到这里是不是有点激动了?原来 Swing 并不是只能像默认的 LookAndFeel 那样寒碜。

别急,放心,看完我的这一系列文章,你就能写出一个属于自己的 LookAndFeel,想多美观就能有多美观,只要你有好的美工基础……

顺便说一下,由于我个人无美工基础,所以我的 UltimaLookAndFeel 外观以简洁为主,求美工……

你问如何换 LookAndFeel?

在启动你的程序前:

UIManager.setLookAndFeel(…);

即可

如果是在程序已经启动之后再换 LookAndFeel,那在上面那句之后,建议再加上:

SwingUtilities.updateComponentTreeUI(…);


--------------------------------------------------------------------------------


三、一个 Swing 控件是怎么由 MVC 结构组成的


说到 LookAndFeel 又不得不提一下 Swing 优美的 MVC 模式。

所谓 MVC 就是:模型(Model)、视图(View)、控制器(Controller)这样的一种结构。

Swing 中,几乎所有的控件都可以清晰的分解成这三大部分。

就拿 JButton 来举例,我们可以这样分解:

JButton——控制器;

ButtonModel——模型,其最常见的具体实现类是:DefaultButtonModel;

ButtonUI——视图,其最常见的具体实现类是:MetalButtonUI;

JButton 负责控制,ButtonModel 提供模型,而 ButtonUI 实现展示。

也就是说,基本上所有的 Swing 控件都是由一个 Control 类、一个 Model 类、一个 UI 类组成的。

部分过于简单和数据无关的控件无 Model 类,例如 JPanel……

所有的 Control 类你都很熟悉;

大部分 Model 类你也很熟悉;

而 UI 类,你可能不是很熟悉。没关系,今天之后,你将熟悉它们!


--------------------------------------------------------------------------------


四、一个 Swing 控件是如何绘制——准确说是如何通过 UI 类来绘制的


那 Swing 是如何通过 UI 类来绘制控件的呢?

要说清楚这个问题,我们先来说一下 Swing 控件在 UI 线程中的绘制过程:

无论是 repaint 还是什么,在绘制控件时,最终都会产生一个 PaintEvent 然后排入 UI 线程的事件处理序列。

而 UI 线程在处理 Swing 控件的 PaintEvent 时,最终都会调用到控件的 paint 方法。

所以我们现在看一下 JComponent 的 paint 方法是怎么写的:

public void paint(Graphics g) {
    //……
    if(!rectangleIsObscured(clipX,clipY,clipW,clipH)) {
        if (!printing) {
            paintComponent(co);
            paintBorder(co);
        } else {
            printComponent(co);
            printBorder(co);
        }
    }
    if (!printing) {
        paintChildren(co);
    } else {
        printChildren(co);
    }
    //……
}

略去了很多,但那不是重点,重点是在 paint 方法中调用了 paintComponent 方法。


好,再看看 paintComponent 方法:

protected void paintComponent(Graphics g) {
    if (ui != null) {
        Graphics scratchGraphics = (g == null) ? null : g.create();
        try {
            ui.update(scratchGraphics, this);
        }
        finally {
            scratchGraphics.dispose();
        }
    }
}

我们看到了什么?抓重点,那就是:

ui.update(scratchGraphics, this);

这个 ui 是什么?看 JComponent 中的声明:

protected transient ComponentUI ui;

原来是个 ComponentUI,顺便说一下,这个 ComponentUI 就是所有 UI 类的祖先类。


看这些是为了干什么?

OK,我们来总结一下:

一个控件要绘制,就必然调用到它的 paint 方法;

而默认的 paint 方法中会调用到 paintComponent 方法;

paintComponent 方法中又会调用 UI 类的 update 方法;

也就是说,一个控件的绘制,和其 UI 类中的 update 方法是息息相关的。

看,UI 类成功的和控件绘制关联上了。

好的,这个问题解决了,我们看下一个问题吧。


--------------------------------------------------------------------------------


五、一个 Swing 控件是如何获得对应于自己的那个 UI 类对象的


各个控件的 UI 类又是怎么被设置到 Control 类中的呢?

为了说明这个问题,我们再来看个例子,这次拿 JPanel 来开刀:

我们要看的是它的构造方法,无论我们怎么构造一个 JPanel,在其内部最终都是调用的这个方法:

public JPanel(LayoutManager layout, boolean isDoubleBuffered){
    setLayout(layout);
    setDoubleBuffered(isDoubleBuffered);
    setUIProperty("opaque", Boolean.TRUE);
    updateUI();
}

重点是最后一句:

updateUI();

其实如果你去仔细研究各个 Swing 控件的构造方法的源代码,会发现其最终都会调用到 updateUI 这个方法


所以现在重点转移了,来看 updateUI 方法:

public void updateUI() {
    setUI((PanelUI)UIManager.getUI(this));
}

setUI 的执行过程我就不再累述,有兴趣的可以自己看源码,总之最后,它会对 ui 对象赋值。


这里出现了一个很重要的类——UIManager,这是什么呢?

在 LookAndFeel 机制中,会有大量的键值对存放在一个 UIDefaults(其实就是个 HashTable)中。

这些键值对记录了控件的边框、各种部分的颜色、字体等等,其中也包括了这个控件对应的 UI 类的类名。

而 UIManager 就是方便我们调用或替换这些键值对的一个管理工具类。


UIManager.getUI(this) 又是怎样返回一个 UI 类的对象呢?

我们来看看:

public static ComponentUI getUI(JComponent target) {
    maybeInitialize();
    ComponentUI ui = null;
    LookAndFeel multiLAF = getLAFState().multiLookAndFeel;
    if (multiLAF != null) {
        ui =multiLAF.getDefaults().getUI(target);
    }
    if (ui == null) {
        ui = getDefaults().getUI(target);
    }
    return ui;
}

现在大家应该都学会抓重点了吧?

重点是:

getDefaults().getUI(target);

它又是怎么执行的呢?

public ComponentUI getUI(JComponent target) {
    Object cl = get("ClassLoader");
    ClassLoader uiClassLoader =
        (cl != null) ? (ClassLoader)cl :target.getClass().getClassLoader();
    Class<? extends ComponentUI> uiClass =
        getUIClass(target.getUIClassID(), uiClassLoader);
    Object uiObject = null;
    //……略去反射部分的源码
    return (ComponentUI)uiObject;
}

看到这句代码没:

getUIClass(target.getUIClassID(), uiClassLoader);

原来是通过控件类中的 getUIClassID 返回的“键”,来获得 UI 类的类名在 UIDefaults 中的“值”,然后反射生成 UI 类的对象。

看一下 JPanel 中的 getUIClassID 方法:

private static final String uiClassID = "PanelUI";

public String getUIClassID() {
    return uiClassID;
}


又到一段总结时……

在控件构造时,都会去调用 updateUI 方法。

在控件的 updateUI 方法中,会通过 UIManager 去获取 ui 对象。

而 UIManager 去获取 ui 对象时,是通过控件的 uiClassID 这个“键”去获得 UIDefaults 中的对应的“值”。

而最后根据返回的类名,反射生成一个 UI 类的对象,返回给 updateUI 方法。

再通过 setUI 方法赋值给 ui 成员变量。


--------------------------------------------------------------------------------


好的,第一篇 LookAndFeel 教程就快结束了。

我们最后来看一下 LookAndFeel 类中的一个方法,你会明白很多事。

MetalLookAndFeel 类,initClassDefaults 方法:

    protected void initClassDefaults(UIDefaults table)
    {
        super.initClassDefaults(table);
        final String metalPackageName = "javax.swing.plaf.metal.";
        Object[] uiDefaults = {
                   "ButtonUI", metalPackageName+ "MetalButtonUI",
                 "CheckBoxUI", metalPackageName+ "MetalCheckBoxUI",
                 "ComboBoxUI", metalPackageName + "MetalComboBoxUI",
              "DesktopIconUI", metalPackageName + "MetalDesktopIconUI",
              "FileChooserUI", metalPackageName + "MetalFileChooserUI",
            "InternalFrameUI", metalPackageName + "MetalInternalFrameUI",
                    "LabelUI", metalPackageName + "MetalLabelUI",
       "PopupMenuSeparatorUI", metalPackageName + "MetalPopupMenuSeparatorUI",
              "ProgressBarUI", metalPackageName + "MetalProgressBarUI",
              "RadioButtonUI", metalPackageName + "MetalRadioButtonUI",
                "ScrollBarUI", metalPackageName + "MetalScrollBarUI",
               "ScrollPaneUI", metalPackageName + "MetalScrollPaneUI",
                "SeparatorUI", metalPackageName + "MetalSeparatorUI",
                   "SliderUI", metalPackageName + "MetalSliderUI",
                "SplitPaneUI", metalPackageName + "MetalSplitPaneUI",
               "TabbedPaneUI", metalPackageName + "MetalTabbedPaneUI",
                "TextFieldUI", metalPackageName + "MetalTextFieldUI",
             "ToggleButtonUI", metalPackageName + "MetalToggleButtonUI",
                  "ToolBarUI", metalPackageName + "MetalToolBarUI",
                  "ToolTipUI", metalPackageName + "MetalToolTipUI",
                     "TreeUI", metalPackageName + "MetalTreeUI",
                 "RootPaneUI", metalPackageName + "MetalRootPaneUI",
        };
        table.putDefaults(uiDefaults);
    }


现在你明白了么?

"ButtonUI" 就是那个“键”;

"javax.swing.plaf.metal.MetalButtonUI" 就是那个“值”;

原来在 updateUI 中,获得 ui 对象时,用到的那个键值对关系,就是在这里对应上的。

所以,如果你打算自己写一套 LookAndFeel,当你写了一个 UI 类之后应该怎么和控件对应上呢?

答案就是改写 LookAndFeel 类中的 initClassDefaults 方法。


第一篇,就到这里了~

to be continue...

  • 24
    点赞
  • 73
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值