ORIGINAL DRAFT

Swing comes with a nice collection of borders that you can attach to any JComponent. If you use these heavily, you may have noticed a few minor shortcomings or wished there were more available choices. This month, we’ll extend Swing’s Border collection, adding a dozen useful borders, more than doubling the number available. Our BorderFactory-compatible JBorder implementation collapses calls more efficiently than BorderFactory and provides more control over individual border creation. We’ll work our way from simple to complex borders, providing a nice range of new border choices to expand your Swing programmer’s arsenal.

The Swing Border interface is very simple, requiring only three methods.

boolean isBorderOpaque();
Insets getBorderInsets(Component c);
void paintBorder(Component c, Graphics g,
  int x, int y, int width, int height);

The isBorderOpaque method lets the Swing paint-handling optimize its work if a border’s drawing fills the whole border area. A rounded border would not completely fill the corners, for example, and would need to return false because the corners would have to show through. The getBorderInsets method helps Swing containers layout child components inside the border, without any overlapping problems.

The paintBorder method does the real work, drawing the chosen border within the specified rectangular boundaries. The x and y values are typically zero, and the w and h values reflect the width and height of the container when there is only one border, though compound borders may have deeper insets. You also get a reference to both the parent Component and the drawing Graphics context.

Swing has a full set of Borders already available. Table 1 shows what they are.

Name Description
AbstractBorder Empty border with no size.
BevelBorder Simple 2 line bevel border.
CompoundBorder Composite Border used to compose two Border objects into a single border by nesting an inside Border object within the insets of an outside Border object.
EmptyBorder Empty, transparent border which takes up space but does no drawing.
EtchedBorder Simple etched border which can either be etched-in or etched-out.
LineBorder Line border of arbitrary thickness and of a single color.
MatteBorder Matte-like border of either a solid color or a tiled icon.
SoftBevelBorder Raised or lowered bevel with softened corners.
TitledBorder Arbitrary border with a String title in a specified position and justification.
Table 1: Standard Swing Borders

The standard borders provide enough functionality to support all the existing Swing look-and-feel implementations, but they don’t provide all the answers. Our extensions will help considerably, but keep in mind they’re no panacea, merely a nice enhancement to the existing library. With these tools in hand, you’ll see how easy it is to implement your own sophisticated borders. Table 2 lists the new borders we’ll be implementing.

Name Description
ThreeDBorder Similar to BevelBorder but provides more control over thickness.
EdgeBorder A single-sided beveled edge, good for separators, aligned on any side.
RoundedBorder Solid-color rounded-corner border with full control over which edges and corners to draw.
PatternBorder Similar to MatteBorder, with a bit mapped pattern abstraction and a full set of built-in patterns.
DragBorder Selection border for dragging and resizing components, with optional corner and side anchor points.
ShadowBorder Drop-shadow in any of the four corners.
GradientBorder Gradient drawing from outside to inside border edges.
CurvedBorder Non-linear, curved-shading border.
PaintBorder Border that uses arbitrary Graphics2D Paint effects.
StyleBorder Border that uses arbitrary Graphics 2D Stroke effects.
GroupBorder Similar to CompoundBorder, with support for an arbitrary number of nested borders.
GrooveBorder Raised or lowered mesa effect achieved by using two ThreeDBorder and an EmptyBorder object in a GroupBorder.
Table 2: Extended Swing Borders

These borders range from existing border enhancements (functionally equivalent, with extensions) to completely new inventions that support various special effects. A few of these rely on the Graphics2D API and are, therefore, suitable for use only on the Java 2 platform. This article assumes you are doing your development under Java 1.2. Some of the borders can be used directly in Java 1.1 but a number of them are specifically designed with more recent functionally it mind. Figure 1 shows the JBorder extensions at work.

Figure 1: Border defaults are shown. The GroupBorder is 
composed of four borders, two of them a simple black LineBorders, with a ShadowBorder on the outside. The image
is part of a PaintBorder using a TexturePaint.

Figure 1: Border defaults are shown. The GroupBorder is composed of four borders, two of them a simple black LineBorders, with a ShadowBorder on the outside. The image is part of a PaintBorder using a TexturePaint.

In most of the borders, the isBorderOpaque and getBorderInsets methods are trivial. The isBorderOpaque method returns true or false, depending on whether the paintBorder paints over every pixel in the border frame. The getBorderInsets method returns the insets (an Insets object) describing the number of pixels from each edge.

Most of our interest lies in the paintBorder method which does all the important work. For the rest of this article, we’ll restrict the explanations to the paintBorder and supporting methods, unless something critical is going on elsewhere. The pattern is to set up parameters at constructor time, so border thickness, color, options, etc. are all saved as instance variables when the constructor is called. Numerous constructor variants are typically implemented to allow for default values and ease of use, but they are otherwise uncomplicated.

Border Implementations

The first border we’ll look at is the ThreeDBorder, which was designed to overcome an annoying problem with BevelBorder that forces you to use a two pixel edge. More subtle applications often use a single pixel bevel and there may be a need to apply more control over the thickness at times. Aside from providing control over the border thickness, ThreeDBorder supports the RAISED and LOWERED types and the optional setting of highlight and shadow colors. If the highlight and/or shadow colors are not specified, we brighten and darken the component background color. This is done in a couple of utility methods called getHighlightColor and getShadowColor, using the specified colors if they were provided.

Listing 1 shows the ThreeDBorder code. The paintBorder method is pretty simple. We first set the hi and lo color variables, based on whether this is a raised or lowered border, and then use drawLine calls in a loop to work from the outside-in for the specified thickness in pixels. Each rectangle draws in highlight color on the upper left sides and shadow color on the lower right sides. The loop tightens the rectangular area as it shrinks by one pixel per side on every pass, creating the beveled corners in the upper right and lower left as they need to be. That’s all there is to it.

Lets take a look at the EdgeBorder. We want to be able to draw an etched line along one of the four sides of a component. This is basically used as a separator but easier to apply than creating a completely new component for the job. We use the cardinal NORTH, SOUTH, EAST, WEST constants from the SwingConstants interface to decide which edge to draw. The getBorderInsets returns a 2 pixel inset on the side we chose, with zero insets on each of the other three sides. The paintBorder method sets a highlight and shadow color for the two lines we need to draw and draws the lines on the edge we specified.

Unfortunately, we don’t have room enough to list every class, but you can find this one and any other unlisted class mentioned in the article online at www.java-pro.com. Listing 2 shows the code for the RoundedBorder class.

The RoundedBorder class is designed to draw borders that have optionally rounded corners. Figure 2 shows how flexible your choices can be. To make sure we have the flexibility we may want, the ability to set corners as active or not is supplemented by control over the sides that need to be drawn, so that we can draw square sides and optional edges, intermixed with rounded corners.

Figure 2: RoundedBorder options include thickness,
color and which sides are drawn as well as whether specific corners are rounded. This example shows 8 SIDE
variants, along with a (centered) CORNER variant.

Figure 2: RoundedBorder options include thickness, color and which sides are drawn as well as whether specific corners are rounded. This example shows 8 SIDE variants, along with a (centered) CORNER variant.

Since the corners are round, the isBorderOpaque method returns false because the outside of the curve falls within the drawing area but never gets drawn. The getBorderInsets method sets each side to the specified thickness, depending on whether the flag is set for each respective side. We optionally get the foreground and background colors from the parent component, if they were not explicitly specified, using utility methods called getForeground and getBackground, both of which are called from the paintBorder method.

To paint the border, we divide the drawing area into non-overlapping corners and sides. If the flag for a given side is set, we draw a rectangle with the appropriate thickness in the proper color. For each corner, we draw either nothing, an arc or a rectangle. If neither of the two sides joined at a given corner are active, nothing is draw in that corner. If one side is present or if the corner flag is inactive (false) for a corner, we draw a rectangle. Otherwise, we draw an 90 degree arc, which we fill with the foreground color.

While none of this is particularly difficult, we have a powerful new border at our disposal. You can use the RoundedBorder to create all kinds of special effects like rounded buttons, tab elements (where only one side has rounded corners), rounded header and footer bars that curve up or down, rounded separators, etc. You might even be able to work with interfaces that resemble the Star Trek look-and-feel if you set your mind to it. Lets move on to a few other interesting borders.

The Swing MatteBorder provides the ability to work with tiled images, but requires an actual image file. There are many common patterns we’d like more immediate access to, preferably without the overhead of working with persistent bitmaps. How about a border that accepts patterns in the form of boolean arrays? Since a Java VMs stores booleans as integers anyway, we’ll store these as zero/non-zero values in an integer array, making the code more readable when we set up default patterns.

The pattern array design is simple enough. The first two values represent the matrix dimensions. The rest of the values represent the bits. Naturally, the array is expected to be 2 + width * height in length, where width and height are the first two integers. We implement a utility method called createIcon which uses the BufferedImage object from Java 1.2 to create an off screen image, which we then convert into an Icon object using Swing’s ImageIcon. The code we use to create icons looks like this:

public static Icon createIcon(int[] array, Color color)
{
  int w = array[0];
  int h = array[1];
  BufferedImage image = new BufferedImage(
    w, h, BufferedImage.TYPE_INT_ARGB);
  int rgb = color.getRGB();
  for (int x = 0; x < w; x++)
  {
    for (int y = 0 ; y < h; y++)
    {
      image.setRGB(x, y,
        array[y * w + x + 2] > 0 ? rgb : 0);
    }
  }
  return new ImageIcon(image);
}

The PatternBorder class extends MatteBorder and supports 20 pre-set patterns. Figure 3 shows these patterns in a window.

Figure 3: The PatternBorder provides 20 preset patterns
which you can easily extend. You can control the thickness of all or individual sides, as well as the color.
Patterns are integer bit sequences which you can also create on-demand.

Figure 3: The PatternBorder provides 20 preset patterns which you can easily extend. You can control the thickness of all or individual sides, as well as the color. Patterns are integer bit sequences which you can also create on-demand.

We extend the PatternBorder to create a DragBorder which adds anchor points, white-filled black squares as big as the border is thick, on each corner and/or in the middle of each side. The constructors let us decide whether the sides and/or corners are drawn, though we don’t provide control over individual anchors. Either all or no corners are drawn and either all or no side anchors are drawn. The DragBorder is primarily designed for use in selection models for component shapes that can be moved and/or resized. The corner anchors should be used to scale, while the side anchors let you stretch or shrink on a single axis. Note that the border does not implement any resizing functionality, merely the esthetic representation.

The ShadowBorder lets us add drop-shadows to components. Because Swing components are lightweight, we can even use transparency to our advantage to make the effect more impressive. The paintBorder method need only draw the shadow in the specified color and the user can use alpha values to support transparency. By default, we’ll use a 50% transparency effect. As you might expect, the isBorderOpaque returns false, not only because parts of the border are not drawn, but because they may need to be transparent to a variable extent. The getBorderInsets method returns an Insets object with two edges set to zero and the opposite two set to the specified thickness. This is a very nice effect and very easy to implement with the Swing Border paradigm.

Lets raise the bar a bit more with a couple of gradient-based borders. The first is fairly straight forward. GradientBorder, presented in Listing 3, draws a border that interpolates from one color to another from outside to inside. The constructors let us specify the thickness of the border and, optionally, the inside and outside colors. If the colors are not specified, we take the current component’s background color as the inside color and the parent container’s background color for the outside, creating a gradient between the component and its parent. The paintBorder method iterates through each thickness pixel, drawing the gradient one pixel frame at a time. To determine the current color, we use a simple method called interpolate that pre calculates a color array.

The CurvedBorder does something similar to GradientBorder but decrements the color by a given percentage. Instead of interpolating between two colors, as the GradientBorder does, we actually brighten and darken the appropriate edges by the specified percentage. To create a three dimensional effect, the upper left and lower right edges are darkened or brightened respectively when the type is set to RAISED, reversing them when the border type is LOWERED. The paintBorder method simply steps through single-pixel frames, moving inward. The constructors allow you to set the RAISED or LOWERED type, a curve (which determines whether the curve is drawn from the inside-out or outside-in with the ROUNDED and PLATEAU constants), the thickness, and the percentage change to use.

The PaintBorder is another simple border that lets us specify a Java 2D API Paint object, which may be a Color or GradientPaint, for example. Because objects that implement the Paint interface determine what coordinates are relevant, the PaintBorder does nothing but create a paintable region that covers only the frame area of the border and calls the fill method to paint the content. Here’s what paintBorder method looks like.

public void paintBorder(Component c,
  Graphics gc, int x, int y, int w, int h)
 {
   Graphics2D g = (Graphics2D)gc;
   Rectangle outside = new Rectangle(x, y, w, h);
   Rectangle inside = new Rectangle(
     x + thickness, y + thickness,
     w - thickness * 2, h - thickness * 2);
  Area area = new Area(outside);
  area.subtract(new Area(inside));
  g.setPaint(paint);
  g.fill(area);
}

The StyleBorder is similar, allowing us to use a BasicStroke object to draw the border. We support several constructors that allow us to specify the stroke in various ways, either as a BasicStroke object or by defining specific parameters. Here’s what the paintBorder method looks like for the StyledBorder class. We draw the, potentially thick, line centered around the rectangle. Because of the way this works, if you use odd thickness values the centering will be more accurate than if you use even values.

public void paintBorder(Component c,
  Graphics gc, int x, int y, int w, int h)
{
  Graphics2D g = (Graphics2D)gc;
  g.setColor(color);
  int thickness = (int)stroke.getLineWidth();
  int mid = (int)(stroke.getLineWidth() / 2f);
  Shape shape = new Rectangle(x + mid, y + mid,
    w - thickness, h - thickness);
  g.setStroke(stroke);
  g.draw(shape);
}

Listing 4 shows the GroupBorder, which is much like Swing’s CompoundBorder but lets you handle an arbitrary set of borders as a single entity. That’s not to say that CompoundBorder is not up to the task, since you can use nesting and effectively end up with a linked list of borders. But GroupBorder uses a Vector to manage borders and, therefore, gives us more control over list operations than CompoundBorder ever could. Both the getBorderInsets and paintBorder methods step through each of the borders in the list and add up the insets and paint borders from the outside-in.

In part to demonstrate how the GroupBorder works, we’ll include a GrooveBorder, which uses a ThreeDBorder to lift the inside and outside edges, with an EmptyBorder separating them. This has the net effect of creating a raised or lowered plateau, depending on the type you chose. GrooveBorder merely extends GroupBorder and adds the three subborders as part of its primary constructor. Constructor variants are provided with default values, but the work is all done by setting up three borders with appropriate arguments, specifying whether they are to be raised or lowered, how wide the ThreeDBorder elements should be and how thick the empty space needs to be.

JBorder Factory

The JBorder component is really a factory object that constructs all of the borders we’ve talked about in this article. Like BorderFactory, JBorder tries to collapse calls to common borders so that only a single instance exists, should the borders be the same. In principle, this is the right idea. But if you take a look at the Swing source code for BorderFactory, you’ll see that not much effort was put into this. The API is well defined and the intentions are good, but the implementation is somewhat lacking. Let’s see if we can fix the problem.

JBorder provides exactly the same methods available in BorderFactory, allowing straight replacement calls if you need them. In addition, object are more carefully checked for previous existence, so memory utilization is more efficient, depending on how much you reuse borders. Finally, each of the extended borders we implemented in this article are part of the JBorder repertoire. We won’t list the whole class, but rather take a quick look at the way the factory is implemented using a simple Hashtable. There’s very little difference between the calls other than their arguments and they all follow the create<BorderName> naming convention that the Swing BorderFactory uses.

The createStyleBorder method is typical of a factory method in JBorder. A set of internal methods named makeKey help create a unique string as a key for the hash table. The variants of makeKey handle from zero to six integer arguments for convenience. The key is a simple representation of a border using its name and comma-delimited integer values in argument parentheses. This createStyleBorder method will have the key "StyleBorder()" because no arguments are used. With the key in hand, we check the hash table for a matching entry. If we do not find one, we create a suitable border instance and add it to the table for the given key. This is called lazy instantiation, creating the border when it first gets used and only if it is used. Finally, we return the border by looking it up in the hash table. If an entry was already available, it gets used without forcing a new instance to be created.

public static Border createStyleBorder()
{
  String key = makeKey(&quot;StyleBorder&quot;);
  if (!table.containsKey(key))
    table.put(key, new StyleBorder());
  return (Border)table.get(key);
}

In principle, all the factory methods work exactly the same way. The distinctions are primarily in the border being created and the arguments used to construct the key. Since there’s a one-to-one mapping between all but a few create methods and their respective border constructors, that aspect is fairly predictable. But not all arguments are integers, so we need to convert them or use their string equivalents. This design uses integers to keep the memory overhead down, so we use the hashCode value for any non-integer argument. Because some arguments may be null, we use another utility method called hash, which returns the object hashCode or zero if the object is null.

There are a few borders we do not keep in the hash table. The GroupBorder, CompoundBorder and TitledBorder reference other borders, which are already cached by the JBorder factory. The TitledBorder instances tend to be unique by their vary nature, since we use them primarily to name viewing areas which require different names. The GroupBorder is subject to change, since it accommodates manipulation of the group’s content.

There are 76 create methods and close to 800 lines of code in the JBorder factory class. If you are concerned about memory footprint and expect to use only a few borders, it makes more sense to create borders directly. For typical applications, however, the benefits of border reuse and convenience easily outweigh this after only a few borders are shared.

We’ve developed 12 new borders and a much more sophisticated border factory for Swing. With a total of 20 borders we can create more useful user interfaces, easier to recognize elements which are more consistent with common metaphors, more visually enticing and more fun to work with. This is a good addition to our programming bag of tricks and a great enhancement to the user experience when used effectively.

Listing 1

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class ThreeDBorder
  implements Border, BorderConstants
{
  protected int type = RAISED;
  protected int thickness = 1;
  protected Color highlight;
  protected Color shadow;

  public ThreeDBorder()
  {
    this(RAISED , 1, null, null);
  }

  public ThreeDBorder(int type)
  {
    this(type , 1, null, null);
  }

  public ThreeDBorder(int type, int thickness)
  {
    this(type, thickness, null, null);
  }

  public ThreeDBorder(int type, int thickness,
    Color highlight, Color shadow)
  {
    this.type = type;
    this.thickness = thickness;
    this.highlight = highlight;
    this.shadow = shadow;
  }

  public boolean isBorderOpaque()
  {
    return true;
  }

  public Insets getBorderInsets(Component component)
  {
    return new Insets(thickness, thickness, thickness, thickness);
  }
  
  public Color getHightlightColor(Component c)
  {
    if (highlight == null)
      highlight = c.getBackground().brighter();
    return highlight;
  }

  public Color getShadowColor(Component c)
  {
    if (shadow == null)
      shadow = c.getBackground().darker();
    return shadow;
  }

  public void paintBorder(Component c, Graphics g,
    int x, int y, int w, int h)
  {
    Color hi = (type == RAISED ?
      getHightlightColor(c) : getShadowColor(c));
    Color lo = (type == RAISED ?
      getShadowColor(c) : getHightlightColor(c));
    
    for (int i = thickness - 1; i &gt;= 0; i--)
    {
      g.setColor(hi);
      g.drawLine(x + i, y + i, x + w - i - 1, y + i);
      g.drawLine(x + i, y + i, x + i, x + h - i - 1);

      g.setColor(lo);
      g.drawLine(x + w - i - 1, y + i, x + w - i - 1, y + h - i - 1);
      g.drawLine(x + i, y + h - i - 1, x + w - i - 1, y + h - i - 1);
    }
  }
}

Listing 2

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class RoundedBorder
  implements Border, BorderConstants
{
  protected Color foreground, background;
  protected int thickness;
  protected int corners;
  protected int sides;

  public RoundedBorder()
  {
    this(Color.red, null, 7, ALL_CORNERS, ALL_SIDES);
  }

  public RoundedBorder(int thickness)
  {
    this(Color.red, null, thickness, ALL_CORNERS, ALL_SIDES);
  }

  public RoundedBorder(Color color, int thickness)
  {
    this(color, color, thickness, ALL_CORNERS, ALL_SIDES);
  }

  public RoundedBorder(Color foreground, Color background,
    int thickness, int corners, int sides)
  {
    this.foreground = foreground;
    this.background = background;
    this.thickness = thickness;
    this.corners = corners;
    this.sides = sides;
  }

  public boolean isBorderOpaque()
  {
    return false;
  }

  public Insets getBorderInsets(Component component)
  {
    boolean north = (sides &amp; N_SIDE) == N_SIDE;
    boolean south = (sides &amp; S_SIDE) == S_SIDE;
    boolean east = (sides &amp; E_SIDE) == E_SIDE;
    boolean west = (sides &amp; W_SIDE) == W_SIDE;
    return new Insets(
      north ? thickness : 0, west ? thickness : 0,
      south ? thickness : 0, east ? thickness : 0);
  }
  
  public Color getForeground(Component c)
  {
    if (foreground == null)
			foreground = c.getForeground();
    return foreground;
  }

  public Color getBackground(Component c)
  {
    if (background == null)
			background = c.getBackground();
    return background;
  }

  public void paintBorder(Component c, Graphics g,
    int x, int y, int w, int h)
  {
    int thick = thickness;
    int diam = thickness * 2;
    
    boolean north = (sides &amp; N_SIDE) == N_SIDE;
    boolean south = (sides &amp; S_SIDE) == S_SIDE;
    boolean east = (sides &amp; E_SIDE) == E_SIDE;
    boolean west = (sides &amp; W_SIDE) == W_SIDE;
    
    boolean nw = (corners &amp; NW_CORNER) == NW_CORNER;
    boolean ne = (corners &amp; NE_CORNER) == NE_CORNER;
    boolean sw = (corners &amp; SW_CORNER) == SW_CORNER;
    boolean se = (corners &amp; SE_CORNER) == SE_CORNER;
    
    g.setColor(getForeground(c));

    if (north)
      g.fillRect(x + thick, y,
        x + w - thick * 2, y + thick);
    if (south)
      g.fillRect(x + thick, y + h - thick,
        x + w - thick * 2, y + thick);
    if (east)
      g.fillRect(x + w - thick, y + thick,
        x + thick, y + h - thick * 2);
    if (west)
      g.fillRect(x, y + thick,
        x + thick, y + h - thick * 2);

    if (north || west)
    {
      g.setColor(getBackground(c));
      g.fillRect(x, y, thick, thick);
      g.setColor(getForeground(c));
      if (nw) g.fillArc(x, y, diam, diam, 90, 90);
      else g.fillRect(x, y, thick, thick);
    }
    
    if (north || east)
    {
      g.setColor(getBackground(c));
      g.fillRect(x + w - thick, y, thick, thick);
      g.setColor(getForeground(c));
      if (ne) g.fillArc(x + w - diam, y, diam, diam, 0, 90);
      else g.fillRect(x + w - thick, y, thick, thick);
    }

    if (south || west)
    {
      g.setColor(getBackground(c));
      g.fillRect(x, y + h - thick, thick, thick);
      g.setColor(getForeground(c));
      if (sw) g.fillArc(x, y + h - diam, diam, diam, 180, 90);
      else g.fillRect(x, y + h - thick, thick, thick);
    }

    if (south || east)
    {
      g.setColor(getBackground(c));
      g.fillRect(x + w - thick, y + h - thick, thick, thick);
      g.setColor(getForeground(c));
      if (se) g.fillArc(x + w - diam, y + h - diam, diam, diam, 270, 90);
      else g.fillRect(x + w - thick, y + h - thick, thick, thick);
    }
  }
}

Listing 3

import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;

public class GradientBorder implements Border
{
  protected int thickness;
  protected Color outside;
  protected Color inside;

  public GradientBorder()
  {
    this(7, Color.orange, Color.red);
  }

  public GradientBorder(int thickness)
  {
    this(thickness, Color.orange, Color.red);
  }

  public GradientBorder(int thickness,
    Color outside, Color inside)
  {
    this.thickness = thickness;
    this.outside = outside;
    this.inside = inside;
  }

  public boolean isBorderOpaque()
  {
    return true;
  }

  public Insets getBorderInsets(Component component)
  {
    return new Insets(
      thickness, thickness, thickness, thickness);
  }
  
  public Color getInsideColor(Component c)
  {
    if (inside == null)
      inside = c.getBackground();
    return inside;
  }

  public Color getOutsideColor(Component c)
  {
    if (outside == null)
      outside = c.getParent().getBackground();
    return outside;
  }

  public void paintBorder(Component c, Graphics g,
    int x, int y, int w, int h)
  {	
    Color[] color = interpolate(
      getOutsideColor(c), getInsideColor(c),
      thickness);
    for (int i = 0; i &lt; thickness; i++)
    {
      g.setColor(color[i]);
      g.drawLine(x + i, y + i, x + w - i - 1, y + i);
      g.drawLine(x + i, y + i, x + i, x + h - i - 1);
      g.drawLine(x + w - i - 1, y + i,
        x + w - i - 1, y + h - i - 1);
      g.drawLine(x + i, y + h - i - 1,
        x + w - i - 1, y + h - i - 1);
    }
  }

  public static Color[] interpolate(
    Color source, Color target, int units)
  {
    Color[] array = new Color[units];
    
    int sRed = source.getRed();
    int sGreen = source.getGreen();
    int sBlue = source.getBlue();
    
    int tRed = target.getRed();
    int tGreen = target.getGreen();
    int tBlue = target.getBlue();
  
    float unitRed, unitGreen, unitBlue;
    unitRed = (float)(tRed - sRed) / (float)(units - 1);
    unitGreen = (float)(tGreen - sGreen) / (float)(units - 1);
    unitBlue = (float)(tBlue - sBlue) / (float)(units - 1);
  
    int red, green, blue;
    for (int i = 0; i &lt; units; i++)
    {
      red = sRed + (int)(unitRed * i);
      green = sGreen + (int)(unitGreen * i);
      blue = sBlue + (int)(unitBlue * i);
      array[i] = new Color(red, green, blue);
    }
    return array;
  }
}

Listing 4

import java.awt.*;
import java.util.*;
import javax.swing.*;
import javax.swing.border.*;

public class GroupBorder extends AbstractBorder
{
  protected Vector borders;

  public GroupBorder()
  {
    borders = new Vector();
  }

  public GroupBorder(Border[] list)
  {
    borders = new Vector(list.length);
    for (int i = 0; i &lt; list.length; i++)
    {
      addBorder(list[i]);
    }
  }

  public void addBorder(Border border)
  {
    borders.addElement(border);
  }

  public void removeBorder(Border border)
  {
    borders.removeElement(border);
  }

  public void removeBorderAt(int index)
  {
    borders.removeElementAt(index);
  }

  public void replaceBorderAt(Border border, int index)
  {
    borders.setElementAt(border, index);
  }

  public void removeAllBorders()
  {
    borders.removeAllElements();
  }

  public Border getBorder(int index)
  {
    return (Border)borders.elementAt(index);
  }

  public int size()
  {
    return borders.size();
  }
    
  public Insets getBorderInsets(Component c)
  {
    Insets insets;
    Border border;
    int top = 0, left = 0, bottom = 0, right = 0;
    for (int i = 0; i &lt; size(); i++)
    {
      border = getBorder(i);
      if(border != null)
      {
        insets = border.getBorderInsets(c);
        top += insets.top;
        left += insets.left;
        right += insets.right;
        bottom += insets.bottom;
      }
    }
    return new Insets(top, left, bottom, right);
  }

  public void paintBorder(Component c, Graphics g,
    int x, int y, int w, int h)
  {
    Insets insets;
    Border border;
    for (int i = 0; i &lt; size(); i++)
    {
      border = getBorder(i);
      if(border != null)
      {
        border.paintBorder(c, g, x, y, w, h);		
        insets = border.getBorderInsets(c);
        x += insets.left;
        y += insets.top;
        w = w - insets.right - insets.left;
        h = h - insets.bottom - insets.top;
      }
    }
  }  
}