by Rich Kadel

Learn how to extend the AWT with your own image buttons

news
Mar 1, 199717 mins

Subclass an AWT component so you can build an image button for toolbars, palettes, and other "icon-friendly" environments

You might take it for granted that every modern-day GUI toolkit has inherent support for icons and icon buttons, but donโ€™t be so quick to assume โ€” Javaโ€™s Abstract Windowing Toolkit (AWT) doesnโ€™t live up to that expectation. Yes, the AWT does contain a Button class, but because Button is implemented through platform-dependent GUI components, you canโ€™t draw into a button and expect your image to display properly, if at all.

Before you start cursing the AWT (yet again), let me assure you that a solution exists. The most straightforward approach and the technique weโ€™ll be examining in this article involves subclasssing the AWTโ€™s Canvas class. Although the primary purpose of the Canvas class is drawing (perfect for our purposes), it doesnโ€™t provide the mouse handling or any of the typical button-press effects (let alone mouse-over effects) you would expect for a button. Weโ€™ll address these limitations by working with several classes Iโ€™ve created to support fully flexible, active image buttons.

Getting started with the ImageButton class

Our first step toward creating image buttons is to implement a new class that works within the Java environment and also supports drawing. To make sure our new class (which weโ€™ll call

ImageButton.java

) works within the Java environment, we should subclass it from an existing AWT component. The AWT contains two candidates โ€”

Canvas

and

Panel

โ€” that support drawing. Although we could use either class,

Panel

has additional capabilities that we donโ€™t need, so weโ€™ll go with

Canvas

. Here is the class declaration:

   
public class ImageButton extends Canvas {

Determining button states

Our image button needs to support a number of different states: UNARMED, OVER, ARMED, and DISABLED. Letโ€™s take a look at the characteristics of each state.

When the user is not interacting with the button in any way, the button is in its UNARMED state. To follow suit with many applications, such as Internet Explorer and the new Netscape Navigator 4.0 pre-release, weโ€™re going to implement an OVER state, which will highlight a button as the user moves the mouse over it. The ARMED state, which weโ€™ll represent by showing a depressed button, occurs when the user clicks and holds on a button. If the user releases the mouse while the button is in the ARMED state, the corresponding action will be performed; however, users can return to the UNARMED state by moving the mouse off of the button without releasing the mouse. The last state weโ€™ll be supporting is DISABLED, which is implemented through the ImageButton programming interface by calling the AWT disabled function. A disabled button should not respond to any user action. Weโ€™ll be using a grayed-out effect to indicate a disabled button.

Our image button will support only one state at a time (a button canโ€™t very well be ARMED and UNARMED at the same time now, can it?). Weโ€™ll represent the four states in a single int, with four constants, as shown here:

public static final int UNARMED = 0; public static final int ARMED = 1; public static final int OVER = 2; public static final int DISABLED = 3;

private int buttonState = UNARMED;

Defining borders

Most buttons have a visible border that may change depending upon the state. But unfortunately, the

Canvas

component is completely blank by default, and it doesnโ€™t provide any built-in support for drawing a border around the rectangular region it encompasses.

We will have to draw the borders ourselves, so our next step is to decide what type of borders we want and which states they will correspond with. By implementing the border features in a separate class (weโ€™ll call this class Border), we can use one or more instances of the class in our ImageButton and switch between them as the states change.

Currently, the most popular kind of border is the shaded 3D-style border. We can actually use this border style for two of our button states: To indicate a button is unarmed, weโ€™ll use this style with darker bottom and right edges and lighter top and left edges; to indicate a button is armed (depressed) weโ€™ll use this style with darker top and left edges and lighter bottom and right edges.

I have implemented a very configurable class (Border.java) that supports multiple border styles, including NONE, THREED_OUT, and THREED_IN, among others. Two of Borderโ€˜s methods โ€” getInsets() and paint() โ€” do the bulk of the work. The getInsets() method returns the number of pixels at the top, left, bottom, and right that are necessary to make space for the border. (Note that the border is not required to be symmetrical.) The getInsets() method is used both to determine the size of the image button and to calculate the appropriate location at which to draw the buttonโ€™s image. As shown in the following snippet, getInsets() takes into account the border thickness and variable margins at each edge:

   
public Insets getInsets() {
        int top = borderThickness + topMargin;
        int left = borderThickness + leftMargin;
        int bottom = borderThickness + bottomMargin;
        int right = borderThickness + rightMargin;
        return new Insets( top, left, bottom, right );
    }

The Border paint() method, which is similar to the AWT Component paint() method, takes a Graphics parameter as input and draws the border in its current configuration. Border does not maintain the x-y coordinates or size of the image button, so these are passed to paint(). Also, the border color, if not set explicitly, can be derived from the buttonโ€™s current background color, another parameter to paint(). (This approach is desirable for borders like the 3D borders that should be given brighter and darker colors relative to the current background color.) Here is the function declaration for paint():

    
public void paint( Graphics g, Color background, 
                       int x, int y, int width, int height );

Different styles for different states

Back in the ImageButton class, a button needs to maintain variables for, potentially, four different borders and four different images for each of the four button states. Since we already defined the states as integers 0 through 3, we can use these values as indexes into the following arrays:

   
private Image images[] = new Image[4];
    private Border borders[] = new Border[4];

For convenience, I subclassed Border to define the default button borders for the ARMED and UNARMED states. The constructor for DefaultImageButtonBorder takes a boolean (true if constructing the default armed border and false if unarmed). These borders can be reused by any or all instances of ImageButton and are, therefore, defined as static variables in ImageButton and assigned in the ImageButton constructor. Here are those declarations:

    
private static final Border defaultUnarmedBorder =
        new DefaultImageButtonBorder( false );
    private static final Border defaultArmedBorder =
        new DefaultImageButtonBorder( true );

Constructing the image button

Weโ€™re done with all the background stuff, so now we can get to the heart of the matter โ€” creating the image button. As application programmers, we create an image button the same way we create a button component โ€” by calling

new ImageButton()

. The are two constructors involved โ€” one has no parameters, and the other takes an image to be used as the unarmed image. Here are those constructors:

public ImageButton() { tracker = new MediaTracker( this ); setUnarmedBorder( defaultUnarmedBorder ); setArmedBorder( defaultArmedBorder ); }

public ImageButton( Image image ) { this(); setUnarmedImage( image ); }

Setting the images and borders

To update the images array and load the images into a MediaTracker, which is used to start and manage the image loading in a background thread, weโ€™ll use a private method,

setImage()

, which is called by the

setUnarmedImage()

,

setArmedImage()

,

setOverImage()

, and

setDisabledImage()

functions.

setImage()

also requests a repaint if the image that was just set is the image for the current state. For example, if the current state is UNARMED and the image id passed to

setImage()

is UNARMED, the image button must be repainted, as shown here:

    
private synchronized void setImage( int id, Image image ) {
        if ( images[id] != image ) {
            images[id] = image;
            if ( image != null ) {
                tracker.addImage( image, id );
                tracker.checkID( id, true );
            }
            if ( buttonState == id ) {
                repaint();
            }
        }
    }

Simple image buttons (for example, those in Netscape Navigator 3.0) typically use the same image for the UNARMED, ARMED, and OVER states. To simplify the creation of a such a button, we can use setUnarmedImage() to set the images for the ARMED and OVER states, provided those images are null (their initial values), to the unarmed image. The DISABLED state, however, is a special case. If the disabled image is null, a โ€œdisabled-lookingโ€ image is generated from the unarmed image. Notice in Listing 1 that setUnarmedImage() optionally calls setDisabledImage() with the value null. If the image passed to setDisabledImage() is null, we can generate the disabled image automatically by blanking out every other pixel in the current unarmed image. This technique actually gives the resulting image a โ€œgrayed-outโ€ effect. The image is generated using a FilteredImageSource, and my own RGBImageFilter DisableImageFilter, which weโ€™ll get into later on in the article.

public void setUnarmedImage( Image image ) { setImage( UNARMED, image ); if ( images[ARMED] == null ) { setArmedImage( image ); } if ( images[OVER] == null ) { setOverImage( image ); } if ( ( images[DISABLED] == null ) || generatedDisabled ) { setDisabledImage( null ); } }

public void setArmedImage( Image image ) { if ( image != null ) { setImage( ARMED, image ); } else { setImage( ARMED, images[UNARMED] ); } }

public void setOverImage( Image image ) { if ( image != null ) { setImage( OVER, image ); } else { setImage( OVER, images[UNARMED] ); } }

public void setDisabledImage( Image image ) { generatedDisabled = false; if ( ( image == null ) && ( images[UNARMED] != null ) ) { generatedDisabled = true; image = createImage( new FilteredImageSource(images[UNARMED].getSource(), new DisableImageFilter() ) ); } setImage( DISABLED, image ); }

Listing 1: The setXxxImage functions

Note: Youโ€™ll see in the source that Iโ€™ve also provided public getXxxImage functions (for example, getUnarmedImage()) to get the current image for each of the states. Borders are set and retrieved through almost identical set and get functions, like setUnarmedBorder() and getDisabledBorder(). The only difference is that no special case for disabled borders exists.

Setting the button state

Now that weโ€™ve seen how to set the images and borders for an image button, our next step is to see how to set the actual button states. Notice from the code snippet below that I use a public method (

getButtonState()

) to get the current button state, but a protected method (

setButtonState()

) to set the button state. I use this approach because button state changes are the result of certain external events, and as such, should be handled by the

ImageButton

class itself. By making

setButtonState()

protected instead of private, however, I purposefully leave a back door open for subclasses that might extend the functionality of

ImageButton

, which may result in setting the state in response to other types of events.

public int getButtonState() { return buttonState; }

protected void setButtonState( int buttonState ) { if ( buttonState != this.buttonState ) { this.buttonState = buttonState; repaint(); } }

The setButtonState() method checks to see if the new state is different then the old one. If so, it assigns the new value to the buttonState variable and calls the repaint() method, causing the image button to be repainted with a potentially different border and/or image.

Handling the DISABLED state

I decided to piggy-back on the existing AWT API for disabling and enabling components so that use of the ImageButton API is consistent with all other AWT components.

ImageButton

overrides the

disable()

and

enable()

functions from

Component

class in order to essentially trap these requests and set the button state appropriately. The original methods are then invoked on the superclass. Therefore, if

disable()

is called, we will not get any mouse events, and no state changes will occur until the button is enabled again. Here are the two functions:

public void disable() { setButtonState( DISABLED ); super.disable(); }

public void enable() { setButtonState( UNARMED ); super.enable(); }

Painting the ImageButton

Now that we have all the pieces, including the borders, images, and states, itโ€™s time to put them all together visually, which is exactly what happens when we โ€œpaintโ€ the button. Componentโ€˜s paint()method is overridden to paint the border and image to the Canvas when necessary. The paint() method typically is called because part or all of the component was exposed by another window, or because the repaint() method was called on the ImageButton. (We call repaint() in several places in ImageButton to indicate that the button state has changed, or that one of the images or borders has been reset for the current state.) As shown in Listing 2, the paint() function takes one argument, a Graphics object, which is used by our draw requests to allow us to draw into the button.

    
public void paint( Graphics g ) {
        Dimension size = size();
        borders[buttonState].paint( g, getBackground(), 
                                    0, 0, size.width, size.height );
        try {
            if ( ! tracker.checkID( buttonState ) ) {
                tracker.waitForID( buttonState );
            }
            if ( ! tracker.isErrorID( buttonState ) ) {
                Insets insets = borders[buttonState].getInsets();
                int imageWidth = images[buttonState].getWidth( this );
                int imageHeight = images[buttonState].getHeight( this );
                int x = insets.left +
                        ( ( ( size.width - ( insets.left + insets.right ) ) -
                            imageWidth ) / 2 );
                int y = insets.top +
                        ( ( ( size.height - ( insets.top + insets.bottom ) ) -
                            imageHeight ) / 2 );
                g.drawImage( images[buttonState], x, y, this );
            }
        }
        catch ( InterruptedException ie ) {
        }
    }
Listing 2: The paint method

Letโ€™s take a closer look at whatโ€™s going on in this code. First, the image buttonโ€™s current size is obtained via Componentโ€˜s size() method. The size is then used in a request to the current Borderโ€˜s paint() method. Next, we use the MediaTracker to ensure that the image we need has been loaded. If not, the current thread waits until it is loaded before proceeding. If there were no errors loading the image, it is drawn using the Graphics classโ€™s drawImage.

The image is drawn at a position offset from the upper-left corner by the top and left values of the current Border insets. If the available area (the space not taken up by parts of the border) is not equal to the size of the image, the image is centered in the available space. It is worth noting that by giving the ARMED border wider top and left margins (and countering with thinner bottom and right margins), the image will move down and to the right slightly when the button is armed. This effect is typically used to enhance the perception that the button has been pushed in.

Calculating the image buttonโ€™s preferred size

Before painting the button even the first time, just about every AWT-style LayoutManager will call preferredSize(), shown in Listing 3, to determine the best possible size for the button. In ImageButton, we use the MediaTracker once again to ensure that the image has been loaded completely, which allows us to obtain the exact height and width of the image. To that size, we add the maximum thickness of all border insets and return the resulting size.

    
public Dimension preferredSize() {
        Dimension pref = new Dimension();
        try {
            if ( ! tracker.checkID( buttonState ) ) {
                tracker.waitForID( buttonState );
            }
            if ( ! tracker.isErrorID( buttonState ) ) {
                Dimension size = size();
                pref.width = images[buttonState].getWidth( this );
                pref.height = images[buttonState].getHeight( this );
            }
            int maxWidthAdd = 0;
            int maxHeightAdd = 0;
            for ( int i = 0; i < DISABLED; i++ ) {
                Insets insets = borders[i].getInsets();
                maxWidthAdd = Math.max( maxWidthAdd, 
                                        insets.left+insets.right );
                maxHeightAdd = Math.max( maxHeightAdd, 
                                         insets.top+insets.bottom );
            }
            pref.width += maxWidthAdd;
            pref.height += maxHeightAdd;
        }
        catch ( InterruptedException ie ) {
        }
        return pref;
    }
Listing 3: Determining the best size for a button

Handling the events

Most of the magic weโ€™re dealing with is done by the event handlers. Weโ€™ll override the convenient mouse handler functions

mouseDown()

,

mouseExit()

,

mouseEnter()

, and

mouseUp()

from the

Component

superclass to save us a little time.

As shown in Listing 4 below, the mousedown flag is set to true when mouseDown() is called, and false when mouseUp() is called. The state is set to ARMED when the mouse is down and has entered the bounds of the image button. If the mouse enters the bounds of the image button and the mouse is not down, the state is set to OVER. If the mouse exits the bounds of the image button, the state is UNARMED. Finally, when mouseUp occurs, and the mouse is still within the bounds of the image button, the corresponding action is performed and the state reverts to OVER.

public boolean mouseDown( Event evt, int x, int y ) { mousedown = true; setButtonState( ARMED ); return true; }

public boolean mouseExit( Event evt, int x, int y ) { setButtonState( UNARMED ); return true; }

public boolean mouseEnter( Event evt, int x, int y ) { if ( mousedown ) { setButtonState( ARMED ); } else { setButtonState( OVER ); } return true; }

public boolean mouseUp( Event evt, int x, int y ) { mousedown = false; if ( inside( x, y ) ) { setButtonState( OVER ); if ( ! action( evt, evt.arg ) ) { Container parent = getParent(); while ( ( parent != null ) && ( ! parent.action( evt, evt.arg ) ) ) { parent = parent.getParent(); } } } return true; }

Listing 4: Mouse handling events

In order to maintain the semantics of the current AWT event model (which, by the way, changes significantly with the pending release of the JDK 1.1), we propagate the function call action() up the component hierarchy. (In the JDK 1.1, you will probably want to implement the MouseListener interface. In this new 1.1 model, you will not need to propagate the change.) The first stop is to this object (in case someone has subclassed ImageButton and overloaded the action() function), then on to its parent, then that componentโ€™s parent, and so on. The propagation will stop when one of the components in the chain returns true, indicating that it has processed the event, or that no more ancestors exist.

The DisableImageFilter

In order to generate the โ€œdisabledโ€ version of the images, Iโ€™ve defined a subclass of RGBImageFilter called DisableImageFilter. We can use this new class in the construction of an image from a FilterImageSource. DisableImageFilterโ€˜s filterRGB() method is called once for every pixel in the image, allowing us to selectively alter pixels according to RGB values. But the RGB values actually include a component in addition to the red, green, and blue values: an alpha transparency component (in the most significant byte of the rgb value). By setting this byte to zero, the pixel becomes transparent, letting the underlying color of the image buttonโ€™s background show through. Here is the source for DisableImageFilter:

class DisableImageFilter extends RGBImageFilter { public DisableImageFilter() { canFilterIndexColorModel = false; }

public int filterRGB( int x, int y, int rgb ) { if ( ( ( x % 2 ) ^ ( y % 2 ) ) == 1 ) { return ( rgb & 0xffffff ); } else { return rgb; } } }

Checking our progress

Weโ€™ve covered a lot of ground here, and I wouldnโ€™t want you to leave this article without at least a small thrill. The

ImageButtonApplet

below contains a sampling of image buttons using various display configurations. Note that some of the buttons, when clicked, toggle the enable/disable state. Also note the use of a โ€œblankโ€ border to emulate Internet Explorerโ€™s buttons, which raise only as the mouse goes over them.

You need a Java-enabled browser to see this applet.
Figure 1: ImageButtonApplet

I created this applet, which creates three buttons, reads the images from URLs, creates and assigns a non-default border to some of the buttons, and overrides the action() method to catch the button click events, with the Applet subclass ImageButtonApplet.java.

Although the AWT provides a very limited set of components, it does provide the hooks you need to extend the components to build image buttons and even more sophisticated components of your own design. Of course, such enhancements donโ€™t come for free โ€” youโ€™re bound to encounter some โ€œgotchas.โ€ The Java 1.0.2 AWT has a number of bugs, especially related to portability between platforms. JavaSoft promises to fix many, if not most, of these problems in the upcoming 1.1 release. In the meantime, ImageButton is straightforward enough to be quite portable. You should be able to use it as-is, or download the source and extend it to suit your own special needs.

Rich Kadel is a senior software engineer and the lead engineer in charge of Java development at DTAI Inc., a progressive technology company based in San Diego, CA that develops advanced software products and provides custom computer solutions to a number of market areas. He was the primary developer of PAL++, a large C++ software library with almost 300 C++ classes for building graphic- and data-oriented applications. He currently leads the development of DTAIโ€™s Interactive Org Chart, a Java-based intranet application first developed for Netscapeโ€™s AppFoundry program, and participates in the development of DTAIโ€™s JustDBC suite of JDBC (Java Database Connectivity) drivers, proxies, and servers.