start page | rating of books | rating of authors | reviews | copyrights

Exploring Java

Previous Chapter 11
Using and Creating GUI Components
Next
 

11.9 Creating custom components

In the previous sections, we've worked with many different user interface objects, and made a lot of new classes that are sort of like components. Our new classes do one particular thing well; a number of them can be added to applets or other containers just like the standard AWT components; and several of them are lightweight components that use system resources efficiently because they don't rely on a peer.

But these new classes still aren't really components. If you think about it, all our classes have been fairly self-contained; they know everything about what to do, and don't rely on other parts of the program to do much processing. Therefore, they are overly specialized. Our menu example created a DinnerFrame class that had a menu of dinner options, but it included all the processing needed to handle the user's selections. If we wanted to process the selections differently, we'd have to modify the class. That's not what we want; we'd like to separate registering the user's choices from processing those choices. In contrast, true components don't do any processing. They let the user take some action, and then inform some other part of the program, which processes the action.

So we need a way for our new classes to communicate with other parts of the program. Since we want our new classes to be components, they should communicate the way components communicate: that is, by generating events and sending those events to listeners. So far, we've written a lot of code that listened for events, but haven't seen any examples that generated events.

Generating events sounds like it ought to be difficult, but it isn't. You can either create new kinds of events, by subclassing AWTEvent, or use one of the standard event types. In either case, you need to register listeners for your events, and provide a means to deliver events to your listeners. If you are using the standard events, AWT provides an AWTEventMulticaster class that handles most of the machinery. We'll focus on that option in this section; at the end, we'll make some comments on how you might manage events on your own.

The AWTEventMulticaster is one of those things that looks a lot more complicated than it is. It is confusing, but most of the confusion occurs because it's hard to believe it's so simple. Its job is to maintain a linked list of event listeners and to propagate events to all the listeners on that linked list. So we can use a multicaster to register (and unregister) event listeners and to send any events we generate to all registered listeners.

The best way to show you how to use the multicaster is through an example. The following example creates a new component called PictureButton. PictureButton looks at least somewhat button-like and responds to mouse clicks (MOUSE_RELEASED events) by generating action events. (Figure 11.10 shows a PictureButton in both depressed and raised modes.) The PictureButtonApplet is passed the events in its actionPerformed() method, just as with any other button, and prints a message each time it's pressed.

Figure 11.10: The PictureButtonApplet

[Graphic: Figure 11-10]

import java.awt.*;
import java.awt.event.*;
public class PictureButtonApplet extends java.applet.Applet implements ActionListener { 
    Image image;
    public void init() {
        image = getImage( getClass().getResource(getParameter("image")) );
        PictureButton pictureButton = new PictureButton( image );
        add ( pictureButton );
        pictureButton.setActionCommand("Aaargh!");
        pictureButton.addActionListener( this );
    }
    
    public void actionPerformed( ActionEvent e ) {
        System.out.println( e );
    }
}
class PictureButton extends Component {
    private Image image;
    boolean pressed = false;
    ActionListener actionListener;
    String actionCommand;
    PictureButton(Image image) {
        this.image = image;
        MediaTracker mt = new MediaTracker(this);
        mt.addImage( image, 0 );
        try { mt.waitForAll(); } catch (InterruptedException e) { /* error */ };
        setSize( image.getWidth(null), image.getHeight(null) );
        enableEvents( AWTEvent.MOUSE_EVENT_MASK );
    }
    public void paint( Graphics g ) {
        g.setColor(Color.white);
        int width = getSize().width, height = getSize().height;
        int offset = pressed ? -2 : 0;  // fake depth
        g.drawImage( image, offset, offset, width, height, this );
        g.draw3DRect(0, 0, width-1, height-1, !pressed);
        g.draw3DRect(1, 1, width-3, height-3, !pressed);
    }
    public Dimension getPreferredSize() {
        return getSize();
    }
    public void processEvent( AWTEvent e ) {
        if ( e.getID() == MouseEvent.MOUSE_PRESSED ) {
            pressed = true;
            repaint();
        } else 
        if ( e.getID() == MouseEvent.MOUSE_RELEASED ) {
            pressed = false;
            repaint();
            fireEvent();
        }
        super.processEvent(e);
    }
    public void setActionCommand( String actionCommand ) {
        this.actionCommand = actionCommand;
    }
    public void addActionListener(ActionListener l) {
        actionListener = AWTEventMulticaster.add(actionListener, l);
    }
    public void removeActionListener(ActionListener l) {
        actionListener = AWTEventMulticaster.remove(actionListener, l);
    }
    private void fireEvent() {
        if (actionListener != null) {
            ActionEvent event = new ActionEvent( this, 
                                ActionEvent.ACTION_PERFORMED, actionCommand );
            actionListener.actionPerformed( event );
        }
    }
}

Before diving into the event multicaster, here are a few notes about the applet and the PictureButton. The applet is an ActionListener because it is looking for events coming from the button. Therefore it registers itself as a listener and contains an actionPerformed() method. The PictureButton doesn't have a label, so the applet explicitly sets the button's action command by calling setActionCommand().

The button itself is concerned mostly with being pretty. It uses a media tracker to make sure that the image has loaded before displaying itself. The paint() method, which we won't discuss in detail, is devoted to making the button appear "pressed" (i.e., recessed) when the mouse is pressed. The getPreferredSize() method lets layout managers size the button appropriately.

Now we'll start with the button's machinery. The button needs to receive mouse events. It could register as a mouse listener, but in this case, it seems more appropriate to override processEvent(). processEvent() receives all incoming events. It first checks whether we have a MOUSE_PRESSED event; if so, it tells the button to repaint itself in its "pressed" mode. If the event is a MOUSE_RELEASED event, it tells the button to paint itself in its "unpressed" mode and calls the private fireEvent() method, which sends an action event to all listeners. Finally, processEvent() calls super.processEvent() to make sure normal event processing occurs; this is a good practice whenever you override a method that performs a significant task.

However, processEvent() doesn't receive events if they aren't generated; and normally, events aren't generated if there are no listeners. Therefore, the button's constructor calls enableEvents() with the argument MOUSE_EVENT_MASK to turn on mouse event processing.

Now we're ready to talk about how to generate events. The picture button has addActionListener() and removeActionListener() methods, for registering listeners. These just call the static methods add() and remove() of the AWTEventMulticaster class. Here's the addActionListener() method:

public void addActionListener(ActionListener l) {
    actionListener = AWTEventMulticaster.add(actionListener, l);
}

If you look back to see how the instance variable actionListener is declared, you'll see that it is an ActionListener. No big surprise--except that this code doesn't appear to make sense. It's saying "add an action listener to an action listener and store the result back in the original action listener."

There are a couple of tricks here. First, an AWTEventMulticaster implements all of the listener interfaces. Therefore, a multicaster can appear wherever an ActionListener (or any other listener) is required. In this case, the actionListener object will be a multicaster--perhaps not what you expected, and certainly not what's being passed in the argument l. Now the code is starting to make sense: earlier, I said that multicasters maintained linked lists of listeners. So this method really adds l to the linked list of action listeners that a multicaster is managing, and saved the new list.

But that begs the question: where does the multicaster come from? The linked list has to start somewhere. This is where the second trick comes in. add() is a static method, so we don't need a multicaster to call it. But we still need some way to start the linked list. The class's constructor is never called--in fact, it's protected, so you can't call it. The answer lies in the add() method, which creates an AWTEventMulticaster when you need it--that is, as soon as you add the second listener to the list. The arguments to add() may be null; one of them probably is null when you register your first action listener.

Removing action listeners works the same way. We use the AWTEventMulticaster's remove() method. After the last listener is taken off the linked list, remove() returns null.

With this machinery in place, sending an event to all registered listeners is very simple. You just create an event by calling its constructor, and then call the appropriate method in the listener interface to deliver it. The AWTEventMulticaster makes sure that the event gets to all the listeners. In this example, we create an ActionEvent and deliver the event to the listeners' actionPerformed() methods by calling actionListener.actionPerformed(event).

The code to generate other kinds of events is almost exactly the same. Remember the multicaster implements all the listener interfaces and has overloaded add() and remove() methods for every standard listener type. Therefore, it can be used for any kind of AWTEvent. It shouldn't be hard to adapt this example to other situations.

What if you want to generate your own event type by subclassing AWTEvent? To make things concrete, let's say you want to create an ExplosionEvent that you generate whenever your monitor catches fire. In this case, you should define your own ExplosionListener interface, and (possibly) your own ExplosionAdapter class. You can't use the AWTEventMulticaster unless your new event is a subclass of a standard event; extending the multicaster to support new event types probably isn't worth the effort. It's easier to write an addExplosionListener() method that maintains a Vector of listeners and to deliver events by calling the appropriate method of each listener in the Vector. We'll demonstrate this approach in the next section, where we implement another new component: a Dial.

A Dial Component

    Things to mention in widgets Dial event example:
        synchronization issues in add/remove/fire.
        You should sync add/remove... but be wary of syncing fire, 
        deadlocks

The standard AWT classes don't have a component that's similar to an old fashioned dial--for example, the volume control on your radio. Such a component is something of a rarity; I don't remember seeing one in any application I've used. But there's no reason we can't build one. In this section, we implement a Dial class. We also define a new event type, DialEvent, and a new listener interface, DialListener. The dial can be used just like any other Java component. It is built entirely in Java and, therefore, is a lightweight component; it extends Component directly and doesn't have a peer.

By defining a new event type, I'm stretching the point slightly. There's no reason our dial couldn't use the standard AdjustmentEvent. However, this gives us a chance to show how to handle event listeners without using the event multicaster. There are many situations in which defining a new event type will be the only appropriate solution.

Figure 11.11 shows what the dial looks like; it is followed by the code.

import java.awt.*;
import java.awt.event.*;
import java.util.*;
public interface DialListener {
    void dialAdjusted( DialEvent e );
}
public class DialEvent extends AWTEvent {
    int value;
    public static final int DIAL_ADJUSTED = RESERVED_ID_MAX + 1;
    DialEvent( Dial source, int id, int value ) {
        super( source, id );
        this.value = value;
    }
    public int getValue() {
        return value;
    }
}
public class Dial extends Component { 
    int minValue = 0, value, maxValue = 100, radius;
    Vector dialListeners;
    Dial( int maxValue ) {
        this.maxValue = maxValue; 
        enableEvents( AWTEvent.MOUSE_MOTION_EVENT_MASK );
    }
    public void paint( Graphics g ) {
        int tick = 10;
        radius = getSize().width/2 - tick;
        g.drawLine(radius*2+tick/2, radius, radius*2+tick, radius); // the tick
        draw3DCircle( g, 0, 0, radius, true );
        draw3DCircle( g, 1, 1, radius-1, true );
        int knobRadius = radius/7;
        double th = value*(2*Math.PI)/(maxValue-minValue);
        int x = (int)(Math.cos(th)*(radius-knobRadius*3)), 
            y = (int)(Math.sin(th)*(radius-knobRadius*3));
        draw3DCircle( g, x+radius-knobRadius, y+radius-knobRadius, knobRadius, false );
    }
    private void draw3DCircle( Graphics g, int x, int y, int radius, boolean raised ) {
        g.setColor( raised ? Color.white : Color.black );
        g.drawArc( x, y, radius*2, radius*2, 45, 180);
        g.setColor( raised ? Color.black : Color.white);
        g.drawArc( x, y, radius*2, radius*2, 225, 180);
    }
    public void processEvent( AWTEvent e ) {
        if ( e.getID() == MouseEvent.MOUSE_DRAGGED ) {
            int y=((MouseEvent)e).getY();
            int x=((MouseEvent)e).getX();
            double th = Math.atan( (1.0*y-radius)/(x-radius) );
            int value = ( (int)(th/(2*Math.PI) * (maxValue-minValue)) );
            if ( x < radius ) 
                setValue( value + maxValue/2 );
            else if ( y < radius )
                setValue( value + maxValue );
            else 
                setValue( value );
            fireEvent();
        }
        super.processEvent( e );
    }
    public void setValue(int value) { 
        this.value = value; 
        repaint(); 
    }
    public int getValue()  { return value; }
    public void setMinimum(int minValue )  { this.minValue = this.minValue; }
    public int getMinimum()  { return minValue; }
    public void setMaximum(int maxValue )  { this.maxValue = maxValue; }
    public int getMaximum()  { return maxValue; }
    public void addDialListener(DialListener listener) {
        if ( dialListeners == null )
            dialListeners = new Vector();
        dialListeners.addElement( listener );
    }
    public void removeDialListener(DialListener listener) {
        if ( dialListeners != null )
            dialListeners.removeElement( listener );
    }
    private void fireEvent() {
        if ( dialListeners == null )
            return;
        DialEvent event = new DialEvent(this, DialEvent.DIAL_ADJUSTED, value);
        for (Enumeration e = dialListeners.elements(); e.hasMoreElements(); ) 
            ((DialListener)e.nextElement()).dialAdjusted( event );
    }
}    
public class DialApplet extends java.applet.Applet 
                                implements DialListener, AdjustmentListener { 
    final int max = 100;
    Scrollbar scrollbar = new Scrollbar( Scrollbar.HORIZONTAL, 0, 1, 0, max );
    Dial dial = new Dial( max );
    public void init() {
        setLayout( new BorderLayout() );
        dial.addDialListener( this );
        add( "Center", dial );
        scrollbar.addAdjustmentListener( this );
        add( "South", scrollbar );
    }
    public void dialAdjusted( DialEvent e ) {
        scrollbar.setValue( e.getValue() );
    }
    public void adjustmentValueChanged( AdjustmentEvent e ) {
        dial.setValue( e.getValue() );
    }
}

Let's start from the top. We'll focus on the event handling and leave you to figure out the trigonometry on your own. The DialListener interface contains a single method, dialAdjusted(), which is called when a DialEvent occurs. The DialEvent itself is simple. It defines a new event ID, DIAL_ADJUSTED, that identifies dial events. This constant is defined so that it won't conflict with the ID numbers reserved for standard AWT events. The event itself only carries one item of data: the dial's new value. It has a single method that returns this value.

The constructor for the Dial class stores the dial's maximum value; its minimum value is 0. It then enables mouse motion events, which the Dial needs to tell how it is being manipulated.

paint(), draw3DCircle(), and processEvent() do a lot of trigonometry to figure out how to display the dial. draw3DCircle() is a private helper method that draws a circle that appears either raised or depressed; we use this to make the dial look three dimensional. processEvent() is called whenever any event occurs within the component's area. We only expect to receive mouse motion events, because these are the only events we enabled. processEvent() first checks the event's ID; if it is MOUSE_DRAGGED, the user has changed the dial's setting. We respond by computing a new value for the dial, repainting the dial in its new position and firing a DialEvent. Any other events (in particular, MOUSE_MOVED) are ignored. However, we call the superclass's processEvent() method to make sure that any other processing needed for this event occurs.

The next group of methods provide ways to retrieve or change the dial's current setting, minimum, and maximum values. They are similar to the methods in the Adjustable interface; you could argue that Dial really ought to implement Adjustable.

Finally, we reach the methods that work with listeners. addDialListener() adds a new listener to a Vector of listeners by calling addElement(). If the vector doesn't already exist, addDialListener() creates it. removeDialListener() simply takes a listener off the list, so that it won't receive any further events. fireEvent() is a private method that creates a DialEvent and sends it to every listener. It does so by converting the Vector into an Enumeration and running through every element in the list by calling nextElement() until hasMoreElements() returns false. To send the event to a listener, it calls the listener's dialAdjusted() method. Note that nextElement() returns an Object; we must cast this object to DialListener before we can deliver the event.

To show how the applet is used, I included a simple applet called DialApplet. This applet places a Dial and a Scrollbar in a border layout. Any change to either the dial or the scrollbar is reflected by the other. The applet implements both DialListener and AdjustmentListener, and therefore has both dialAdjusted() and adjustmentValueChanged() methods. Although this isn't necessarily a good argument for creating a new event type, it's worth noticing that the logic of the listener methods is much simpler than it would have been if the dial generated adjustment events.


Previous Home Next
Dialogs Book Index Layout Managers

Java in a Nutshell Java Language Reference Java AWT Java Fundamental Classes Exploring Java