ORIGINAL DRAFT
Sometimes all you need is a little decorating. This is especially true in multimedia applications and games, but many applications can benefit from slicker visual effects, so long as they are used in moderation. This month, we’re going to make it possible to manage panels with graphic borders and texture backgrounds. We’ll also demonstrate how you can make existing Swing components transparent in order to take advantage of textured backgrounds.
To ensure flexibility, we’ll be using an interface to define the elements required by a JSurface component. The Surface interface allows us to access the background image and the elements for each side and corner. We’ll develop a SurfaceImage class that implements this interface. By using an interface like this, we can allow different mechanisms to exist in the future, such as dynamically generated surfaces or surfaces from alternate data sources.
The Surface interfaces exposes the following methods:
public interface Surface
{
public Insets getInsets();
public Image getNorthWestImage();
public Image getNorthEastImage();
public Image getSouthWestImage();
public Image getSouthEastImage();
public Image getNorthImage();
public Image getSouthImage();
public Image getEastImage();
public Image getWestImage();
public Image getFillImage();
}
We expect to request any corner, side or center image, as well as the Insets we’ll use to draw with. The images, in the case of the fill or sides, will often be tiled to cover a larger area. We can provide a small 10x10 image, for example, and scale it to 100x100 by repeating sides horizontally or vertically and tiling the center image throughout the central area.
To define a suitable image, you must take this tiling into account and ensure that it will not produce undesirable edges. Fortunately, the Linux community has been developing themes for graphical environments for quite some time and provides a ready source of suitable images, though you are certainly free to create your own.
When you use JSurface, all you have to do is provide an image and a suitable Insets value, which defines the border area. In fact, because Swing already defines a suitable interface for supporting borders, we’ll implement a SurfaceBorder class that you can use outside the JSurface component if you like, maximizing flexibility and reusability.
Figure 2 shows the classes we’ll use in this project. There are two utility classes which implement static methods. The ImageUtil class implements loadImage, toBufferedImage and tileImage methods. The loadImage method uses the ImageIcon implementation to create a blocking loader that returns a loaded Image. The toBufferedImage method produces a BufferedImage from a normal Image object. The tileImage method tiles a specified image into a given Graphics context, within a specified rectangular clipping area.
The SurfaceUtil class provides a set of methods to calculate rectangular areas from specified width and height values, and an Insets object. These calculations are required by both the SurfaceImage and SurfaceBorder classes, as well as the JSurface component. None of these are very complicated but accessing them in a common class clarifies the rest of the code, making it considerably more maintainable. The choice of static methods over instance methods is somewhat arbitrary but follows the pattern established by the Java Math class for simple functions.
Having already seen the Surface interface and explained the code in ImageUtil and SurfaceUtil, we’ll concentrate on the SurfaceImage, SurfaceBorder and JSurface classes in the rest of this article.
I’ll briefly mention the JSurfaceTest class and an undocumented TransparentRenderer included in the download, which you’ll find at www.java-pro.com. The TransparentRenderer implements the ListCellRenderer, TreeCellRenderer and TableCellRenderer interfaces and is used to show how easy it is to create transparent Swing components.
The JSurfaceTest class is broken up into smaller methods that set various attributes in each of the Swing component that’s demonstrated. This makes it easy for you to take a look at the methods and decide how you want to approach this in your own applications. Naturally, you could create transparent subclasses for each component, but I tried to keep things as simple as possible for demonstration purposes.
Listing 1 shows the code for the SurfaceImage class, which implements the Surface interface. The constructor stores the image width and height, along with the insets, and uses the toBufferedImage method in ImageUtil to translate the image to a BufferedImage. This allows us to use the getSubimage method in BufferedImage to extract rectangular parts of the main image. Because our SurfaceUtil methods return Rectangle objects, we implement a utility getSubimage method to make this easier to work with.
The rest of the class implements the Surface interface, delegating most of the work to the SurfaceUtil methods and the getSubimage method we implemented. The only additional method is getInsets, which merely returns the instance variable we captured in the constructor.
Listing 2 shows the code for SurfaceBorder, which implements the Swing Border interface. We save the Surface and Insets values in the constructor and return true for the isBorderOpaque method. The insets value is returned by the getBorderInsets method. Most of the work is done in the paintBorder method and most of that is delegated to the ImageUtil and SurfaceUtil methods, drawing the corners and sides for each element, tiling the image as necessary for the sides.
Listing 3 shows the code for JSurface. As it turns out there isn’t much work left for us to do, since most of it is handled by SurfaceImage and SurfaceBorder. We implement a constructor that uses the provided image file name and and Insets instance to create a suitable SurfaceImage, after using the loadImage method from the ImageUtil class, we can create a SurfaceBorder and assign it to the JSurface component. We set the opaque value to true because we expect JSurface to fully cover anything behind it.
The paintComponent method merely has to tile the fill image in the area within the border insets. We use the static getFillArea method in the SurfaceUtil class to calculate the drawing rectangle and then reset the clipping rectangle after tiling the image. You’ll notice that getting the Rectangle that represents the area inside border Insets is pretty common if you implement your own components. You may find the SurfaceUtil class and associated methods useful in alternate situations.
The techniques used in this month’s implementation can be useful in various circumstances. If you need a more sophisticated solution, Swing makes it possible to plug in a custom look-and-feel and some developers have already offered look-and-feel solutions that support textured backgrounds. Still, it’s always good to understand how this might be done under more controlled circumstances and JSurface points the way to a few simple answers. Have fun with it.
Listing 1
import java.awt.*;
import java.awt.image.*;
public class SurfaceImage implements Surface
{
protected BufferedImage image;
protected int width, height;
protected Insets insets;
public SurfaceImage(Image image, Insets insets)
{
width = image.getWidth(null);
height = image.getHeight(null);
this.image = ImageUtil.toBufferedImage(image);
this.insets = insets;
}
protected Image getSubimage(Rectangle rect)
{
return image.getSubimage(rect.x, rect.y,
rect.width, rect.height);
}
public Insets getInsets()
{
return insets;
}
public Image getNorthWestImage()
{
return getSubimage(SurfaceUtil.
getNorthWestArea(width, height, insets));
}
public Image getNorthEastImage()
{
return getSubimage(SurfaceUtil.
getNorthEastArea(width, height, insets));
}
public Image getSouthWestImage()
{
return getSubimage(SurfaceUtil.
getSouthWestArea(width, height, insets));
}
public Image getSouthEastImage()
{
{
return getSubimage(SurfaceUtil.
getSouthEastArea(width, height, insets));
}
}
public Image getNorthImage()
{
{
return getSubimage(SurfaceUtil.
getNorthArea(width, height, insets));
}
}
public Image getSouthImage()
{
return getSubimage(SurfaceUtil.
getSouthArea(width, height, insets));
}
public Image getEastImage()
{
return getSubimage(SurfaceUtil.
getEastArea(width, height, insets));
}
public Image getWestImage()
{
return getSubimage(SurfaceUtil.
getWestArea(width, height, insets));
}
public Image getFillImage()
{
return getSubimage(SurfaceUtil.
getFillArea(width, height, insets));
}
}
Listing 2
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.*;
public class SurfaceBorder
implements Border
{
protected Surface surface;
protected Insets insets;
public SurfaceBorder(Surface surface, Insets insets)
{
this.surface = surface;
this.insets = insets;
}
public boolean isBorderOpaque()
{
return false;
}
public Insets getBorderInsets(Component parent)
{
return insets;
}
public void paintBorder(
Component parent, Graphics g,
int x, int y, int w, int h)
{
// We are assuming that x and y are zero;
ImageUtil.tileImage(g,
SurfaceUtil.getNorthWestArea(w, h, insets),
surface.getNorthWestImage());
ImageUtil.tileImage(g,
SurfaceUtil.getNorthEastArea(w, h, insets),
surface.getNorthEastImage());
ImageUtil.tileImage(g,
SurfaceUtil.getSouthWestArea(w, h, insets),
surface.getSouthWestImage());
ImageUtil.tileImage(g,
SurfaceUtil.getSouthEastArea(w, h, insets),
surface.getSouthEastImage());
ImageUtil.tileImage(g,
SurfaceUtil.getNorthArea(w, h, insets),
surface.getNorthImage());
ImageUtil.tileImage(g,
SurfaceUtil.getSouthArea(w, h, insets),
surface.getSouthImage());
ImageUtil.tileImage(g,
SurfaceUtil.getEastArea(w, h, insets),
surface.getEastImage());
ImageUtil.tileImage(g,
SurfaceUtil.getWestArea(w, h, insets),
surface.getWestImage());
g.setClip(x, y, w, h);
}
}
Listing 3
import java.awt.*;
import javax.swing.*;
public class JSurface extends JPanel
{
protected Surface surface;
public JSurface(String imageFile, Insets insets)
{
setOpaque(true);
Image image = ImageUtil.loadImage(imageFile);
surface = new SurfaceImage(image, insets);
setBorder(new SurfaceBorder(surface, insets));
}
public void paintComponent(Graphics g)
{
int w = getSize().width;
int h = getSize().height;
Insets insets = getInsets();
ImageUtil.tileImage(g,
SurfaceUtil.getFillArea(w, h, insets),
surface.getFillImage());
g.setClip(0, 0, w, h);
}
}