Using communication channels in applets, Part 3

Develop Visual Basic-style techniques to applet design -- convert temperatures, too!

Java has a very traditional framework for laying out and implementing user interfaces. If you are familiar with the event loop type of programming you would do in an X11 or a Windows application, you will feel pretty much at home with Java. But (and there wouldn't be any point in writing this if there wasn't a "but" in there somewhere) Java is not typically running in a "traditional environment."

The environment that applets find themselves running in is already rich with expressive power for laying out documents. Why not do that with applet applications? You use tables to lay out forms, don't you?

To take advantage of this unique position that applets find themselves in, I set out to develop some building-block applets that could be combined into unique applications. To some extent this idea is "stolen" from Visual Basic, but in truth it bears nothing in common with that system, only a common way of explaining some of its features so that if you are familiar with Visual Basic then you can understand what these applets are trying to do. However, I'll start by looking at the traditional interface before jumping into this new one.

In nearly all modern window systems there is a notion of a containing window or frame and some subsidiary controls, which are sometimes called widgets. The basic application that controls such an application is written, at a gross level, to be something like:

begin 
    initialize window system, create base window.
    Create some controls, add them to the window.
    Open up the window.
    while ( not done ) do
    Get an event from the window system.
    switch on EVENT:
        case MENU
        do menus.
        case SELECT
        do select.
        case KEYBOARD
        do keyboard.
        case I/O
        do I/O.
        case EXIT
        leave.
    end while.
end application.

One of the disadvantages of writing a window-based tool in this way is that it doesn't necessarily take advantage of threads easily. Another disadvantage is that if the subroutines for handling events are "slow" getting back to the event loop, response can be problematic. Finally, it can be very difficult to generate "synthetic" events -- events that are generated as a computed result of a user's interaction, not necessarily direct user interaction.

So what is different about the applet environment? First, the window in which the applet operates already exists. The browser has created it for you and is already dealing with input from it. Secondly, applets today mostly live in WWW browsers, which already know how to lay out pages based on formatting codes that the user has input.

How might one take advantage of this? I did it by designing a collection of applets that were each a single "control" running in its own thread, and communicating with other controls using inter-Thread data channels. (So you see, part 1 and part 2 of this series were not a waste of time.) At the highest level these applets appear as individual items on the page, so you can lay them out with HTML tags like you would lay out images or forms. However, unlike forms, applets can interoperate without constraint. A typical form might appear in HTML as follows:

<html><head><title>Figure 1. Communicating Applets</title></head>
<body>
<h2>Communicating Applets</h2>
<p>
<table>
<tr>
    <td rowspan=2>
    <applet code="Slider.class" width=21 height=100>
    <param name="datachannel" value="slider1">
    <param name="orientation" value="vertical">
    </applet>
    </td><td align=left>
    <applet code="Numeric.class" width=30 height=21">
    <param name="datachannel" value="slider1">
    <param name="minvalue" value="32">
    <param name="maxvalue" value="212">
    </applet>
    </td>
</tr>
<tr>
    </td><td align=left>
    <applet code="Numeric.class" width=30 height=21">
    <param name="datachannel" value="slider1">
    <param name="minvalue" value="0">
    <param name="maxvalue" value="100.0">
    </applet>
    </td>
</tr>
</table>
</p>
</body>
</html>

Depending on how well you read raw HTML, you may see that this document is organized as two columns. In the left column is an applet of type Slider and in the right column are two applets, both of type Numeric. I'll cover what they do next, but the interesting bit here is that they are automatically layed out with the two numeric applets to the right of the slider applet.

Note: As of this writing, the release version of Netscape Navigator 2.0 has a serious bug where applets that are embedded in tables like the one above will not operate correctly if the page is left and then revisited via the back or reload button. So the above HTML works exactly once when you load the page.

If you are using these applets a lot, you may wish to put them in a page outside of a table so that they will work correctly at all times.

The Slider class

The next step is to look at the applets. The simpler one is probably the Slider applet. The code for this applet is shown below. However, large sections that simply do graphics have been excised for the sake of clarity and brevity.

import java.applet.Applet;
import java.awt.*;
import util.comm.*;
public class Slider extends Applet implements Runnable {
   Thread updater = null;
   DataChannel rr = null;
   Image altImage = null;
   Graphics offscreen = null;
   final static int SLIDER_RESOLUTION = 2000;
   final static int HORIZONTAL = 1;
   final static int VERTICAL = 2;
   int myOrientation;
   int currentPosition = SLIDER_RESOLUTION / 2; // midpoint
   String myChannel;

The preamble of the applet defines the variables that are used in each instance. Of particular interest is the variable myChannel, which contains a string that will identify the data channel associated with this slider, and the variable rr, which is the DataChannel object. Because this applet wants to have flickerless animation of the slider, it uses an offscreen image and graphics context.

 public void init() { ... }

The first public interface in the applet is the implementation of init. This function collects parameters from the HTML code. The only two parameters of interest above are orientation, which can take on the value horizontal or vertical, and datachannel, which contains the name of the data channel that this slider should write to.

The slider display

The slider is displayed as a box with two square end caps and a rectangular elevator that moves between the two ends. This causes it to look a lot like a Windows 95 slider.

void drawSquare(Graphics g, int x, int y, int size) {...}
void leftCap(Graphics g, int x, int y) {...}
void rightCap(Graphics g, int x, int y) { ... }
void topCap(Graphics g, int x, int y) { }
void bottomCap(Graphics g, int x, int y) { ... }
void elevator(Graphics g) { ... }

The first few methods, shown above, are helper methods. These methods draw the basic components of a slider, the end caps, and the elevator. Note that while the end caps take an X and Y coordinate, the elevator determines its position based on the current position.

 1    public void update(Graphics g) {
 2        if (offscreen == null) {
 3            altImage = createImage(myWidth, myHeight);
 4        offscreen = altImage.getGraphics();
 5        }
 6        offscreen.setColor(bgColor);      
 7        offscreen.fillRect(0, 0, myWidth, myHeight);
 8    offscreen.setColor(Color.black);
 9    rect3D(offscreen, 0, 0, myWidth, myHeight, 2, false);
10
11        if (myOrientation == HORIZONTAL) {
12            leftCap(offscreen, 0, 0);
13            rightCap(offscreen, myWidth - 21, 0);
14        } else {
15            topCap(offscreen, 0, 0);
16            bottomCap(offscreen, 0, myHeight - 21);
17        }
18        elevator(offscreen);
19    g.drawImage(altImage, 0, 0, null);
20    }

This is the update function. The function initializes the offscreen graphics context and then sets up by filling it with the background color. Then, depending on the orientation, it draws either left and right end caps or top and bottom ones, then it draws the elevator. Finally it writes the offscreen image. All in all, a very basic update function. I override the update method rather than paint since the default update method clears the background before drawing, which leads to flashing.

Next comes the applet version of the start method:

1    public void start() {
2     updater = new Thread(this);
3
4     rr = DataChannel.getChannel(myChannel, updater, 8);
5     updater.start();
6   }

start creates a new thread using the current object instance as its basis. Then it creates a link to the named data channel. And then to finish up, it starts the new thread running.

1    public void stop() {
2     rr.releaseChannel(updater);
3     updater = null;
4   }

stop is pretty simple, too. Its primary job is to release the data channel and to null out the pointer to the updater thread. This latter item is necessary if the thread object created by start is to be later collected by the garbage collector.

The most critical method as far as the thread is concerned is run. In the slider applet, the run method is responsible for monitoring the data channel and updating the position of the elevator based on what should be the current position.

 1    public void run() {
 2    while (true) {
 3        try {
 4            currentPosition = ((Integer) rr.getValue()).intValue();
 5        } catch (DataChannelOverrun e) {
 6        System.out.println("Slider: OVERRUN!");
 7        } catch (DataChannelShutdown e) {
 8            System.out.println("Slider thread shutting down.");
 9            return;
10        } catch (DataChannelException ee) {
11        System.out.println("We're dead.");
12        ee.printStackTrace();
13        }
14        currentPosition = Math.max(0, currentPosition);
15        currentPosition = Math.min(SLIDER_RESOLUTION, currentPosition);
16        repaint();
17    }
18    }

You might ask yourself, "Why should the slider update its appearance from the data channel? Doesn't it already know where it is supposed to be?" The answer is simplicity of implementation and the desire to have other things update the slider's position.

The implementation of the run method is very straightforward. The thread sleeps when it calls the getValue method of the data channel. It assumes that the value coming out of the channel will be an object of type Integer. If it isn't, the thread will bomb out so one enhancement would be to put a check to make sure an Integer was received. The thread will wake up because it got a value or an exception. The data channel exceptions were covered in the previous article so I won't go over them here. Suffice it to say that should the thread receive a shutdown exception it will exit. Finally, the value returned is clipped to be a legal value between 0 and SLIDER_RESOLUTION. This prevents the elevator-drawing method from going berzerk. The last step of the method is to call repaint, which causes the elevator location to be updated in the applet.

The slider user interface

The mouse event functions are used to implement the slider user interface. While it could have been fairly sophisticated, the current implementation isn't. The interface is as follows:

  • Click an end cap and the elevator moves 2.5% closer to that end cap;
  • Click above or below the elevator and the elevator moves to that point;
  • Click and drag the elevator and the elevator follows the mouse.

First there is a helper routine called newPosition, which is shown below.

 1  void newPosition(int x, int y) {
 2      int loVal, hiVal, midRange, sample;
 3
 4  // zero is when the midpoint of the elevator is against the bottom
 5      loVal = 18 + (elevatorSize/2); // lowest value
 6      hiVal = ((myOrientation == HORIZONTAL) ? myWidth : myHeight) -
 7               18 - (elevatorSize/2);
 8      midRange = hiVal - loVal;
 9      sample = Math.max(((myOrientation == HORIZONTAL) ? x : y), loVal);
10      sample = Math.min(sample, hiVal);
11      rr.putValue(new Integer(((sample - loVal) * SLIDER_RESOLUTION) / midRange));
12  }

This method uses the midpoint of the elevator as the control point for the slider. Using the midpoint ensures that the slider will reach its maximum and minimum values in an intuitive way. As in the run method, the resulting value is clipped to keep it in the range of 0 to SLIDER_RESOLUTION.

Next the applet processes mouse down events (clicks).

 1  public boolean mouseDown(Event ev, int x, int y) {
 2      int result = currentPosition;
 3      if (((myOrientation == HORIZONTAL) && ((x <= 20) && (x >= 0))) ||
 4         (((myOrientation == VERTICAL) && (y <= 20) && (y >= 0)))) {
 5         result -= (SLIDER_RESOLUTION * .025);
 6         if (result < 0) result = 0;
 7         rr.putValue(new Integer(result));
 8         return true;
 9      }
10      if (((myOrientation == HORIZONTAL) && ((x >= (myWidth - 20)) && (x <= myWidth))) ||
11         (((myOrientation == VERTICAL) && (y >= (myHeight - 20)) && (y <= myHeight)))) {
12         result += (SLIDER_RESOLUTION * .025);
13         if (result > SLIDER_RESOLUTION) result = SLIDER_RESOLUTION;
14         rr.putValue(new Integer(result));
15         return true;
16      }
17      return true;
18  }
Related:
1 2 Page 1
Page 1 of 2