Jive Community Forums

 

  JavadesktopFAQ JavadesktopWIKI JavadesktopBLOG JavadesktopPROJECTS JavadesktopHOME javadotnet javadesktopFORUMS javadesktopARTICLES

The Synth Look and Feel

A Skinnable Look and Feel for 1.5

by Scott Violet

March 26th, 2004

 

 

One of Swing's early design goals was to enable applications to support a multitude of look and feels without changing code. This feature allows developers to seamlessly run their applications with numerous look and feels. While a handful of custom look and feels have been developed — most notably the INCORS Alloy Look and Feel and L2F's Skin Look and Feel — creating a new look and feel requires programming and an extensive knowledge of Swing's pluggable look-and-feel architecture, limiting this feature mostly to experts.

This article provides an overview of a new look and feel, Synth, which can be completely customized without writing code, enabling even non-programmers to create new look and feels. The Synth look and feel is part of version 1.5 of the JavaTM 2 Platform, Standard Edition (J2SETM).


 
Motivation

At past JavaOneSM conferences we have created distinct looks for our demo applications by writing custom code. One year we created a custom border to make text fields look like this:

Here's a portion of the code that draws this border (see the article Painting with Fill Objects article for more details on this):

    public void paintBorder(Component c, Graphics g, int x, int y,
                            int width, int height) {
        g.drawImage(images[0], x, y, null);
        g.drawImage(images[2], x + width - insets.right, y, null);
        g.drawImage(images[4], x + width - insets.right,
                    y + height - insets.bottom, null);
        g.drawImage(images[6], x, y + height - insets.bottom, null);
        ...
    }
                  

Writing code like this is fun the first couple of times, but it soon becomes obvious there needs to be a better way to provide a custom image-based look and feel without writing code.


 
Goals

After reviewing numerous skinnable toolkits we decided upon the following goals:

  • Enable creating a custom look without writing any code.
  • Allow appearance to be configured from images.
  • Provide the ability to customize the look of a component based on its name property.
  • Provide a centralized point for overriding the look of all components.
  • Enable custom rendering based on images.

It is also worth mentioning what Synth does not provide:

  • No way to specify layout.
  • No default look! Synth is an empty canvas. It has no default bindings and, unless customized, does not paint anything.

 
Example

For the anxious, here's a quick example. The following XML code, taken from example1.xml, defines a style named textfield and binds it to all text fields in Synth. The result is that text fields look like the one in the Motivation section.

<synth>
  <style id="textfield">
    <state>
      <color value="white" type="BACKGROUND"/>
    </state>
    <imagePainter method="textFieldBorder" path="textfieldborder.png"
                  sourceInsets="5 6 6 7" paintCenter="false"/>
    <insets top="5" left="6" bottom="6" right="7"/>
  </style>
  <bind style="textfield" type="region" key="TextField"/>
</synth>
                  

Here's some code that loads the XML file into Synth and sets the current look and feel to Synth:

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

 
Architecture

This section assumes that you understand Swing's Pluggable look-and-feel architecture, which is described in the Swing Connection article Swing Architecture Overview. The text in this section is rife with links to the Synth API documentation and to the Synth File Format document, which describes the XML file format for Synth.

The previous XML code illustrates a number of the core concepts of Synth. It defines a style and a painter for that style, and then binds that style to a set of components.

The style element defines a SynthStyle object. SynthStyle is very similar to UIDefaults, in that a SynthStyle consists of a set of style-related properties: fonts, insets, opacity, painters, and so on. Each component becomes associated with at least one SynthStyle. The ComponentUIs for Synth use a SynthStyle to obtain all style-related information. For example, each ComponentUI in Synth does the following to install the font:

  Font componentFont = component.getFont();
  if (componentFont == null || (componentFont instanceof UIResource)) {
    component.setFont(style.getFont(context));
  }
                  

Insets and colors are installed in a similar manner.

The previous XML example creates a style with the ID textfield, a background of white, and insets of 5, 6, 6, and 7 pixels. It also registers a painter for the paintTextFieldBorder method.

The bind element is used to associate a SynthStyle with a set of components. You can bind a style either using component types or the value of the component name property. To bind to component types, you specify type equals "region", as in the previous XML example. To bind to component names, you specify that type equals "name".

The previous example binds the textfield style to all text fields — all components corresponding to the TEXT_FIELD constant defined in the Region class.

The Region class defines constants corresponding to each kind of component and sometimes to parts of components. For example, Region defines the constants INTERNAL_FRAME and INTERNAL_FRAME_TITLE_PANE. You can find all the constants in the Region API documentation.

To bind a style to a particular kind of component, you use the name of the Region constant for that component, minus the underscores, and using any capitalization you like. For example, you can specify all text fields by specifying a type of "region" and a key of "textfield", "TEXTFIELD", or "TextField".

Each ComponentUI in Synth obtains a SynthStyle at two distinct points: when the ComponentUI is first created, and when the component's name property changes. The ComponentUI uses a SynthStyleFactory to obtain a SynthStyle. SynthStyleFactory is an abstract class that contains the single method getStyle. The load method of SynthLookAndFeel creates a custom SynthStyleFactory that returns SynthStyles based on the contents of the file that was loaded.

Each ComponentUI may match multiple styles. For example, in the following XML code, all components match the style all. Text fields match both the all and textfield styles. Refer to the documentation of the bind element for details on how multiple matches are resolved into a single style.

<synth>
  <style id="all">
    <opaque value="true"/>
    <state>
      <color value="#404F97" type="BACKGROUND"/>
    </state>
    <font name="Lucida" size="12"/>
  </style>
  <bind style="all" type="region" key=".*"/>

  <style id="textfield">
    <state>
      <color value="white" type="BACKGROUND"/>
    </state>
    <imagePainter method="textFieldBorder" path="textfieldborder.png"
                  sourceInsets="5 6 6 7" paintCenter="false"/>
    <insets top="5" bottom="6" right="7" left="6"/>
  </style>
  <bind style="textfield" type="region" key="TextField"/>
</synth>
                  

Each SynthStyle has a SynthPainter. SynthPainter is used to render the distinct parts of each Swing component. For example, it has methods such as paintPanelBackground, paintPanelBorder, paintProgressBarBackground, paintProgressBarBorder, and paintProgressBarForeground. In the previous example the imagePainter element specified that an image-based SynthPainter should be used to paint the text field's border.

As its name implies, an imagePainter element creates a SynthPainter that paints from an image. An image painter is commonly used to paint a border from a small image, where the top, left, bottom, and right edges are stretched or tiled. An image painter breaks the source image into 9 distinct areas: top, top right, right, bottom right, bottom, bottom left, left, top left, and center. Each of the these areas is drawn into the destination. The stretch attribute dictates whether the top, left, bottom, and right edges are tiled or stretched. You can specify whether the center area should be painted with the paintCenter attribute. The following image shows the nine areas:


 
Embedding Objects

Synth's file format allows for embedding arbitrary objects by way of the long-term persistence for JavaBeansTM mechanism. This ability is particularly useful in providing your own painters beyond the image-based ones we provide. For example, the following XML code specifies that a gradient should be rendered in the background of text fields:

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

Where the GradientPainter class looks like this:

public class GradientPainter extends SynthPainter {
    public void paintTextFieldBackground(SynthContext context,
                                          Graphics g, int x, int y,
                                          int w, int h) {
        // For simplicity this always recreates the GradientPaint. In a
        // real app you should cache this to avoid garbage.
        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);
    }
}
                  

 
Programmatically Using Synth

While we hope Synth's file format accommodates most developers' needs, you can also programmatically use Synth. Rather than using the load method you'll need to create a custom SynthStyleFactory and install it by way of SynthLookAndFeel's setStyleFactory method. For example:

  SynthLookAndFeel laf = new SynthLookAndFeel();
  UIManager.setLookAndFeel(laf);
  SynthLookAndFeel.setStyleFactory(new MyStyleFactory());
                  

Your SynthStyleFactory must override the getStyle method to return a SynthStyle. For example:

  public SynthStyle getStyle(JComponent c, Region id) {
    if (id == Region.BUTTON) {
      return buttonStyle;
    }
    else if (id == Region.TREE) {
      return treeStyle;
    }
    return defaultStyle;
  }
                  

 
Examples

You can get this article's examples by downloading this JAR file:

examples.jar

It contains these files:

Example1.java
Contains a simple app that sets the look and feel to Synth and contains a single JTextField. The example loads the file passed into the command line, or example1.xml if one has not been specified.

You can run Example1 directly from examples.jar like this:

1.5/bin/java -cp examples.jar Example1 (UNIX)
c:\java\jdk1.5\bin\java  -cp examples.jar Example1 (Microsoft Windows)
      
example1.xml
Contains the XML code shown in this article's first example.
textfieldborder.png
Contains a border image required for example1.xml.
GradientPainter.java
Contains the source for the custom painter GradientPainter, which is described in the Embedding Objects section.
example2.xml
An XML file that uses GradientPainter. You can load this file, and thus GradientPainter, into Example1 by specifying it on the command line. For example:
1.5/bin/java Example1 example2.xml (UNIX)
c:\java\jdk1.5\bin\java Example1 example2.xml (Microsoft Windows)
      

Further Reading

The Synth File Format document provides more details about the format of the XML files used by Synth. The Synth API documentation provides more information about the architecture.



Read or Post a Comment