Java and Lightweight Components

JDK 1.1 lightweight components let you give programs exactly the same look-and-feel -- no matter which platform hosts the VM. To examine lightweight component development, David presents his dph.awt.lightweight package.


February 01, 1999
URL:http://www.drdobbs.com/jvm/java-and-lightweight-components/184410847

Feb99: Java and Lightweight Components

Implementing platform-independent look-and-feel

David develops client/server Java software at NetGenics Inc. in Cleveland, Ohio, and teaches Java for Sun Microsystems. He can be reached at [email protected].


By now you know that you can write Java code once, then run it in any virtual machine -- no matter what platform is hosting the VM. However, if you write code that uses UI components native to the platform hosting the VM (which is what the Abstract Windowing Toolkit (AWT) does for each UI widget in the toolkit), the write-once/run-anywhere quality of Java has to have a door to the native implementation. That makes Java code using the AWT dependent on platform-specific resources.

If you've paid attention to how Java AWT components look on different platforms, you know that UI components look like those of the platform proper. Java achieves this by forwarding requests for the creation of AWT UI components to platform-dependent UI peers, which render their on-screen representations using native platform-dependent UI resources. This means that with the AWT there is no single cross-platform implementation of any UI component. It also means that every VM that can display AWT components has access to a platform-specific set of peer classes to render the AWT components.

Forwarding to peers is dubbed the "heavyweight component" technique because peers make heavy use of resources -- each heavyweight component renders itself using a separate native window with its own native graphics resources. Native windows also impose certain, perhaps unwanted but immutable, characteristics, including a square shape and opacity, which are inherited when you subclass the AWT, thus making it difficult to build a set of, say, round or partially transparent components.

In contrast to the heavyweights, JDK 1.1 lightweight components let you give programs the same look for components, no matter which platform hosts the VM, because lightweights don't use peers. Instead, lightweights get rendered by programmer-defined code, and can have any shape -- even transparent sections. They use fewer resources than their heavyweight siblings because, instead of using a native window for every component, lightweights render themselves using the native window graphics resource of their nearest heavyweight ancestor. An example of a full-fledged lightweight library is Sun's SwingSet, delivered with the JDK 1.1.

To examine lightweight component development, I present my dph.awt.lightweight package and an alarm-clock application made of lightweight pieces, which runs in a Java Frame and can be used as an application or applet. The code for this application (available electronically; see "Resource Center," page 5) requires JDK 1.1 and will not compile under JDK 1.02. When run as an applet, the alarm clock works under Netscape Navigator 4.04, but requires the latest JDK 1.1 patch.

The Overall Picture

The dph.awt.lightweight package I present here demonstrates the steps of lightweight component development. It is not a complete lightweight component set. Nor am I providing exact replacements for AWT components (the AWT doesn't even have a Spinner class). Instead, I'm providing components that overlap with the AWT. My FlatButton and Label, for example, could be replacements for AWT Button and Label, but my Label lets you designate a border, and my FlatButton does not look or behave like an AWT Button.

There are five components that make up the dph.awt.lightweight package: Label, Checkbox, Spinner, FlatButton, and TabPanel. There is also a RoundButton class, but it is not used in the alarm clock. Except for TabPanel, these inherit from class dph.awt.lightweight.CommonBase, which principally encapsulates a behavioral pattern used to size lightweight components. CommonBase inherits from java.awt.Component, while TabPanel inherits from java.awt.Container. These are the only JDK parents lightweights can have. Figure 1 provides an overall view of the classes making up the dph.awt.lightweight package.

Additional classes include dph.play.alarmclock.AlarmClockFrame (the window in which the alarm clock appears), dph.play.alarmclock.AlarmClockApplet (the applet that invokes AlarmClockFrame to let the alarm clock run as an applet), dph.play.alarmclock.ClockCanvas (the clock face), and dph.play.alarmclock.SecondsThread (a subclass of java.lang.Thread that does the timing of the clock). There are also nested inner classes for event handlers.

There are three canonical steps of lightweight development. Lightweights are required to:

In addition, if the lightweight component you are creating is a container (like TabPanel) and not just a component, then it is essential that you call super.paint, because containers are responsible for drawing the components they hold, and calling super.paint ensures that this responsibility will be fulfilled.

The First Canonical Step

The first step is that all lightweights extend java.awt.Component, java.awt.Container, or another lightweight. For the alarm clock, the class CommonBase performs this step for the noncontainer lightweights, such as Label, Spinner, FlatButton, and Checkbox. The lightweight container TabPanel extends directly from java.awt.Container. Listing One is the skeleton class definition for CommonBase.

Sizing, painting, and enabling/disabling behaviors are partly managed in this base class. Many sizing activities rely on a font to determine how large to draw a component, and, in Java, a font is part of the graphics context for a component. Thus, it is often important to delay size calculating until a graphics context has been established. The call to addNotify ensures a graphics context is available, so CommonBase overrides this to call figureMySize once the graphics context has been supplied. Each child class only needs to define the hook method figureMySize to return the Dimension object, which represents the preferred size of the object.

The second behavior managed by this class is painting. Lightweights tend to have noticeable flicker that can be resolved by the use of off-screen image buffering, sometimes called "double-buffering." Double-buffering prepares the screen's contents initially in an image buffer in memory, and then with the paint method whisks the image in memory onto the screen.

The solution involves placing lightweights in containers which do the buffering, then overriding update in the components' call to paint, thus omitting the default behavior of clearing the screen in update. CommonBase implements the override of update so none of its child components need to. The double-buffering in the container classes DoubleBufferedPanel and DoubleBuffered- ExitingFrame is based on examples in the demo/awt-1.1/lightweight directory of the JDK1.1.

The third behavior is enabling/disabling, which is overridden merely to enforce a repaint occurring as a result of a change to an enabled state.

The Second Canonical Step

The second step is to provide constructors and methods for manipulating the state of the component, including its appearance. If you are creating a component to replace one of the AWT components, extending java.awt.Component gives you the methods and data of the Component class, so look at the methods of the AWT class you are replacing, and supply the constructors, data members, and method implementations needed to make your lightweight component behave like the AWT component. Most Java reference books itemize the data members and class methods for the AWT components, but I recommend Java AWT Reference, by John Zukowski (O'Reilly & Associates, 1997).

I provide at least a minimum set of constructors for lightweight components; see Label in Listing Two. I define a set of constructors that creates a Label with a textual String. I also let users designate whether there should be a rectangular border around the Label. Label's flaw is that it will always draw its text left justified. This means that if users call setSize to give a Label a wide graphical representation, the text won't appear in the middle of that area.

One of the more interesting classes is FlatButton, which lies flat and pops up when the cursor moves over it. FlatButton differs from the AWT Button in that it lets you have a textual label, an image label, or both, on the surface of the button. If both are there, FlatButton draws the text over the image. To get it to appear with an image, pass the constructor a String representing the URL of the image you want painted on its surface. You can also call setImageURL after it has been constructed, and pass in the String representing the URL. One of the tricks this class employs is the use of a MediaTracker object to ensure that the image is loaded. It is essential to have a valid image before trying to use the image to determine the FlatButton's dimensions. Listing Three shows the steps FlatButton takes to ensure that the image is fully loaded before figuring its size.

You must control all aspects of a lightweight's appearance, and FlatButtons appear to pop up when the cursor enters them, pop down when pressed, or go flat when the cursor exits them. Consequently, the class must track these states, and the drawing code in the paint method must shift the image and/or text when it changes state. You must even handle the differences in appearance and behavior between the enabled and the disabled states for each lightweight component.

In fact, because you must control every aspect of the lightweight component's appearance, you must override the paint method to draw the component. Bear in mind that the double-buffering (to control flickering) does not take place in the components themselves, but rather should take place in the container the components are placed in. Therefore, you won't see any evidence of double-buffering in the paint methods of these lightweights. In fact, they are not aware of being double-buffered. If done correctly, however, the Graphics object that is used in their paint methods will originate from the buffered Image of the highest-level container they are in.

The only lightweight under discussion, which extends java.awt.Container and not java.awt.Component is TabPanel. To ensure that this class behaves more like a tabbed folder than a Panel, the TabPanel class does not implement a host of methods inherited from java.awt.Container, such as add, getComponent, getComponentCount, remove, and removeAll. The class data supporting the TabPanel are minimal:

To use TabPanel, users build up a Panel outside of TabPanel by adding components to a Panel, then invoking one of the TabPanel add methods to add the Panel to the TabPanel along with a label that appears in a tab for that Panel. This TabPanel has two flaws:

The real work of this component is done in the methods called from the paint method, which is split into three chores (see Listing Four), the first of which is the all-important task of any lightweight container, calling super.paint. The second task is to draw the TabPanel so that it has a background color and border; the third task is to paint the tabs, showing which one is currently selected. It is possible to have the TabPanel show first without any tab being selected. The code in the set-SelectedTab method (Listing Five) shows how the panels are swapped when users click on different tabs.

The Third Canonical Step

Except for TabPanel (which calls it directly by itself), all constructors eventually call enableEvents(AWTEvent.MOUSE_EVENT_MASK) through their common parent lightweight component CommonBase. The final canonical step, therefore, is to provide the means to let event listeners register with lightweights and receive notifications of the types of events appropriate to the component. This is a multipart process that involves:

I'll show how this is done using the FlatButton, Checkbox, and Spinner lightweight classes.

The java.awt.Component method enableEvents(int eventMask) allows a component to listen for those events designated by the eventMask parameter (defined in the AWTEvent class to have values such as WINDOW_EVENT_MASK and MOUSE_EVENT_MASK), and to do so without having any registered listeners. In Java 1.1, events are not normally produced when there are no registered listeners, but this technique ensures that events are generated. You can turn off listening by calling disableEvents(int eventMask).

Because each of my lightweight UI components is the target of some mouse activity in the constructor class CommonBase, I enable each lightweight to receive mouse events by calling enableEvents with the argument set to MOUSE_EVENT_MASK. Each of the child classes uses the event notifications differently. For example, a FlatButton can pop up in reaction to a mouse-entered event, Spinner can start a thread that changes its value in response to a mouse-pressed event, and it can stop the thread when it gets a mouse-released event; and Checkbox can change its state in reaction to a mouse-clicked event.

With every instance of an event source (like a button object), there is a list of listeners that have registered interest in being notified that an event has taken place. All you need to know is what type of event is being listened for, then keep a reference to the list of listeners in your own class. The way to access this list is through the AWTEventMulticaster class, which lets you have access to this listener chain so that you can insert or remove listeners, and, so that you can then deliver your own event to the chain.

Buttons normally let listeners register to listen for ActionEvent, so you define the addActionListener and removeActionListener methods (like standard AWT Buttons have), and use the static add and remove methods of the AWTEventMulticaster class to add or remove an ActionListener to instances of your buttons; see Listing Six. The adding/removing of listeners is done at the ButtonBase class level and is inherited by both FlatButton and RoundButton. In Listing Six, the reference being assigned the return from AWTEventMulticaster.add or remove is actionListener, which is a protected data member in the ButtonBase class. The dph.awt.lightweight.Spinner class also registers ActionListeners. Checkboxes register ItemListeners that want notification when the state of the checkbox changes, so you keep a reference to ItemListener and use the AWTEventMulticaster class to reference the checkbox's list of listeners.

Finally, you need to process events appropriate to the component. Because every button type should respond to a mouse-click by generating an ActionEvent for its listeners, I've put that behavior in the ButtonBase class. Listing Seven tests if the button is enabled by using the java.awt.Component's isEnabled method, then tests if it has any registered listeners, and then calls actionPerformed on the list of listeners. The parameters to actionPerformed are: a reference to the source of the event (which you provide by a reference to this), an event ID, and the command string. Placing this responsibility in the parent class necessitates a call to super.processEvent in the FlatButton and RoundButton child classes.

As Listing Eight shows, you respond to mouse activity in FlatButton by manipulating state information, which you use to draw the current state of the button. The call to super.process- Event is outside of the tests and takes place every time.

Finally, dph.awt.lightweight.Spinner handles mouse activity by tracking whether users press on the spinner's up- or down-arrow, then calls spin, which starts a thread to increment or decrement the spinner's value. It uses a thread because users might leave the mouse button pressed down while on the spinner arrow, and the expected behavior would be to continue changing the spinner's value while the button is pressed. Because it wants to start a thread in reaction to every mouse-down on a spinner arrow, and because users might repeatedly click on it in rapid succession, there is a built-in time delay, which prevents threads from being built willy-nilly and causing the control to spin too rapidly.

The spin method (Listing Nine) shows a reasonable use of a locally defined class, which is a class nested not only within another class, but within a method as well. Usually this is a maintenance headache, but here, the sole purpose of the thread is to spin the value up or down, and the class is short and can be understood fairly quickly. It makes sense to keep it together with the function that uses it. You can see that the spin function does only two things -- it defines the SpinThread class and calls start on an instance of it.

DDJ

Listing One

package dph.awt.lightweight;public abstract class CommonBase extends java.awt.Component {
    public void addNotify() {
        super.addNotify();
        java.awt.Dimension d = getSize();
        if( d.width == 0  ||  d.height == 0 ) {
            setSize( this.figureMySize( this.getGraphics() ) );
        }
    }
    public void setEnabled( boolean state ) {
        super.setEnabled( state );
        repaint();
    }
    public void update( java.awt.Graphics g ) {
        paint( g );    
    }
    public java.awt.Dimension getMinimumSize() {
        return getPreferredSize();
    }
    public java.awt.Dimension getPreferredSize() {
        if( getSize().width != 0  &&  getSize().height != 0 )
            return getSize();
        else
            return figureMySize( getGraphics() );
    }
    protected abstract java.awt.Dimension figureMySize( java.awt.Graphics g );
}


Back to Article

Listing Two

public Label() {    this( "", false );
}
public Label( String text ) {
    this( text, false );
}
public Label( String text, boolean showBorder ) {
    super();
    this.text = text;
    this.showBorder = showBorder;


Back to Article

Listing Three

public FlatButton ( java.awt.Image image ) {    super( "" );
    this.image = image;
    this.setup();
}
private final void setup() {
    if( image != null  &&  (image.getWidth(this)==-1  ||  
                                          image.getHeight(this)==-1) )
        this.ensureImageLoaded( image );
}
private final void ensureImageLoaded( java.awt.Image image ) {
    try {
        java.awt.MediaTracker tracker = new java.awt.MediaTracker( this );
        tracker.addImage( image, 0 );
        tracker.waitForID( 0 );
        Util.assert( tracker.statusID(0, false) == 
                        java.awt.MediaTracker.COMPLETE, "image complete" );
    } catch( InterruptedException ex ) {
        Util.debug( "Image loading interrupted" );
    }
}


Back to Article

Listing Four

public void paint( Graphics g ) {    super.paint( g );
    this.paintTabPanel( g );
    this.paintTabs( g );
}


Back to Article

Listing Five

public void setSelectedTab( int tabNum ) {    if( tabNum >= 0  &&  tabNum < tabLabels.length ) {
        this.remove( currentPanel );
        selectedTab = tabNum;
        currentPanel = (DoubleBufferedPanel)cards.get( tabLabels[tabNum] );
        this.add( currentPanel );
        currentPanel.setLocation( 2, 27 );
        currentPanel.setSize( this.getPanelSize() );
    }
    repaint();
}


Back to Article

Listing Six

public void addActionListener( ActionListener al ) {    actionListener = AWTEventMulticaster.add( actionListener, al );
}
public void removeActionListener( ActionListener al ) {
    actionListener = AWTEventMulticaster.remove( actionListener, al );
}


Back to Article

Listing Seven

public void processMouseEvent( MouseEvent me ) {    if( isEnabled() ) {
      switch( me.getID() ) {
        case MouseEvent.MOUSE_CLICKED:
            if( actionListener != null ) {
              actionListener.actionPerformed( new ActionEvent (
                this, ActionEvent.ACTION_PERFORMED, this.getActionCommand()
                 ) );
            }
        }
    }
    super.processMouseEvent( me );
}


Back to Article

Listing Eight

public void processMouseEvent( java.awt.event.MouseEvent evt ) {    if( isEnabled() ) {
      switch( evt.getID() ) {
      case java.awt.event.MouseEvent.MOUSE_ENTERED:
         mouseIsIn = true;
         repaint();
         break;
      case java.awt.event.MouseEvent.MOUSE_EXITED:
         mouseIsIn = false;
         repaint();
         break;
      case java.awt.event.MouseEvent.MOUSE_PRESSED:
         mouseIsDown = true;
         repaint();
         break;
      case java.awt.event.MouseEvent.MOUSE_RELEASED:
         mouseIsDown = false;
         repaint();
         break;
      }
   }


Back to Article

Listing Nine

public void spin() {    class SpinThread extends Thread {
        SpinThread() {
            super();
            this.setPriority( Thread.MIN_PRIORITY );
        }
        public void run() {
            try {
                if( upPressed )
                    increment();
                if( dnPressed )
                    decrement();
                repaint();
                Thread.sleep( 1250 );
                while( upPressed || dnPressed ) {
                    if( upPressed )
                        increment();
                    if( dnPressed )
                        decrement();
                    repaint();    
                    Thread.sleep( 400 );
                }
            } catch( InterruptedException ex ) {
                // ignore it
            }
        }
        new SpinThread().start();
    }

Back to Article


Copyright © 1999, Dr. Dobb's Journal
Feb99: Java and Lightweight Components

Java and Lightweight Components

By David K. Perelman-Hall

Dr. Dobb's Journal February 1999

Figure 1: Classes that make up the dph.awt.lightweight package.


Copyright © 1999, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.