Skip to main content.

Web Based Programming Tutorials

Homepage | Forum - Join the forum to discuss anything related to programming! | Programming Resources

Tricks of the Java Programming Gurus

Chapter 9 -- Extending AWT Components

Chapter 9

Extending AWT Components


CONTENTS


The Java Abstract Window Toolkit (AWT) consists of classes that encapsulate basic GUI controls. Java is a multi-platform solution so the AWT provides a lowest common denominator interface. Any interface you develop should appear about the same on any platform. Often the AWT is called Another Window Toolkit or affectionately, Awful Window Toolkit.

Now don't be misled; the AWT provides many useful controls and your applications or applets may not require anything more. Of course, you are reading this chapter because you want to learn how to extend the functionality of the AWT controls. To do this, you learn a technique called subclassing.

Subclassing is just a fancy object-oriented term for changing the way a class works. The actual method is to create a new class from the old one and add new features along the way. Other ways exist to extend the AWT (see Chapter 10, "Combining AWT Components") but this chapter focuses on extending by subclassing.

In this chapter, you will learn how to extend TextField to create a self-validating TextField. The new class will be a TextField that keeps track of user input and only allows entry of valid data. You could use such a control to enable the user to enter color choices for some graphic object. This control would force the user to enter only valid colors and reject any other entries.

The text also looks at extending the Button class to create a multi-state toggle button. This toggle button will display a different label each time it is pressed. You could use it in place of separate on and off buttons.

Components-an Overview



In discussions of Java, people often use the word component to mean two different things. Sometimes, people use the generic meaning and refer to any GUI object as a component. However in Java, Component has a very specific meaning. Component is a class derived from Object. The major GUI widgets derive from Component as illustrated in Figure 9.1.

Figure 9.1 : The Java AWT class hierarchy.

The Component class is the base GUI class. This class provides functions to handle events and set or query attributes.

What Is a Peer?

If you have looked at the Java API documentation, you have probably seen a class called ComponentPeer. Derived from it are peer classes associated with each of the component classes.

The purpose of the peer classes is to bridge the gap between the AWT classes and the underlying platform-specific UI widgets. By using a peer, the AWT provides a uniform programming interface across all platforms. The peer classes are rarely used directly in Java programming, except when porting the AWT to other platforms.

Why Are Image Buttons Hard?

If you were to compile a list of language features that Java users would like to see in a 1.5 or 2.0 release, image buttons would appear near the top. An image button is a button that has an image on its face instead of text. Most modern GUIs include image buttons, so why doesn't the AWT?

The reason the AWT doesn't have image buttons has to do with the nature of Java itself. Because the Java AWT is a multi-platform GUI finding, a universal solution becomes difficult. The problem is that the implementation of an AWT button gets tied up between the classes Button and ButtonPeer. You can change the behavior of the Button class, but not its associated peer.

One possible solution would be to create an image button by extending some class other than Button. You could derive such a class from Canvas. You would need to create multiple images to represent the up and down states of the button and switch them and repaint when the user clicked in the Canvas. The problem is that such a button would look exactly the same on every platform rather than looking like a native implementation of an image button.

New Components from Old

When you design a user interface, you use the widgets provided in the Toolkit as the basic building blocks. Sometimes the design calls for a control that is just slightly different from the AWT version. Rather than try to develop new controls, you modify the existing controls in the AWT. To accomplish this, you use a method called subclassing. In object oriented terminology, this technique is often called class derivation or inheritance.

A Self-Validating TextField

In this example, you create a self-validating version of a TextField. You will derive a class from TextField called SelfValidatingTextField. The class will have a list of acceptable entries and users will only be allowed to enter values from this list.

This control allows you to limit the possible inputs from the user and to anticipate the user's input. When the user enters a character, you try to determine which string they are typing and fill in the blank for them.

Overview

You create the SelfValidatingTextField class by subclassing the TextField class. Start with a list of valid strings. When the user enters a character, you catch the key down Event. At this point, the bulk of the work begins. You must look at the text already in the control and determine whether the new keystroke is valid. If it so, add it to the control and find the best match for the entered text.

When you add the string to the control, you select the portion that the user did not type. So if the user types s and the matching string is spray, the last three letters (ray) are selected.

An Example with Valid Strings

Now create a SelfValidatingTextField in an Applet with the following valid strings; Janice, Jedidiah, Jonathan, Joy, Joshua, Jennifer, Jason.

If the user types a character other than J nothing happens because all the valid strings begin with J. When the user types a J, the control displays the J and the remainder of the first matching string in alphabetical order-in this case Janice. Figures 9.2 to 9.6 illustrate a typical user session.

Figure 9.2 : Type a J. Janice is displayed and the anice is selected.

Figure 9.3 : Type an a. Janice is still displayed, but now the nice is selected.

Figure 9.4 : Type a d. Nothing happens since none of the valid strings begin with Jad.

Figure 9.5 : Now type an s. The control displays Jason with the on selected.

Figure 9.6 : Pressing the delete key causes the selected portion of the text to be deleted.

As Figures 9.2 through 9.6 illustrate, you have created a new control that retains much of the functionality of the original TextField while providing significant enhancements. The new control still takes input from the user, but it now anticipates the user's input. This function means that less typing is necessary to enter the desired string.

You have retained the functionality of delete, backspace, and other special keystrokes. These keys operate in the control just like they do in the AWT version. The SelfValidatingTextField can be used as a drop in replacement for the TextField control.

What the Class Needs to Know

The AWT TextField needs to know very little. To use one, you simply create it and add it to your layout. When you want to get the data that the user has entered, simply call the getText() method.

Because the SelfValidatingTextField enhances the functionality of TextField, it needs more information. The control must know what strings to accept and how to interpret keystrokes. Our enhanced TextField should also be able to anticipate what the user is entering and display the best match string.

Text-matching algorithms must deal with the issue of case-sensitivity. In other words, does the string "Jennifer" match "jennifer"? Your control enables you to be either case-sensitive or case-insensitive, which makes the control more versatile, but requires some extra processing.

You let the class store the information it needs by adding the following instance variables:

String[] strings ;
boolean  caseSensitive ;
int      pos ;

The strings variable is used to store all the acceptable string values. Eventually, you will sort this array so your matches display in alphabetical order.

The caseSensitive variable is a flag that indicates whether the string matching you do will be case-sensitive. You need to set this variable whenever you create an instance of the SelfValidatingTextField class. During the data validation, you use the variable to determine whether to accept a given keystroke.

The pos variable is used by the class to keep track of the position of the last character entered by the user. This information becomes important when you display the best match string for a given input. You will need to update pos whenever you get input from the user.

Use this constructor to pass the information needed to the class:

public SelfValidatingTextField( String[] a,
                                boolean cs,
                                int chars ) {
    super( chars );

    strings       = a;
    caseSensitive = cs;
    pos           = 0 ;

    sortStrings() ;
}

The constructor takes three parameters: the array of valid strings, the case-sensitivity flag, and an integer parameter chars. The chars parameter is used to call the overloaded TextField constructor. The specific constructor you call is TextField(int n), which creates a TextField big enough to hold n characters.

The call to the parent class constructor looks like

super( chars );

This statement invokes a super class constructor. Because the super class is TextField, the TextField(int n) constructor is called.

The next three statements in the constructor initialize the class instance variables. You pass the values for strings and caseSensitive into the constructor. The function initializes pos to zero because at the time you create the control, no keystrokes have yet been entered.

Sorting the Strings

The last thing the constructor does is call sortStrings(). This function uses a bubble sort algorithm to sort the array of strings in ascending order. The implementation is

void sortStrings() {
    for (int i = 0; i < strings.length - 1; i++) {
        boolean swaps = false ;
        for (int j = strings.length - 2; j >= i; j--) {

            if (strings[j].compareTo(strings[j+1]) > 0) {
                String temp  = strings[j];
                strings[j]   = strings[j+1];
                strings[j+1] = temp;
                swaps = true;
            }

        }

        if ( swaps == false ) {
            break ;
        }

    }
}

This is the traditional bubble sort. It has been modified slightly to use the swaps variable to terminate the sort if any iteration fails to produce a single swap.

Capturing Keystrokes

One of the most important things this class needs to do is respond to individual keystrokes. In Java, a keystroke is an event. You use the event-handling mechanism of Java to capture keystrokes.

In event-driven programming, you often have to decide which object in the system captures which events. In many Java applets, it is the container class that captures the events generated by its embedded controls. Your control is designed to be self-contained so that you capture the keystroke events in the control itself.

To capture the keystrokes, you override the keyDown() function from the Component class (Remember: Component is the parent class of TextComponent, which is the parent class of TextField):

public boolean keyDown( Event e, int key ) {
    if ( key > 31 && key < 127 ) {
        return validateText( key );
    }
    return false;
}

The function receives two parameters: an Event object and the value of the keystroke. The first parameter is an Event object. Events in Java are class objects (see Chapter 11, "Advanced Event Handling"); they contain both data and functions. In this case, you only need the value of the keystroke, not the specific combination of keys that produced it.

In the overridden method, you handle some keystrokes yourself, while passing others on to the superclass method. In the implementation of the class constructor, you made an explicit call to the superclass constructor. Notice that you make no such call here.

Component.keyDown() is a special function. Instead of calling the superclass function directly, you call it by specifying the function return value. If the return value is true, it means that the function handled the event internally and the superclass method does not need to be called. If the return value is false, the function has not fully handled the event and the superclass method will be called. When overriding Component.keyDown(), you should not call the superclass method explicitly.

In the if statement, you compare the key to two values: 31 and 127. These values represent the minimum and maximum values for printable characters. If the character is printable, then you call the validateText() method and return its value. In this case, validateText() always returns true.

For non-printing characters, the expression is false and the function returns false. This causes the superclass version of keyDown() to be called. Thus, all non-printing characters are simply passed on to the superclass.

Validating Text

Most of the work in your control gets done in the validateText() method. This method must handle all of these different tasks:

Given all that it does, validateText() is a small function. It uses the Java String class to do as much work as possible.

The validateText() method starts by updating the pos variable:

boolean validateText( int key ) {
    pos = Math.min( pos, getText().length() ) ;

Start by setting pos to the index of the last character the user entered. If characters have been deleted, you update pos to reflect the current contents of the control.

Next, you get the text from the control:

String editField = getText().substring( 0, pos )
                                      + (char)key;

You need to instantiate a local String object. The editField variable holds all of the characters the user has entered until this point. That is, the first pos characters and the value the user just entered. This is accomplished by calling the getText() method that is inherited from the superclass.

Now that you have gotten the text, you must determine whether the control is case-sensitive:

if ( !caseSensitive ) {
    editField = editField.toLowerCase();
}

You handle case insensitivity by calling the toLowerCase() method from the string class. By converting both the text from the control and the array of valid strings to lowercase, you can now make a case-insensitive comparison.

Note
The String.toLowerCase() method returns a lowercase version of the String. It does not actually modify the String. Therefore, it is necessary to assign the result to another string if you want the change to persist.

For case-sensitive comparisons, you will work with the unconverted strings.

The issue of case sensitivity has been settled for the string. You must now check to see whether the characters entered thus far match any of the valid strings. The for loop below performs the appropriate comparisons:

for ( int i = 0; i < strings.length; i++ ) {
    try {
        if ( caseSensitive ) {
            if( strings[i].lastIndexOf(editField,0)
                                          &nbs p; != -1 ) {
                setText( strings[i] ) ;
                select( ++pos, strings[i].length() ) ;
                return true;
            }
        } else {
            if( strings[i].toLowerCase().lastIndexOf(
                                 editField,0) != -1 ) {
                setText( editField +
                         strings[i].substring(
                             editField.length() ) ) ;
                select( ++pos, strings[i].length() ) ;
                return true;
            }
        }
    } catch ( Exception e ) {
        // ignore any exceptions here
    }
}

The for loop iterates through the array of valid strings. During each iteration, you need to check for case sensitivity. You will call toLowerCase() if necessary.

To make the actual comparison, you call the String.lastIndexOf() method. Notice that you pass two parameters to lastIndexOf(); editField and 0. The function searches the String for the subscript you pass in. The String is searched backwards starting at the index-in this case 0. Because you are searching backwards from 0, you are in effect searching from the beginning.

If a match exists in the array of valid String, you need to update the control. In a case-sensitive instance of the control, you simply put the matching valid String in the control. If the control is not case-sensitive, you replace characters that the user has entered so far and append the remainder of the matching String.

Next, you select or highlight the portion of the string that you supplied from the out array of valid Strings so that the next character typed by the user replaces the selected portion of the String.

Finally, you return true. In every case, this control returns true. This value is returned by the calling function as well. In the calling function, keyDown(), this return value indicates that the superclass implementation will not be invoked.

Note
The try/catch block is included because String.substring() throws a StringIndexOutOfBoundsException. This particular exception is non-critical here, so you catch it in an empty catch block.

Putting it Together

To use the SelfValidatingTextField class in your own applets or applications, you must pass it: An array of valid Strings, a boolean value for case sensitivity, and the number of characters you wish to display.

The control is self-contained and takes care of all of its own validation. To get the text from the control, you call SelfValidatingTextField.getText() just as you would if you were using the AWT TextField.

The entire SelfValidatingTextField class is shown in Listing 9.1.


Listing 9.1. The SelfValidatingTextField class.
package COM.MCP.Samsnet.tjg ;
import java.awt.*;

public class SelfValidatingTextField extends TextField {

    String[] strings ;
    boolean  caseSensitive ;
    int      pos ;

    public SelfValidatingTextField( String[] a,
                                    boolean cs,
                                    int chars ) {
        super( chars );

        strings       = a;
        caseSensitive = cs;
        pos           = 0 ;

        sortStrings() ;
    }

    void sortStrings() {
        for ( int i = 0 ; i < strings.length - 1 ; i++ ) {

            boolean swaps = false ;

            for (int j = strings.length - 2; j >= i; j--) {
                if (strings[j].compareTo(strings[j+1]) > 0) {

                    String temp  = strings[j];
                    strings[j]   = strings[j+1];
                    strings[j+1] = temp;

                    swaps = true;
                }
            }
            if ( swaps == false ) {
                break ;
            }
        }
    }

    public boolean keyDown( Event e, int key ) {
        if ( key > 31 && key < 127) {
            return validateText( key ) ;
        }
        return false;
    }

     boolean validateText( int key ) {
        pos = Math.min( pos, getText().length() ) ;

        String editField = getText().substring( 0, pos )
                                          &nbs p;   + (char)key;

        if ( !caseSensitive ) {
            editField = editField.toLowerCase();
        }

        for ( int i = 0; i < strings.length; i++ ) {
            try {
                if ( caseSensitive ) {
                    if(strings[i].lastIndexOf(editField,0)!=-1) {

                        setText( strings[i] ) ;
                        select( ++pos, strings[i].length() ) ;
                        return true;

                    }
                } else {

                    if( strings[i].toLowerCase().lastIndexOf(
                                          edit Field,0) != -1 ) {

                        setText( editField +
                                 strings[i].substring(
                                        editField.length () ) ) ;
                        select( ++pos, strings[i].length() ) ;
                        return true;

                    }
                }
            } catch ( Exception e ) {
                // ignore any exception here
            }
        }
        return true;
    }
}

A Multi-State Toggle Button

For our second example, you create a multi-state ToggleButton. You derive a class from the Button called ToggleButton. You also create an array of button values and pass them to your class. When a user presses the button, the text on the face changes. You can use this ToggleButton to replace multiple Buttons.

An application that might normally have on and off Buttons could now have a ToggleButton that switched between on and off. It could also replace show and hide buttons.

Overview

The ToggleButton is derived from Button. This new class enables a button to display a different String each time it is pressed. Actually it displays all the Strings in the array and then starts over. The class provides public methods to return the index or the string associated with each press.

A Self-Destructive Example

Imagine that you want a Java applet that causes your computer to self-destruct. Don't be too worried, because applet security won't let us really self-destruct (see Chapter 20, "A User's View of Security"). Now give your applet a self-destruct button and an enable/disable button. You also create a ToggleButton for enable/disable and a Button for self-destruct.

When you start the applet, the self-destruct button is enabled and the ToggleButton displays "Disable." Figures 9.7 through 9.9 illustrate the self-destruct sequence.

Figure 9.7 : The fully armed Self Destruct button.

Figure 9.8 : Press Disable. The Self Destruct button is disabled. The ToggleButton now displays Enable.

Figure 9.9 : Press Enable. The Self Destruct button is now armed. The ToggleButton now displays Disable.

You are now ready to self-destruct!

State Information

When you create a Button, you pass it a String that will be displayed on its face. The ToggleButton class needs to know what Strings to display. The class must also know how to order the strings.

The class must be able to respond to button presses and modify itself accordingly. You will also give the class a means of responding to queries about its previous state.

The previous state information is actually more important than it seems. For example, the container that owns your control may not respond immediately to button presses. It may need to query the control in response to some other event. If the ToggleButton is currently displaying "Off" it means that the current state is "On" because the ToggleButton now displays the next state of the control. Of course, in a two-state button (like an on/off button) it is a simple matter to determine what the previous state was, but your button is not limited to only two states. You may pass it an array of any size.

The class defines two constants:

       static final int BUTTON_BORDER = 12 ;
public static final int NO_Prev       = -1 ;

Note
Java provides a mechanism for declaring constants. The Java implementation is superior to C or C++ manifest constants. Manifest constants (or #defines) are a way of getting the C or C++ preprocessor to substitute a value for a symbol.
Java constants are class members. Use the static and final modifiers when declaring them. They must be initialized and have two definite advantages over manifest constants: They are strongly typed, and as class members, they have globally unique names, thus eliminating namespace collisions.

BUTTON_BORDER is the number of pixels between the button text and the edge of the button. The actual border on each side is BUTTON_BORDER/2.

NO_Prev is a flag value. You use it when you first create an object to indicate that no previous value exists. NO_Prev is declared to be public because it may be returned from the getPrevN() method.

The ToggleButton class uses the following instance variables to keep track of the current object state:

String[] strings ;
int      n ;
int      prevN ;

The strings variable is an array that stores the states that will be displayed on the button. The ordering of the strings in this array is the order in which they will be displayed. Because Java arrays are objects, you can determine the number of states directly from the array.

The two integer variables n and prevN keep track of the current and previous states. Strictly speaking, only one of these variables is necessary. You could calculate the previous state from the current state. Your class does this calculation every time the state changes and stores the results.

The class constructor gets the initial values from the user:

public ToggleButton( String[] a ) {
       super( a[0] );

        strings = a;
        n       = 0;
        prevN   = NO_Prev;
    }

The constructor takes only one parameter. You pass the array of states into the class through the constructor. The array encapsulates the information about its size so you do not need another parameter.

First, call the superclass constructor:

super( a[0] );

You pass the first state value to the superclass so that it gets displayed in the control when it starts up.

Note
Calls to a superclass constructor should only be made from a derived class constructor. They must be the first line of code in the derived class constructor.
Other superclass methods may be called from any derived class method. The syntax is super.superclassMethod().

Next you initialize the class's instance variables. You assign the array parameter to the member array. You need to set n to 0 to indicate the index of the currently displayed string. Then set prevN to NO_Prev to indicate that no previous value exists.

Updating the Button

Every time the button is pressed you need to update its text. To do this, you need to capture the button press Event. In the SelfValidatingTextField class, you overrode the Component.keyDown() method to capture keystrokes. Here, you override Component.action() to capture Events.

The action() method takes two parameters: an Event and an Object. The event contains information about the specific UI action that has occurred. The second parameter is an arbitrary Object that varies depending on the type of control that initiates the action. In the case of a Button, it is the text on its face.

The beginning of the overridden action() method appears in the following string of code:

public boolean action(Event evt, Object what) {
    prevN = n ;

    if ( n < strings.length - 1 ) {
         n++ ;
    } else {
         n = 0 ;
    }

First, the method sets the prevN variable to the current index value. The compound if that follows takes care of updating the index value. The index value gets increased until it reaches the number of Strings in the array. Once it has reached this maximum, the index is set to 0.

The following text contains the rest of the action() method:

    FontMetrics fm = getFontMetrics( getFont() );

    int width = fm.stringWidth( strings[ n ] );
    int height = bounds().height;

    resize( width + BUTTON_BORDER, height );
    setLabel( strings[ n ] );

    return false ;
}

Here you resize the button to fit the text if necessary. Start by creating a FontMetrics object. The FontMetrics constructor takes a reference to a Font. This class is the Java mechanism for providing detailed information about a Font on the current platform. You then call the stringWidth() method to get the width of the new label in pixels using the current Font.

The height of the button will not change unless you use a different Font. Now that you have the width and height, you call resize() to adjust the button to fit the String.

Finally, the function returns false. The function must return false so that the Container in which it is embedded can also capture the button presses. Returning false means that this object has not fully processed the Event so other objects may need to do further processing.

Accessing the Data

The ToggleButton class provides methods that enable a Container to ignore button press events when they occur. These methods are used to query the control about its state when last pressed. The two methods return either the index of the previously displayed label or the label itself.

Here is the first method:

public int getPrevN() {
    return prevN ;
}

The getPrevN() method may return NO_Prev, which would mean that the button has never been pressed.

Here is the second method:

public String getPrevString() {
    if ( prevN == NO_Prev ) {
        return "" ;
    }
    return strings[ prevN ] ;
}

The getPrevString() method indicates that the button has never been pressed by returning an empty >String.

Putting it Together

To put a ToggleButton object in your applet or application, you need to pass it an array of states(Strings). The order of the states is important because the states will be cycled in this order.

The complete ToggleButton class is shown in Listing 9.2.


Listing 9.2. The ToggleButton class.
package COM.MCP.Samsnet.tjg ;
import java.awt.*;

public class ToggleButton extends Button {

           static final int BUTTON_BORDER = 12 ;
    public static final int NO_Prev       = -1 ;

    String[] strings ;
    int      n ;
    int      prevN ;

    public ToggleButton( String[] a ) {
        super( a[0] );

        strings = a;
        n       = 0;
        prevN   = NO_Prev;
    }

    public boolean action(Event evt, Object what) {
        prevN = n ;

        if ( n < strings.length - 1 ) {
             n++ ;
        } else {
             n = 0 ;
        }

        FontMetrics fm = getFontMetrics( getFont() );

        int width = fm.stringWidth( strings[ n ] );
        int height = bounds().height;

        resize( width + BUTTON_BORDER, height );

        setLabel( strings[ n ] );

        return false ;
    }

    public int getPrevN() {
        return prevN ;
    }

    public String getPrevString() {
        if ( prevN == NO_Prev ) {
            return "" ;
        }
        return strings[ prevN ] ;
    }
}

Usage

When you embed a ToggleButton in a Container, you may catch button presses by overriding the Container's handleEvent() method. Normally, you would use the label displayed on the Button to identify it. In your class, you need to modify this slightly because your button displays many different labels.

A possible implementation of handleEvent() is:

public boolean handleEvent(Event evt)
        {
        int i;
        for( i = 0; i < validLabels.length; i++ ) {
            if( validLabels[i].equals(evt.arg) )
                {
                handleButton(i);
                return true;
                }
        }
        return false;
        }

In this method, you iterate through the array of button states that was passed to the ToggleButton constructor. If you find a match, you call the handleButton() method. You provide this method to respond to button presses for specific button states.

HandleEvent() returns true if it actually handles the Event or false if it does not.

Summary

Sometimes your applets or applications require UI functionality beyond that provided by the AWT. You can use subclassing to extend the AWT and create new classes from the basic AWT classes. By subclassing you can take the best features of an existing control class and add new functionality. You can also modify existing functionality.

When you subclass, you can create self-contained controls that respond to their own Events. Your subclassed controls can often be used as drop-in replacements for the associated AWT control.

Other ways exist to customize the AWT. In Chapter 10, "Combining AWT Components," you look at another method of extending or enhancing AWT functionality-combining controls.