Put your user interface on a diet

Replace those heavyweight components with leaner, meaner lightweight components

A year or so ago, Java programmers building user interfaces (UI) had to live within the narrow confines of the Abstract Windowing Toolkit (AWT). Consequently, user interface designers had to contend with a paucity of user interface components, wide variations in their appearance and behavior across platforms, and a host of other undesirable features. It was an unpleasant time. Then, around the middle of last year, came the first release of Swing. Swing offered many more components, a common appearance, and identical behavior across platforms. It was a significant addition to the Java class library.

One of the key factors contributing to Swing's importance is that each user interface component in the Swing set is a lightweight component.

The user interface components supplied with early versions of Java -- which are still present in the java.awt package -- are called heavyweight components. They are called heavyweight components because a native user interface component is used to display each Java component -- a technique that gives Java applications the same look and feel as other applications written for a particular platform. These components are considered heavy because they require twice as many classes to implement -- a Java class plus its associated native class. They also have the unfortunate side effect of being opaque, which means that they can't be used to implement components with transparent regions, or components of non-rectangular shapes.

Lightweight components, on the other hand, have no native twin -- they are free to implement their own look and feel. Consequently, lightweight components were used as the basis for the Swing components in the JFC.

To better understand the technology behind the Swing set of user interface components and to help you see just how powerful lightweight components are, we'll build a lightweight component of our own. Because buttons are ubiquitous user interface components and are easy to understand in terms of their behavior, we'll implement a circular button. I'll assume you know how the Java UI model works as defined by the AWT and that you understand the new Java 1.1 event model.

Now, let's go to work!

Building a simple lightweight button component

We begin by creating a subclass of class java.awt.Component. Both heavyweight components and lightweight components share this common ancestor. By default, subclasses of class Component aren't associated with a native component, so they are "light" by default.

Because lightweight components lack an associated native class, they have no visual representation. Therefore, they must handle their own presentation, which they do by redefining their paint() method.

The following paint() method draws a circular button with a label. The label value (in _strLable) is set in the constructor.

public void paint(Graphics g)

{

Dimension dim = getSize();

int n = (dim.width < dim.height ? dim.width : dim.height) - 1;

// fill the interior of the button

g.setColor(getBackground());

g.fillArc(6, 6, n - 12, n - 12, 0, 360);

// draw a thin border around the button's interior

g.setColor(getBackground().darker().darker().darker().darker());

g.drawArc(6, 6, n - 12, n - 12, 0, 360);

// draw a thin ring around the entire button

g.drawArc(0, 0, n, n, 0, 360);

// draw the button's label

Font font = getFont();

if (font != null)

{

FontMetrics fontmetrics = getFontMetrics(font);

g.setColor(getForeground());

g.drawString(_strLabel,

n/2 - fontmetrics.stringWidth(_strLabel)/2,

n/2 + fontmetrics.getMaxDescent());

}

Here's what the circular buttons look like so far. Go ahead and try them out.

You need a Java-enabled browser to see this applet.

Note: This applet, and those that follow, require a fully JDK 1.1-compliant browser. Internet Explorer 4.0 qualifies, however, Netscape 4.0 does not -- at least not right out of the box. If you're using Netscape 4.0, surf on over to http://developer.netscape.com/software/jdk/download.html and install the upgrade.

Something's not quite right with the way the buttons work. Can you figure out what's missing?

Incorporating button feedback

Normally when you click on a button, you receive some sort of visual

feedback. For example, the selected button is generally highlighted to indicate it's selected.

To incorporate this type of feedback, we need to modify the paint() method. Let's assume that there is a variable named _boolPressed that reflects whether or not the button is being pressed. With that in mind, here's the new paint() method:

public void paint(Graphics g)

{

Dimension dim = getSize();

int n = (dim.width < dim.height ? dim.width : dim.height) - 1;

// fill the interior of the button **

if(_boolPressed)

{

g.setColor(getBackground().darker().darker());

}

else

{

g.setColor(getBackground());

}

g.fillArc(6, 6, n - 12, n - 12, 0, 360);

// draw a thin border around the button's interior

g.setColor(getBackground().darker().darker().darker().darker());

g.drawArc(6, 6, n - 12, n - 12, 0, 360);

// draw a thin ring around the entire button

g.drawArc(0, 0, n, n, 0, 360);

// draw the button's label

Font font = getFont();

if (font != null)

{

FontMetrics fontmetrics = getFontMetrics(font);

g.setColor(getForeground());

g.drawString(_strLabel,

n/2 - fontmetrics.stringWidth(_strLabel)/2,

n/2 + fontmetrics.getMaxDescent());

}

The sole change is marked with a double asterisk (**). The color of the interior of the button now depends on the value of the _boolPressed variable. This variable must in turn be set to true or false in response to user input with the mouse. To do that, we need to make our circular button listen to mouse events and mouse motion events.

First, we must enable the delivery of the specified events. We do this in the constructor:

public CircularButton(String strLabel)

{

enableEvents(AWTEvent.MOUSE_EVENT_MASK);

enableEvents(AWTEvent.MOUSE_MOTION_EVENT_MASK);

// store the label

_strLabel = strLabel;

}

The two invocations of the enableEvents() method tell the user interface framework that our component expects to receive both mouse events (for example, mouse button pressed, mouse button released) and mouse motion events (for example, mouse dragged) when they occur in regard to our button.

Before I show you how to implement the handlers for these events, let's take a brief look at how buttons behave.

A button is typically in an unpressed state. When a user moves the mouse cursor over the button and clicks, the button changes to its pressed state. The change of state is usually indicated visually. If the user clicks the mouse while its cursor is over the button, the button executes whatever action was associated with it, then returns to its unpressed state. In the case of the AWT, for example, the button generates an action event that is distributed to any interested listeners.

If, while the button is in the pressed state, the user does not click, but instead drags the mouse away from the button, the button will return to its unpressed state without executing its associated action. If the user continues to click the mouse while dragging its cursor back over the button, the button will return to its pressed state.

Now let's see how to make this work on our buttons.

In order to obtain this behavior it's necessary to observe three mouse events: presses, releases, and drags.

These three events are monitored by the processMouseEvent() method and the processMouseMotionEvent() method.

Let's take a look at them.

public void processMouseEvent(MouseEvent e)

{

switch (e.getID())

{

case MouseEvent.MOUSE_PRESSED:

// store the target component for future use

_component = e.getComponent();

_boolPressed = true;

repaint();

break;

case MouseEvent.MOUSE_RELEASED:

_component = null;

if (_boolPressed == true)

{

_boolPressed = false;

repaint();

}

break;

}

// don't forget to pass the event along to the supertype

super.processMouseEvent(e);

}

public void processMouseMotionEvent(MouseEvent e)

{

switch (e.getID())

{

case MouseEvent.MOUSE_DRAGGED:

// 1. does the target component still match?

// 2. are we outside the button now and were we inside

// before?

if (_component == e.getComponent() &&

!contains(e.getX(), e.getY()) &&

_boolPressed == true)

{

_boolPressed = false;

repaint();

}

// 1. does the target component still match?

// 2. are we inside the button now and were we outside

// before?

else if (_component == e.getComponent() &&

contains(e.getX(), e.getY()) &&

_boolPressed == false)

{

_boolPressed = true;

repaint();

}

break;

}

// don't forget to pass the event along to the supertype

super.processMouseMotionEvent(e);

}

Look again at the processMouseMotionEvent() method. It calls the contains() method. This method indicates whether or not a given point is within a component. By default, all of the points within the rectangular region returned by the getSize() method are considered to be within the border of the button. This is great for rectangular buttons, but not so great for buttons like ours. Here's what our contains() method looks like:

public boolean contains(int x, int y)

{

Dimension dim = getSize();

int nX = dim.width/2;

int nY = dim.height/2;

// a little basic geometry tells us whether we're in or not

return ((nX-x)*(nX-x)+(nY-y)*(nY-y)) <= (nX*nY);

}

The AWT user interface framework also calls this method. In fact, it calls this method frequently, so it must execute quickly.

Here's the result so far. If you click on the button now, it should respond with visual feedback.

You need a Java-enabled browser to see this applet.

When a button is in the pressed state and the user releases the mouse, the button generates an action event. The action event simply states that the button has been pushed. Other components (those that need to do something when the button is pushed) indicate their interest in this event by adding themselves as listeners on the button for this event. To allow other components to add themselves as listeners, we must provide an addActionEventListener() method (and its associated removeActionEventListener() method).

First we need a reference to an event listener:

private ActionListener _al = null;

Then we must create both the addActionEventListener() method and the removeActionEventListener() method:

public void addActionListener(ActionListener al)

{

_al = AWTEventMulticaster.add(_al, al);

}

public void removeActionListener(ActionListener al)

{

_al = AWTEventMulticaster.remove(_al, al);

}

These small methods use the add() and remove() methods of the AWTEventMulticaster class to manage a chain of event listeners in a thread-safe manner. When the action event is generated, it will be delivered to each listener in the chain.

Now, let's take a look at how the action event is actually generated:

public void processMouseEvent(MouseEvent e)

{

switch (e.getID())

{

case MouseEvent.MOUSE_PRESSED:

// store the target component for future use

_component = e.getComponent();

_boolPressed = true;

repaint();

break;

case MouseEvent.MOUSE_RELEASED:

_component = null;

if (_boolPressed == true)

{

if (_al != null)

{

// created and dispatch the event **

_al.actionPerformed

(

new ActionEvent

(

this,

ActionEvent.ACTION_PERFORMED,

_strLabel,

e.getModifiers()

)

);

}

_boolPressed = false;

repaint();

}

break;

}

// don't forget to pass the event along to the supertype

super.processMouseEvent(e);

}

Once again, the new code is indicated with a double asterisk (**).

The new operator creates an instance of the action event. The supplied parameters describe the event. The actionPerformed() method passes the newly created action event to all of the registered event listeners.

Here's Take Three of our button applet. I've registered an action event listener to write a short text message in the text field at the bottom of the applet whenever a circular button is pressed.

You need a Java-enabled browser to see this applet.
1 2 Page 1
Page 1 of 2