Features Banner

Writing Custom Components In Java

By R. J. Celestino

Introduction

he AWT is a great starting point for creating portable graphical interfaces. But it is just that, a starting point. In this article, we will look at how to extend the AWT to create custom components or widgets that we can use in all of our Java applications. Many newcomers to Java see the AWT as a mystery. This article is not for them. Here we assume that the reader has a good understanding of the AWT and wants to extend that knowledge to create new and better Components.

Why make a custom component?

Modern user interfaces often call for GUI controls beyond the standard push button or slider. The AWT provides a spartan set of GUI controls, and when our designs exceed these, we need to create our own.

Consider a component as simple as a push button. The AWT provides this of course, but what if we wanted a button with an image on it, or a toggle button? Using the current AWT, we would need to write our own.

There are generally two ways of creating a custom component: by composition or by specialization. Your choice will depend upon your situation, and of course the two techniques can be combined. Before continuing, let's take a moment to discuss each technique.

Creating a super-component by composition

Components that are created by composition can be thought of as super-components, clustered components, or grouped components. This type of component is simply a collection of available components arranged in a particular way to perform a specific task.

You will want to create a super-component whenever you have a recurring task that requires a number of sub-components working in concert. Examples include an order form, customer name and address form, file dialog, color chooser, and so on.

Creating a component by specialization

We can create components by specializing the behavior of existing components. You can do this by subclassing and adding, extending or overriding existing behavior.

You should create these components whenever you desire a special GUI control or widget. Examples include image buttons, separators, borders and so on.

Your component and the AWT

I suppose you could break out on your own and create components from scratch without using the AWT. You could ignore the existing framework, container model and event model. You may go blind in the process, but you could. If you are that sort of rugged individualist, there is no need to continue reading here, you need to get to work right now!

Despite the allure of doing it from scratch, I would recommend working within the AWT framework for creating components. Let's look at some of the aspects of the AWT that are relevant to component creation.

Superclass

All of your components will have Component as its ancestor, but you can often inherit some very useful behavior by subclassing further down the hierarchy. Here are some suggestions.

Super components should generally subclass Panel. Panels are great all purpose containers. Remember that the default layout manager of a panel is the flow layout. This layout manager is not always appropriate, so change it if need be.

Class Canvas is good choice for components that are completely drawn. Our example Separator will subclass Canvas. I do not mean to imply that anytime you need to draw, you should subclass Canvas. In fact, all components have a graphics context that can be drawn upon. The WideBorder example subclasses Panel and draws to a portion of it.

I have found that the vast majority of components can be conveniently subclassed from Panel. The Panel class provides virtually all of the necessary infrastructure needed to create good looking components.

Drawing

Modern GUI components produce a 3D appearance by shading. By convention, the "light source" that produces the shading is at the upper left. This means that for a button that is raised, its upper and left borders will be "bathed in light" and its lower and right sides will be "in shadow". The reverse is true when it is depressed.

rjc1.gif

Your component may or may not need to be drawn. If it does, keep in mind its 3D aspects. The AWT has the responsibility of knowing when the component needs to be redrawn, so we only need to be concerned with how to draw it. Drawing occurs in the paint method, so be sure to override this method if your component needs to be drawn.

Sizing and layout

Components in the AWT have a notion of size. They can respond with their preferred size and their minimum size. It is important to override the preferredSize() and minimumSize() methods to return useful information.

Few objects in the AWT are as interested in the size of your component as are the layout managers. It is the layout manager that has control over where and how big your component will appear. By providing this size information, the layout manager can do it's job better.

However, these values are not guaranteed to be honored. Some layout managers completely ignore the component's size preferences, while others grant each component exactly the space it desires. Still others ignore a portion of the component's size wishes.

Here is a table of LayoutManagers and how they feel about your component's desires to be sized:

Layout manager How preferred size is used.
BorderLayout
  • North and South: preferred height is respected. Preferred width is ignored.
  • East and West: preferred width is respected. Preferred height is ignored.
  • Center: Preferred size is ignored.
CardLayout Preferred size is ignored.
FlowLayout Preferred size is respected.
GridLayout Preferred size is ignored.
GridBagLayout Depends upon constraints.

Table 1. Layout manger comparison with respect to preferred size.

Remember, if you fail to override the preferred and minimum sizes you could get in unexpected layouts. Worse yet, components that subclass Canvas, may not appear at all unless you override these methods! This is because the default preferred size of a canvas is (0,0).

Note: In the 1.02 version of the JDK, a component's minimum size is ignored in virtually all cases. I recommend overriding this method anyway for compatibility with JDK 1.1 and later versions.

Component examples

I could write all day about this, but even though I don't get paid by the word, I think a few examples will illustrate the important points better.

Separator

One of the first components I noticed that was missing from the AWT was the separator. It is simple really, a separator is a horizontal line that appears etched.

We will create the separator component by specialization, specifically by subclassing Canvas. I chose Canvas because I recognized the need to draw this component, and canvases are provided explicitly for our drawing needs.

There are a number of things to think about for the separator. The first that came to my mind was, "how do I draw an etched line?".

Drawing an etched line

To create a horizontal line with a 3D look, draw two lines next to each other, one light and the other dark. If the light line is above the dark, it will appear raised, otherwise it will appear etched. Following is a code fragment that draws two lines, one raised, one etched.

             public void paint( Graphics g ) {
                // draw a raised line
                g.setColor( light ) ;
                g.drawLine( 0, 10, 40,10 ) ;
                g.setColor( dark ) ;
                g.drawLine( 0, 11, 40, 11 ) ;
                // draw an etched line
                g.setColor( dark ) ;
                g.drawLine( 0, 20, 40, 20 ) ;
                g.setColor( light ) ;
                g.drawLine( 0, 21, 40, 21 ) ;
             }

And here is how it looks.

rjc2.gif

You might notice the variables light and dark are not shown in this code fragment. They are instances of Color, and here is how I set them.

                light = getBackground().brighter().brighter() ;
                dark  = getBackground().darker().darker() ;

I could have set them directly to the color I wanted like so.

                light = Color.darkGray ;
                dark  = Color.lightGray ;

But I chose to be more general. Using the brighter() and darker() methods allow the component to draw itself appropriately regardless of background color.

Now that we know how to draw a 3D line, let's continue.

Sizing the separator

The separator is a line that fills the horizontal space it is given, and centers itself in the vertical space it is given.

By the time the paint method is called, the layout manager has laid out all its components and allocated the space for them. This is good news; the paint method can obtain the size of the region, and draw the line appropriately.

Following is a code fragment from the paint method:

size = size() ;
int length = size.width ;
int yPosition = ( size.height )/2 ;
g.setColor( dark ) ;
g.drawLine( 0, yPosition, length, yPosition ) ;
g.setColor( light ) ;
g.drawLine( 0, yPosition+1, length, yPosition+1 ) ;

Hold on, we are not done yet! Layout managers can be very cooperative with the components they are laying out. Remember, components have a notion of the size that they would like to be ( their preferred size ) and the size they must "at least be" (their minimum size ). Some layout managers take this into consideration when laying out the components.

The separator class should override preferredSize(). I have chosen its preferred size to be a 4x40 region, and the minimum size is the same. Why 4x40? Recall that layout managers will respect all, some or none of the size cues given. I chose these so that the separator will be visible regardless of layout manager used.

Following is the completed Separator component

package eyeOnObjects ;
import java.awt.* ;

public class Separator extends Canvas {
        private Color light ;
        private Color dark ;
        private Dimension size ;
        public void paint( Graphics g ) {
                resetColors() ;
                size = size() ;
                int length = size.width ;
                int yPosition = ( size.height )/2 ;
                g.setColor( dark ) ;
                g.drawLine( 0, yPosition, length, yPosition ) ;
                g.setColor( light ) ;
                g.drawLine( 0, yPosition+1, length, yPosition+1 ) ;
        }
        
        public Dimension preferredSize() {
                return new Dimension( 4,4 ) ;
        }
        public Dimension minimumSize() {
                return preferredSize() ;
        }
        private void resetColors() {
                light = getBackground().brighter().brighter() ;
                dark = getBackground().darker().darker() ;
        }
}

Remember, the separator will fill the horizontal space it is given. The space it is given will depend upon the layout manager used. So it makes a lot of sense to use separators in the north or south regions of a border layout, or in grid layouts. Here is an example.

Look at the SeparatorApplet source code to see how to use the Separator class.

Exercises for the reader

There are a few interesting extensions that you could make to this class, and I leave this to you, you reader you:

  • Vertical separator. We have created a horizontal separator. To create a vertical separator you might want to create a VerticalSeparator class. But wouldn't it be nice to have a single class that does both depending on how it was laid out? You will need to endow this class with the smarts to know that when it's in a vertical space, its a vertical line, and when it's in a horizontal space it's a horizontal line.
  • Definable width. Our separator has a fixed width of 2 pixels. Modify the class to support an arbitrary width.

Wide border component

The wide border component is a container that contains exactly one component, and surrounds that component in a wide, colored border. The restriction that it contains exactly one component is so that we can control the placement exactly. However it is not a very limiting restriction, since we can build a panel that contains numerous components.

Layout

The WideBorder class is a subclass of Panel. In the constructor I install a border layout, and place the surrounded component in the Center. Here is a code fragment:

     public WideBorder( Component c ) {
        setLayout( new BorderLayout() ) ;
        add( "Center", c ) ;
     }

Remember, the BorderLayout manager will ignore the center component's preferred size. By placing a component in the center, the border layout manger will use all of the available space, and expand the component to fill that space. If the layout manager does this, we will not be able to draw the border.

In order to draw a border, we need to reserve space that will remain untouched by the layout manager. We do this by overriding the insets() method. Here is how.

     public Insets insets() {
        int b = _borderWidth ;
        return new Insets( b,b,b,b ) ;
     }

By implementing this method, we tell the layout manager to reserve an area for our use. Insets are specified as a number of pixels in from the edge of the available space, in counterclockwise order starting from the top. For example, the inset:

new Inset( 5, 6,7,8 )

Defines a space 5 pixels in from the top, 6, from the left, 7 from the bottom and 8 from the right.

rjc3.gif

Drawing

Now that we have some space reserved for our use, we can think about drawing in that space. To create a wide border, simply fill in the area with a color using the fillRect method. Here is how:

     public void paint( Graphics g ) {
        Dimension s = size() ;
        Insets i = insets() ;
        g.setColor( _borderColor ) ;
        g.fillRect( 0,0,s.width,i.top ) ;
        g.fillRect( 0,0,i.left,s.height-i.bottom ) ;
        g.fillRect( s.width-i.right, 0, i.right, s.height-1 ) ;
        g.fillRect( 0,s.height-i.bottom,s.width,s.height ) ;
     }

As you can see, the border is drawn as 4 rectangles, and filled with the border color.

Completed WideBorder component

Following is the code for the completed class.

package eyeOnObjects ;
import java.applet.Applet;
import java.awt.* ;

public class WideBorder extends Panel {
  private Color _borderColor = Color.gray ;
  private int _borderWidth = 10 ;

  public WideBorder( Component c ) {
          setLayout( new BorderLayout() ) ;
          add( "Center", c ) ;
  }
  
  public Insets insets() {
          int b = _borderWidth ;
          return new Insets( b,b,b,b ) ;
  }

  public void paint( Graphics g ) {
          Dimension s = size() ;
          Insets i = insets() ;
          g.setColor( _borderColor ) ;
          g.fillRect( 0,0,s.width,i.top ) ;
          g.fillRect( 0,0,i.left,s.height-i.bottom ) ;
          g.fillRect( s.width-i.right,0,i.right,s.height-1 ) ;
          g.fillRect( 0,s.height-i.bottom,s.width,s.height ) ;
  }

  public void setBorderColor( Color c ) {
          _borderColor = c ;
  }

  public void setBorderWidth( int w ) {
          _borderWidth = w ;
  }
}

And here is an example of the applet running

Look at the WideBorderApplet source code to see how to use the WideBorder class.

Exercises for the reader

  • Etched border. Create a border component that draws an etched box around the bordered component. Remember the lighting, when creating an etched box!
  • Experiment with bordering. The sky's the limit here! Think up some interesting borders and implement them.

Name (and address) fields

Name and address are common things to request on web pages. Wouldn't it be nice to have a reusable component to use every time we needed to collect this information? We will create a component by composition, a super component, to accomplish this.

In the interest of brevity, to illustrate this type of component I will create a name field only. Feel free to extend it to collect an address or any other information.

This component is created by composition. That means that we build up a component from other smaller components. The name field that we will create contains 3 standard AWT components: Label, Button, and TextField. Here is the constructor.

      public NameField() {
        Label l = new Label( "Name: " ) ;
        setLayout( new BorderLayout() ) ;
        Panel p = new Panel() ;
        p.add( _doneButton ) ;

        add( "West",  l ) ;
        add( "Center", _nameField ) ;
        add( "South", p ) ;
      }

Note that a more complex component would have many more sub components. More importantly, you will want to think about implementing the model-view-controller relationship (MVC) as the component gets more complex. However implementing the MVC is beyond the scope of this article and is overkill in our simple example.

Our component must be able to communicate with its client. In the case of a name field, the client should be able to set and get the value of the name it is holding. Here are the accessor methods for our class.

      public String name() { 
        return _nameField.getText() ; 
      }

      public void setName( String n ) { 
        _nameField.setText( n ) ; 
      }

This component does not need a preferredSize() method, since it is able to compute its size based on its components. This ability is built into the Panel class and will work as long as each sub component knows its preferred size. In some cases, you may want to override this automatic calculation, and if so simply write the preferredSize() method as we did previously in the separator example.

Handle Events

We will need to handle events so that clients can use our new components effectively. There are a number of ways to handle the events that occur within our component. Here are a few.

1. Hide all AWT events, generate a custom event when the button is pressed.

2. Generate a custom event, but allow all AWT events to pass.

3. Hide only the action events, and let all other AWT events pass.

I feel that the last item is most appropriate here. When the "done" button is pressed, we will trap the standard AWT event, and generate a custom event.

Remember, the standard AWT event that is generated by clicking on the button would be an action event, with the done button as its target. This is not acceptable, since clients of our name field do not (and should not) have any knowledge of the name field's instance variables.

We want to shield clients from these irrelevant events, and also generate a more meaningful event. To stop an event from propagating outward to its containers, simply return true. But this is not enough, we must also create a new event and send it out.

There are many issues involved in creating custom events, but discussing them all is beyond the scope of this article. However for our purposes, we will create a new action event, using the name field as the target (remember, the actual AWT event that was generated had the done button as its target).

Following is the handleEvent method that we will use to accomplish this. Note that I invoke the deliverEvent method and then return true. The Event that is delivered is a new event using this as its target, and reuses all the other fields of the original event.

      public boolean handleEvent( Event e ) {
        if ( e.id == Event.ACTION_EVENT ) {
           if ( e.target == _doneButton ) {
             deliverEvent(new Event(this,e.id,e.arg));
             return true ;
           }
        }
        return super.handleEvent( e ) ;
      }

Using this code, a client can use the name field transparently, and listen for action events. They can check the target of the action event against the instance of the name field.

Following is an applet that places a NameField in a gray border, and sends the updated text to another bordered field.

Look at the NameFieldApplet source code to see how to use the NameField class.

Conclusion

We have explored through example a number of component types. Using specialization, we created the Separator and WideBorder classes. These classes gave us an understanding of how to subclass the AWT to create a custom look and feel. We also explored super components, and saw how to customize their participation in the AWT event model.

This article is a starting point, and I encourage the reader to explore and experiment with custom components.

For further information

  • "Core Java" by Gary Cornell and Cay S. Hortstman. Published by SunSoft Press.
    • An excellent book for the advanced Java developer.
  • "Graphic Java" by David M. Geary and Alan L. McClellan. Published by SunSoft Press.
    • The best graphics treatment for the AWT master.
  • "The Java Report" published by SIGS Publishing.
    • One of the better Java Periodicals.

Enjoy the article? Subscribe to Eye on Objects!

R. J. Celestino (Bob) is credited with originating the concept of "subjective perspectives" first presented in the SIGS White paper "Java as an OO Language". He holds a Masters of Engineering degree in Electrical and Computer Engineering from North Carolina State University. His love of UNIX is surpassed only by his love of chowder.

Navigator Bar

Home Page