13.11 Scrollbars and Sliders

The AWT uses the same basic component for scrollbars (used to scroll windows) and sliders (used to interactively select values), the Scrollbar class. This is a bit inconvenient when using them for sliders, so in Section 13.12 we will develop a Slider class that combines a Scrollbar with a TextField to show the current value. The "preferred" size and shape of a Scrollbar is not generally usable, so they are not usually placed in a window that uses FlowLayout. Instead, they are typically placed in the east or south sections of a BorderLayout, used in a GridLayout, or resized by hand.

Unfortunately, there are a number of bugs in the way scrollbars are implemented in the most widely used Java systems, so creating reliable scrollbars for multiplatform applications requires significantly more effort than necessary. The situation is so bad, in fact, that you really cannot count on scrollbars working in a cross-platform environment unless you thoroughly test on all the platforms your application is likely to run on. Fortunately, if you are using scrollbars for scrolling (rather than for selecting values), many applications can be replaced by ScrollPane in Java 1.1, which works reliably. For simple scrolling text, TextArea is an even easier alternative, and is available in both Java 1.0 and Java 1.1.

Core Warning


In current implementations, scrollbars are the least reliable interface element. If you use them, test thoroughly on all platforms on which you will be delivering.


Constructors

public Scrollbar()

This creates a vertical scrollbar. The "bubble" (or "thumb," the part that actually moves) size defaults to 10% of the trough length. The internal min and max values (see below) are set to zero.

public Scrollbar(int orientation)

This creates a horizontal or vertical scrollbar. The orientation can be either Scrollbar.HORIZONTAL or Scrollbar.VERTICAL. The "bubble" ("thumb") size defaults to 10% of the trough length. The internal min and max values (see below) are set to zero.

public Scrollbar(int orientation, int initialValue, int bubbleSize, int min, int max)

This constructor is the one you use when you want to make a "slider" for interactively selecting values (see Section 13.12 for a slider implementation). It creates a horizontal or vertical slider or scrollbar with a customized bubble (or "thumb," -- i.e., the part that actually moves) thickness and a specific internal range of values. The bubble thickness is in terms of the scrollbar's range of values, not in pixels, so that if max minus min was 5, a bubble size of 1 would specify 20% of the trough length. Note, however, that some operating systems (MacOS, in particular) do not support varying sizes to scrollbar thumbs, nor does Netscape Navigator 2 or 3 on Windows 95 or Windows NT. Also, the value corresponds to the location of the left (horizontal sliders) or top (vertical sliders) edge of the bubble, not its center. Unfortunately, Java 1.1 implementations tend to interpret the maximum value differently than in Java 1.0. For a horizontal scrollbar, rather than having the maximum correspond to the highest value the left side of the slider can reach, it is the highest value the right side of the slider can reach. Similarly for vertical scrollbars. This means that in current 1.1 implementations the actual values that can be set range from the minimum value to the bubble size less than the maximum value. For example, in Java 1.0, the following would make a scrollbar that can range from 0 to 50, with the initial value at 25 and a bubble size of 5:

      new Scrollbar(Scrollbar.HORIZONTAL, 25, 5, 0, 50);

In Java 1.1, however, the same code makes a scrollbar that can range only from 0 to 45. It is not clear from the 1.1 specification if this is the new intended behavior (a strange design, if so) or if it is simply an implementation bug (strange that it has persisted to JDK 1.1.3, then). But in any case, this happens in Sun's JDK 1.1.1 through 1.1.3 on Unix and Windows platforms. This is illustrated in Figures 13-32 and 13-33, which show the exact same code (Listing 13.33) run in JDK 1.02 and JDK 1.1.3 on Windows 95.

Core Warning


In Java 1.1, to get a scrollbar with a thumb size of t that can range in value from min to max , you have to create it with a maximum value of max+t.



Figure 13-32 In Java 1.02, scrollbar max values are interpreted the way most people would expect.



Figure 13-33 In Java 1.1, scrollbar max values are interpreted strangely.


Listing 13.33 ScrollbarValues.java

import java.awt.*

public class ScrollbarValues extends QuittableFrame {
  public static void main(String[] args) {
    new ScrollbarValues(0, 80, 40);
  }

  private Font headingFont =
    new Font("Helvetica", Font.BOLD, 16);
  private Font bodyFont =
    new Font("Courier", Font.BOLD, 14);
  
  public ScrollbarValues(int min, int max, int bubble) {
    super("Scrollbar Values in Java " +
          System.getProperty("java.version"));
    setLayout(new GridLayout(4, 2, 5, 0));
    add(makeHeadingLabel("Scrollbar"));
    add(makeHeadingLabel("Values"));
    Scrollbar bar;
    for(int i=0; i<3; i++) {
      bar = new Scrollbar(Scrollbar.HORIZONTAL,
                          bubble*i, bubble, min, max);
      add(bar);
      add(makeScrollbarLabel(bar, bubble*i));
    }
    pack();
    show();
  }

  private Label makeHeadingLabel(String heading) {
    Label headingLabel =
      new Label(heading, Label.CENTER);
    headingLabel.setFont(headingFont);
    return(headingLabel);
  }

  private Label makeScrollbarLabel(Scrollbar bar,
                                   int valueSet) {
    Label scrollbarLabel =
      new Label("Min=" + bar.getMinimum() +
                ", Max=" + bar.getMaximum() +
                ", Value Set=" + valueSet +
                ", Actual Value=" + bar.getValue() +
                " ");
    scrollbarLabel.setFont(bodyFont);
    return(scrollbarLabel);
  }
}

Note the small amount of empty space to the right of the bottom scrollbar in Figure 13-32. This, unfortunately, is common in Windows 95/NT implementations of Java 1.0. It appears in Internet Explorer 3, Netscape Communicator, and Sun's JDK. Interestingly, neither Netscape 2 nor 3 suffer from the problem, however. The percent of the total scrollbar length wasted this way tends to be proportional to the range of scrollbar values, with a shorter range having more serious problems. Listing 13.34 creates a scrollbar with range from only 1 to 4, at its maximum value of 4. Figure 13-34 shows the result in JDK 1.02 on Windows 95.

Listing 13.34 WastedSpace.java

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

public class WastedSpace extends Applet {
  public void init() {
    setLayout(new BorderLayout());
    Label label =
      new Label("Scrollbar is at its maximum value",
                Label.CENTER);
    add("North", label);
    Scrollbar bar =
      new Scrollbar(Scrollbar.HORIZONTAL, 4, 1, 1, 4);
    add("Center", bar);
  }
}

Figure 13-34 Java 1.0 scrollbar with a range of only three values has a lot of dead space at the right.


Core Warning


If you create a scrollbar with a small range in Internet Explorer, Netscape Communicator, or JDK 1.02 on Windows 95/NT, you will have a large amount of wasted space at the right or bottom.


Example

Listing 13.35 creates a variety of horizontal and vertical sliders (scrollbars for selecting values). The values range from 0 to 100 with an initial value of 50 and various different bubble thicknesses. Figures 13-35, 13-36, and 13-37 show the results in Java 1.0 on Windows 95, MacOS, and Solaris. Figure 13-38 shows the result on Solaris in Java 1.1.

Listing 13.35 Scrollbars.java

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

public class Scrollbars extends Applet {
  public void init() {
    int i;
    setLayout(new GridLayout(1, 2));
    Panel left = new Panel(), right = new Panel();
    left.setLayout(new GridLayout(10, 1));
    for(i=5; i<55; i=i+5)
      left.add(new Scrollbar(Scrollbar.HORIZONTAL,
                             50, i, 0, 100));
    right.setLayout(new GridLayout(1, 10));
    for(i=5; i<55; i=i+5)
      right.add(new Scrollbar(Scrollbar.VERTICAL,
                              50, i, 0, 100));
    add(left);
    add(right);
  }
}

Figure 13-35 Scrollbars with varying bubble sizes but constant ranges and initial values, shown in Java 1.0 on Windows 95.



Figure 13-36 Scrollbars in MacOS always have the same bubble size.



Figure 13-37 Scrollbars with varying bubble sizes but constant ranges and initial values, shown in Java 1.0 on Solaris.



Figure 13-38 Scrollbars with varying bubble sizes but constant ranges and initial values, shown in Java 1.1 on Solaris.


Other Scrollbar Methods

public void addAdjustmentListener(AdjustmentListener listener) [Java 1.1]
public void removeAdjustmentListener(AdjustmentListener listener) [Java 1.1]

These Java 1.1 methods add/remove an AdjustmentListener to the scrollbar, used to monitor when the user adjusts the scrollbar. An AdjustmentListener needs to implement adjustmentValueChanged, which takes an AdjustmentEvent as an argument. The AdjustmentEvent class contains a getAdjustmentType method that returns one of AdjustmentEvent.UNIT_INCREMENT (the right or down arrow was clicked), AdjustmentEvent.UNIT_DECREMENT (the left or up arrow was clicked), AdjustmentEvent.BLOCK_INCREMENT (the trough to the right of or below the bubble was clicked), AdjustmentEvent.BLOCK_DECREMENT (the trough to the left of or above the bubble was clicked), or AdjustmentEvent.TRACK (the bubble was dragged).

public int getLineIncrement()
public int getUnitIncrement() [Java 1.1]

These methods are intended to return the amount that the value will be adjusted when the arrows at either end of the scrollbar are pressed. It is usually 1 if the you haven't specified anything different with setLineIncrement (Java 1.0) or setUnitIncrement (Java 1.1). Unfortunately, however, many implementations ignore this. For instance, the line increment value has no effect on Solaris in JDK 1.02 or 1.1, or on Windows 95/NT in JDK 1.02. For many applications it is possible to fix this yourself, however. See Listing 13.36 for an example.

public int getMaximum()

This returns the scrollbar's maximum possible value. Recall that this is the largest value that can be selected in Java 1.0, but in most Java 1.1 implementations it is getVisibleAmount more than the largest possible selectable value.

public int getMinimum()

This returns the scrollbar's minimum possible value.

public int getOrientation()

This returns either Scrollbar.HORIZONTAL or Scrollbar.VERTICAL.

public int getPageIncrement()
public int getBlockIncrement() [Java 1.1]

This supposedly tells you how much the value will change when the user clicks inside the trough above or below the scrollbar bubble. The default is platform dependent; common defaults are 10 (Win95) or the bubble size (Solaris). Unfortunately, many implementations ignore this value. For instance, it has no effect in JDK 1.02 on either Solaris or Windows 95/NT. For many applications it is possible to fix this yourself, however. See Listing 13.36 for an example.

public int getValue()

This returns the current value.

public int getVisible()
public int getVisibleAmount() [Java 1.1]

This returns the size of the bubble (thumb), represented in terms of the units used for the scrollbar range, not in terms of pixels.

public void processAdjustmentEvent(AdjustmentEvent event) [Java 1.1]

In Java 1.1, if you want to have a scrollbar handle its own events, you first enable adjustment events as follows:

      enableEvents(AWTEvent.ADJUSTMENT_EVENT_MASK);

Next, you override processAdjustmentEvent, which takes an AdjustmentEvent as an argument. Remember to call super.processAdjustmentEvent in case there are listeners on the scrollbar. See the following subsection for an example.

public void setLineIncrement(int increment)
public void setUnitIncrement(int increment) [Java 1.1]

This supposedly changes the amount the scrollbar will move when you click on the arrows, but is poorly supported. See getLineIncrement.

public void setPageIncrement(int increment)
public void setBlockIncrement(int increment) [Java 1.1]

This supposedly changes the amount the scrollbar will move when you click in the trough above or below the scrollbar bubble, but is poorly supported. See getPageIncrement.

public void setMaximum(int maxValue) [Java 1.1]

This changes the maximum possible value. Recall that in Java 1.1 implementations, the maximum selectable value is this maximum minus the bubble size (see getVisible). In Java 1.0, setMaximum is not available; you have to use setValues instead.

public void setMinimum(int minValue) [Java 1.1]

This changes the minimum possible value. In Java 1.0, you have to use setValues instead of this.

public void setOrientation(int orientation) [Java 1.1]

This changes orientation of the scrollbar. In Java 1.0, you have to use setValues instead of this.

public void setValue(int value)

This changes the stored value and moves the scrollbar appropriately. Specifying a value below the minimum value does not cause an error; the minimum value is simply stored. Specifying a value above the maximum (or above the maximum minus the thumb size in Java 1.1) does not cause an error either. Instead, the scrollbar takes on either the maximum value (Java 1.0) or the maximum minus the thumb size (Java 1.1).

public void setValues(int value, int bubbleSize, int min, int max)

This changes several parameters in one fell swoop. In Java 1.0, this is the only way to change the bubble size, the minimum value, or the maximum value.

As a subclass of Component, the setForeground and setBackground methods are available, but setForeground is not generally supported. On Windows 95/NT, setBackground sets the color of the scrollbar trough. On Solaris, it sets the color of the bubble and the arrows. Current MacOS implementations do not support scrollbar colors.

Handling Scrollbar Events

In Java 1.0, you process scrolling events in handleEvent. In Java 1.1, you can either use processAdjustmentEvent or attach an AdjustmentListener.

Scrollbar Events in Java 1.0

Unfortunately, in Java 1.0 the Scrollbar class does not generate events that are passed onto a "helper" method like action or mouseDown. So the events need to be trapped in handleEvent. There are 5 different types of events that can be generated: Event.SCROLL_LINE_DOWN (when the right/bottom arrow is clicked), Event.SCROLL_LINE_UP (when the left/top arrow is clicked), Event.SCROLL_PAGE_DOWN (when the right/bottom trough is clicked), Event.SCROLL_PAGE_UP (when the left/top trough is clicked), and Event.SCROLL_ABSOLUTE (when the bubble is dragged). There is rarely a reason to handle each of these events separately, especially since page and line increments are implemented inconsistently. Instead you can simply look at the current value, which can be obtained by the getValue method or from the arg instance variable of the event, which is an Integer. However, since Java 1.1 implementations and some 1.0 implementations send mouse and/or keyboard events to scrollbars, you should at least check that the event was one of the specified five before taking action. Here's the basic idea, shown assuming you are capturing events in an enclosing window.

      public boolean handleEvent(Event event) {
        if (event.target == scrollbar &&
            isScrollEvent(event.id)) {
          doAction(scrollbar.getValue());
          return(true);
        } else
          return(super.handleEvent(event));
      }
      
      private boolean isScrollEvent(int eventID) {
        return(eventID == Event.SCROLL_LINE_UP ||
               eventID == Event.SCROLL_LINE_DOWN ||
               eventID == Event.SCROLL_PAGE_UP ||
               eventID == Event.SCROLL_PAGE_DOWN ||
               eventID == Event.SCROLL_ABSOLUTE);
      }

Unfortunately, there is still another bug we need to be concerned about. In many Windows 95/NT implementations (including both Java 1.0 and 1.1), scrollbars will sometimes "pop" back to their original position when you release the mouse after dragging them. In many cases, this can be fixed by sleeping (Thread.sleep) for a short time after each Event.SCROLL_ABSOLUTE event. I add this ugly but necessary hack to the Slider class shown in the Section 13.12.

Slider Events in Java 1.1

The Java 1.1 event model simplifies things considerably. Rather than trying to sort through the various events to figure out which are the scrolling events, Java will decide for you. If you override processAdjustmentEvent, you can let a scrollbar handle its own events. If you attach an AdjustmentListener, an external object can handle the events. In either case, note that the AdjustmentEvent class has two methods of particular import: getValue and getAdjustmentType. The first returns an int giving the scrollbar's current value, while the second returns one of AdjustmentEvent.UNIT_INCREMENT, AdjustmentEvent.UNIT_DECREMENT, AdjustmentEvent.BLOCK_INCREMENT, AdjustmentEvent.BLOCK_DECREMENT, or AdjustmentEvent.TRACK. For instance, Listing 13.36 shows how you could use processAdjustmentEvent to make a scrollbar that honors the unit increment and block increment values, regardless of whether the underlying implementation already does. Note, however, that it only works if all scrollbar adjustment is by the user. Calling setValue will not properly update the lastValue variable, but there is no way around this since you cannot tell the difference between a system-generated setValue call (when the scrollbar is adjusted) and a user-generated one.

Listing 13.36 BetterScrollbar.java

import java.awt.*;
import java.awt.event.*;

/** The beginnings of a better Scrollbar. This one
 *  adjusts for the fact that many implementations
 *  ignore the line and/or page increment. Created
 *  to demonstrate low-level 1.1 scrollbar events.
 */

public class BetterScrollbar extends Scrollbar {
  private int lastValue;
  
  public BetterScrollbar(int orientation,
                         int initialValue,
                         int bubbleSize,
                         int min,
                         int max) {
    super(orientation, initialValue, bubbleSize,
          min, max);
    enableEvents(AWTEvent.ADJUSTMENT_EVENT_MASK);
    lastValue = initialValue;
  }

  public void processAdjustmentEvent(AdjustmentEvent e) {
    int type = e.getAdjustmentType();
    switch(type) {
      case e.UNIT_INCREMENT:
        setValue(lastValue + getUnitIncrement());
        break;
      case e.UNIT_DECREMENT:
        setValue(lastValue - getUnitIncrement());
        break;
      case e.BLOCK_INCREMENT:
        setValue(lastValue + getBlockIncrement());
        break;
      case e.BLOCK_DECREMENT:
        setValue(lastValue - getBlockIncrement());
        break;
    }
    lastValue = getValue();
    super.processAdjustmentEvent(e);
  }
}

Continue to Section 13.12 (A Slider Class). Return to Chapter 13 table of contents.