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

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

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

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

你会发现 Swing 原来能够这样美。


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


引言:


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

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

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

非常奇妙的。在公司某技术大牛的帮助下完毕了一个 HelloWorld 之后,我便開始接触 Swing。

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

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

尽管毕设做完之后许久没有再碰 Java……


直到两年前,工作转型。从project实施转为软件开发,才又拾起了 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...

posted @ 2018-04-06 08:15  zhchoutai  阅读(5121)  评论(1编辑  收藏  举报