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