Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

JVM Languages

Java and Lightweight Components


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:

  • Inherit from java.awt.Component or java.awt.Container or another lightweight.
  • Do all the drawing required to represent the component on screen, including any graphical behavior that would normally occur in reaction to mouse events.
  • Do the event handling appropriate to the component. This means you must provide the hooks to let listeners register and deregister for notification that an event has taken place, and you must pass the event notification to the listeners.

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:

  • An array of String to hold the tab labels.
  • An array of Rectangle designating regions of the screen where the tabs are drawn so that mouse clicks can be associated with particular tabs.
  • An int to designate the currently selected tab.
  • A Hashtable to hold the contents of each tab in the form of Panels keyed to their tab labels.
  • A DoubleBufferedPanel called currentPanel.

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:

  • Tabs are only positioned across the top of the folder. In fact, there is a trick to how tabs appear as a separate row of features above the folder for each tab. Because the TabPanel itself is a Container, it has a LayoutManager which controls how the components on it are laid out. The TabPanel nulls out its LayoutManager, and each Panel selected by a mouseclick on a tab is positioned by the call currentPanel.setLocation(2,27). Thus, there is a row 27 pixels tall above each Panel; this is where the tabs are drawn.
  • The row of tabs across the top of the TabPanel will not intelligently handle more tabs than there is horizontal space for as determined by the width of the TabPanel.

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:

  • Enabling lightweight components to receive event notifications appropriate for them (this is what's going on in the call to enableEvents).
  • Giving event listeners a way to register with a lightweight component so that the listeners can be notified when the lightweight component has an event of interest.
  • Having the lightweight component disperse the event to registered listeners.

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 );
}


</p>

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;


</p>

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" );
    }
}


</p>

Back to Article

Listing Four

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


</p>

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();
}


</p>

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 );
}


</p>


</p>


</p>

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 );
}


</p>

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;
      }
   }


</p>

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

Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.