文档

Java™教程
隐藏目录
Synth外观
路径: 使用Swing创建GUI
课程: 修改外观和感觉

合成外观

创建自定义外观或修改现有外观可能是一项令人生畏的任务。可以使用javax.swing.plaf.synth包来更轻松地创建自定义外观。您可以通过编程或使用外部XML文件创建Synth外观。下面的讨论重点介绍使用外部XML文件创建Synth外观。有关通过编程创建Synth外观的讨论,请参阅API文档。

在Synth外观中,您提供“外观”,而Synth本身提供“感觉”。因此,您可以将Synth L&F视为“皮肤”。

Synth架构

回顾前面的主题,每个L&F都负责为Swing定义的许多ComponentUI子类提供具体实现。相比之下,使用Synth L&F时,您必须为每个组件创建一个SynthStyle

为了使用Synth,您需要直接(即以编程方式)或间接(从XML文件)提供SynthStyleFactorySynthStyles。Synth本身会根据SynthStyles创建必要的ComponentUI实现。

Synth中的每个ComponentUI实现都派生自一个SynthStyle,每个Region有一个SynthStyle(大多数组件只有一个Region,因此只有一个SynthStyle)。SynthStyle用于访问所有与样式相关的属性:字体、颜色和其他组件属性。此外,SynthStyles用于获取用于绘制背景、边框、焦点和其他部分的SynthPainters

ComponentUIsSynthStyleFactorygetStyle(JComponent c, Region id)方法获取SynthStyles。可以通过以下方式直接(以编程方式)提供SynthStyleFactory

SynthLookAndFeel.setStyleFactory(javax.swing.plaf.synth.SynthStyleFactory)
SynthLookAndFeel.load(java.io.InputStream, java.lang.Class)

当您使用外部XML文件时,您定义了<style>元素,Synth将其映射到SynthStyles

ComponentUIComponentUI

Synth在组件的更精细级别上操作,这个级别被称为“区域”。每个组件都有一个或多个区域。许多组件只有一个区域,例如JButton。其他组件有多个区域,例如JScrollBar。Synth提供的每个ComponentUI都将SynthStyleComponentUI定义的每个区域关联起来。例如,Synth为JScrollBar定义了三个区域:轨道、滑块和滚动条本身。Synth为JScrollBar定义的ScrollBarUI(为JScrollBar定义的ComponentUI子类)实现将SynthStyle与这些区域中的每个区域关联起来。

Synth Architecture Drawing.

SynthStyle提供了Synth ComponentUI实现使用的样式信息。例如,SynthStyle定义了前景色和背景色、字体信息等。此外,每个SynthStyle都有一个SynthPainter用于绘制区域。例如,SynthPainter定义了paintScrollBarThumbBackgroundpaintScrollBarThumbBorder这两个方法,用于绘制滚动条的滑块区域。

Synth中的每个ComponentUI都使用SynthStyleFactory获取SynthStyles。有两种方法可以定义SynthStyleFactory:通过Synth XML文件或以编程方式。以下代码显示了如何加载指定了Synth外观的XML文件 - 在内部,这将创建一个从XML文件中填充了SynthStylesSynthStyleFactory实现:

  SynthLookAndFeel laf = new SynthLookAndFeel();
  laf.load(MyClass.class.getResourceAsStream("laf.xml"), MyClass.class);
  UIManager.setLookAndFeel(laf);

以编程方式的路线涉及创建一个返回SynthStylesSynthStyleFactory实现。以下代码创建了一个自定义的SynthStyleFactory,它为按钮和树返回不同的SynthStyles

 class MyStyleFactory extends SynthStyleFactory {
     public SynthStyle getStyle(JComponent c, Region id) {
         if (id == Region.BUTTON) {
             return buttonStyle;
         }
         else if (id == Region.TREE) {
             return treeStyle;
         }
         return defaultStyle;
     }
 }
 SynthLookAndFeel laf = new SynthLookAndFeel();
 UIManager.setLookAndFeel(laf);
 SynthLookAndFeel.setStyleFactory(new MyStyleFactory());

XML文件

Synth XML文件的DTD说明可以在javax.swing.plaf.synth/doc-files/synthFileFormat.html找到。

当加载Synth外观时,只会呈现那些有定义的GUI组件(或区域)(绑定到该区域的“样式”,如下所讨论)。没有任何组件的默认行为 - 在Synth XML文件中没有样式定义的情况下,GUI是一个空白画布。

要指定组件(或区域)的渲染,您的XML文件必须包含一个<style>元素,然后使用<bind>元素将其绑定到该区域。例如,让我们定义一个包括字体、前景色和背景色的样式,然后将该样式绑定到所有组件。在开发Synth XML文件时包含这样一个元素是个好主意 - 这样,您尚未定义的任何组件至少会有颜色和字体:

<synth>
  <style id="basicStyle">
    <font name="Verdana" size="16"/>
    <state>
      <color value="WHITE" type="BACKGROUND"/>
      <color value="BLACK" type="FOREGROUND"/>
    </state>
  </style>
  <bind style="basicStyle" type="region" key=".*"/>
</synth>

让我们分析这个样式定义:

  1. <style>元素是Synth XML文件的基本构建块。它包含描述区域渲染所需的所有信息。一个<style>元素可以描述多个区域,就像这里做的一样。不过,通常最好为每个组件或区域创建一个<style>元素。请注意,<style>元素被赋予了一个标识符,即字符串“basicStyle”。这个标识符将在后面的<bind>元素中使用。

  2. <style>元素的<font>元素设置字体为Verdana,大小为16。

  3. <style>元素的<state>元素将在下面讨论。区域的<state>元素可以具有七个可能值中的一个或混合使用。当未指定值时,定义适用于所有状态,这是本例的意图。因此,在此元素中定义了“所有状态”的背景色和前景色。

  4. 最后,刚刚定义的具有标识符“basicStyle”的<style>元素被绑定到所有区域。<bind>元素将“basicStyle”绑定到“region”类型。绑定适用于哪个区域类型由“key”属性给出,此处为“.*”,表示“所有”的正则表达式。

在创建一些工作示例之前,让我们来看一下Synth XML文件的各个部分。我们将从<bind>元素开始,展示给定的<style>如何应用于组件或区域。

<bind>元素

每当定义一个<style>元素时,必须将其绑定到一个或多个组件或区域,然后才能生效。<bind>元素用于此目的。它需要三个属性:

  1. style是先前定义的样式的唯一标识符。

  2. type可以是"name"或"region"。如果type是name,则使用component.getName()方法获取名称。如果type是region,则使用javax.swing.plaf.synth包中Region类中定义的适当常量。

  3. key是用于确定样式绑定到哪些组件或区域的正则表达式。

Region是标识组件或组件部分的方式。Region基于Region类中的常量,通过去掉下划线进行修改:

例如,要识别SPLIT_PANE区域,您可以使用SPLITPANE、splitpane或SplitPane(不区分大小写)。

当您将样式绑定到区域时,该样式将应用于所有具有该区域的组件。您可以将样式绑定到多个区域,也可以将多个样式绑定到一个区域。例如:

<style id="styleOne">
   <!-- styleOne的定义在此处 -->
</style>

<style id="styleTwo">
   <!-- styleTwo的定义在此处 -->
</style>

<bind style="styleOne" type="region" key="Button"/>
<bind style="styleOne" type="region" key="RadioButton"/>
<bind style="styleOne" type="region" key="ArrowButton"/>

<bind style="styleTwo" type="region" key="ArrowButton"/>


您可以绑定到单个命名组件,无论它们是否也作为区域绑定。例如,假设您希望将GUI中的“OK”和“Cancel”按钮与其他按钮区别对待。首先,您需要使用component.setName()方法给OK和Cancel按钮命名。然后,您需要定义三种样式:一个用于一般按钮(region = "Button"),一个用于OK按钮(name = "OK"),一个用于Cancel按钮(name = "Cancel")。最后,您可以像这样绑定这些样式:

<bind style="样式按钮" type="区域" key="按钮">
<bind style="样式确定" type="名称" key="确定">
<bind style="样式取消" type="名称" key="取消">

结果是,"确定"按钮绑定到"样式按钮"和"样式确定",而"取消"按钮绑定到"样式按钮"和"样式取消"。

当一个组件或区域绑定到多个样式时,这些样式将会合并。


注意: 

就像一个样式可以绑定到多个区域或名称一样,多个样式也可以绑定到一个区域或名称。这些多个样式将会合并到该区域或名称中。后定义的样式具有优先权。


<state>元素

<state>元素允许你为一个区域定义与其"状态"相关的外观。例如,你通常希望一个被"按下"的按钮与其"启用"状态下的按钮外观不同。在Synth XML DTD中定义了七个可能的<state>值。它们是:

  1. 启用
  2. 鼠标悬停
  3. 按下
  4. 禁用
  5. 焦点
  6. 选中
  7. 默认

你也可以有由'and'分隔的复合状态,例如启用和焦点。如果你没有指定一个值,定义的外观将适用于所有状态。

以下是一个指定了每个状态绘制器的样式示例。所有按钮都以某种方式绘制,除非状态为"PRESSED",在这种情况下它们将以不同的方式绘制:

<style id="按钮样式">
  <property key="Button.textShiftOffset" type="整数" value="1"/>
  <insets top="10" left="10" right="10" bottom="10"/>

  <state>
    <imagePainter method="buttonBackground" path="images/button.png"
                         sourceInsets="10 10 10 10"/>
  </state>
  <state value="PRESSED">
    <color value="#9BC3B1" type="BACKGROUND"/>
    <imagePainter method="buttonBackground" path="images/button2.png"
                        sourceInsets="10 10 10 10"/>
  </state>
</style>
<bind style="按钮样式" type="区域" key="按钮"/>

忽略<property>和<insets>元素,你可以看到按下的按钮与未按下的按钮绘制方式不同。

使用的<state>值是与区域状态最匹配的定义状态。匹配是根据与区域状态匹配的值的数量来确定的。如果没有匹配的状态值,则使用没有值的状态。如果有匹配的状态,则选择具有最多个体匹配的状态。例如,以下代码定义了三个状态:

<state id="零">
  <color value="红色" type="BACKGROUND"/>
</state>
<state value="选中和按下" id="一">
  <color value="红色" type="BACKGROUND"/>
</state>
<state value="选中" id="二">
  <color value="蓝色" type="BACKGROUND"/>
</state>

如果区域的状态至少包含SELECTED和PRESSED,则选择状态一。如果状态包含SELECTED,但不包含PRESSED,则使用状态二。如果状态既不包含SELECTED也不包含PRESSED,则使用状态零。

当当前状态与两个状态定义的相同数量的值匹配时,使用的是样式中首先定义的那个。例如,MOUSE_OVER状态始终对于PRESSED按钮为真(除非鼠标位于其上方,否则无法按下按钮)。因此,如果首先声明MOUSE_OVER状态,则始终选择它而不是PRESSED,并且不会执行为PRESSED定义的任何绘画。

<state value="PRESSED"> 
   <imagePainter method="buttonBackground" path="images/button_press.png"
                          sourceInsets="9 10 9 10" />
   <color type="TEXT_FOREGROUND" value="#FFFFFF"/>      
</state>
      
<state value="MOUSE_OVER">    
   <imagePainter method="buttonBackground" path="images/button_on.png"
                          sourceInsets="10 10 10 10" />
   <color type="TEXT_FOREGROUND" value="#FFFFFF"/>
</state>

上面的代码将正常工作。但是,如果您颠倒文件中MOUSE_OVER和PRESSED状态的顺序,则永远不会使用PRESSED状态。这是因为任何被定义为PRESSED状态的状态也是MOUSE_OVER状态。由于MOUSE_OVER状态首先被定义,因此它是将被使用的状态。

颜色和字体

<color>元素需要两个属性:

  1. value可以是java.awt.Color常量之一,例如RED、WHITE、BLACK、BLUE等。它也可以是RGB值的十六进制表示,例如#FF00FF或#326A3B。

  2. type描述颜色应用的位置,可以是BACKGROUND、FOREGROUND、FOCUS、TEXT_BACKGROUND或TEXT_FOREGROUND。

例如:

  <style id="basicStyle">
    <state>
      <color value="WHITE" type="BACKGROUND"/>
      <color value="BLACK" type="FOREGROUND"/>
    </state>
  </style>

<font>元素有三个属性:

  1. name - 字体的名称。例如,Arial或Verdana。

  2. size - 字体的大小,以像素为单位。

  3. style(可选) - BOLD、ITALIC或BOLD ITALIC。如果省略,则得到一个普通字体。

例如:

  <style id="basicStyle">
    <font name="Verdana" size="16"/>
  </style>

每个<color>元素和<font>元素都有一个备用用法。每个都可以具有id属性或idref属性。使用id属性,可以定义一个可以在稍后使用idref属性重用的颜色。例如,

<color id="backColor" value="WHITE" type="BACKGROUND"/>
<font id="textFont" name="Verdana" size="16"/>
...
...
...
<color idref="backColor"/>
<font idref="textFont"/>

内边距

内边距(insets)是在绘制组件时添加的尺寸。例如,没有内边距,一个标题为Cancel的按钮将刚好足够容纳所选择字体的标题。使用以下<insets>元素:

<insets top="15" left="20" right="20" bottom="15"/>,

按钮将会在标题的上下各增加15像素,在标题的左右各增加20像素。

使用图像绘制

Synth的文件格式允许通过图像来自定义绘制。Synth的图像绘制器将图像分为九个不同的区域:顶部、右上、右侧、右下、底部、左下、左侧、左上和中心。这些区域中的每一个都会被绘制到目标中。顶部、左侧、底部和右侧的边缘会被平铺或拉伸,而角部分(sourceInsets)保持固定。


注意: 

<insets>元素和sourceInsets属性之间没有关联。<insets>元素定义了区域占用的空间,而sourceInsets属性定义了如何绘制图像。<insets>和sourceInsets通常是相似的,但不必相同。


您可以使用paintCenter属性指定是否绘制中心区域。以下图片显示了这九个区域:

九个图像区域。

我们以创建一个按钮为例。我们可以使用以下图像(比实际尺寸要大):

按钮图像。

左上角的红色框是10像素的正方形(包括框的边界)- 它显示了在绘制时不应拉伸的角区域。为了实现这一点,应将顶部和左侧的sourceInsets设置为10。我们将使用以下样式和绑定:

<style id="buttonStyle">
   <insets top="15" left="20" right="20" bottom="15"/>
   <state>
      <imagePainter method="buttonBackground" path="images/button.png"
        sourceInsets="10 10 10 10"/>
   </state>
</style>
<bind style="buttonStyle" type="region" key="button"/>

<state>元素内的行指定了按钮的背景应使用图像images/button.png进行绘制。该路径是相对于传递给SynthLookAndFeel的load方法的Class而言的。sourceInsets属性指定了不应拉伸的图像区域。在本例中,顶部、左侧、底部和右侧的插入区域都是10。这将导致绘制器不会拉伸图像每个角的10 x 10像素区域。

<bind>将buttonStyle绑定到所有按钮上。

<imagePainter>元素提供了渲染区域的所有所需信息。它只需要几个属性:

下面的列表显示了根据按钮的<state>加载不同图像的XML代码

<style id="buttonStyle">
    <property key="Button.textShiftOffset" type="integer" value="1"/>
    <insets top="15" left="20" right="20" bottom="15"/>
    <state>
      <imagePainter method="buttonBackground" path="images/button.png"
                    sourceInsets="10 10 10 10"/>
    </state>
    <state value="PRESSED">
      <imagePainter method="buttonBackground" path="images/button2.png"
                    sourceInsets="10 10 10 10"/>
    </state>
  </style>
  <bind style="buttonStyle" type="region" key="button"/>

button2.png显示了button.png的按下版本,向右移动了一个像素。行

<property key="Button.textShiftOffset" type="integer" value="1"/>

相应地移动按钮文本,如下一节所讨论的。

<property>元素

<property>元素用于向<style>元素添加键值对。许多组件使用键值对来配置它们的视觉外观。

<property>元素有三个属性:

有一个属性表(componentProperties.html)列出了每个组件支持的属性:javax/swing/plaf/synth/doc-files/componentProperties.html

由于button2.png图像在按下时会使可视按钮移动一个像素,我们还应该移动按钮文本。有一个按钮属性可以做到这一点:

<property key="Button.textShiftOffset" type="integer" value="1"/>

示例

下面是一个示例,使用上面定义的按钮样式。按钮样式和一个“背景样式”结合在一起,背景样式中定义了字体和颜色,这些都与所有区域绑定(类似于上面标题为“XML文件”的部分中显示的“basicStyle”)。这些样式在buttonSkin.xml中组合起来。下面是buttonSkin.xml的列表:

<!-- 包含按钮图像的Synth样式 -->
<synth>
  <!-- 所有区域都使用的样式 -->
  <style id="backingStyle">
    <!-- 让使用该样式的所有区域不透明-->
    <opaque value="TRUE"/>
    <font name="Dialog" size="12"/>
    <state>
      <!-- 提供默认颜色 -->
      <color value="#9BC3B1" type="BACKGROUND"/>
      <color value="RED" type="FOREGROUND"/>
    </state>
  </style>
  <bind style="backingStyle" type="region" key=".*"/>
  <style id="buttonStyle">
    <!-- 按下时将文本移动一个像素 -->
    <property key="Button.textShiftOffset" type="integer" value="1"/>
    <insets top="15" left="20" right="20" bottom="15"/>
    <state>
      <imagePainter method="buttonBackground" path="images/button.png"
                    sourceInsets="10 10 10 10"/>
    </state>
    <state value="PRESSED">
      <imagePainter method="buttonBackground" path="images/button2.png"
                    sourceInsets="10 10 10 10"/>
    </state>
  </style>
  <!-- 将buttonStyle绑定到所有JButtons上 -->
  <bind style="buttonStyle" type="region" key="button"/> 
</synth>

我们可以加载这个XML文件,为一个名为SynthApplication.java的简单应用程序使用Synth外观。这个应用程序的GUI包括一个按钮和一个标签。每次点击按钮,标签会增加。


注意: 

即使buttonSkin.xml中没有为标签定义样式,但标签仍然会被绘制。这是因为有一个通用的“backingStyle”包含了字体和颜色。


下面是SynthApplication.java文件的列表。


试试这个: 

点击“启动”按钮,使用Java™ Web Start运行SynthApplication示例(下载JDK 7或更高版本)。或者,要自己编译和运行示例,请参考示例索引

启动SynthApplication示例

使用图标绘制

单选按钮和复选框通常通过固定大小的图标来渲染它们的状态。对于这些,您可以创建一个图标并将其绑定到适当的属性(参考属性表,javax/swing/plaf/synth/doc-files/componentProperties.html)。例如,要绘制选择或未选择的单选按钮,使用以下代码:

<style id="radioButton">
   <imageIcon id="radio_off" path="images/radio_button_off.png"/>
   <imageIcon id="radio_on" path="images/radio_button_on.png"/>
   <property key="RadioButton.icon" value="radio_off"/>
   <state value="SELECTED">   
      <property key="RadioButton.icon" value="radio_on"/>
   </state>
</style>
<bind style="radioButton" type="region" key="RadioButton"/>        

自定义绘制器

Synth的文件格式允许通过JavaBeans组件的长期持久化来嵌入任意对象。这种能力特别适用于提供自己的绘制器,超出了Synth提供的基于图像的绘制器。例如,以下XML代码指定在文本字段的背景中渲染渐变:

<synth>
  <object id="gradient" class="GradientPainter"/>
  <style id="textfield">
    <painter method="textFieldBackground" idref="gradient"/>
  </style>
  <bind style="textfield" type="region" key="textfield"/>
</synth>

其中GradientPainter类如下所示:

public class GradientPainter extends SynthPainter {
   public void paintTextFieldBackground(SynthContext context,
                                        Graphics g, int x, int y,
                                        int w, int h) {
      // 为了简单起见,这里总是重新创建GradientPaint。在实际应用程序中,您应该缓存它以避免产生垃圾。
      Graphics2D g2 = (Graphics2D)g;
      g2.setPaint(new GradientPaint((float)x, (float)y, Color.WHITE,
                 (float)(x + w), (float)(y + h), Color.RED));
      g2.fillRect(x, y, w, h);
      g2.setPaint(null);
   }
}

结论

在这节课中,我们讲解了使用javax.swing.plaf.synth包创建自定义的外观。本课程的重点是使用外部的XML文件来定义外观。下一节课将介绍一个使用Synth框架和XML文件创建搜索对话框的示例应用程序。


上一页:如何设置外观
下一页:合成示例