Java Swing 皮肤
本文将深入透视 Synth 外观,他是 java 5.0 中为 Swing 引入的最新内容。通过为 Java UI 编程引入“皮肤”的概念,Synth 使研发人员能够为应用程式创建和部署定制的外观。软件工程师 Michael Abernethy 将带您从头开始逐步构建一个具备 synth 外观的应用程式,让您充分了解 Synth 的概念。阅读本文之后,您应该能够在短时间内创建具备专业外观的 UI。
就在 Sun 一如既往地试图“再次引入 Java Desktop”之际,Java UI 研发人员的抱怨之词亦已表面化:要创建完全定制的外观实在太难。这样做不但要花费太多的时间,并且 Swing UI 代码的编写和文档的编制也极为不堪,经常是乱杂一气,缺乏规划。为了创建完整的外观,研发人员需要继续 Metal 外观的 39 个类,或继续 Basic 外观的 60 个类。谁想通过重写整个包来改变应用程式呈现外观的方式呢?用 Swing 创建定制外观有多难,通过下面的事实同样可窥见一斑:在很多研发人员为开源项目添砖加瓦的时代,Internet 上可用的自定义 Swing 外观几乎是凤毛麟角 ―― 总共大约是 20 个,其中少数在 SourceForge.net 上(请参阅参考资料)。
漂亮只是肤浅的东西
进入 Synth,Sun 希望他能使应用程式外观的个性化过程变得轻易。Synth 的目标很简单 ―― 让研发人员不必编写任何代码就能够创建新的外观。这似乎是个不错的解决方案。程式员一般没有突出的艺术才华,而图像设计人员通常也不是 Java 编程专家。Synth 把对外观的任何描述从代码中分离出来,而将其放入外部的 xml 文档和图像文档中,为上述问题提供了大快人心的解决之道。这种完全在外部文档中描述的外观被称作皮肤(SKIN)。
Sun 的皮肤概念并不是什么创新。例如,WinAMP 有数百种皮肤,Firefox 也有几十种皮肤,这些皮肤很轻易创建,只需更改一个 XML 文档即可。想像一下,仅仅修改一个 XML 文档,就能快速、轻易地为 Java 应用程式创建一个外观。再想想这样一来的结果 ―― 几百个互不相同的 Swing 外观。Java UI 研发人员当然有理由欢呼了。
本文将深入分析 Synth 外观,向您展示创建一个完整的外观或皮肤所需知道的一切。您会看到一个带有示例皮肤的应用程式,这个应用程式使用了 Synth 任何重要的概念。然后,我会逐步剖析这个皮肤,在构建 XML 文档的过程中,一一教会您 Synth 的各个概念。
本文最后一节将尽力回答研发人员关于 Synth 性能、bug 和缺陷连同 Synth 在省时方面的表现等种种问题。阅读本文之后,您应该会愿意拥护Synth 作为外观解决方案,并预备马上使用他来创建自己的 Swing 外观。
Synth 基础
Synth 是个白板(tabula rasa)外观 ―― 一块完全空白的画布,表现为一个完全空白的面板(panel),只有在 XML 文档中定义了组件时,他才会显示东西。一旦定义了组件,在应用程式上配置 Synth 外观就再轻易但是了,如清单 1 所示:
清单 1. 配置 Synth 外观
SynthLookAndFeel synth = new SynthLookAndFeel();
synth.load(SynthFrame.class.getResourceAsStream("demo.xml"), SynthFrame.class);
UIManager.setLookAndFeel(synth);
但是,对于 Synth,最重要的是要理解他是 XML 代码,而不是 Java 代码。虽然 Synth XML 格式一开始看上去比较吓人,但实际上很简单。假如使用 KISS (Keep It Simple Stupid)这道符咒,您能够快速地创建一个 XML 文档,并得到一个新的、能够运行的外观。
考虑到 KISS 指令,我将首先介绍 Synth XML 文档的主要构件 ―― <style> 标签。<style> 标签包含描述一个组件的式样的任何信息,例如颜色、字体、图像文档、状态,连同一些特定于组件的属性。虽然一个 <style> 标签能够描述多个组件,但构建 Synth 文档的最简便方法是为每个 Swing 组件创建一个式样。
创建好式样之后,便能够将式样链接到一个组件。<bind> 标签通知 Synth 引擎将一个已定义的式样链接到一个组件,如清单 2 所示。这样的组合便完全创建了组件的新外观。
清单 2. 将一种式样链接到一个组件
<style id="textfield">
// describe colors, fonts, and states</style><bind style="textfield" type="region" key="Textfield"/><style id="button">
// describe colors, fonts, and states</style><bind style="button" type="region" key="Button"/>
关于 <bind> 标签,要注重的一点是:<bind> 标签中的 key 属性映射到 javax.swing.plaf.synth.Region 类中的常量。Synth 引擎使用这些常量将式样和一个实际的 Swing 组件链接。简单的组件,例如 JButton 和 JTextField,使用一个常量。有些更复杂的组件,例如 JScrollBar 和 JTabbedPane,则有多个常量,用于不同的部分。
我建议您在更熟悉 Synth 格式并且能够配置 XML 中的继续模型之前,使用每个组件一种式样(one-style-per-component)的配置。这种结构虽然没有利用任何 XML 的分层结构功能,但他是最轻易配置、编写代码和调试的。
在处理 Synth XML 文档时,更有一点很重要,并不是任何形式都是合法的。假如有输入错误,或在 XML 中使用了不正确的属性,这些错误只有当外观装载期间抛出一个运行时异常时才能发现。解决方法:在将 XML 文档发布给客户之前,对其进行测试。
Demo 应用程式
我将带您构建一个简单的登录屏幕,用他作为例子应用程式,向您展示 Synth XML 文档的工作原理。该屏幕提供了足够多的组件,通过这些组件,能够看到 XML 文档的任何重要部分,假如使这些部分结合起来便能够创建一个完整的外观。
通过比较图 1 和图 2,具备 Ocean 外观的登录屏幕看上去和您预期的相同 ―― 简单,直接,也令人厌烦。具备 Synth 外观的登录屏幕则完全不同。
图 1. 具备 Ocean 外观的 Demo 应用程式
图 2. 具备 Synth 外观的 Demo 应用程式
更改颜色和字体
为 demo 应用程式创建外观的第一步是配置默认颜色和字体。您将把 white Aharoni 字体作为每个组件的默认字体,假如没有非凡配置组件的话,就使用这种字体。
您能够将更改字体的 XML 放在 <style> 标签内的任何地方。还能够将颜色嵌入到一个 <state> 标签中。在本文的后面部分,我将更具体地讨论 <state> 标签,但现在只需知道,一个简单的、不带属性的 <state> </state> 标签能够包含任何状态,这个标签正是您在这里所需要的。
color 标签本身需要两个属性:
value 能够是 java.awt.Color 常量的任何 String 表示(例如 RED、BLUE),或,他能够是一种颜色的十六进制表示,前面加上 "#" (例如#669966)。
type 描述文档应该配置哪个区域的颜色。选择有 BACKGROUND、FOREGROUND、TEXT_FOREGROUND、TEXT_BACKGROUND 和 FOCUS。
font 标签有两个必需的属性和一个可选属性。这三个属性直接映射到 java.awt.Font 类中的三个参数:
name :字体的名称(例如,Verdana、Arial)。
size :字体大小,以像素为单位。
style :假如不使用这个可选标签,那么将得到常规外观的字体。其他选项包括 BOLD 和 ITALIC。您还能够通过在这两个属性之间加一个空格来指定粗体加斜体的字体:BOLD ITALIC(这种组合属性的技术对于 Synth XML 文档中的任何属性都适用)。
最后,通过使用 .* wildcard,将这个式样绑定到应用程式中的每个组件,而不是将其绑定到每个 JLabel 和每个JButton。这个通配符告诉 Synth 外观为每个组件指定一个默认的 white Aharoni 字体。清单 3 展示了用于配置组件字体和颜色的完整 XML 代码:
清单 3. 更改多个组件的字体和颜色
<style id="default">
<font name="Aharoni" size="14"/>
<state>
<color value="#FFFFFF" type="FOREGROUND"/>
</state></style><bind style="default" type="region" key=".*"/>
使用图像
图 2 中的 textfield 边框不是常规外观的单像素矩形边框。能够使用一个图像来创建这些边框。这不是我们所熟悉的概念 ―― 图像用在 button和 label 中已有些时候了 ―― 但您能够想像在哪些地方会出问题。怎样知道光标移动到什么地方,怎样显示文本,怎样创建不同大小的文本域?这些问题能够通过图像拉伸(image stretching)的概念来解决。一个图像文档必须描述应用程式中文本域各个边的长度,因此需要有一种方式来告诉 XML 文档怎样适当地拉伸图像,连同怎样处理常规的 textfield 活动(carat 和文本控制)。
幸运的是,从早期带皮肤的应用程式起,就有一个方法可用于处理这种类型的拉伸。图像必须分成 9 个区域 ―― 顶部、右上、右部、右下、底部、左下、左部、左上和中间 ―― 这些区域是通过 XML 文档中的一个属性来指定的。然后呈现程式能够通过一定的方式拉伸图像,以适合指定的空间。图 3 展示了文本域图像是怎样拉伸的。
图 3. 在 Synth 中图像怎样拉伸
图 3 中绿色填充区只会垂直拉伸。也就是说,当文本域比图像高的时候,这些区域就会变高。当文本域比图像长的时候,那些红色填充区只会水平拉伸。而黄色填充区则是大小固定的。不管文本域的大小怎样,这些区域都会如他们在图像文档中那样显示。因为这些区域不会拉伸,因此他们应该包含任何画布、非凡底色、阴影和任何一旦拉伸就会看起来很古怪的东西。最后,中间区域是可选的。您能够选择画出或忽略该区域。在我们的例子中,文本域的中间被忽略。此后,呈现程式使用这个区域来处理文本控制和 carat。也就是说,使用一个图像文档完全画出文本域。
imagePainter 标签提供了在外观中使用图像所需的任何信息。他只需要几个属性:
path :所使用的图像的路径。
sourceInsets :按像素计算的 insets,表示图 3 中绿色区域的宽度和粉红色区域的高度。他们依次映射到顶部、左部、底部和右部。
method :这也许是最令人费解的属性。他直接映射到 javax.swing.plaf.synth.SynthPainter 类中的一个函数。这个类包含大约 100 个函数,任何这些函数都以 paint 开始。每个函数映射到在一个 Swing 组件中某个特定的绘画任务。您只需找到一个合适的函数,然后去掉 paint 字符串,并使随后的首个字母为小写形式,便能够配置该属性。例如,paintTextFieldBorder 是 textFieldBorder 的属性。呈现程式(renderer)负责剩下的工作。
paintCenter :该属性答应您保留或舍弃图像的中间区域(例如在一个按钮中)。在这个例子中,textfield 舍弃了中间区域,以便显示文本。
使用图像画边框的最后一步是加大默认的 insets,以便处理用来画这些 insets 的图像。假如没有更改 insets,那么就看不见任何图像。您需要添加一个 <insets> 标签来增加 insets,以便在其中画出图像。在大多数情况下,insets 的值应该和在图像中使用的 insets 的值相同。
清单 4 展示了用于装载图像的 XML 代码。注重 sourceInsets 怎样确保图像只有适当的部分被拉伸。
清单 4. 装载图像
<style id="textfield">
<opaque value="true"/>
<state>
<font name="Aharoni" size="14"/>
<color value="#D2DFF2" type="BACKGROUND"/>
<color value="#000000" type="TEXT_FOREGROUND"/>
</state> <imagePainter method="textFieldBorder" path="images/textfield.png"
sourceInsets="4 6 4 6" paintCenter="false"/>
<insets top="4" left="6" bottom="4" right="6"/></style><bind style="textfield" type="region" key="TextField"/>
处理不同的状态
从前面的例子能够看到,<state> 标签是定义一个组件的焦点所在。在清单 3 和清单 4 中,color 和 font 标签都处在 <state> 标签内。现在我将解释 <state> 标签的作用。
默认状态是在 <state> 标签中没有指定属性,这对于定义文本域和 label 中的颜色和字体已足够了,因为这两种组件的状态不会改变。但是在那些状态会改变的组件中(例如按钮),能够为每种状态定义完全不同的外观。每种状态能够有他自己的颜色、字体和图像。您能够比较登录屏幕中 Cancel 按钮在默认状态(图 4)和 mouse-over 状态(图 5)下的不同。
图 4. DEFAULT 状态下的 Cancel 按钮
图 5. MOUSE_OVER 状态下的 Cancel 按钮
<state> 标签只需要一个 value 属性,该属性定义了实际的组件状态。假如没有指定 value,如清单 3 和 4 所示,那么每种状态都使用默认值。假如指定 value 属性,那么能够选择 ENABLED、MOUSE_OVER、PRESSED、DISABLED、FOCUSED、SELECTED 和 DEFAULT。这些选择包含 Swing 中任何组件任何可能的状态。您还能够在不同选择间添加 and 来组合各种状态。例如,假如您想在鼠标位于按钮之上连同按钮被按下的时候改变按钮上的字体,那么能够使用状态值 MOUSE_OVER and PRESSED。
清单 5 展示了用于处理 demo 应用程式状态的 XML。注重每种状态是怎样定义不同的图像和文本颜色的。
清单 5. 处理状态
<style id="button">
<state>
<imagePainter method="buttonBackground" path="images/button.png"
sourceInsets="9 10 9 12" paintCenter="true" stretch="true"/>
<insets top="9" left="10" bottom="9" right="12"/>
<font name="Aharoni" size="16"/>
<color type="TEXT_FOREGROUND" value="#FFFFFF"/>
</state> <state value="MOUSE_OVER">
<imagePainter method="buttonBackground" path="images/button_on.png"
sourceInsets="9 10 9 12" paintCenter="true" stretch="true"/>
<insets top="9" left="10" bottom="9" right="12"/>
<color type="TEXT_FOREGROUND" value="#FFFFFF"/>
</state>
<state value="PRESSED">
<imagePainter method="buttonBackground" path="images/button_press.png"
sourceInsets="10 12 8 9" paintCenter="true" stretch="true"/>
<insets top="10" left="12" bottom="8" right="9"/>
<color type="TEXT_FOREGROUND" value="#FFFFFF"/>
</state> <property key="Button.margin" type="insets" value="0 0 0 0"/></style><bind style="button" type="region" key="Button"/>
处理 <state> 标签的一个重要方面是知道哪些组件有哪些状态。显然,在这个例子中,按钮能够拥有默认状态、鼠标悬停(mouse-over)状态和被按下(pressed) 状态。对于这个例子,还能够定义一个聚焦(focused)和禁用(disabled)状态。但是对于一个面板,选中(selected)状态根本不适用,当鼠标处于面板之上时假如改变面板的状态,那么只能招来抱怨。
处理特定于组件的属性
定义对每种组件都通用的 XML 属性时,总是忽略了一些特定于组件的属性。例如 list 的行高、单选钮的图标和菜单的箭头图标,这些都是特定于组件的属性。能够定义的特定于组件的属性有 100 多种,但是为每个这样的属性定义一个 XML 属性就有些过分了。因此,Synth XML 文档答应配置特定于组件的属性。<property> 标签就像一个 Hashtable,他定义一个键/值对来配置属性。
登录屏幕示例的复选框演示了怎样为特定于组件的属性编写代码。通过定义 imageIcon,能够配置默认状态和选中状态下的 CheckBox.icon。这就像是翻遍 100 个属性找到您想要的属性那样简单。
清单 6 展示了为登录屏幕中特定于组件的属性编写代码的 XML。注重要首先定义 imageIcon。然后,通过使用图像图标的 ID,能够为复选框的每种状态配置一个图标。
清单 6. 定义特定于组件的属性
<style id="checkbox">
<imageIcon id="check_off" path="images/checkbox_off.png"/>
<imageIcon id="check_on" path="images/checkbox_on.png"/>
<property key="CheckBox.icon" value="check_off"/>
<state value="SELECTED">
<property key="CheckBox.icon" value="check_on"/>
</state></style><bind style="checkbox" type="region" key="Checkbox"/>
使用定制 painter
定义图 2 中登录屏幕例子的最后工作是用曲线绘制渐变背景。用 XML 来实现这种背景似乎有些别扭,坦白地说,真是这样。但这样我便有机会展示 Synth,不限制您在 UI 设计中只使用图像和简单的颜色。您能够使用他来画任何东西。
Synth 答应重写其 paint 方法(即在 javax.swing.plaf.synth.SynthPainter 类中的方法),该方法继续自 SynthPainter,他将覆盖那些您想要定制绘画方式的特定函数。在这个例子中,需要定义 paintPanelBackground 方法,因为这种设计不能以 Synth XML 格式描述。
为了使用定制的 painter,或在 XML 中以任何方式创建一个类,能够使用 <object> 标签。<object> 标签答应创建和保持用于弥补 Synth 呈现程式的任何 Java 类。<object> 标签带有两个元素:
class :将创建的类的全名。
id :用于在 XML 文档中引用这个类实例的 ID 名。
通过使用对象,不但能够创建 BackgroundPainter 类的实例 ―― 这个类将用于绘制背景,而且还能够创建 ColorUIResource 类的实例,在这个类中能够定义背景颜色。想一想:在 BackgroundPainter 类中定义背景中使用的颜色,这和 Synth 的目标是矛盾的,Synth 的目标是在一个外部 XML 文档中定义一切,而不是在一个 Java 文档中进行硬编码。
使用定制 painter 的最后一步是告诉 Synth 呈现引擎,是您自己而不是 SynthPainter 类来提供函数。在这个例子中,首先在 BackgroundPainter 类中定义 paintPanelBackground 函数,并让 SynthPainter 类定义剩下的绘画函数。<painter> 标签让您能够覆盖 SynthPainter 函数。他带有两个元素:
method :定制 painter 应该覆盖的方法。从 使用图像 一节中您已得知,您能够在 javax.swing.plaf.synth.SynthPainter 类中找到这些函数,但是应该删除每个函数开始部分的 paint 字符串(例如,SynthPainter 中的 paintPanelBackground 在 XML 文档中应该是 panelBackground)。
id:对将覆盖此方法的类的引用。
为了在定制 painter 中使用颜色,必须将颜色保存在 javax.swing.UIDefaults 类中。在清单 7 和清单 8 中能够看到,将颜色保存在 UIDefaults 中十分简单,对于那些接触过 UI 创建的人来说应该,应该比较熟悉这些内容。在 XML 文档中定义的键将成为 UIManager 中的引用,在 BackgroundPainter 的 Java 代码中,能够使用 UIManager 来获得颜色。
清单 7 展示了在例子应用程式中使用定制 painter 的 XML 代码。注重必须首先定义颜色。
清单 7. 使用定制 painter
<style id="panel">
<object id="background" class="demo.synth.BackgroundPainter"/>
<object class="javax.swing.plaf.ColorUIResource" id="startColor">
<int>30</int>
<int>123</int>
<int>235</int>
</object>
<defaultsProperty key="Panel.startBackground" type="idref" value="startColor"/>
<object class="javax.swing.plaf.ColorUIResource" id="endColor">
<int>1</int>
<int>20</int>
<int>80</int>
</object>
<defaultsProperty key="Panel.endBackground" type="idref" value="endColor"/>
<painter method="panelBackground" idref="background"/></style><bind style="panel" type="region" key="Panel"/>
清单 8 展示了例子应用程式的定制绘画类的 Java 代码:
清单 8. 定制绘画的 Java 代码
public class BackgroundPainter extends SynthPainter{ public void paintPanelBackground(SynthContext context,
Graphics g, int x, int y,
int w, int h) {
Color start = UIManager.getColor("Panel.startBackground");
Color end = UIManager.getColor("Panel.endBackground");
Graphics2D g2 = (Graphics2D)g;
GradientPaint grPaint = new GradientPaint(
(float)x, (float)y, start,
(float)w, (float)h, end);
g2.setPaint(grPaint);
g2.fillRect(x, y, w, h);
g2.setPaint(null);
g2.setColor(new Color(255, 255, 255, 120));
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
CubicCurve2D.Double arc2d = new CubicCurve2D.Double(
0, h/4, w/3, h/10, 66 * w, 1.5 * h, w, h/8);
g2.draw(arc2d);
g2.setRenderingHint(
RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
}}
更高级的配置
本节包含两个超出登录屏幕例子范围的技术。在创建您自己的 Synth 外观时,您可能发现这两项技术很有用。
绘制非 Swing 组件
能够改变每个 Swing 组件的外观这一点虽然很棒,但是还应该能够改变其他组件 ―― 研发人员创建的用于填补 Swing 空缺的组件 ―― 的外观。在这种情况下,<bind> 标签需要作出改变,以反映正在绘制的不是个 Swing 组件。type 属性能够有两种值:假如映射到一个 Swing 组件,则该值为 region,假如映射到非 Swing 组件,则该值为 name。因此,假如将 <bind> 标签变为 <bind style="mystyle" type="name" key="Custom.*"/>,则会改变每个类名以 Custom 开始的组件(例如,CustomTextField 或 CustomLabel),使他们使用 mystyle 式样。
式样的分层结构
除了在创建 XML 文档时使用 KISS 式样之外,还能够构建分层次的一些式样,并将这些式样应用于组件中。清单 9 应该能够清楚地演示这一点。注重,Synth 使用最后定义的属性来显示组件。
清单 9. 分层结构的例子
<style id="base">
<color value="BLACK" type="BACKGROUND"/>
<state>
<font size="14"/>
</state>
</style>
<bind style="base" type="region" key=".*"/>
<style id="sublevel" clone="base">
<color value="RED" type="BACKGROUND"/>
</style>
<bind style="sublevel" type="region" key="Label"/>
清单 9 中的代码使每个组件有一个黑色的背景,字体大小为 14,但 label 组件除外,label 组件拥有红色的背景。通过克隆 sublevel 中的 base 式样,清单 9 复制了整个式样。然后,您能够覆盖所需的任何特定属性。
检验 Synth 的性能、可靠性和效率
至此,您已看到怎样创建用于 Synth 的 XML 文档,连同怎样通过更改字体、更改颜色和添加图像来创建定制的外观,但对于 Synth 可能更有些疑问。假如您使用 Swing 已有一段时间,那么我能够肯定,您首先想到的是性能问题。我设计了一些性能测试,这些测试表明,Synth 不会令您的 UI 慢如蜗牛。为了调查您可能看到的问题(并讨论我在使用 Synth 时已碰到过的一些问题),我查看了 Java Bug Parade (请参阅 参考资料)。最后,我将回答最重要的问题 ―― Synth 真的能够节省您的时间吗?
装载那么多图像会不会使 Synth 变得更慢?
为了回答这个问题,我创建了两个测试,并让您更深切地体会 Synth 在性能方面和其他外观的比较。第一个测试将测试示例登录应用程式的装载时间。该测试装载 6 个 Synth 图像,并将这个装载时间和一个研发人员可能创建的一般屏幕的装载时间进行比较。第二个测试是关于装载时间的压力测试 ―― 一个帧中有 100 多个组件。
两个测试都将测试 Ocean 和 Motif 外观的装载时间,以便进行比较。为了公正起见,我在三种机器上运行了这两个测试 ―― 一种是安装 Windows xp 的手提电脑,一种是 SuSE linux box,更有一种是 Red Hat Linux box。结果显示在表 1 和表 2 中。
表 1. 登录屏幕的平均装载时间
机器配置 Ocean Motif Synth
Windows XP - 1.7GHz - 2GB RAM .32 seconds .29 seconds .57 seconds
SuSE Linux 9.0 - 3.3GHz - 2GB RAM .23 seconds .20 seconds .45 seconds
Red Hat Linux 3.0 - 1.4GHz - 512MB RAM .37 seconds .32 seconds .61 seconds
表 2. 包含 100 个组件的屏幕的平均装载时间
机器配置 Ocean Motif Synth
Windows XP - 1.7GHz - 2GB RAM .33 seconds .32 seconds .34 seconds
SuSE Linux 9.0 - 3.3GHz - 2GB RAM .23 seconds .23 seconds .30 seconds
Red Hat Linux 3.0 - 1.4GHz - 512MB RAM .40 seconds .40 seconds .43 seconds
您能够看到,Synth 外观的装载时间只比 Ocean 和 Motif 慢一点点。但是请注重,登录屏幕和压力测试会比装载更慢一些。乍一看来,这似乎很希奇,但假如仔细研究,便能够发现起因。压力测试没有装载复选框中所使用的图像,而登录屏幕却装载了这些图像。据此能够下结论,在 Synth 外观中使用的每个附加图像增加了装载时间。和含有两个使用两种不同图像的组件的应用程式相比,使用相同图像的 100 个组件装载起来要更快一些。减少所使用图像的数量能够提高 Synth 装载时间方面的性能。
Synth 是不是像 Swing 相同,在第一次发布时满是 bug?
根据 Sun Java 研发者网站上 Bug Parade 的评判,Synth 看上去是个比较干净、没有 bug 的产品。然而,没有哪个软件是完美的。Synth 曾有 125 个 bug,这和 Synth 处理 JTabbedPane 的方式不成比例。因此,假如您经历到一些问题,不要感到惊奇。然而,根据 Sun 的辩护,这些缺陷都处于“关闭(Closed)”状态。但通常的情况是,假如以前存在某些问题,那么这些问题在将来也很可能会出现。
虽然 bug 数据库为 Synth 赋予了一个相对干净的形象,我在处理登录屏幕的时候还是碰到一些问题。我第一次尝试更改 JPanel 背景颜色时遭到失败。我创建了一个特定于 JPanel 的式样,并将其绑定到任何 JPanel,但这样行不通。而当我决定使用自己的定制 painter 时,事情就解决了。
一个更大的问题是当状态改变时对组件进行重新绘制。在处理按钮及其状态时,我发现,按钮上的文本不能正确地改变颜色。当初始化时,作为默认颜色的白色没有如期显示,并且直到触发了状态变化之后才出现,然后就被重新配置为默认颜色。假如仔细研究关于 Synth 的文档,就能够发现这个小花絮:“虽然能够为每种状态提供不同的字体,但在一般情况下,当组件的状态变化时,组件不会重新生效,所以,假如您试图为不同状态使用有明显不同大小的字体时,有可能会碰到字体大小的问题”。听起来似乎他们遇上了试图让 Synth 使用老的 Swing 代码的问题。因此,假如要在状态改变时更改字体,那么要小心。
Synth 看上去的确很少有 bug。但假如随处出点小问题,那些本应该行得通的代码就会行不通,我不会对此感到惊奇。但是,变通的办法不难找到。对于在工作中碰到的每个问题,我总能找到一个变通的办法。
利用 Synth 能够创建出完全专业的外观吗?
回答是肯定的。Java 1.4 中发布的 GTK+ 和 Windows XP 外观就完全是用 Synth 创建的。(那时他不是个已公开的 API。) 所以这方面显然没有问题。
用 Synth 创建一个完整的外观比用 Java 代码编写这样的外观要快多少?
这很轻易计算。这两种方法各自都包含两个步骤:
创建外观,这通常是由图像设计人员负责的工作。
将图像界面转化成代码。
不管是用 Java 编写代码还是使用 Synth,图像界面设计这部分工作所花的时间是相同的。根据我创建定制外观的经验,我估计为一个应用程式创建一个完整的外观需要两个图像设计人员两周的时间。也就是说,图像设计工作需要 4 人一周(person-week)的人力。
通常,根据我的经验,通过类继续的方式将图像界面翻译成立即可用的外观需要三个 Java 编程人员花大约两个月的时间。也就是说,编写 Java代码需要 6 个人一个月(person-month)的人力。加上图像界面设计工作,通过重写 UI 类,用 Swing 创建一个完全定制的外观总共需要 7 个人一个月的工作量。这些数据有助于您明白为什么 Internet 上可供下载的定制外观是那么少。
通过将图像界面转换成一个 XML 文档,Synth 能够节省大量的时间。通过 Java 编程创建外观需要 6 个人一个月的工作量,而一个研发人员将图像界面转换成 Synth XML 文档只需两个星期。用 Synth 创建完整外观所需的工作量减少到仅仅 6 个人一周的工作量 ―― 通过使用 Synth 节省了超过 5 个月的时间。对于一个由两个图像设计师和两个程式员组成的团队,在短短三个星期内便能够创建出一个完整的 Synth 外观。
结束语
Synth 将皮肤的概念引入到 Swing 中。相对于传统的用 Java 代码编写定制外观的方法,Synth 最大的优势是节省时间。一个完整的 Swing 外观能够在不到一个月的时间里完成,这比用 Java 语言编程的方法要快 5 倍。对于有干劲的研发人员,在用 Java 代码编写一个外观的时间里,他能够创建 5 个 Synth 外观。
然而,Synth 并非毫无瑕疵。通过编写 Java 代码覆盖 Swing 外观,能够同时改变应用程式的外观和感觉 。而 Synth 只答应改变应用程式的外观。这是个很大的不同之处。外观是指应用程式中使用的颜色、字体和图像。另一方面,感觉则对应于应用程式在交互期间展现出来的行为 ―― 这里指单击一下鼠标右键,那里按下一个键。例如,假如您想改变一个 JList 的行为,希望通过单击鼠标左键选中条目,然后再通过单击鼠标右键来删除条目,那么用 Synth 是无法做到这些的。您需要为新的外观编写 Java 代码。Synth 实际上应该称为一种新的 Swing 外观,而不是一种普通外观。通过 Synth 能够快速改变 UI 的外观,但 UI 的感觉永远都是默认的 Swing 感觉。
当然,假如您想通过为应用程式提供新的外观来使之整洁漂亮,或渴望看到比令人讨厌的 Metal 外观(谢天谢地,在 Java 5.0 中他已成为历史)更好的 Swing 应用程式外观,那么 Synth 是很好的一个选择。他不存在性能问题,并且看上去 bug 也很少。Sun 已表示,通过发布 GTK+ 外观,用 Synth 能够创建完整的外观。
令人吃惊的是,Synth 文档和实例现在还很少。阅读本文之后,对于 Synth 的工作原理您应该有一个更深的理解,并且能够使用一个组件一个样式标签(one-style-tag-per-one-component)的设计来生成一个完整的 Synth XML 文档。Synth 的继续和分层模型为创建 style 标签提供了更强大的方法,但没有他们仍然能够创建完整的外观。理想情况是:随着对 Synth 熟悉的加深,Swing UI 社区将出现皮肤数量的大爆炸。有了数百个可供选择的外观,通常那些加在 Swing 应用程式身上的“长相恐怖”、“丑陋”之类的责骂之词也将永远消失。