Moving to JDK 1.1: Using the delegation event model to create custom AWT components

Learn how a move to JDK 1.1 affects the way you create custom components

Although many applications currently require JDK 1.0.2, a transition to 1.1 is inevitable for any serious development efforts. This move introduces significant changes to the Abstract Windowing Toolkit (AWT), some of which we'll address in this article. Over the next few virtual pages, we'll step through the how-tos of building reusable AWT components that operate within the new event delegation model. The concepts we cover also will be helpful when you need to update existing custom components to work within the new event model. Those with an eye to the future should make an effort to be aware of the additional changes JDK 1.2 will bring, but that's another story....

To keep things simple, we're going to look at a fairly basic example. My goal is to show you how to acquire, process, and dispatch events, without getting bogged down by the complicated color selection details.

The ColorPicker interface

As you can see in the figure below, the ColorPicker component consists of three regions: The left-hand region displays a swatch of colors, with the red level varying from left to right across the swatch and the green level varying from top to bottom. The user selects red and green levels by clicking in this swatch. The middle region displays a vertical bar of blue. The user specifies the amount blue by clicking on the proper location in the bar. The right-hand region displays the current color, which is a combination of the red, green, and blue levels the user selects. Clicking in this region selects the current color, causing an appropriate AWT event to occur.

The ColorPicker interface

New AWT essentials

The following list includes the essential elements of the AWT that are affected by JDK 1.1, as they apply to custom components:

  • Event model -- Events do not percolate up the container hierarchy as before; instead, interested listeners register themselves with AWT components, and events are multicast through listener interfaces.

  • Event types -- A single monolothic Event class is no longer used for the delivery of all events; instead, different event classes are derived from java.util.EventObject (or, with respect to the AWT, java.awt.AWTEvent) and provide an appropriate interface to the relevant occurrence.

  • Event methods -- Events are no longer delivered to components through the traditional handleEvent() method; instead, a processEvent() method is used along with various associated helpers.

  • Event masks -- Event masks of the events that a particular component should generate are now maintained. This new approach is more efficient because a particular event will not be generated and processed if no target is listening for it. As a result, if you subclass a component, then it will, by default, not generate the usual AWT events. If you wish to receive certain events, you must explicitly flag those event types that you wish generated.

  • Method names -- Many methods have been renamed to achieve a more consistent and Beans-like interface. This change was necessary for a transition to the new event model.

The ColorPicker implementation

Here are the four classes that implement our component:

  • ColorEvent is a custom event class that transports the ColorPicker's Color result.

  • ColorListener is the interface through which interested parties listen for ColorEvents.

  • ColorPicker is the actual graphical color picker component.

  • ColorEventMulticaster is used by the ColorPicker class to maintain a list of registered ColorListeners.

Let's take a detailed look at each of these classes, and then I'll show how you the ColorPicker component works.

Class ColorEvent

Events of type ColorEvent are posted by the ColorPicker component when the user selects a color by clicking in the right-hand GUI region. The event contains a Color field, which is the color selected by the user and is extractable through the getColor() method.

Any event that will be used by an AWT component must be a subclass of the AWTEvent class. In this case, we declare a ColorEvent subclass to transport color events:

import java.awt.AWTEvent;
import java.awt.Color;
public class ColorEvent extends AWTEvent {

All AWT events must be assigned integer identifiers; a valid user identifier is any value above AWTEvent.RESERVED_ID_MAX.

public static final int COLOR_PICKED = AWTEvent.RESERVED_ID_MAX + 1;

Because the class of an event is now used as a distinguishing feature and not just its ID, user components no longer have to choose globally unique values. Instead, this identifier can be used to distinguish among different types of a particular event class. For example, we also could define a COLOR_CHANGED identifier that identifies when the user has changed the selection but not yet accepted the result. We then could distinguish between the two events with one ColorEvent class and two identifiers instead of two separate event classes.

The color associated with this event is stored in the color variable:

protected Color color;

The constructor for this event accepts the source of the event, source, which for this example will be a ColorPicker, and the Color color that was picked:

public ColorEvent (Object source, Color color) {
  super (source, COLOR_PICKED);
  this.color = color;
}

The getColor method allows the event's recipient to extract the associated color:

public Color getColor () {
  return color;
}

The paramString method simply is a convenience method that is used when an AWTEvent is printed to the console:

public String paramString () {
    return "COLOR_PICKED,color=" + color;
}

Interface ColorListener

Classes interested in receiving ColorEvents must implement the ColorListener interface that declares a colorPicked() method through which such events will be delivered.

All event listener interfaces must extend the EventListener dummy interface:

import java.util.EventListener;
public interface ColorListener extends EventListener {

The colorPicked() method will be called on all interested parties when a color has been picked. The parameter e contains the relevant ColorEvent:

  
public void colorPicked (ColorEvent e);

Class ColorPicker

The ColorPicker class is a simple color-picking component that uses the new event delegation model of JDK 1.1. You add it to a container as you would any normal AWT component, and it delivers a new-paradigm event when the user selects a color.

I've intentionally made the color selection user interface very primitive because we are concerned with the internals of 1.1 eventing, not usability. Lines of code that bear particular relevance to JDK 1.1 are highlighted in red. I recommend you experiment with creating a more friendly interface.

We extend Canvas because ColorPicker is an entirely custom-drawn component. If we wanted to build our color picker from other components, we would extend Panel instead:

import java.awt.*;
import java.awt.event.MouseEvent;
public class ColorPicker extends Canvas {

The color picker quantizes the 0-255^3 RGB space into six levels of each component: 0, 51, 102, 153, 204, 255. This corresponds to the typical 256-color browser color cube. For finer granularity of color selection, use more levels:

protected static final int LEVELS = 6;

The current levels of red, green, and blue are stored in the variables r, g, and b, respectively:

protected int r, g, b;

In the constructor we extract the various color levels from the initial color color, and then enable mouse events using the enableEvents() method and a mask of AWTEvent.MOUSE_EVENT_MASK. If we did not enable events in this manner, the mouse events would not be generated by this component. We'll get into this in greater detail later when we discuss the processMouseEvent() method.

public ColorPicker (Color color) {
  r = color.getRed ();
  g = color.getGreen ();
  b = color.getBlue ();
  enableEvents (AWTEvent.MOUSE_EVENT_MASK);
}

The following table shows the event masks in the AWTEvent class that correspond to the different AWT event listeners. Multiple event types can be enabled by either ORing together the masks or by calling enableEvents() repeatedly. Registering a listener for an event type automatically enables the relevant event type.

Event maskListener interface
ACTION_EVENT_MASKActionListener
ADJUSTMENT_EVENT_MASKAdjustmentListener
COMPONENT_EVENT_MASKComponentListener
CONTAINER_EVENT_MASKContainerListener
FOCUS_EVENT_MASKFocusListener
ITEM_EVENT_MASKItemListener
KEY_EVENT_MASKKeyListener
MOUSE_EVENT_MASKMouseListener
MOUSE_MOTION_EVENT_MASKMouseMotionListener
TEXT_EVENT_MASKTextListener
WINDOW_EVENT_MASKWindowListener
Event masks and their corresponding listeners

An alternative means for this component to receive its own mouse events would be to implement the MouseListener interface and register as a listener.

This constructor calls the other constructor with an initial color value of black:

public ColorPicker () {
    this (Color.black);
  }

The getPreferredSize() method, shown next, chooses an appropriate size for the component. Under JDK 1.0.2 this method was called preferredSize(). For sake of completeness, we should also implement the getMinimumSize() and getMaximumSize() methods; however, for clarity (not to mention brevity) I've omitted these from this example:

public Dimension getPreferredSize () {
  return new Dimension (150, 60);
}

Moving right along, the paint() method draws a color swatch on the left, a blue bar with a small blue level marker in the middle, and the current color on the right. The details are not particularly interesting; We choose a block size based on the number of levels desired and the component size and then fill in the blanks:

public void paint (Graphics g) {
  int h = getSize ().width / (LEVELS + 3 + LEVELS);
  int v = getSize ().height / (LEVELS);
  for (int red = 0; red < LEVELS; ++ red) {
    for (int green = 0; green < LEVELS; ++ green) {
      g.setColor (new Color (red * 255 / (LEVELS - 1),
                 green * 255 / (LEVELS - 1),
                 b));
      g.fillRect (red * h, green * v, h, v);
    }
  }
  int x = LEVELS * h  + h / 2;
  int y = v / 2 + v * (b * (LEVELS - 1) / 255);
  g.setColor (getForeground ());
  g.drawLine (x, y, x + 2 * h - 1, y);
  for (int blue = 0; blue < LEVELS; ++ blue) {
    g.setColor (new Color (0, 0, blue * 255 / (LEVELS - 1)));
    g.fillRect ((LEVELS + 1) * h, blue * v, h, v);
  }
  g.setColor (new Color (r, this.g, b));
  g.fillRect ((LEVELS + 3) * h, 0, h * LEVELS, v * LEVELS);
}

The processMouseEvent() method is called automatically by Component's processEvent() method when a mouse event is generated. We override this method to call our own mousePressed() method for mouse-press events, and then we call the superclass processMouseEvent() to perform further appropriate processing. If there are other registered listeners for our own mouse events, the superclass method will appropriately inform them through their MouseListener interface:

/*
 * This code allows us to catch mouse events without registering listeners.
 */
protected void processMouseEvent (MouseEvent e) {
  if (e.getID () == MouseEvent.MOUSE_PRESSED) {
    mousePressed (e);
  }
  super.processMouseEvent (e);
}

We call mousePressed when the user clicks on the color picker. If the user clicks in the swatch of colors, we assign new red and green color levels, quantized to the chosen number of color levels. If the user clicks in the blue bar, we assign a new blue level. If the user clicks in the right-hand region, we call the postColorEvent() method to post an appropriate event. Let's see how this works:

public void mousePressed (MouseEvent e) {
  int h = getSize ().width / (LEVELS + 3 + LEVELS);
  int v = getSize ().height / (LEVELS);
  if (e.getX () < LEVELS * h) { // in swatch area
    r = (e.getX () / h) * 255 / (LEVELS - 1);
    r = (r < 0) ? 0 : (r > 255) ? 255 : r;
    g = (e.getY () / v) * 255 / (LEVELS - 1);
    g = (g < 0) ? 0 : (g > 255) ? 255 : g;
    repaint ();
  } else if (e.getX () < (LEVELS + 3) * h) { // in blue bar
    b = (e.getY () / v) * 255 / (LEVELS - 1);
    b = (b < 0) ? 0 : (b > 255) ? 255 : b;
    repaint ();
  } else { // in select square
    postColorEvent ();
  }
}

Now take a look at the following snippet:

/*
 * This code posts a new ColorEvent to the system event queue.
 */ 
protected void postColorEvent () {
  ColorEvent e = new ColorEvent (this, new Color (r, g, b));
  Toolkit toolkit = getToolkit ();
  EventQueue queue = toolkit.getSystemEventQueue ();
  queue.postEvent (e);
}
// alternatively:  
// dispatchEvent (new ColorEvent (this, new Color (r, g, b)));

The postColorEvent method creates a new ColorEvent with this as its origin and the currently selected color as its payload, and posts it to the system event queue. Under JDK 1.0.2 we called the postEvent() method to cause an event to percolate up the container hierarchy. Under JDK 1.1 we can either call dispatchEvent() to immediately dispatch an event or we can post an event to the system event queue. This event queue is monitored by an AWT thread (EventDispatchThread), which simply extracts AWTEvents and calls dispatchEvent() on the component that is the source of the event.

In practice, applets must use the dispatchEvent() method to dispatch an event because the security manager restricts access to the system event queue. For an application, which has the choice, the difference between using dispatchEvent() and the system event queue is fairly subtle: An event inserted in the queue will be delivered later by the AWT event thread; otherwise, the event will be delivered immediately by the caller. Among other things, using the queue protects the caller against RuntimeExceptions that may be thrown by a recipient.

In both cases, however, the dispatchEvent() method is called, which in turn calls processEvent(). We must override processEvent() to appropriately handle the new event type. We'll return to this method in a moment.

Under the new event delegation model, we must maintain a list of registered listeners to which events will be delivered:

/*
 * This code maintains a list of current ColorListeners.
 */
protected ColorListener colorListener;

We could use a Vector for this purpose, but instead we use the ColorEventMulticaster class to maintain a list in the colorListener variable.

Next we have the addColorListener() method, which adds the ColorListener l to the list colorListener using the static method add() of class ColorEventMulticaster. This listener will be notified when the user selects a color with this color picker component:

public synchronized void addColorListener (ColorListener l) {
  colorListener = ColorEventMulticaster.add (colorListener, l);
}

Likewise, the removeColorListener() method removes the ColorListener l from the list colorListener using the static method remove() of class ColorEventMulticaster:

public synchronized void removeColorListener (ColorListener l) {
  colorListener = ColorEventMulticaster.remove (colorListener, l);
}

The processEvent() method, shown next, is called by dispatchEvent() to distribute AWT events generated by this component to any registered listeners. As I indicated earlier, we override the method to correctly process color events: If the event is of class ColorEvent, we call processColorEvent(); otherwise, we call the superclass processEvent() method to appropriately handle normal AWT events:

/*
 * This code processes the dispatch of ColorEvents to the ColorListeners.
 */
protected void processEvent (AWTEvent e) {
  if (e instanceof ColorEvent) {
    processColorEvent ((ColorEvent) e);
  } else {
    super.processEvent (e);
  }
}

Next, we distribute a ColorEvent to the list of registered listeners in colorListener through the colorPicked() method of interface ColorListener. The ColorEventMulticaster class provides this simple interface to performing an event multicast; we call colorPicked() once and it is automatically distributed to every listener in the list:

protected synchronized void processColorEvent (ColorEvent e) {
  if (colorListener != null)
    colorListener.colorPicked (e);
}

Alternatively, we can implement a Vector of listeners that loop through the Vector, calling colorPicked() on each element to perform a multicast:

/*
 * Using a Vector to store the listeners
 */
protected Vector colorListeners = new Vector ();
 public void addColorListener (ColorListener l) {
   colorListeners.addElement (l);
 }
 public void removeColorListener (ColorListener l) {
   colorListeners.removeElement (l);
 }
 protected void processColorEvent (ColorEvent e) {
   synchronized (colorListeners) {
     for (int i = 0; i < colorListeners.size (); ++ i) {
       ((ColorListener) colorListeners.elementAt (i)).colorPicked (e);
     }
   }
 }

We use the addElement() and removeElement() methods of class Vector to maintain the list, and simply sequence through the list to multicast color events.

Class ColorEventMulticaster

The ColorEventMulticaster class maintains a list of ColorListeners. This class itself implements the ColorListener interface and maintains references to two other ColorListeners, as illustrated on the left side of the figure below. When the colorPicked() method is called, the event is passed onto both attached ColorListeners. By stringing together a sequence of ColorEventMulticasters, as indicated on the right side of the figure below, we can maintain a list of listeners.

An event multicaster
A chain of multicasters
An event multicasterA chain of multicasters

We implement the ColorListener interface and maintain references to two other ColorListeners a and b:

class ColorEventMulticaster implements ColorListener {
  protected ColorListener a, b;

The constructor accepts references to two ColorListeners a and b:

protected ColorEventMulticaster (ColorListener a, ColorListener b) {
  this.a = a;
  this.b = b;
}

The colorPicked() method then simply passes the supplied event onto both attached ColorListeners:

public void colorPicked (ColorEvent e) {
    a.colorPicked (e);
    b.colorPicked (e);
  }

To add a listener, we use the following static method. This method returns a ColorListener, which constitutes a list of the two supplied ColorListeners a and b. As indicated in the figure Adding a listener, if either a or b is null, we can simply return the other; otherwise, we return a ColorEventMulticaster that multicasts any call to colorPicked() onto both a and b:

static ColorListener add (ColorListener a, ColorListener b) {
    if (a == null)
      return b;
    else if (b == null)
      return a;
    else
      return new ColorEventMulticaster (a, b);
  }
Adding a listener
Adding a listener

To remove a listener, we use the following static method. This method returns a ColorListener, which constitutes the list of ColorListeners a with the ColorListener b removed. As indicated in the figure Removing a listener, if a is either null or b, we return null; however, if a is a ColorEventMulticaster list then we remove b from both of its subtrees and combine the result. Otherwise, we return a because it is a ColorListener that is neither a list nor b:

static ColorListener remove (ColorListener a, ColorListener b) {
    if ((a == null) || (a == b))
      return null;
    else if (a instanceof ColorEventMulticaster)
      return add (remove (((ColorEventMulticaster) a).a, b),
                  remove (((ColorEventMulticaster) a).b, b));
    else
      return a;
  }
Removing a listener
Removing a listener

Using the ColorPicker

We can use the ColorPicker as we would use any typical JDK 1.1 AWT component: We add it to a container and then register any interested listeners through the addColorListener() method. When the user selects a color, the listeners will be notified through their colorPicked() methods.

The following example demonstrates the picker's usage, opening a Frame with a color picker, and registering a listener that prints out any selections:

import java.awt.*;
public class PickerTester implements ColorListener {
  public void colorPicked (ColorEvent e) {
    System.out.println ("Picked: " + e.getColor ());
  }
  public static void main (String args[]) {
    Frame frame = new Frame ("Test");
    ColorPicker picker = new ColorPicker ();
    PickerTester tester = new PickerTester ();
    picker.addColorListener (tester);
    frame.add ("Center", picker);
    frame.pack ();
    frame.setVisible (true);
  }
}

We simply create a Frame, a ColorPicker, and a PickerTester. We register the PickerTester as a listener for the ColorPicker, which we add to the Frame, which we then pack and show.

Conclusion

The new event delegation model of the AWT is important because it enables us to develop more complex graphical applications more easily than under JDK 1.0.2. We no longer have to repeatedly subclass containers and components in order to process their events, and interaction between AWT and non-AWT systems is now possible.

I hope this example provides you with a solid background that you can draw on when you sit down to develop your own custom components. A few comments: A component that extends Panel and adds other components can simply register itself as a listener for their events and proceed in a similar manner. Before you start building a custom event class, find out if an existing AWT event class will suit your needs. You will cut down significantly on the coding you need to do (you won't need your own event or listener classes), because you can use the existing Component event dispatch mechanism.

You eagle-eyed folks may have noticed that we did not use adapters in this example. An adapter, such as java.awt.event.MouseAdapter, is a class that implements a listener interface with empty method bodies. A subclass can then implement only those methods in which it is interested and not bother with those methods that are not relevant and are supplied by the superclass. Because our ColorListener interface provides only one method, an adapter would not provide any added convenience.

Merlin Hughes is VP of Technology at Prominence Dot Com, lead author of Java Network Programming by Manning Publications Company, and co-author of JavaWorld's monthly Java Step By Step column. Merlin has studied Feng Shui for the past 20 years and has failed to attain any sense of balance, harmony, or order.

Learn more about this topic

  • Previous Step By Step articles

Join the discussion
Be the first to comment on this article. Our Commenting Policies
See more