13.12 A Slider Class

Scrollbars are frequently used for creating scrolling windows or graphics. In some windowing packages, there is a separate "slider" class that is used for interactively choosing values in an integral range. In Java, Scrollbar doubles for both. This is inconvenient, and I find a separate slider quite useful. The class presented here places a textfield to the right of a horizontal scrollbar. When the scrollbar value is changed, it is displayed right-justified in the textfield, which is just wide enough to hold the largest value. Optionally, the text area can accept input, letting you enter an integer to be used to set the slider value. The preferred width is 250 pixels, but can be changed. The preferred height is based on the textfield height (which uses 12-point Courier by default), but top and bottom margins can be set to make it thinner than the text area. Min and max values are interpreted identically in Java 1.0 and 1.1, and the Windows 95 drag-bug is prevented by use of a small pause between absolute scroll events.

The most common usage is to simply drop one in a window, let the user manipulate it, then read the value with getValue whenever you need it. The setEditable method can be used to let the user enter values in the textfield as well as by dragging the slider. For example:

      setLayout(new BorderLayout());
      Slider simulationRunsSlider = new Slider(1, 25, 5);
      simulationRunsSlider.setEditable(true);
      add(simulationRunsSlider);
      ...
      startSimulation(simulationRunsSlider.getValue());

The following subsections describe the Slider behavior; Listing 13.38 presents the source code.

Constructors

public Slider(int min, int max, int initialValue)

This creates a slider with the specified min, max, and initial values. The bubble size is set to one tenth of the slider range, top/bottom margins are set to 4, and the preferred width is set to 250.

public Slider(int min, int max, int initialValue, int bubbleSize)

This creates a slider with the specified min, max, initial value, and bubble size. The top/bottom margins are set to 4, and the preferred width is set to 250.

Example

Listing 13.37 creates several sliders with values that range from 0 to 120, cycling through three different top and bottom margin settings: the default, the default plus one, and the default plus two. Figures 13-39 and 13-40 show the results in Windows 95 and Solaris, respectively.

Listing 13.37 Sliders.java

import java.awt.*;

public class Sliders extends QuittableFrame {
  public static void main(String[] args) {
    new Sliders();
  }
  
  public Sliders() {
    super("Sliders with Varying Margins");
    setLayout(new FlowLayout());
    setFont(new Font("TimesRoman", Font.BOLD, 14));
    setBackground(Color.lightGray);
    add(makeSliderPanel(10));
    add(makeSliderPanel(12));
    add(makeSliderPanel(14));
    pack();
    show();
  }

  private Panel makeSliderPanel(int fontSize) {
    Panel panel = new Panel();
    panel.setLayout(new GridLayout(0, 1));
    panel.add(new Label("Font size: " + fontSize,
                        Label.CENTER));
    Slider slider;
    for(int i=0; i<6; i++) {
      slider = new Slider(0, 50, 10*i);
      slider.setFontSize(fontSize);
      slider.setMargins(slider.getMargins() + i-2);
      panel.add(slider);
    }
    return(panel);
  }
}

Figure 13-39 Sliders with various margins, font sizes, and initial values in Windows 95.


Figure 13-40 Sliders with various margins, font sizes, and initial values in Solaris.

Other Slider Methods

public void doAction()

This is a placeholder that you can override in Slider subclasses to put side effects that you want to take place every time the slider value changes.

public Font getFont()

This returns the font used in the textfield, 12 point bold monospaced is the default.

public int getFontSize()

This returns the size of the current textfield font.

public int getMargins()

This returns the value of the top and bottom margins. This is empty space reserved above and below the scrollbar so that it is not as tall as the textfield. The default is 4.

public int getPreferredWidth()

This returns the width part of the preferredSize.

public Scrollbar getScrollbar()

This returns the Scrollbar used in the left part of the Slider. All of the normal Scrollbar methods are available with it.

public String getText()

This calls the scrollbar's getText method. The string returned will represent an integer, but will be padded with blanks.

public TextField getTextField()

This returns the TextField used in the right part of the Slider. All of the normal TextField methods are available with it.

public int getValue()

This returns the scrollbar value.

public boolean isEditable()

This determines if you can enter an integer in the textfield to change the scrollbar value, in addition to dragging the slider. Noninteger values are ignored when entered.

public void setEditable(boolean editFlag)

This lets you specify whether or not the user can enter values into the textfield.

public void setFont(Font textFieldFont)

This lets you change the textfield font. Be careful; proportional fonts may not display numeric values as well as Courier.

public void setFontSize(int size)

This sets the textfield font to Courier, bold, and the specified size.

public void setMargins(int topAndBottomMargins)

This changes the amount of empty space at the top and bottom of the scrollbar. The default is 4.

public void setPreferredWidth(int preferredWidth)

The normal preferredSize of a Scrollbar is too small to be usable. The height part will be taken from the TextField. This method sets the width portion; the default is 250. Of course, as with other graphical components you can always enforce exact sizes instead of relying on the preferred size.

public void setText(String text)

This changes the value in the TextField. In general, you want to use setValue instead, since that will make sure the value is an integer in the proper range and will right-justify the string.

public void setValue(int value)

This changes the slider value. Out of bound values do not generate errors; the minimum or maximum value is set instead.

Handling Slider Events

Scrollbar and TextField events are already handled by the Slider class; dragging the scrollbar updates the textfield, and entering a value in the textfield (if it is editable) changes the slider value. In most cases, if you have additional actions you want to take place when the slider changes, the only thing you need to do is to override doAction. However, if you need to customize handleEvent or action, be sure to call super.handleEvent or super.action when done.

Source Code

Listing 13.38 presents the source code for the Slider class. To enforce top/bottom margins for the scrollbar part, it uses ScrollbarPanel, a small helping class shown in Listing 13.39.

Listing 13.38 Slider.java

import java.awt.*;

/** A class that combines a horizontal Scrollbar and
 *  a TextField (to the right of the Scrollbar).
 *  The TextField shows the current scrollbar value,
 *  plus, if setEditable(true) is set, it can be used
 *  to change the value as well.
 *
 * @author Marty Hall (hall@apl.jhu.edu)
 */

public class Slider extends Panel {
  private Scrollbar scrollbar;
  private TextField textfield;
  private ScrollbarPanel scrollbarPanel;
  private int preferredWidth = 250;

  //----------------------------------------------------
  /** Construct a slider with the specified min, max
   *  and initial values. The "bubble" (thumb)
   *  size is set to 1/10th the scrollbar range.
   *  In JDK 1.1.x, it tries to adjust for the max
   *  value bug by adding the bubble thickness to
   *  the max value.
   */
  public Slider(int minValue, int maxValue,
                int initialValue) {
    this(minValue, maxValue, initialValue,
         (maxValue - minValue)/10);
  }

  /** Construct a slider with the specified min, max,
   *  and initial values, plus the specified "bubble"
   *  (thumb) value. This bubbleSize should be
   *  specified in the units that min and max use,
   *  not in pixels. Thus, if min is 20 and max is
   *  320, then a bubbleSize of 30 is 10% of the
   *  visible range.
   */
  public Slider(int minValue, int maxValue,
                int initialValue, int bubbleSize) {
    setLayout(new BorderLayout());
    maxValue = adjustFor1_1(maxValue, bubbleSize);
    scrollbar = new Scrollbar(Scrollbar.HORIZONTAL,
                              initialValue,
                              bubbleSize,
                              minValue, maxValue);
    scrollbarPanel = new ScrollbarPanel(6);
    scrollbarPanel.add("Center", scrollbar);
    add("Center", scrollbarPanel);
    textfield = new TextField(numDigits(maxValue) + 1);
    setFontSize(12);
    textfield.setEditable(false);
    setTextFieldValue();
    add("East", textfield);
  }
  
  //----------------------------------------------------
  /** A place holder to override for action to be taken
   *  when scrollbar changes
   */
  public void doAction(int value) {
  }

  //----------------------------------------------------
  /** When scrollbar changes, sets the textfield */
  
  public boolean handleEvent(Event event) {
    if (event.target == scrollbar &&
        isScrollEvent(event.id)) {
      setTextFieldValue();
      doAction(scrollbar.getValue());
      fixWindowsProblem(event.id);
      return(true);
    } else
      return(super.handleEvent(event));
  }

  //----------------------------------------------------
  /** When textfield changes, sets the scrollbar */
  
  public boolean action(Event event, Object object) {
    if (event.target == textfield) {
      String value = textfield.getText();
      int oldValue = getValue();
      try {
        setValue(Integer.parseInt(value.trim()));
      } catch(NumberFormatException nfe) {
        setValue(oldValue);
      }
      return(true);
    } else
      return(false);
  }
  
  //----------------------------------------------------
  /** Returns the Scrollbar part of the Slider. */
  
  public Scrollbar getScrollbar() {
    return(scrollbar);
  }

  /** Returns the TextField part of the Slider */
  
  public TextField getTextField() {
    return(textfield);
  }

  //----------------------------------------------------
  /** Changes the preferredSize to take a minimum
   *  width, since super-tiny scrollbars are
   *  hard to manipulate.
   *
   * @see #getPreferredWidth
   * @see #setPreferredWidth
   */
  public Dimension preferredSize() {
    Dimension d = super.preferredSize();
    d.height = textfield.preferredSize().height;
    d.width = Math.max(d.width, preferredWidth);
    return(d);
  }

  /** This just calls preferredSize */
  
  public Dimension minimumSize() {
    return(preferredSize());
  }
  
  //----------------------------------------------------
  /** To keep scrollbars legible, a minimum width is
   *  set. This returns the current value (default is
   *  150).
   *
   * @see #setPreferredWidth
   */
  public int getPreferredWidth() {
    return(preferredWidth);
  }

  /** To keep scrollbars legible, a minimum width is
   *  set. This sets the current value (default is
   *  150).
   *
   * @see #getPreferredWidth
   */
  public void setPreferredWidth(int preferredWidth) {
    this.preferredWidth = preferredWidth;
  }

  //----------------------------------------------------
  /** This returns the current scrollbar value */
  
  public int getValue() {
    return(scrollbar.getValue());
  }

  /** This assigns the scrollbar value. If it is below
   *  the minimum value or above the maximum, the value
   *  is set to the min and max value, respectively.
   */
  public void setValue(int value) {
    scrollbar.setValue(value);
    setTextFieldValue();
  }

  //----------------------------------------------------
  /** Sometimes horizontal scrollbars look odd if they
   *  are very tall. So empty top/bottom margins
   *  can be set. This returns the margin setting.
   *  The default is four.
   *
   * @see setMargins
   */
  public int getMargins() {
    return(scrollbarPanel.getMargins());
  }
  
  /** Sometimes horizontal scrollbars look odd if they
   *  are very tall. So empty top/bottom margins
   *  can be set. This sets the margin setting.
   *
   * @see getMargins
   */
  public void setMargins(int margins) {
    scrollbarPanel.setMargins(margins);
  }

  //----------------------------------------------------
  /** Returns the current textfield string. In most
   *  cases this is just the same as a String version
   *  of getValue, except that there may be padded
   *  blank spaces at the left.
   */
  public String getText() {
    return(textfield.getText());
  }

  /** This sets the TextField value directly. Use with
   *  extreme caution since it does not right-align
   *  or check if value is numeric.
   */
  public void setText(String text) {
    textfield.setText(text);
  }

  //----------------------------------------------------
  /** Returns the Font being used by the textfield.
   *  Courier bold 12 is the default.
   */
  
  public Font getFont() {
    return(textfield.getFont());
  }

  /** Changes the Font being used by the textfield. */
  
  public void setFont(Font textFieldFont) {
    textfield.setFont(textFieldFont);
  }

  //---------------------------------------------------
  /** The size of the current font */

  public int getFontSize() {
    return(getFont().getSize());
  }

  /** Rather than setting the whole font, you can
   *  just set the size (Courier bold will be used
   *  for the family/face).
   */
  public void setFontSize(int size) {
    setFont(new Font("Courier", Font.BOLD, size));
  }
  
  //----------------------------------------------------
  /** Determines if the textfield is editable. If it
   *  is, you can enter a number to change the
   *  scrollbar value. In such a case, entering a value
   *  outside the legal range results in the min or
   *  max legal value. A non-integer is ignored.
   *
   * @see #setEditable
   */
  public boolean isEditable() {
    return(textfield.isEditable());
  }

  /** Determines if you can enter values directly
   *  into the textfield to change the scrollbar.
   *
   * @see #isEditable
   */
  public void setEditable(boolean editable) {
    textfield.setEditable(editable);
  }

  //----------------------------------------------------
  // Sets a right-aligned textfield number.
  
  private void setTextFieldValue() {
    int value = scrollbar.getValue();
    int digits = numDigits(scrollbar.getMaximum());
    String valueString = padString(value, digits);
    textfield.setText(valueString);
  }

  //----------------------------------------------------
  // Repeated String concatenation is expensive, but
  // this is only used to add a small amount of
  // padding, so converting to a StringBuffer would
  // not pay off.
  
  private String padString(int value, int digits) {
    String result = String.valueOf(value);
    for(int i=result.length(); i<digits; i++)
      result = " " + result;
    return(result + " ");
  }
  
  //----------------------------------------------------
  // Determines the number of digits in a decimal
  // number.
  
  private static final double LN10 = Math.log(10.0);
  
  private static int numDigits(int num) {
    return(1 + 
           (int)Math.floor(Math.log((double)num)/LN10));
  }

  //----------------------------------------------------
  // Since several implementations generate extraneous
  // scrollbar events, you shouldn't just check
  // the event target, but verify a correct
  // event type also. Used by handleEvent.
  
  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);
  }

  //----------------------------------------------------
  // KLUDGE ALERT!
  // Many Windows 95 Java implementations (including
  // most browsers and Sun's JDK through version 1.1.3)
  // fail when you drag the "thumb", often bouncing back
  // to their original location when the button is
  // released. Enforcing short pauses between the events
  // appears to solve the problem in many cases, but
  // it slows things down, the exact sleep amount
  // needed depends on the system speed, and there is
  // absolutely no guarantee that this will always work.
  // The only "real" solution is to get the vendors to
  // fix the implementations.
  
  private void fixWindowsProblem(int eventID) {
    if (eventID == Event.SCROLL_ABSOLUTE)
      pause(100);
  }

  private void pause(long millis) {
    try {
      Thread.sleep(millis);
    } catch(InterruptedException ie) {}
  }
  
  //----------------------------------------------------
  // KLUDGE ALERT!
  // In all or most Java 1.02 implementations (JDK,
  // Netscape 2, 3, 4, Internet Explorer 3.01),
  // the max value is the largest possible value that
  // can be set. But in JDK 1.1.1-1.1.3 on Unix and
  // Windows, you can only set values as big
  // as max-bubble (ie getMaximum() *minus*
  // getVisible()), despite what getMaximum()
  // returns. This adjusts for that, but of course
  // has the problem that a 1.1 implementation that
  // *was* consistent with 1.02 would no longer work.
  // Trying to fix bugs on a per-implementation basis
  // is a risky proposition indeed; this is not the
  // high point of Java's platform independence.
  
  private int adjustFor1_1(int max, int bubble) {
    String version = System.getProperty("java.version");
    if (version.startsWith("1.1"))
      return(max+bubble);
    else
      return(max);
  }

  //----------------------------------------------------
}

Listing 13.39 ScrollbarPanel.java

import java.awt.*;

/** A Panel with adjustable top/bottom insets value.
 *  Used to hold a Scrollbar in the Slider class
 */

public class ScrollbarPanel extends Panel {
  private Insets insets;

  public ScrollbarPanel(int margins) {
    setLayout(new BorderLayout());
    setMargins(margins);
  }

  public Insets insets() {
    return(insets);
  }

  public int getMargins() {
    return(insets.top);
  }
  
  public void setMargins(int margins) {
    this.insets = new Insets(margins, 0, margins, 0);
  }
}

Continue to Section 13.13 (Popup Menus). Return to Chapter 13 table of contents.