Newsletter sign-up
View all newsletters

Enterprise Java Newsletter
Stay up to date on the latest tutorials and Java community news posted on JavaWorld

JavaWorld Daily Brew

Java Tutor

Java Tutor is my platform for teaching about Java 7+ and JavaFX 2.0+, mainly via programming projects.


New Border Classes for Swing

 

Swing provides an infrastructure for adding borders (objects describing component boundary areas and providing code to paint these areas) to its components. This infrastructure consists of the javax.swing.BorderFactory class, the javax.swing.border package's types, and javax.swing.JComponent's void setBorder(Border border) and Border getBorder() methods. Consider the following example:

// A label is useful for implementing a status bar.

String defaultStatusText = "specify default text here";
JLabel statusBar = new JLabel(defaultStatusText);
statusBar.setBorder(BorderFactory.createEtchedBorder());

This example instantiates javax.swing.JLabel, invokes BorderFactory's static Border createEtchedBorder() factory method to create an etched border, and adds this border to the label by invoking setBorder(Border). Behind the scenes, createEtchedBorder() returns a shared instance of javax.swing.border's EtchedBorder class.

The javax.swing.border package provides several useful border classes (which are easy to instantiate via BorderFactory's various convenience methods), but Swing also lets you easily create additional border classes when its default repertoire doesn't meet your needs. For example, you might want to create a gradient-based border. This tutorial shows you how to create additional border classes.

Border Basics

A border is an instance of a class that either implements the javax.swing.border.Border interface or subclasses the javax.swing.border.AbstractBorder class. If the border class implements Border, it must implement this interface's three methods (AbstractBorder provides default implementations for these methods):
  • Insets getBorderInsets(Component c) returns the border's insets. These insets identify the boundary area in which the border can paint -- it cannot paint anywhere else. c identifies the component to which these insets apply.
  • boolean isBorderOpaque() returns true when the border is opaque (the border is responsible for painting all of its pixels) or false when the border isn't opaque (not painting every pixel lets you achieve transparency).
  • void paintBorder(Component c, Graphics g, int x, int y, int width, int height) paints the border. c identifies the component whose border is being painted, g identifies the component's graphics context, x and y identify the upper-left corner of the border area, and width and height identify the border area's dimensions.

In addition to defaulting getBorderInsets() to return a new java.awt.Insets object containing zeroes, defaulting isBorderOpaque() to return false, and defaulting paintBorder() to do nothing, AbstractBorder provides the following useful methods (I ignore the two baseline methods introduced by Java SE 6 in this tutorial):

  • Insets getBorderInsets(Component c, Insets insets) reinitializes insets with this border's current insets. c identifies the component for which this border insets value applies.
  • static Rectangle getInteriorRectangle(Component c, Border b, int x, int y, int width, int height) returns a rectangle using the arguments minus the border's insets. This is useful for determining the area in which components paint -- this area will not intersect the border. c identifies the component for which this border is being calculated.
  • Rectangle getInteriorRectangle(Component c, int x, int y, int width, int height) calls the previous method with its arguments and a reference to the current border. c identifies the component for which this border is being calculated.

Note: To learn more about borders, check out Amy Fowler's Understanding Borders article.

Many border classes extend AbstractBorder, providing suitable constructors, overriding the default implementations of Border's three methods, and overriding the default implementation of the getBorderInsets(Component c, Insets insets) method. Listing 1 demonstrates these tasks by presenting a border class that paints a red border with a specific boundary area width.

import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Insets;

import javax.swing.border.AbstractBorder;

public class RedBorder extends AbstractBorder
{
   private Insets insets;

   private int width;

   public RedBorder(int width)
   {
      this.width = width;
   }

   public Insets getBorderInsets(Component c)
   {
      return new Insets(width, width, width, width);
   }

   public Insets getBorderInsets(Component c, Insets i)
   {
      i.left = i.right = i.top = i.bottom = width;
      return i;
   }

   public boolean isBorderOpaque()
   {
      return true; // Entire border area is painted.
   }

   public void paintBorder(Component c, Graphics g, int x, int y, int w,
                           int h)
   {
      if (insets == null)
         insets = getBorderInsets(c);
      else
         insets = getBorderInsets(c, insets);
            
      g.setColor(Color.red);
      g.fillRect(x, y, w, insets.top);
      g.fillRect(x, y, insets.left, h);
      g.fillRect(x+w-insets.right, y, insets.right, h);
      g.fillRect(x, y+h-insets.bottom, w, insets.bottom);
   }
}

Listing 1: RedBorder.java

The paintBorder() method reveals a pattern that results in only one Insets object being created no matter how many times this method is invoked. Because this method is constantly invoked during a component resize operation, you want to be careful not to unnecessarily create objects. Excessive object creation reduces heap space and can impact performance.

The paintBorder() method also reveals the manner in which it paints the red border. Care is taken to ensure that painting is restricted to the component's boundary area by taking the border's insets into account -- never paint outside this area. Also, each pixel in the boundary area is painted because the border's isBorderOpaque() method returns true.

Gradient Border

Despite its usefulness from a learning perspective, the RedBorder class is not appropriate for general use. Even if you were to rename this class to ColorBorder and allow an arbitrary color to be specified, the resulting class would not be necessary because you can use the existing MatteBorder class to produce the same result.

A more useful border class is one that paints a gradient for a border -- this is something that MatteBorder cannot do. I've created a GradientBorder class that lets you specify a vertical gradient via two colors: the start color appears at the top of the border and transitions to the end color, which appears at the bottom. Listing 2 presents this border class.

import java.awt.Color;
import java.awt.Component;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;

import javax.swing.border.AbstractBorder;

public class GradientBorder extends AbstractBorder
{
   private Color startColor, endColor;

   private GradientPaint gp;

   private Insets insets;

   private int width;

   public GradientBorder(Color startColor, Color endColor, int width)
   {
      this.startColor = startColor;
      this.endColor = endColor;
      this.width = width;
   }

   public Insets getBorderInsets(Component c)
   {
      return new Insets(width, width, width, width);
   }

   public Insets getBorderInsets(Component c, Insets i)
   {
      i.left = i.right = i.top = i.bottom = width;
      return i;
   }

   public boolean isBorderOpaque()
   {
      return true; // Entire border area is painted.
   }

   public void paintBorder(Component c, Graphics g, int x, int y, int w,
                           int h)
   {
      gp = new GradientPaint(x, y, startColor, 0, h, endColor);

      if (insets == null)
         insets = getBorderInsets(c);
      else
         insets = getBorderInsets(c, insets);

      ((Graphics2D) g).setPaint(gp);
      g.fillRect(x, y, w, insets.top);
      g.fillRect(x, y, insets.left, h);
      g.fillRect(x+w-insets.right, y, insets.right, h);
      g.fillRect(x, y+h-insets.bottom, w, insets.bottom);
   }
}

Listing 2: GradientBorder.java

Although GradientBorder is similar to RedBorder (such as painting an opaque border and obtaining border insets in an identical fashion), there are differences. For example, a new java.awt.GradientPaint object must be created each time paintBorder() is called to account for the changing height of the border.

Now that we have a border class capable of displaying a gradient paint, we need an application that reveals the resulting border. Consider a GradientBorderDemo application that creates a GradientBorder instance and adds this border to the GUI's root pane. This application's source code is presented in Listing 3.

import java.awt.Color;
import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JLabel;

public class GradientBorderDemo extends JFrame
{
   public GradientBorderDemo()
   {
      super("GradientBorderDemo");
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      getRootPane().setBorder(new GradientBorder(Color.red, Color.blue, 10));
      getContentPane().add(new JLabel("Welcome to GradientBorderDemo",
                                      JLabel.CENTER));
      setSize(350, 150);
      setVisible(true);
   }

   public static void main(String[] args)
   {
      Runnable r = new Runnable()
      {
         public void run()
         {
            new GradientBorderDemo();
         }
      };
      EventQueue.invokeLater(r);
   }
}

Listing 3: GradientBorderDemo.java

GradientBorderDemo creates a GradientBorder instance with red as its starting (top) color and blue as its ending (bottom) color, and with a boundary area width of 10 pixels (on each side). After installing this border as the root pane's border, this application creates a label and adds it to the content pane. Figure 1 reveals the resulting user interface.

Figure 1: Resize the GUI to see how the gradient changes.

Exercises

  1. Create a RedBorderDemo application that demonstrates RedBorder.
  2. Enhance GradientBorder by adding accessor ("get") methods that return the start and end colors, and boundary area width. Furthermore, add mutator ("set") methods that allow you to specify new colors and a new width. Finally, add support for a horizontal gradient to complement the vertical gradient.
  3. Enhance GradientBorderDemo to demonstrate the enhanced GradientBorder class. Accomplish this task by adding a button to the GUI. In response to a button click, toggle between the original red/blue/10/vertical border properties and alternate green/yellow/40/horizontal border properties.

Code

You can download this post's code and answers here. Code was developed and tested with JDK 7u4 on a Windows 7 platform.

* * *

I welcome your input to this blog, and will write about relevant topics that you suggest. While waiting for the next blog post, check out my TutorTutor website to learn more about Java and other computer technologies (and that's just the beginning).

Learn more about the Java 7 language and many of its APIs by reading my book Beginning Java 7. You can obtain information about this book here and here.