Painting with Fill Objects
At the 2001 JavaOne
Conference, we gave a talk called Rich
Clients for Web Services (TS-2734) that featured an eye-catching user interface
for the iPlanet Calendar Server. You can see
a snapshot of the GUI to the left. Later we expanded upon
the demo for 2002 JavaOne.
This is the first in a series of articles that describe the techniques we
used to create the calendar demo. In this article we tell you how to share painting
code, and we give you some tools to help you create enticing user interfaces.
First, we describe how painting works in Swing components
(which, for this article, we'll define as
components that inherit from the JComponent class).
Then we present a base class, Fill,
that encapsulates the task of painting a rectangular area.
Next, we show some increasingly complex subclasses of Fill,
including a fill that paints a background image,
another that tiles a fill (such as an image),
and one that paints an image-based border.
Finally, we give you a mouse-based tool
to help you specify the images to use in the image-based border.
How Painting Occurs in Swing Components
Swing components are painted by four tightly coupled methods:
paint,
paintChildren,
paintBorder,
and paintComponent.
The paint method is called automatically.
It, in turn,
calls the paintComponent method to paint the component itself,
paintBorder to paint
a border in the component's insets area, and then paintChildren to
recursively paint the component's descendants.
This is explained in more detail in
Painting,
a section in The Java Tutorial.
You can change the way a Swing component's border is painted by setting
its border property. The value of this property is a simple delegate
object
(a
Border)
whose paintBorder method paints the area defined by the
component's insets.
You can customize some of the painting performed by
paintComponent
by changing
the values of component properties such as
foreground,
background, and
font.
Unlike for borders, however,
you can't provide a paintComponent delegate.
One reason for this is that paintComponent
methods tend to be highly dependent on their class --
a paintComponent method for one class usually can not be easily shared
with other classes.
You can, however, override paintComponent in a subclass.
To avoid unnecessarily duplicating code,
you can factor some common painting tasks
into reusable classes.
The Base Class: Fill
The examples in this article use a simple base class called Fill that can
be used to create reusable, combinable
objects that paint a rectangular
area. The Fill class is similar to Border --
it's really just an
encapsulation of paint methods:
public class Fill {
public void paintFill(Component c, Graphics g, Rectangle r) {...}
public void paintFill(Component c, Graphics g) {...}
public void paintFill(Component c, Graphics g, int x, ...) {...}
}
|
The first paintFill method just paints within the area defined by
the rectangle
r
using the Graphics object g. Typically the
component parameter, c, is the target of the Graphics object although
it's not required to be. The other methods are convenience methods.
The second one
computes the coordinates of the component's insets rectangle and then
passes them to the first method.
The third one
creates a Rectangle based on the parameters
and passes it to the first method.
What the paintFill method paints can
reflect the values of the specified component's properties. The default
implementation of the paintFill method does this --
it just fills the
specified rectangle with the component's background color.
To use a Fill object to paint a Swing component,
you just create a subclass of the component's
class and override the
paintComponent method like this:
public void paintComponent(Graphics g) {
Graphics gFill = g.create();
myFillObject.paintFill(this, gFill);
gFill.dispose();
}
|
Note that we've passed a copy of the Graphics object to the
paintFill method. This insulates any other painting code we
might add to the paintComponent method from changes made to the
Graphics object by the paintFill method.
You can find the complete source code for the Fill
class in
Fill.java.
To get all the source files for this article's examples, download
border.zip.
Example: CrossFill
Here's an example of a Fill subclass that paints a cross in its rectangular area:
public class CrossFill extends Fill {
...
private void configureGraphics(...) {...}
public void paintFill(Component c, Graphics g, Rectangle r) {
int x = r.x, y = r.y, w = r.width, h = r.height;
configureGraphics(c, g, x, y, w, h);
g.drawLine(x, y, x+w, y+h);
g.drawLine(x+w, y, x, y+h);
}
}
|
As you can see, the paintFill method just paints two diagonal lines
between the corners of the rectangle. The configureGraphics method,
which we omitted for brevity, sets properties of the Graphics
object to paint a wide line that's filled with a gradient.
Here's a screenshot of an application, called TestCrossFill, that
creates a BorderLayout of five JPanels whose paintComponent methods
paint using a shared instance of the CrossFill class.
You can find the code for this example in
TestCrossFill.java
and
CrossFill.java.
You can run the example by downloading the examples
JAR file
and then running TestCrossFill, using J2SE v 1.4, like this:
java -cp examples.jar TestCrossFill
Example: ImageFill
A more practical example of a Fill subclass is
ImageFill,
which paints a single
BufferedImage.
ImageFill's paintFill method
scales the BufferedImage to match the width and height arguments.
Since image scaling is expensive relative to painting, we cache
a small number of the most recently used scaled copies of the original
image. Caching scaled copies of the images will be a big benefit
later on, when we apply the ImageFill class to painting borders.
To create an instance of an ImageFill object we need to create
a BufferedImage. This can be done quite simply with the new
(in JDK 1.4)
ImageIO class:
BufferedImage image = ImageIO.read("background.jpg");
imageFill = new ImageFill(image);
The static ImageIO.read method reads the image file and creates the
BufferedImage synchronously. There's no need for a
MediaTracker or
ImageObserver.
The paintFill method for our ImageFill class looks
like this:
public void paintFill(Component c, Graphics g, Rectangle r) {
if ((r.width > 0) && (r.height > 0)) {
BufferedImage fillImage = getFillImage(c, r.width, r.height);
g.drawImage(fillImage, r.x, r.y, c);
}
}
|
The private method getFillImage
takes care of scaling and caching.
The screenshot below shows the TestImageFill demo application in
action. Each panel is an instance of the TestImageFill class, which
paints its interior with a single shared ImageFill object. To
create new panels, just drag out a bounding rectangle with the mouse.
You can find the code for this example in
TestImageFill.java
and
ImageFill.java.
(You also need Fill.java.)
You can run the example by downloading the examples
JAR file
and then running TestImageFill, using J2SE v 1.4, like this:
java -cp examples.jar TestImageFill
Example: TiledFill
TiledFill is a Fill subclass that wraps
another Fill object.
TiledFill uses the wrapped Fill object to paint each
tileWidth x tileHeight cell in a grid
whose bounds are defined by the
paintFill rectangle. The paintFill method for TiledFill paints the first
tile in the upper left corner of the incoming rectangle.
Here is the painting code:
public void paintFill(Component c, Graphics g, Rectangle r) {
int x = r.x, y = r.y, w = r.width, h = r.height;
...
Graphics clippedG = g.create(x, y, w, h);
for (int tx = 0; tx < w; tx += tileWidth) {
for (int ty = 0; ty < h; ty += tileHeight) {
tile.paintFill(c, clippedG, tx, ty, tileWidth, tileHeight);
}
}
clippedG.dispose();
}
|
The paintFill method clips its graphics object because the
full size tiles painted in the rightmost column and bottom row may
extend beyond the paintFill rectangle. The diagram below
illustrates this.
The TestTiledFill application combines a TiledFill
object with an
ImageFill object to paint a window. The composite object,
tiledFill, is created
like this:
BufferedImage image = ImageIO.read(file);
ImageFill fill = new ImageFill(image);
tiledFill = new TiledFill(fill, image.getWidth(), image.getHeight());
The paintComponent method of TestTiledFill is a
little more complicated than the
usual implementation
because we want to paint the area
within the clipBounds of the incoming paintComponent graphics object with tiles
that are aligned with a tileWidth by tileHeight grid whose origin
is at 0,0. To do so we create a paintFill rectangle that's based
on the clipBounds: we enlarge the clipBounds rectangle so that
its origin is aligned with the origin of a tile and its size
is a multiple of the tile size.
The TestTiledFill application allows you to interactively change
the tile size by dragging on the lower right corner of the green
rectangle.
You can find the code for this example in
TestTiledFill.java
and
TiledFill.java.
(You also need
Fill.java
and
ImageFill.java.)
You can run the example by downloading the examples
JAR file
and then running TestTiledFill, using J2SE v 1.4, like this:
java -cp examples.jar TestTiledFill
Example: BorderFill
Our final example is a
Fill subclass called
BorderFill
that uses an array of eight Fill objects to paint a border.
Each of the corners and each of the sides has a Fill object.
An array of eight
Dimension objects defines the "natural size" of
the Fill objects. The four corners are always painted with their natural
size; the four sides are scaled vertically or horizontally to fit.
The diagram below shows how the eight Fills are arranged:
Because a common application of BorderFill is
painting a border based on eight small images,
we provide constructors to simplify this case:
BorderFill(BufferedImage image, Rectangle[] rectangles, boolean[] isTile)
BorderFill(BufferedImage image, Rectangle[] rectangles)
The first constructor extracts the eight subimages specified by the array
of rectangles
and creates an ImageFill object for each one.
If any sides should be tiled,
the constructor wraps a TiledFill object
around the ImageFill for each tiled side.
You specify the sides to be tiled
with the tile array,
where isTile[n]
specifies whether fill object n*2
should be tiled or scaled to fit.
For example,
to specify that only the right and bottom be tiled,
you need a tile array of
{false, true, true, false}.
This means that fill object 0 (the top) should not be tiled,
2 (right) and 4 (bottom) should be, and 6 (left)
should not be tiled.
The second BorderFill constructor is a convenience that
calls the first with an isTile array of
{true, false, true, false}.
This makes each corner be painted with a
fixed size ImageFill,
the vertical sides of the border be
painted with a ImageFill scaled (vertically) to fit,
and the
horizontal sides be painted tiled with TiledFill objects. This
particular arrangement of images is common in borders painted for look
and feel "skins", such as
GTK+ themes.
It is well suited to defining
borders for user interface components such as text fields that tend to
be much wider than they are high.
Because the BorderFill class implements the Border interface,
a BorderFill object can be used as a border for any Swing component.
The TestBorderFill application contains a single panel whose
border is implemented by a BorderFill object. You can drag out
copies of the panel with the left mouse button and configure a few
properties with the panel's right button menu. To move or resize a
bordered panel,
press the left mouse button near the panel's edge and drag.
You can find the code for this example in
TestBorderFill.java
and
BorderFill.java.
(You also need
Fill.java,
ImageFill.java,
and
TiledFill.java.)
You can run the example by downloading the examples
JAR file
and then running TestBorderFill, using J2SE v 1.4, like this:
java -cp examples.jar TestBorderFill
A Border Editor
To help you use BorderFill,
we've written a border editor.
It lets you easily define the
eight border parts,
given an image of a complete "prototypical"
border.
The following screenshot shows the border editor in action. As you can
see, the user is working with a picture of a border that has been
magnified to simplify defining the precise boundaries of
the border parts.
To launch the border editor, download
editor.jar
and then run it using the -jar option
of the java command.
For example:
e:/java/jdk1.4/bin/java -jar editor.jar
If you'd like to look at the source code for the border editor,
download
editor.zip.
To compile the border editor,
you also need the source files from
border.zip.
To read the instructions for using the border editor,
choose the Using BorderEditor... item
from the border editor's Help menu.
Acknowledgments
The examples in this article depend heavily on other people's work.
BorderEditor Application
Scott Violet wrote the border editor application. The original plan
for building the editor was to spend an afternoon banging something
out and then, well, just live with it. As it turned out, usability
issues turned this into a longer project then we expected and
eventually we had to stop working on it. The editor is quite usable
as it is. However, it also seems to be holding out a tantalizing
opportunity to build something exceptional, something that would
be usable by those folks whose stock-in-trade isn't Java.
TestTiledFill Image
The image used in TestTiledFill
was created by
Marc Hesse, titled "Seamless Texture 39", and can be found
in his gallery at
marc-h.deviantart.com.
View the original.
TestImageFill Image
The image used in TestImageFill was
created by Kit Schroeder, titled "Marble", and can be found in
her gallery at
euphoria.deviantart.com
View the original.
For More Information
See Amy Fowler's article
Painting in
AWT and Swing.
|