Chapter 13. Progress Bars, Sliders, and Scroll Bars

In this chapter:

  • Bounded-range components overview
  • Basic JScrollBar example
  • JSlider date chooser
  • JSliders in a JPEG image editor
  • JProgressBar in an FTP client application
13.1 Bounded-range components overview

JScrollBar, JSlider, and JProgressBar provide visualization and selection within a bounded interval, allowing the user to conveniently select a value from that interval, or simply observe its current state. In this section we'll give a brief overview of these components and the significant classes and interfaces that support them.

13.1.1 The BoundedRangeModel interface

abstract interface javax.swing.BoundedRangeModel

The BoundedRangeModel interface describes a data model used to define an integer value between minimum and maximum values. This value can have a subrange called an extent, which can be used to define the size of, for instance, a scrollbar "thumb." Often the extent changes dynamically corresponding to how much of the entire range of possible values is visible. Note that the value can never be set larger than the maximum or minimum values, and the extent always starts at the current value and never extends past the maximum. Another property called valueIsAdjusting declared and expected to be true when the value is in the state of being adjusted.

Implementations are expected to fire ChangeEvents when any of the minimim, maximum, value, extent, or valueIsAdjusting properties change state. Thus, BoundedRangeModel includes method decarations for adding and removing ChangeListeners: addChangeListener() and removeChangeListener(). This model is used by JProgressBar, JSlider, and JScrollBar.

 

UI Guideline : Why choose a bounded range component

The bounded range components are essentially analog devices in nature. They are good at providing relative, positional, approximate or changing (in time) data. They are also excellent at visually communicating the bounds or limits of a data selection and at communicating percentage of the whole through approximate visual means. Where you have several values which share the same bounds e.g. RGB values for a Color Chooser, then you can easily communicate relative values of the three choices through use of a bounded range component. The position of each component shows the relative value of one against the other.

So use bounded range components when there is advantage in communicating the range of values and an approximate, relative, position or changing value needs to be communicated to the User.

 

13.1.2 DefaultBoundedRangeModel

class javax.swing.DefaultBoundedRangeModel

DefaultBoundedRangeModel is the default concrete implementation of the BoundedRangeModel interface. The default constructor initializes a model with 0 for minimum, 100 for maximum, and 0 for the value and extent properties. Another constructor allows specification of each of these initial values as int parameters. As expected, this implementation does fire ChangeEvents whenever one of its properties changes.

13.1.3 JScrollBar

class javax.swing.JScrollBar

Scroll bars can be used to choose a new value from a specified interval by sliding a knob (often referred to as the thumb) between given maximum and minimum bounds, or by using small buttons at the ends of the component. The area not occupied by the thumb and buttons is known as the paging area, and this can also be used to change the current scrollbar value. The thumb represents the extent of this bounded-range component, and its value is stored in the visibleAmount property.

JScrollBar can be oriented horizontally or vertically, and its value increases to the right or upward respectively. To specify orientation, stored in the orientation property, we call the setOrientation() method and pass it one of the JScrollBar.HORIZONTAL or JScrollBar.VERTICAL constants.

Clicking on a button moves the thumb (and thus the value--recall that a bounded-range component’s value lies at the beginning of the extent) by the value of JScrollBar’s unitIncrement property. Similarly, clicking the paging area moves the thumb by the value of JScrollBar’s blockIncrement property.

 

Note: It is common to match the visibleAmount property with the blockIncrement property. This is a simple way to visually signify to the user how much of the available range of data is currently visible.

 

UI Guideline : Usage of a Scrollbar Background

The Scrollbar is really a computer enhanced development from an original analog mechanical idea. Scrollbars are in some respects more advanced than Sliders (see section 13.1.4). The Thumb of the Scrollbar can very cleverly be used to show the current data as a percentage of a whole (as described in the Note above). If the scrollbar is placed onto an image and the thumb is approximately 50% of the total size, then the User is given a clear indication that the viewing area is roughly half of the total. The ability for the Thumb in a scrollbar to change size to accurately reflect this, is something which could not have been achieved with a mechanical device. Scrollbar is in this respect a very good example of taking a metaphor based on a mechanical device and enhancing it to improve usability.

Choosing Position

By far the best use of a scrollbar is position selection. They are in nature analog and to the viewer are giving only an approximate position. Position selection is how they are used throughout Swing inside a JScrollPane. Users have become used to this usage and its very natural. For most other occasions where you wish to use a sliding control for selection, a Slider is probably best.

As expected, JScrollBar uses a DefaultBoundedRangeModel by default. In addition to the ChangeEvents fired by this model, JScrollPane fires PropertyChangeEvents when its orientation, unitIncrement, or blockIncrement properties change state. JScrollPane also fires AdjustmentEvents whenever any of its bound properties change, or when any of it’s model’s properties change (this is done solely for backward compa! tibility with the AWT scrollbar class). Accordingly, JScrollPane provides methods to add and remove AdjustmentListeners (we don’t need to provide methods for adding and removing PropertyChangeListeners because this functionality is inherited from JComponent).

 

Note: AdjustmentListeners receive AdjustmentEvents. Both are defined in java.awt.event--see API docs.

 

13.1.4 JSlider

class javax.swing.JSlider

Sliders can be used to choose a desirable numerical value from a specified interval by sliding a knob between given borders (using the mouse, arrow keys, or PageDown and PageUp). Sliders are very useful when we know in advance the range of input the user should be able to choose from.

JSlider supports horizontal and vertical orientations, and its orientation property can be set to one of JSlider.HORIZONTAL or JSlider.VERTICAL. The extent property specifies the number of values to skip forward/up or back/down when PageUp or PageDown is pressed, respectively. Tick marks can be used to denote value locations. Minor and major tick marks are supported, where major ticks are usually longer and spread farther apart than minor ticks. In the case where a major and minor tick fall on the same location, the major tick takes precedence and the minor tick will not be displayed. Spacing between minor tick marks is specified by the minorTickSpacing property, and spacing between major tick marks is specified by the majorTickSpacing property.

 

Note: The tick spacing properties specify the number of values to be skipped between each successive tick. Their names are somewhat misleading because they actually have nothing to do with the physical space (in pixels) between each tick. They would be more appropriately named "minorTickDisplayInterval" and "majorTickDisplayInterval. "

Setting either spacing property to 0 has a disabling effect, and the paintTicks property also provides a way of turning ticks on and off.

 

Note: The snapToTicks property is intended to only allow the slider knob to lie on a tick-marked value, however, this feature does not work as expected as of Java 2 FCS.

Major ticks can be annotated by components, and by default each of JSlider’s major ticks are adorned with JLabel’s denoting the integer tick value. We can turn on/off this functionality by changing the paintLabels property, and we can customize which components are used to annotate, and at what values they are placed, by passing a Dictionary of Integer/Component pairs to the setLabelTable() method. The createStandardLabels() method is used by default to set up JSlider with its JLabel’s at each major tick value. This method returns a Hashtable (sub-class of Dictionary) which can then be assigned to JSlider using setLabelTable().

By default JSlider’s values increment from left-to-right or bottom-to-top depending on whether horizontal or vertical orientation is used respectively. To reverse the direction of incrementation we can set the inverted property to true.

 

UI Guideline : Usage of a Slider

By origin a slider is an analog device. Sliders are really a close graphical and behavioural representation of a real world analog slider from for example, a hi-fi system or an older TV volume control. As such sliders are analog devices and are designed for use in determining an approximate or positional setting for something. Usually they rely on direct user feedback to help with the choice of position. With the TV volume control example, the volume would go up and down as the slider is moved and the User would stop moving it when the volume was at a comfortable level.

The Swing version of a slider is actually a digital device disguised as an analog one. Each tick of the slider is a digital increment. The slider can therefore be used to determine an accurate value providing some additional digital feedback is given for the User such as a numeric display of the absolute value or a scale along the side of the slider. Where accurate values are important, such as with a colour chooser, be sure to provide an absolute value as output along side the slider.

Feedback
Immediate feedback is important with sliders due to the analog nature of the device. Provide actual feedback such as the brightness of a picture which increases or decreases as the slider is moved or provide an absolute numeric value readout which can be observed to change as the slider is moved. Therefore, judicious use of the Change Event with a ChangeListener is important so that the feedback mechanism can be updated e.g. brightness or contrast in an image.

Movement
The two default orientations of Slider are conventions which date back to original analog electronic devices. When vertical, the down position is lower and you move it up to increase in value. When horizontal, left is lower and you move it right to increase in value. Users are likely to be very familiar with this convention. If you wish to switch it, then you should have a very very good reason for doing. We wouldn't recommend it!

Slider vs. Scrollbar

On the whole, use a Slider for choosing a value when the value needed is approximate and subjective such as color, volume, brightness and requires User feedback to make the subjective judgement. Use a scroll bar for positional choice, where the desired position is again approximate and judged relative to the whole.

The paintTrack property specifies whether the whole slider track is filled in or not. The Metal L&F UI delegate for JSlider pays attention to client property with key "JSlider.isFilled" and Boolean value. Adding this property to a JSlider’s client properties hashtable (using putClientProperty()--see chapter 2) with a value of Boolean.TRUE, will fill in only the lower half of the slider track from the position of the knob. Note that this client property will have no effect if the paintTrack property is s! et to true, and will only work if the slider is using the Metal L&F UI delegate.

As expected, JSlider uses a DefaultBoundedRangeModel by default. In addition to the ChangeEvents fired by this model, JSlider fires PropertyChangeEvents when any of its properties desribed above change state. Unlike JScrollBar, this class provides the ability to add and remove ChangeListeners directly, vs. AdjustmentListeners.

13.1.5 JProgressBar

class javax.swing.JProgressBar

Progress bars can be used to display how far or close a given numerical value is from the bounds of a specified interval. They are typically used to indicate progress during a certain lengthy job to provide simple feedback to the user showing that the job being monitored is alive and active. As with JScrollBar and JSlider, JProgressBar can be oriented horizontally or vertically. Note also that JProgressBar acts identically to JSlider with respect to incrementing: left-to-right in horizontal orientation, bottom-to-top in vertical orientation.

A JProgressBar is painted filled from the minimum value to its current value (with the exception of the Windows L&F, which paints a series of small rectangles). A percentage representing how much of a job has been completed can optionally by displayed in the center of JProgressBar. The string property represents the String to be painted (usually of the form XX%, where X is a digit), stringPainted specifies whether or not string should be painted, and percentComplete is a double between 0 and 1 specifying how much! of the job has been completed so far.

 

Note: We normally do not need to take control of this rendering functionality, because by setting the string property to null, and the stringPainted property to true, the percentComplete property is convereted to the XX% form for us, and displayed in the progress bar.

JProgressBar’s foreground and background can be assigned as any JComponent, however, the color used to render its status text is not directly modifiable. Instead this is handled by the UI delegate, and the easiest way to assign specific colors is to replace the appropriate UI resources in UIManager’s defaults table (see chapter 21 for more about L&F customization).

The borderPainted property (defaults to true) specifies whether or not a border is rendered around JProgressBar. As expected, JProgressBar uses a DefaultBoundedRangeModel by default, and ChangeListeners can be added to receive ChangeEvents when any of JProgressBar’s properties change state.

During a monitored operation we simply call setValue() on a JProgressBar and all updating is taken care of for us. Note that we must be careful to make this call in the event dispatching thread. Consider the following basic example, figure 13.1 illustrates:

Figure 13.1 A basic JProgressBar example showing custom colors and proper updating.

The Code: JProgressBarDemo.java

see \Chapter13\1

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

import javax.swing.*;
import javax.swing.event.*;

public class JProgressBarDemo 
  extends JFrame
{
  protected int m_min = 0;
  protected int m_max = 100;
  protected int m_counter = 0;
  protected JProgressBar jpb;

  public JProgressBarDemo() 
  {
    super("JProgressBar Demo");
    setSize(300,50);

    UIManager.put("ProgressBar.selectionBackground", Color.black);
    UIManager.put("ProgressBar.selectionForeground", Color.white);
    UIManager.put("ProgressBar.foreground", new Color(8,32,128));

    jpb = new JProgressBar();
    jpb.setMinimum(m_min);
    jpb.setMaximum(m_max);
    jpb.setStringPainted(true);

    JButton start = new JButton("Start");
    start.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        Thread runner = new Thread() {
          public void run() {
            m_counter = m_min;
            while (m_counter <= m_max) {
              Runnable runme = new Runnable() {
                public void run() {
                  jpb.setValue(m_counter);
                }
              };
              SwingUtilities.invokeLater(runme);
              m_counter++;
              try { 
                Thread.sleep(100); 
              } 
              catch (Exception ex) {}
            }
          }
        };
        runner.start();
      }
    });
        
    getContentPane().add(jpb, BorderLayout.CENTER);
    getContentPane().add(start, BorderLayout.WEST);

    WindowListener wndCloser = new WindowAdapter()
    {
      public void windowClosing(WindowEvent e) 
      {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
    setVisible(true);
  }

  public static void main(String[] args) 
  { 
    new JProgressBarDemo();
  }
}

 

Note: The JProgressBar UI delegate centers the progress text horizontally and vertically. However, its centering scheme enforces a certain amount of white space around the text and has undesirable effects when using thin progress bars. In order to fix this we can override BasicProgressBarUI’s getStringPlacement() method (see API docs and BasicProgressBarUI.java source) to return the desired Point location where the text should be rendered.

 

UI Guideline : Usage of Progress Bar Long Operations

Progress Bars are commonly used as a fill in for operations which will take a long time. A long time in human interaction is often defined as around 1 second or longer. The progress bar is usually rendered inside a JOptionPane.

Special attention will need to be paid to the business logic code so that it is capable of notifying a Progress Bar of the progress of an operation.

Progress Bars are inherintly analog in nature. Analog data is particularly good for displaying change and for relative comparison. It is not good for exact measurement. The analog nature of a Progress Bar means that it is good for showing that something is happening and that progress is taking place. However, it is not good for giving an exact measure of completeness. If your problem domain requires the ability to measure exactly how complete a task is then you may to to supplement the progress bar with a digital reading of progress. This is common with Internet download dialogs and option panes.

A digital readout is particularly useful when the task to be completed will take a very long time. The progress bar may only be giving you a granularity of around 3% for each graphic. If it takes significantly long to progress by such a jump, say greater than 5 seconds, then the digital readout will give you a finer grained reading at 1% and will change approximately 3 times faster than your progress bar. The combination of the two helps to pass the time for the viewer and gives them the reassurance that something is happening whilst also giving them a very accurate view of progress. That is why the dual combination of digital and analog progress is popular with Internet download dialogs, as the task can be very long and cannot be determined by the application developer.

 

13.1.6 ProgressMonitor

class javax.swing.ProgressMonitor

The ProgressMonitor class is a convenient and intelligent means of deploying a dynamic progress bar in an application that performs time-consuming operations. This class is a direct sub-class of Object (thus it does not exist in the component hierarchy).

ProgressMonitor displays a JDialog containing a JOptionPane-style component. The note property represents a String that can change during the course of an operation and is displayed in a JLabel above the JProgressBar (if null is used this label is not displayed).

Two buttons, "OK" and "Cancel," are placed at the bottom and serve to dismiss the dialog and abort the operation respectively. The "OK " button simply hides the dialog. The "Cancel" button also hides the dialog, and sets the canceled property true, providing us with a way to test whether the user has canceled the operation or not. Since most time-consuming operations occur in loops, we can test this property during each iteration and abort if necessary.

The millisToDecideToPopup property is an int value specifying the number of milliseconds to wait before ProgressMonitor should determine whether to pop up a dialog (defaults to 500). This is used to allow a certain amount of time to pass before questioning whether the job is long enough to warrant popping up a dialog. The millisToPopup property is an int value specifying the minimum time a job must take in order to warrant popping up a dialog (defaults to 2000). If ProgressMonitor determines that the job will take less than millisToPopup milliseconds the dialog will not be shown.

The progress property is an int value specifying the current value of the JProgressBar. During an operation we are expected to update the note and progress in the event-dispatching thread (as demonstrated in the example above).

 

Warning: In light of theses properties we should only use a ProgressMonitor for simple, predictable jobs. ProgressMonitor bases the estimated time to completion on the value of its JProgressBar from start time to current evaluation time and assumes that a constant rate of progression will exist throughout the whole job. For transfering a single file this may be a fairly valid assumption. However, the rate of progress is highly dependant on how the job is constructed. If

 

Note: ProgressMonitor does not currently give us access to its JProgressBar component. We hope that in future implementations this will be accounted for, as this makes customization more difficult.

13.1.6 ProgressMonitorInputStream

class javax.swing.ProgressMonitorInputStream

This class extends java.io.FilterInputStream and contains a ProgressMonitor. When used in place of an InputStream, this class provides a very simple means of displaying job progress. This InputStream’s overloaded read() methods read data and update the ProgressMonitor at the same time. We can access ProgressMonitorInputStream’s ProgressMonitor with getProgressMonitor() but we cannot assign it a new one. (See the API docs for more about InputStreams.)

13.2 Basic JScrollBar example

The JScrollBar component is most often seen as part of a JScrollPane. We rarely use this component alone, unless customized scrolling is desired. In this section we'll show how to use JScrollBar to create a simple custom scrolling pane from scratch.

Figure 13.2 Running ScrollDemo example showing an image in the custom scroll pane.

The Code: ScrollDemo.java

see \Chapter13\2

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

import javax.swing.*;
import javax.swing.event.*;

public class ScrollDemo extends JFrame
{
  public ScrollDemo() {
    super("JScrollBar Demo");
    setSize(300,250);

    ImageIcon ii = new ImageIcon("earth.jpg");
    CustomScrollPane sp = new CustomScrollPane(new JLabel(ii));
    getContentPane().add(sp);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
    setVisible(true);
  }

  public static void main(String[] args) { 
    new ScrollDemo();
  }
}

class CustomScrollPane extends JPanel
{
  protected JScrollBar m_vertSB;
  protected JScrollBar m_horzSB;
  protected CustomViewport m_viewport;
  protected JComponent m_comp;

  public CustomScrollPane(JComponent comp) {
    setLayout(null);
    m_viewport = new CustomViewport();
    m_viewport.setLayout(null);
    add(m_viewport);
    m_comp = comp;
    m_viewport.add(m_comp);

    m_vertSB = new JScrollBar(
      JScrollBar.VERTICAL, 0, 0, 0, 0);
    m_vertSB.setUnitIncrement(5);
    add(m_vertSB);

    m_horzSB = new JScrollBar(
      JScrollBar.HORIZONTAL, 0, 0, 0, 0);
    m_horzSB.setUnitIncrement(5);
    add(m_horzSB);

    AdjustmentListener lst = new AdjustmentListener() {
      public void adjustmentValueChanged(AdjustmentEvent e) {
        m_viewport.doLayout();
      }
    };
    m_vertSB.addAdjustmentListener(lst);
    m_horzSB.addAdjustmentListener(lst);
  }

  public void doLayout() {
    Dimension d = getSize();
    Dimension d0 = m_comp.getPreferredSize();
    Dimension d1 = m_vertSB.getPreferredSize();
    Dimension d2 = m_horzSB.getPreferredSize();

    int w = Math.max(d.width - d1.width-1, 0);
    int h = Math.max(d.height - d2.height-1, 0);
    m_viewport.setBounds(0, 0, w, h);
    m_vertSB.setBounds(w+1, 0, d1.width, h);
    m_horzSB.setBounds(0, h+1, w, d2.height);

    int xs = Math.max(d0.width - w, 0);
    m_horzSB.setMaximum(xs);
    m_horzSB.setBlockIncrement(xs/5);
    m_horzSB.setEnabled(xs > 0);

    int ys = Math.max(d0.height - h, 0);
    m_vertSB.setMaximum(ys);
    m_vertSB.setBlockIncrement(ys/5);
    m_vertSB.setEnabled(ys > 0);

    m_horzSB.setVisibleAmount(m_horzSB.getBlockIncrement());
    m_vertSB.setVisibleAmount(m_vertSB.getBlockIncrement());
  }

  public Dimension getPreferredSize() {
    Dimension d0 = m_comp.getPreferredSize();
    Dimension d1 = m_vertSB.getPreferredSize();
    Dimension d2 = m_horzSB.getPreferredSize();
    Dimension d = new Dimension(d0.width+d1.width, 
      d0.height+d2.height);
    return d;
  }

  class CustomViewport extends JPanel 
  {
    public void doLayout() {
      Dimension d0 = m_comp.getPreferredSize();
      int x = m_horzSB.getValue();
      int y = m_vertSB.getValue();
      m_comp.setBounds(-x, -y, d0.width, d0.height);
    }
  }
}

Understanding the Code

Class ScrollDemo

This simple frame-based class creates a CustomScrollPane instance to scroll a large image. This class is very similar to the first example in the chapter 7 and does not require additional explanation.

Class CustomScrollPane

This class extends JPanel to represent a simple custom scroll pane. Four instance variables are declared:

JScrollBar m_vertSB: vertical scroll bar.

JScrollBar m_horzSB: horizontal scroll bar.

CustomViewport m_viewport: custom viewport component.

JComponent m_comp: component to be placed in our custom viewport.

The CustomScrollPane constructor takes a component to be scrolled as parameter. It instantiates the instance variables described above and adds them to itself using a null layout (because this component acts as its own layout manager). Note that the JScrollBars are created with proper orientation and zero values accross the board (because these are meaningless if not based on the size of the component being scrolled).

An AdjustmentListener is created and added to both scroll bars. The adjustmentValueChanged() method calls the doLayout() method on the m_viewport component to perform the actual component scrolling according to the new scroll bars values.

The doLayout() method sets the bounds for the viewport (in the center), vertical scroll bar (on the right), and horizontal scroll bar (on the bottom). New maximum values and block increment values are set for the scroll bars based on the sizes of the scrolling pane and component to be scrolled. Note that if the maximum value reaches zero, the corresponding scroll bar is disabled. The visibleAmount property of each is set to the corresponding blockIncrement value to provide proportional thumb sizes.

The getPreferredSize() method simply calculates the preferred size of this component based on the preferred sizes of it's children.

Class CustomViewport

This class extends JPanel and represents a simple realization of a viewport for our custom scrolling pane. The only implemented method, doLayout(), reads the current scroll bar values and assigns bounds to the scrolling component accordingly.

Running the Code

Figure 13.2 shows an image in the custom scroll pane. Use the horizontal and vertical scroll bars to verify that scrolling works as expected. Resize the frame component to verify that the scroll bar values and thumbs are adjusted correctly as the container's size is changed.  

13.3 JSlider date chooser

In this example we’ll show how three JSliders can be combined to allow date selection. We will also address some resizing issues and show how to dynamically change JSlider’s annotation components and tick spacing based on size constraints.

 

Note: While month and day are limited values, year is not. We can use a JSlider to select year only if we define a limited range of years to choose from. In this example we bound the year slider value between 1990 and 2010.

Figure 13.3 JSliders with dynamically changable bound values, tick spacing, and annotation components.  

UI Guideline :

Feedback in Readable form

Using Sliders to pick the values for a date may be an interesting method for data input, however, it does not lend itself to reading and clear output communication. This is fixed by the use of the clearly human readable form at the top of the dialog. This directly follows the advice that Sliders should be used with immediate visual feedback.

Visual Noise

Visual noise or clutter is avoided through the spacing of annotations and the avoiding the temptation to annotate each day and each year. The change in rendering as the device is resized to smaller is also a clear example of how extra coding and the adoption of an advanced technique can aid visual communication and usability.

Figure 13.4 JSliders showing altered maximum bound and annotation labels.

The Code: DateSlider.java

see \Chapter13\3

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.text.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

public class DateSlider extends JFrame
{
  public final static Dimension RIGID_DIMENSION = 
    new Dimension(1,3);

  protected JLabel  m_lbDate;
  protected JSlider m_slYear;
  protected JSlider m_slMonth;
  protected JSlider m_slDay;
  protected Hashtable m_labels;
  protected GregorianCalendar m_calendar;
  protected SimpleDateFormat m_dateFormat;

  public DateSlider() {
    super("Date Slider");
    setSize(500, 340);

    m_calendar = new GregorianCalendar();
    Date currDate = new Date();
    m_calendar.setTime(currDate);
    m_dateFormat = new SimpleDateFormat("EEE, MMM d, yyyyy");

    JPanel p1 = new JPanel();
    p1.setLayout(new GridLayout(4, 1));

    JPanel p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(),
      "Selected Date"));
    m_lbDate = new JLabel(
      m_dateFormat.format(currDate) + "     ");
    m_lbDate.setFont(new Font("Arial",Font.BOLD,24));
    p.add(m_lbDate);
    p1.add(p);

    m_slYear = new JSlider(JSlider.HORIZONTAL, 1990, 2010,
      m_calendar.get(Calendar.YEAR));
    m_slYear.setPaintLabels(true);
    m_slYear.setMajorTickSpacing(5);
    m_slYear.setMinorTickSpacing(1);
    m_slYear.setPaintTicks(true);
    DateListener lst = new DateListener();
    m_slYear.addChangeListener(lst);     

    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), "Year"));
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p.add(m_slYear);
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p1.add(p);

    m_slMonth = new JSlider(JSlider.HORIZONTAL, 1, 12, 
      m_calendar.get(Calendar.MONTH)+1);
    String[] months = 
      (new DateFormatSymbols()).getShortMonths();
    m_labels = new Hashtable(12);     
    for (int k=0; k<12; k++)
      m_labels.put(new Integer(k+1), new JLabel(
        months[k], JLabel.CENTER ));
    m_slMonth.setLabelTable(m_labels);
    m_slMonth.setPaintLabels(true);
    m_slMonth.setMajorTickSpacing(1);
    m_slMonth.setPaintTicks(true);
    m_slMonth.addChangeListener(lst);

    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), "Month"));
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p.add(m_slMonth);
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p1.add(p);

    int maxDays = m_calendar.getActualMaximum(
      Calendar.DAY_OF_MONTH);
    m_slDay = new JSlider(JSlider.HORIZONTAL, 1, maxDays, 
      m_calendar.get(Calendar.DAY_OF_MONTH));
    m_slDay.setPaintLabels(true);
    m_slDay.setMajorTickSpacing(5);
    m_slDay.setMinorTickSpacing(1);
    m_slDay.setPaintTicks(true);
    m_slDay.addChangeListener(lst);

    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), "Day"));
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p.add(m_slDay);
    p.add(Box.createRigidArea(RIGID_DIMENSION));
    p1.add(p);

    getContentPane().add(p1, BorderLayout.CENTER);

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
      
    enableEvents(ComponentEvent.COMPONENT_RESIZED);
    setVisible(true);
  }

  protected void processComponentEvent(ComponentEvent e) {
    if (e.getID() == ComponentEvent.COMPONENT_RESIZED) {
      int w = getSize().width;

      m_slYear.setLabelTable(null);
      if (w > 200)
        m_slYear.setMajorTickSpacing(5);
      else
        m_slYear.setMajorTickSpacing(10);
      m_slYear.setPaintLabels(w > 100);

      m_slMonth.setLabelTable(w > 300 ? m_labels : null);
      if (w <= 300 && w >=200)
        m_slMonth.setMajorTickSpacing(1);
      else
        m_slMonth.setMajorTickSpacing(2);
      m_slMonth.setPaintLabels(w > 100);

      m_slDay.setLabelTable(null);
      if (w > 200)
        m_slDay.setMajorTickSpacing(5);
      else
        m_slDay.setMajorTickSpacing(10);
      m_slDay.setPaintLabels(w > 100);
    }
  }

  public void showDate() {
    m_calendar.set(m_slYear.getValue(),
    m_slMonth.getValue()-1, 1);
    int maxDays = m_calendar.getActualMaximum(
      Calendar.DAY_OF_MONTH);

    if (m_slDay.getMaximum() != maxDays) {
      m_slDay.setValue(
        Math.min(m_slDay.getValue(), maxDays));
      m_slDay.setMaximum(maxDays);
      m_slDay.repaint();
    }
        
    m_calendar.set(
      m_slYear.getValue(), m_slMonth.getValue()-1,
      m_slDay.getValue());
    Date date = m_calendar.getTime();
    m_lbDate.setText(m_dateFormat.format(date));
  }

  class DateListener implements ChangeListener
  {
    public void stateChanged(ChangeEvent e) {
      showDate();
    }
  }

  public static void main(String argv[]) {
    new DateSlider();
  }
}

Understanding the Code

Class DateSlider

DateSlider extends JFrame and declares seven instance variables and one class constant. Class constant:

Dimension RIGID_DIMENSION: used to create rigid areas above and below each slider.

Instance variables:

JLabel m_lbDate: label to display the selected date.

JSlider m_slYear: slider to select year.

JSlider m_slMonth: slider to select month.

JSlider m_slDay: slider to select day.

Hashtable m_labels: collection of labels to denote months by short names rather than numbers.

GregorianCalendar m_calendar: calendar to perform date manipulations.

SimpleDateFormat m_dateFormat: object to format the date as a string.

The DateSlider constructor initializes the m_calendar instance defined above, and date format m_dateFormat. A JPanel with a GridLayout of one column and four rows is used as a base panel, p1. JLabel m_lbDate using a large font is created, embedded in a JPanel with a simple TitledBorder, and placed in the first row.

The m_slYear slider is created and placed in the second row. This is used to select the year from the interval 1990 to 2010. Note that it takes its initial value from the current date. A number of settings are applied to m_slYear. The paintLabels and paintTicks properties are set to true to allow drawing ticks and labels, majorTickSpacing is set to 5 to draw majors ticks for every fifth value, and minorTickSpacing is set to 1 to draw minor ticks for every value. Finally a new DateListener instanc! e (see below) is added as a ChangeListener to monitor changes to this slider’s properties. Note that m_slYear is placed in a JPanel surrounded by a TitledBorder. Two rigid areas are added to ensure vertical spacing between our slider and this parent panel (see chapter 4 for more about Box and its invisible Filler components).

The m_slMonth slider is created and placed in the third row. This slider is used to select the month from the interval 1 to 12. This component is constructed similar to m_slYear, but receives a Hashtable of JLabels to denote months by short names rather than numbers. These names are taken from an instance of the DateFormatSymbols class (see API docs) and used to create pairs in a local m_labels Hashtable in the form: Integer representing slider value (from 1 to 12) as key, and JLabe! l with the proper text as value. Finally, the setLabelTable() method is invoked to assign these custom labels to the slider.

The m_slDay slider is created and placed in the fourth row. It is used to select the day of the month from an interval which dynamically changes depending on the current month and, for February, the year. Aside from this difference, m_slDay is constructed very similar to m_slYear.

Because a slider’s tick annotation components may overlap each other and become unreadable if not enough space provided, it is up to us to account for this possibility. This becomes a more significant problem when (as in this example) slider components can resized by simply resizing the parent frame. To work around this problem we can simply enforce a certain frame size, however, this may not be desirable in all situations. If we are ever in such a situation we need to change our slider’s properties dynamically depending on its size. For this reason the processComponentEvent() method is overridden to process resizing events that occur on the parent frame. Processing of these events is enabled in the DateSlider constructor with the enableEvents() method.

The processComponentEvent() method only responds to ComponentEvents with ID COMPONENT_RESIZED. For each of our three sliders this method changes the majorTickSpacing property based on the container's width. m_slDay and m_slYear receive a spacing of 5 if the width if greater than 200, and 10 otherwise. m_slMonth receives a majorTickSpacing of 1 if the conatiner’s width is anywhere from 200 to 300, and 2 otherwise. If this width is greater than 300 our custom set of labels is used to annotate m_slMonth’s major ticks. The default numerical labels are used otherwise. For each slider, if the width is less than 100 the paintLabels property is set to false, which disables all annotations. Otherwise paintLabels is set to true.

Our custom showDate() method is used to retrieve values from our sliders and display them in m_lbDate as the new selected date. First we determine the maximum number of days for the selected month by passing m_calendar a year, a month, and 1 as the day. Then, if necessary, we reset m_slDay‘s current and maximum values. Finally, we pass m_calendar a year, month, and the selected (possibly adjusted) day, retrieve a Date instance corresponding to these values, and invoke format() to retrieve a textual representation of the date.

 

Note: Java 2 does not really provide a direct way to convert a year, month, and day triplet into a Date instance (this functionality has been deprecated). We need to use Calendar.set() and Calendar.getTime() for this. Be aware that the day parameter is not checked against the maximum value for the selected month. Setting the day to 30 when the month is set to February will be silently treated as March, 2.

 

Class DateSlider.DateListener

The DateListener inner class implements the ChangeListener interface and is used to listen for changes in each of our sliders’ properties. Its stateChanged() method simply calls the showDate() method described above.

Running the Code

Note how the date is selected and displayed, and the range of the "Day" slider is adjusted when a new month is selected. Figure 13.3 shows selection of February 29th 2000, demonstrating that this is a leap year.

 

Note: A leap year is a year evenly divisible by four, but not evenly divisible by 100. The first rule takes precedence, so years evenly divisible by 400 are leap years (2000 is a leap year, while 1900 is not).

Now try resizing the application frame to see how the slider annotations and ticks change to their more compact variants as the available space shrinks. Figure 13.4 illustrates.  

UI Guideline :

Exact value selection

Although Sliders are best used for selection when an exact value is not needed, this example gets around it by providing an adequate gap between ticks, making an exact choice easy to achieve.

The use of a Slider for Year is an unusual choice, as Year is not normally a bounded input. However, in certain domains it may be a more suitable choice. You may for example know the limits of available years e.g. years on which an Olympic games was held. The tick value would be 4 and the bound would be from the first games in 1896 to the next in 2000. Once Year and Month have been displayed using Sliders it is visually attractive and consistent to use a Slider for Day. There may be some debate about doing so as the bound will change depending on the month selected. However, it is fair to argue that the changing bound on Day, as Month is selected gives a clear, instant, visual feedback of how many days are in the month, which meets with the criteria of providing instant feedback when using a Slider.

 

13.4 JSliders in a JPEG image editor

Java 2 ships with a special package, com.sun.image.codec.jpeg, providing a set of classes and interfaces for working with JPEG images (created at least in part by Eastman Kodak Company). Although this package is not a part of Swing, it can be very useful in Swing-based applications. By reducing image quality (which is actually a result of compression), required storage space can be decreased. Using reduced quality JPEGs in web pages increases response time (by decreasing download time), and our editor application developed here allows us to load an existing JPEG, modify its quality, and then save the result. JSliders are used for the main editing components.

 

Note: JPEG stands for Joint Photographic Experts Group. It is a popular graphical format allowing compression of images up to 10 or 20 times.

Before deciding to use functionality in this package, you should know that, even though this package is shipped with Java 2, "... the classes in the com.sun.image.codec.jpeg package are not part of the core Java APIs. They are a part of Sun's JDK and JRE distributions. Although other licensees may choose to distribute these classes, developers cannot depend on their availability in non-Sun implementations. We expect that equivalent functionality will eventually be available in a core API or standard extension."

13.4.1 The JPEGDecodeParam

abstract interface com.sun.image.codec.jpeg

abstract interface com.sun.image.codec.jpeg.JPEGDecodeParam

This interface encapsulates parameters used to control the decoding of a JPEG image. It provides a rich set of getXX() and isXX() accessor methods. Instances contain information about how to decode a JPEG inpout stream, and are created automatically by JPEGImageDecoder (see below) if none is specified when an image is decoded. A JPEGImageDecoder’s associated JPEGDecoderParam can be obtained with its getJPEGDecodeParam() method.

13.4.2 The JPEGEncodeParam interface

abstract interface com.sun.image.codec.jpeg.JPEGEncodeParam

This interface encapsulates parameters used to control the encoding of a JPEG image stream. It provides a rich set of getXX() and setXX() accessor methods. Instances contain information about how to encode a JPEG to an output stream, and a default instance will be created atomatically by JPEGImageEncoder (see below) if none is specified when an image is encoded. A JPEGImageEncoder’s associated JPEGEncodeParam can be obtained with its getJPEGEncodeParam() method, or one of its overriden getDefaultJPEGEncodeParam() methods.

Particularly relevant to this example are JPEGEncodeParam’s xDensity, yDensity, and quality properties, which all can be assigned using typical setXX() methods. xDensity and yDensity represent horizontal and vertical pixel density, which depends on JPEGEncoderParam’s current pixel density setting. The pixel density setting is controlled with JPEGEncodeParam’s setDensityUnit() method and can be, for instance! , DENSITY_UNIT_DOTS_INCH, which means pixel density will be interpreted as pixels per inch. The quality property is specified as a float within the range 0.0 to 1.0, where 1.0 means perfect quality. In general: 0.75 means high quality, 0.5 means medium quality, and 0.25 means low quality.

13.4.3 The JPEGImageDecoder interface

abstract interface com.sun.image.codec.jpeg.JPEGImageDecoder

This interface describes an object used to decode a JPEG data stream into an image. We invoke method decodeAsBufferedImage() to perform the actual decoding into a BufferedImage instance, or decodeAsRaster() to perform decoding into a Raster instance. An instance of this interface can be obtained with one of the JPEGCodec.createJPEGDecoder() methods, which takes the delivering data InputStream as parameter. JPEGImageDecoder performs decoding according to its associated JPEGDecodeParam, and a ! default instance will br provided for is we do not specify one.

13.4.4 The JPEGImageEncoder interface

abstract interface com.sun.image.codec.jpeg.JPEGImageEncoder

This interface describes an object used to encode an image into a JPEG data stream. We invoke the overloaded encode() method to perform the actual encoding. Instances of this interface can be obtained with one of the JPEGCodec.createJPEGEncoder() methods, which takes an OutputStream to output data to as parameter. JPEGImageEncoder performs encoding according to its associated JPEGImageEncoder, and a default instance will br provided for is we do not specify one.

13.4.5 JPEGCodec

class com.sun.image.codec.jpeg.JPEGCodec

This class contains a collection of static methods used to create JPEG encoders and decoders. Particularly useful are the overloaded createJPEGDecoder() and createJPEGEncoder() methods which take an InputStream and OutputStream, respectively, as parameter (along with an optional JPEGDecodeParam or JPEGEncodeParam instance).

Figure 13.6 JPEGEditor showing a high-quality image of Earth (using JSliders with "isFilled client property).

Figure 13.7 JPEGEditor showing a reduced quality image of Earth.

The Code: JPEGEditor.java

see \Chapter13\4

import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.util.*;
import java.io.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.filechooser.*;

import com.sun.image.codec.jpeg.*;

public class JPEGEditor extends JFrame 
{
  public final static Dimension VERTICAL_RIGID_SIZE
    = new Dimension(1,3);
  public final static Dimension HORIZONTAL_RIGID_SIZE
    = new Dimension(3,1);

  protected File m_currentDir = new File(".");
  protected File m_currentFile = null;

  protected JFileChooser m_chooser;
  protected JPEGPanel m_panel;
  protected JSlider m_slHorzDensity;
  protected JSlider m_slVertDensity;
  protected JSlider m_slQuality;

  protected BufferedImage m_bi1, m_bi2;

  public JPEGEditor() {
    super("JPEG Editor");
    setSize(600, 400);

    m_chooser = new JFileChooser(); 
    SimpleFilter filter = new SimpleFilter("jpg", 
      "JPEG Image Files");
    m_chooser.setFileFilter(filter);
    m_chooser.setCurrentDirectory(m_currentDir);

    m_panel = new JPEGPanel();
    JScrollPane ps = new JScrollPane(m_panel,
      JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
      JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
    getContentPane().add(ps, BorderLayout.CENTER);

    JPanel p, p1;

    m_slVertDensity = new JSlider(JSlider.VERTICAL, 
      100, 500, 300);
    m_slVertDensity.setExtent(50);
    m_slVertDensity.setPaintLabels(true);
    m_slVertDensity.setMajorTickSpacing(100);
    m_slVertDensity.setMinorTickSpacing(50);
    m_slVertDensity.setPaintTicks(true);
    m_slVertDensity.putClientProperty(
      "JSlider.isFilled", Boolean.TRUE);
        
    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), 
      "Vert. dens."));
    p.add(Box.createRigidArea(HORIZONTAL_RIGID_SIZE));
    p.add(m_slVertDensity);
    p.add(Box.createRigidArea(HORIZONTAL_RIGID_SIZE));
    getContentPane().add(p, BorderLayout.EAST);

    m_slHorzDensity = new JSlider(JSlider.HORIZONTAL, 
      100, 500, 300);
    m_slHorzDensity.setExtent(50);
    m_slHorzDensity.setPaintLabels(true);
    m_slHorzDensity.setMajorTickSpacing(100);
    m_slHorzDensity.setMinorTickSpacing(50);
    m_slHorzDensity.setPaintTicks(true);
    m_slHorzDensity.putClientProperty(
      "JSlider.isFilled", Boolean.TRUE);

    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), 
      "Horizontal density"));
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(Box.createRigidArea(VERTICAL_RIGID_SIZE));
    p.add(m_slHorzDensity);
    p.add(Box.createRigidArea(VERTICAL_RIGID_SIZE));
    p1 = new JPanel();
    p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS));
    p1.add(p);

    m_slQuality = new JSlider(JSlider.HORIZONTAL, 
      0, 100, 100);
    Hashtable labels = new Hashtable(6);
    for (float q = 0; q <= 1.0; q += 0.2)
      labels.put(new Integer((int)(q*100)), 
      new JLabel("" + q, JLabel.CENTER ));
    m_slQuality.setLabelTable(labels);
    m_slQuality.setExtent(10);
    m_slQuality.setPaintLabels(true);
    m_slQuality.setMinorTickSpacing(10);
    m_slQuality.setPaintTicks(true);
    m_slQuality.putClientProperty(
      "JSlider.isFilled", Boolean.TRUE);

    p = new JPanel();
    p.setBorder(new TitledBorder(new EtchedBorder(), 
      "Quality"));
    p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
    p.add(Box.createRigidArea(VERTICAL_RIGID_SIZE));
    p.add(m_slQuality);
    p.add(Box.createRigidArea(VERTICAL_RIGID_SIZE));
    p1.add(p);

    JButton btApply = new JButton("Apply");
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        apply();
      }
    };
    btApply.addActionListener(lst);
    p1.add(btApply);

    JButton btReset = new JButton("Reset");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        reset();
      }
    };
    btReset.addActionListener(lst);
    p1.add(btReset);
    getContentPane().add(p1, BorderLayout.SOUTH);

    setJMenuBar(createMenuBar());

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
        
    setVisible(true);
  }

  protected JMenuBar createMenuBar() {
    JMenuBar menuBar = new JMenuBar();
      
    JMenu mFile = new JMenu("File");
    mFile.setMnemonic('f');

    JMenuItem mItem = new JMenuItem("Open...");
    mItem.setMnemonic('o');
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {  
        if (m_chooser.showOpenDialog(JPEGEditor.this) != 
          JFileChooser.APPROVE_OPTION)
            return;
        m_currentDir = m_chooser.getCurrentDirectory();
        File fChoosen = m_chooser.getSelectedFile();
        openFile(fChoosen);
      }
    };
    mItem.addActionListener(lst);
    mFile.add(mItem);

    mItem = new JMenuItem("Save");
    mItem.setMnemonic('s');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        saveFile(m_currentFile);
      }
    };
    mItem.addActionListener(lst);
    mFile.add(mItem);

    mItem = new JMenuItem("Save As...");
    mItem.setMnemonic('a');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) { 
        m_chooser.setSelectedFile(m_currentFile);
        if (m_chooser.showSaveDialog(JPEGEditor.this) != 
          JFileChooser.APPROVE_OPTION)
        return;
        m_currentDir = m_chooser.getCurrentDirectory();
        File fChoosen = m_chooser.getSelectedFile();
        if (fChoosen!=null && fChoosen.exists()) {
          String message = "File " + fChoosen.getName()+
            " already exists. Override?";
          int result = JOptionPane.showConfirmDialog(
            JPEGEditor.this, message, getTitle(), 
            JOptionPane.YES_NO_OPTION);
          if (result != JOptionPane.YES_OPTION)
            return;
        }
        setCurrentFile(fChoosen);
        saveFile(fChoosen);
      }
    };
    mItem.addActionListener(lst);
    mFile.add(mItem);

    mFile.addSeparator();

    mItem = new JMenuItem("Exit");
    mItem.setMnemonic('x');
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        System.exit(0);
      }
    };
    mItem.addActionListener(lst);
    mFile.add(mItem);
    menuBar.add(mFile);
    return menuBar;
  }

  protected void setCurrentFile(File file) {
    if (file != null) {
      m_currentFile = file;
      setTitle("JPEG Editor ["+file.getName()+"]");
    }
  }

  protected void openFile(final File file) {
    if (file == null || !file.exists())
      return;
    setCurrentFile(file);

    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    Thread runner = new Thread() {
      public void run() {
        try {
          FileInputStream in = new FileInputStream(file);
          JPEGImageDecoder decoder = 
            JPEGCodec.createJPEGDecoder(in);
          m_bi1 = decoder.decodeAsBufferedImage();
          m_bi2 = null;
          in.close();
          SwingUtilities.invokeLater( new Runnable() {
            public void run() { reset(); }
          });
        } 
        catch (Exception ex) {
          ex.printStackTrace();
          System.err.println("openFile: "+ex.toString());
        }
        setCursor(Cursor.getPredefinedCursor(
          Cursor.DEFAULT_CURSOR));
      }
    };
    runner.start();
  }

  protected void saveFile(final File file) {
    if (file == null || m_panel.getBufferedImage() == null)
      return;

    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    Thread runner = new Thread() {
      public void run() {
        try {
          FileOutputStream out = new FileOutputStream(file);
          JPEGImageEncoder encoder = 
            JPEGCodec.createJPEGEncoder(out);
          encoder.encode(m_panel.getBufferedImage());
          out.close();
        } 
        catch (Exception ex) {
          ex.printStackTrace();
          System.err.println("apply: "+ex.toString());
        }
        setCursor(Cursor.getPredefinedCursor(
          Cursor.DEFAULT_CURSOR));
      }
    };
    runner.start();
  }
    
  protected void apply() {
    if (m_bi1 == null)
      return;

    setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
    Thread runner = new Thread() {
      public void run() {
        try {
          ByteArrayOutputStream out = new ByteArrayOutputStream();
          JPEGImageEncoder encoder = 
            JPEGCodec.createJPEGEncoder(out);
          JPEGEncodeParam param = 
            encoder.getDefaultJPEGEncodeParam(m_bi1);
            
          float quality = m_slQuality.getValue()/100.0f;
          param.setQuality(quality, false);
            
          param.setDensityUnit(
            JPEGEncodeParam.DENSITY_UNIT_DOTS_INCH);
          int xDensity = m_slHorzDensity.getValue();
          param.setXDensity(xDensity); 
          int yDensity = m_slVertDensity.getValue();
          param.setYDensity(yDensity);

          encoder.setJPEGEncodeParam(param);
          encoder.encode(m_bi1);

          ByteArrayInputStream in = new ByteArrayInputStream(
            out.toByteArray());
          JPEGImageDecoder decoder = 
            JPEGCodec.createJPEGDecoder(in);
          final BufferedImage bi2 = decoder.decodeAsBufferedImage();
          SwingUtilities.invokeLater( new Runnable() {
            public void run() { 
              m_panel.setBufferedImage(bi2); 
            }
          });
        } 
        catch (Exception ex) {
          ex.printStackTrace();
          System.err.println("apply: "+ex.toString());
        }
        setCursor(Cursor.getPredefinedCursor(
          Cursor.DEFAULT_CURSOR));
      }
    };
    runner.start();
  }
    
  protected void reset() {
    if (m_bi1 != null) {
      m_panel.setBufferedImage(m_bi1);
      m_slQuality.setValue(100);
      m_slHorzDensity.setValue(300);
      m_slVertDensity.setValue(300);
    }
  }

  public static void main(String argv[]) {
    new JPEGEditor();
  }
}

class JPEGPanel extends JPanel
{
  protected BufferedImage m_bi = null;

  public JPEGPanel() {}

  public void setBufferedImage(BufferedImage bi) {
    if (bi == null)
      return;
    m_bi = bi;
    Dimension d = new Dimension(m_bi.getWidth(this), 
      m_bi.getHeight(this));
    setPreferredSize(d);
    revalidate();
    repaint();
  }

  public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Dimension d = getSize();
    g.setColor(getBackground());
    g.fillRect(0, 0, d.width, d.height);
    if (m_bi != null)
      g.drawImage(m_bi, 0, 0, this);
  }
    
  public BufferedImage getBufferedImage() {
    return m_bi;
  }
}
//class SimpleFilter taken from chapter 14

Understanding the Code

Class JPEGEditor

Class variables:

Dimension VERTICAL_RIGID_SIZE: size of rigid area used for vertical spacing.

Dimension HORIZONTAL_RIGID_SIZE: size of rigid area used for horizontal spacing.

Instance variables:

File m_currentDir: current directory navigated to by our JFileChooser.

File m_currentFile: JPEG image file currently in our editing environment.

JFileChooser m_chooser: file chooser used for loading and saving JPEGs.

JPEGPanel m_panel: custom component used to display JPEGs.

JSlider m_slHorzDensity: slider to choose horizontal pixel density.

JSlider m_slVertDensity: slider to choose vertical pixel density.

JSlider m_slQuality: slider to choose image quality.

BufferedImage m_bi1: original image.

BufferedImage m_bi2: modified image.

JPEGEditor‘s constructor starts by instantiating our JFileChooser and applying a SimpleFilter (see chapter 14) file filter to it, in order to restrict file selection to JPEG images (files with a ".jpg" extension). creates and initializes the GUI components for this example. Custom panel m_panel is used to display a JPEG image (see the JPEGPanel class below). It is added to a JScrollPane to provide scrolling capabilities. Three sliders are used to select JPEGEncodeParam properties as described above: xDensity, yDensity, and quality. Each is surrounded by a TitledBorder with an appropriate title. Similar to the previous example, RigidAreas are used to ensure proper spacing between the slider and the border. Note that each slider makes use of the Metal L&F client property "JSlider.isFilled" with value Boolean.TRUE to force the lower portion of each slider track to be filled.

Note that the m_slQuality slider must represent values from 0 to 1.0. We scale this interval to [0, 100], but display annotation labels 0.0, 0.2, 0.4,…,1.0 stored in Hashtable labels. The selected image quality value is the slider's value divided by 100. Also note the usage of setExtent() for each slider in this example. Recall that the value of the extent property is used when the slider has focus and the user presses the PgUp or PgDn key to increment or decrement the slider's value respectively.

An "Apply" button is created and assigned an ActionListener to retrieve current slider settings and apply them to current JPEG image by calling our custom apply() method (because of the large amount of work the apply method performs, it does not make sense to do this on-the-fly by listening for slider change events). A "Reset" button reverts any changes and returns the image to its original state by calling our custom reset() method. Finally a JMenuBar is created with our createMenuBar() method.

The createMenuBar() method creates and returns a JMenuBar containing one menu titled "File," which, in turn, contains four menu items: "Open...," "Save," "Save As...," and "Exit." Each item receives its own ActionListener.

The "Open..." menu item invokes our JFileChooser for selecting a JPEG image file. After a successful selection the current directory is stored in our m_currentDir variable for future use, and our custom openFile() method is invoked to load the image into our environment. The "Save" menu item invokes our custom saveFile() method to save the image currently in our environment. The "Save As…" menu item instructs JFileChooser to prompt the user for a new name, and possibly location, to save the current image file to. This code is fairly similar to the code for the "Open..." menu, except that showSaveDialog() is used instead of showOpenDialog(). If the selected file already exists, a request for confirmation is invoked uwing JOptionPane.showConfirmDialog() (interestingly, this is not a standard feature of JFileChooser--see chapter 14 for more about JOptionPane and JFileChooser). Finally our saveFile() method is invoked to save the current image as the selected file. The "Exit" menu item calls System.exit(0) to quit this application.

The setCurrentFile() method stores a reference to the newly opened file in m_currentFile. This method also modifies the frame's title to display file name. It is called whenever the "Open…" and "Save As…" menu items are invoked.

The openFile() method opens a given File corresponding to a stored JPEG image. First it checks whether or not the selected file exists. If so, a new thread is created to execute all remaining code in this method to avoid clogging up the event-dispatching thread. A FileInputStream is opened and a JPEGImageDecoder is created for the given file. Then a call to decodeAsBufferedImage() retrieves a BufferedImage from the JPEGImageDecoder and stores it in our m_bi1 variable. The file stream is closed and our image is pa! ssed to JPEGPanel by calling the reset() method (see below). Note that because our reset method directly modifies the state of Swing components, we place this call in a Runnable and send it to the event-dispatching queue with SwingUtilities.invokeLater() (see chapter 2 for more about invokeLater()).

The saveFile() method saves the current image into the given File. In a separate thread, a FileOutputStream is opened and a JPEGImageEncoder is created corresponding to this File. Then a call to the JPEGImageEncoder’s encode() method saves the current image (retrieved by our JPEGPanel’s getBufferedImage() method) to the opened stream.

The apply() method applies the current slider settings to the current image. In a separate thread, this method creates a ByteArrayOutputStream to stream the operations in memory. Then a JPEGImageEncoder is created for this stream, and a JPEGEncodeParam is retrieved corresponding to the original image, m_bi1 (which is assigned in openFile()). Three property values are retrieved from our sliders and sent to a JPEGEncodeParam object via setXX() methods: quality, xDensity and yDensity (note that quality is converted to a float through division by 100.0f). Then this JPEGEncodeParam object is assigned to our JPEGImageEncoder, and the encode() method is used to perform the actual encoding of the m_bi1 image. Next a new image is retrieved from this encoder by first retrieving a ByteArrayInputStream from our ByteArrayOutputStream using its t! oByteArray() method. A JPEGImageDecoder is created for this stream, and the decodeAsBufferedImage() method retrieves a BufferedImage instance. Finally, in a Runnable sent to SwingUtilities.invokeLater(), this image is assigned to our image panel for display with JPEGPanel’s setBufferedImage() method.

The reset() method, as you might guess from its name, resets the current image to its original state (the state it was in when opened) and resets the slider values.

Class JPEGPanel

JPEGPanel extends JPanel and provides a placeholder for JPEG images. It declares a single instance variable:

BufferedImage m_bi: holds the current JPEG.

Method setBufferedImage() assigns the given image to m_bi, and changes this panel's preferred size to the size of that image. The panel is then revalidated and repainted to display the new image properly.

 

Note: We learned in chapter 2 that when a revalidate() request is invoked on a component, all ancestors below the first ancestor whose validateRoot property is true get validated. JRootPane, JScrollPane, and JTextField are the only Swing components with a true validateRoot property by default. Thus, calling revalidate() on our JPEGPanel will result in validation of the JScrollPane it is contained in within our JPEGEditor application. This results in proper layout and display of JPEGPanel which would not occur by simply calling repaint().

The paintComponent() method clears the background and draws the current image (if any). The getBufferedImage() method simply returns the most recent image associated with this panel.

Running the Code

Figure 13.5 shows JPEGEditor displaying a high-quality image of Earth. Applying our sliders to reduce the quality, and clicking the "Apply" button, we produce the image shown in Figure 13.6. By saving this image as a new file, we obtain a representation occupying much less disk space than the original. A balance between quality and size is a decision that often needs to be made when space or latency issues are important.  

UI Guideline : Component Selection

This example provides some tricky problems for the designer. The nature of the calculation means that instant feedback is not possible. However, the User needs to see what the result of a choice would mean. This has been solved by the introduction of the "Apply" button. This is justifiable in a case such as this due to the complex and time consuming nature of the effect of the selection. It is not otherwise recommended.

The introduction of the shaded area on the Sliders gives a clear indication that an amount or quantity rather than an exact, discrete value is being selected and the amount is a percentage of the bounded whole. This adds to the visual affordance of the component and aids the viewer in understanding what is happening.  

13.5 JProgressBar in an FTP client application

The following example uses JProgressBar to display progress in downloading and uploading files using File Transfer Protocol (FTP). The support for this protocol is provided in the sun.net and sun.net.ftp packages.

13.5.1 FtpClient

class sun.net.ftp.FtpClient

This class provides functionality for an FTP client. Methods particularly relevant to this example include:

FTPClient(String host): constructor to create a new instance and connect to the given host address.

login(String user, String password): login to an FTP host with given username and password.

cd(String directory): change directory.

binary(): set mode to binary for proper file transfering.

closeSever(): disconnect from host.

list(): returns an InputStream supplying the printout of the ls -l command (list contents of directories, oner per line).

get(String filename): returns an InputStream for retrieving the specified file from the host.

put(String filename): returns an OutputStream for writing the specified file to the host.

 

Note: This application’s GUI is layed out using our custom DialogLayout2 layout manager, developed in chapter 4. Refer back to this chapter for more information about how this manager works.

Figure 13.8 FTP Client application with JProgressBar to show upload/download status.

The Code: FTPApp.java

see \Chapter13\5

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
import java.net.*;
import java.lang.reflect.*;

import sun.net.ftp.*;
import sun.net.*;

import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

import dl.*;

public class FTPApp extends JFrame 
{
  public static int BUFFER_SIZE = 10240;

  protected JTextField m_txtUser;
  protected JPasswordField m_txtPassword;
  protected JTextField m_txtURL;
  protected JTextField m_txtFile;
  protected JTextArea  m_monitor;
  protected JProgressBar m_progress;
  protected JButton m_btPut;
  protected JButton m_btGet;
  protected JButton m_btFile;
  protected JButton m_btClose;
  protected JFileChooser m_chooser;

  protected FtpClient m_client;
  protected String m_sLocalFile;
  protected String m_sHostFile;

  public FTPApp() {
    super("FTP Client");
        
    JPanel p = new JPanel();
    p.setLayout(new DialogLayout2(10, 5));
    p.setBorder(new EmptyBorder(5, 5, 5, 5));

    p.add(new JLabel("User name:"));
    m_txtUser = new JTextField("anonymous");
    p.add(m_txtUser);

    p.add(new JLabel("Password:"));
    m_txtPassword = new JPasswordField();
    p.add(m_txtPassword);

    p.add(new JLabel("URL:"));
    m_txtURL = new JTextField();
    p.add(m_txtURL);

    p.add(new JLabel("File:"));
    m_txtFile = new JTextField();
    p.add(m_txtFile);

    p.add(new DialogSeparator("Connection Monitor"));

    m_monitor = new JTextArea(5, 20);
    m_monitor.setEditable(false);
    JScrollPane ps = new JScrollPane(m_monitor);
    p.add(ps);

    m_progress = new JProgressBar();
    m_progress.setStringPainted(true);
    m_progress.setBorder(new BevelBorder(BevelBorder.LOWERED,
      Color.white, Color.gray));
    m_progress.setMinimum(0);
    JPanel p1 = new JPanel(new BorderLayout());
    p1.add(m_progress, BorderLayout.CENTER);
    p.add(p1);

    p.add(new DialogSeparator());
    m_btPut = new JButton("Put");
    ActionListener lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        if (connect()) {
          Thread uploader = new Thread() {
            public void run() {
              putFile();
              disconnect();
            }
          };
          uploader.start();
        }
      }
    };
    m_btPut.addActionListener(lst);
    m_btPut.setMnemonic('p');
    p.add(m_btPut);

    m_btGet = new JButton("Get");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        if (connect()) {
          Thread downloader = new Thread() { 
            public void run() {
              getFile();
              disconnect();
            }
          };
          downloader.start();
        }
      }
    };
    m_btGet.addActionListener(lst);
    m_btGet.setMnemonic('g');
    p.add(m_btGet);

    m_btFile = new JButton("File");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        if (m_chooser.showSaveDialog(FTPApp.this) != 
          JFileChooser.APPROVE_OPTION)
            return;
        File f = m_chooser.getSelectedFile();
        m_txtFile.setText(f.getPath());
      }
    };
    m_btFile.addActionListener(lst);
    m_btFile.setMnemonic('f');
    p.add(m_btFile);

    m_btClose = new JButton("Close");
    lst = new ActionListener() { 
      public void actionPerformed(ActionEvent e) {
        if (m_client != null)
          disconnect();
        else
          System.exit(0);
      }
    };
    m_btClose.addActionListener(lst);
    m_btClose.setDefaultCapable(true);
    m_btClose.setMnemonic('g');
    p.add(m_btClose);

    getContentPane().add(p, BorderLayout.CENTER);

    m_chooser = new JFileChooser();
    m_chooser.setCurrentDirectory(new File("."));
    m_chooser.setApproveButtonToolTipText(
      "Select file for upload/download");

    WindowListener wndCloser = new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        disconnect();
        System.exit(0);
      }
    };
    addWindowListener(wndCloser);
       
    setSize(340,340);
    setResizable(false);
    setVisible(true);
  }

  public void setButtonStates(boolean state) {
    m_btPut.setEnabled(state);
    m_btGet.setEnabled(state);
    m_btFile.setEnabled(state);
  }

  protected boolean connect() {
    m_monitor.setText("");
    setButtonStates(false);
    m_btClose.setText("Cancel");
    setCursor(Cursor.getPredefinedCursor(
      Cursor.WAIT_CURSOR));

    String user = m_txtUser.getText();
    if (user.length()==0) {
      message("Please enter user name");
      setButtonStates(true);
      return false;
    }
    String password = new String(m_txtPassword.getPassword());
    String sUrl = m_txtURL.getText();
    if (sUrl.length()==0) {
      message("Please enter URL");
      setButtonStates(true);
      return false;
    }
    m_sLocalFile = m_txtFile.getText();

    // Parse URL
    int index = sUrl.indexOf("//");
    if (index >= 0)
    sUrl = sUrl.substring(index+2);
       
    index = sUrl.indexOf("/");
    String host = sUrl.substring(0, index);
    sUrl = sUrl.substring(index+1);
        
    String sDir = "";
    index = sUrl.lastIndexOf("/");
    if (index >= 0) {
      sDir = sUrl.substring(0, index);
      sUrl = sUrl.substring(index+1);
    }
    m_sHostFile = sUrl;
        
    try {
      message("Connecting to host "+host);
      m_client = new FtpClient(host);
      m_client.login(user, password);
      message("User "+user+" login OK");
      message(m_client.welcomeMsg);
      m_client.cd(sDir);
      message("Directory: "+sDir);
      m_client.binary();
      return true;
    }
    catch (Exception ex) {
      message("Error: "+ex.toString());
      setButtonStates(true);
      return false;
    }
  }
 
  protected void disconnect() {
    if (m_client != null) {
      try { m_client.closeServer(); }
      catch (IOException ex) {}
      m_client = null;
    }
    Runnable runner = new Runnable() {
      public void run() {
        m_progress.setValue(0);
        setButtonStates(true);
        m_btClose.setText("Close");
        FTPApp.this.setCursor(Cursor.getPredefinedCursor(
          Cursor.DEFAULT_CURSOR));
      }
    };
    SwingUtilities.invokeLater(runner);
  }

  protected void getFile() {
    if (m_sLocalFile.length()==0) {
      m_sLocalFile = m_sHostFile;
      SwingUtilities.invokeLater( new Runnable() {
        public void run() {
          m_txtFile.setText(m_sLocalFile);
        }
      });
    }
    byte[] buffer = new byte[BUFFER_SIZE];
    try {
      int size = getFileSize(m_client, m_sHostFile);
      if (size > 0) {
        message("File " + m_sHostFile + ": " + size + " bytes");
        setProgressMaximum(size);
      }
      else
        message("File " + m_sHostFile + ": size unknown");
      FileOutputStream out = new 
        FileOutputStream(m_sLocalFile);
      InputStream in = m_client.get(m_sHostFile);
      int counter = 0;
      while(true) {
        int bytes = in.read(buffer);
        if (bytes < 0)
          break;

        out.write(buffer, 0, bytes);
        counter += bytes;
        if (size > 0) {
          setProgressValue(counter);
          int proc = (int) Math.round(m_progress.
            getPercentComplete() * 100);
          setProgressString(proc + " %");
        }
        else {
          int kb = counter/1024;
          setProgressString(kb + " KB");
        }
      }
      out.close();
      in.close();
    }
    catch (Exception ex) {
      message("Error: "+ex.toString());
    }
  }

  protected void putFile() {
    if (m_sLocalFile.length()==0) {
      message("Please enter file name");
    }
    byte[] buffer = new byte[BUFFER_SIZE];
    try {
      File f = new File(m_sLocalFile);
      int size = (int)f.length();
      message("File " + m_sLocalFile + ": " + size + " bytes");
      setProgressMaximum (size);
      FileInputStream in = new 
        FileInputStream(m_sLocalFile);
      OutputStream out = m_client.put(m_sHostFile);

      int counter = 0;
      while(true) {
        int bytes = in.read(buffer);
        if (bytes < 0)
          break;
        out.write(buffer, 0, bytes);
        counter += bytes;
        setProgressValue(counter);
        int proc = (int) Math.round(m_progress.
          getPercentComplete() * 100);
        setProgressString(proc + " %");
      }

      out.close();
      in.close();
    }
    catch (Exception ex) {
      message("Error: " + ex.toString());
    }
  }

  protected void message(final String str) {
    if (str != null) {
      Runnable runner = new Runnable() { 
        public void run() {
          m_monitor.append(str + '\n');
          m_monitor.repaint();
        }
      };
      SwingUtilities.invokeLater(runner);
    }
  }

  protected void setProgressValue(final int value) {
    Runnable runner = new Runnable() { 
      public void run() {
        m_progress.setValue(value);
      }
    };
    SwingUtilities.invokeLater(runner);
  }

  protected void setProgressMaximum(final int value) {
    Runnable runner = new Runnable() { 
      public void run() {
        m_progress.setMaximum(value);
      }
    };
    SwingUtilities.invokeLater(runner);
  }

  protected void setProgressString(final String string) {
    Runnable runner = new Runnable() { 
      public void run() {
        m_progress.setString(string);
      }
    };
    SwingUtilities.invokeLater(runner);
  }

  public static void main(String argv[]) {
    new FTPApp();
  }

  public static int getFileSize(FtpClient client, String fileName)
   throws IOException {
    TelnetInputStream lst = client.list();
    String str = "";
    fileName = fileName.toLowerCase();
    while(true) {
      int c = lst.read();
      char ch = (char) c;
      if (c < 0 || ch == '\n') {
        str = str.toLowerCase();
        if (str.indexOf(fileName) >= 0) {
          StringTokenizer tk = new StringTokenizer(str);
          int index = 0;
          while(tk.hasMoreTokens()) {
            String token = tk.nextToken();
            if (index == 4)
              try {
                return Integer.parseInt(token);
              }
              catch (NumberFormatException ex) {
                return -1;
              }
            index++;
          }
        }
        str = "";
      }
      if (c <= 0)
        break;
      str += ch;
    }
    return -1;
  }
}

Understanding the Code

Class FTPApp

Class variable:

int BUFFER_SIZE: the size of the buffer used for input/ouput operations.

Instance variables:

JTextField m_txtUser: login user name text field.

JPasswordField m_txtPassword: login password field.

JTextField m_txtURL: field for URL of file to be downloaded/uploaded on the remote site.

JTextField m_txtFile: field for file name of the file to be uploaded/downloaded on the local machine.

JTextArea m_monitor: used as a log to display various status messages.

JProgressBar m_progress: used to indicate the progress of an upload/download operation.

JButton m_btPut: used to initiate uploading.

JButton m_btGet: used to initiate downloading.

JButton m_btFile: used to bring up a file chooser dialog to choose a local file or specify a file name and location.

JButton m_btClose: used to close the application.

JFileChooser m_chooser: used to choose a local file or specify a file name and location.

FtpClient m_client: client connection to host which manages I/O operations.

String m_sLocalFile: name of the most recent local file involved in a data transfer.

String m_sHostFile: name of the most recent host file involved in a data transfer.

The FTPApp constructor first creates a panel using a DialogLayout2 layout manager, and instantiates and adds our four text fields with corresponding labels (recall that our DialogLayout2 manager requires that label/input field paris are added in the specific label1, field1, label2, field2, etc., order). The m_monitor text area is created and placed in a JScrollPane, and separated from the label/field panel (by an instance of our custom DialogSeparator class titled "Connection Monitor"). The m_progress JProgressBar is created and placed in a JPanel with a BorderLayout to ensure that DialogLayout2 allows it to occupy the maximum width across the frame, as well as its preferred height).

A plain DialogSeparator is added below the progress bar, and our four buttons are created with attached ActionListeners, and added to the DialogLayout2 panel resulting in a horizontal row at the bottom of the frame. The button titled "Put" attempts to connect to a host using our connect() method. If it is successful a new thread is started which calls putFile() to upload a selected file, and then calls disconnect() to terminate the connection to the host. Similarly, the button titled "Get" attempts to connect to a host, and, if successful, starts a thread which calls our ! getFile() method to download a file, and then disconnect(). The button titled "File" brings up our JFileChooser dialog to select a local file or specify a new file name and location. The button titled "Close" invokes disconnect() to terminate a connection to the host if FTP transfer is in progress (i.e. if m_client instance is not null). If a transfer is not in progress the application is terminated.

The setButtonStates() method takes a boolean parameter and enables/disables the "Put," "Get," and "File" buttons accordingly.

The connect() method establishes a connection to the remote host and returns true in the case of success, or false otherwise. First this method disables our "Put," "Get," and "File" push buttons, and sets the text of the last button to "Cancel." Then this method reads the content of the text fields to obtain login name, password, URL, and local file name. The URL is parsed and split into the host name, remote directory, and the host file name. A new FtpClient instance is created to connect to the remote host and stored in our m_client instance variable. Then the login() method is invoked! on m_client to login to the server using the specified user name and password. If the login is successful we change the remote directory and set the connection type to binary (which is almost always required for file transfers). If no exceptions have been thrown during this process, connect() returns true. Otherwise it shows an exception in our m_monitor text area, re-enables our buttons, and returns false.

 

Note: The connect() method code involving connecting to a host and changing directory would be better off in a separate thread, and we suggest this enhancement for more professional implementations. All other time-intensive code in this example is executed in separate threads.

The disconnect() method invokes closeServer() on the current m_client FTPClient instance if it is in use. It then sets the m_client reference to null, allowing garbage collection of the FTPClient object. This method also clears the progress bar component, enables all push buttons which may have been disabled by the connect() method, and restores the text of the "Close" button. Note that all component updates are wrapped in a Runnable and sent to the ev! ent-dispatching queue with SwingUtilities.invokeLater().

The getFile() method downloads a prespecified file from the current host. If the name of the destination local file is not specified, the name of the remote file is used. This method tries to determine the size of the remote file by calling our getFileSize() helper method (see below). If that succeeds, the file size is set as the maximum value of the progress bar (the minimum value is always 0) with our custom setProgressMaximum() method. Then a FileOutputStream is openned to write to the local file, and an InputStream is retrieved from the FTPClient to read from the remote file. A while loop is set up to perform typical read/write operations until all content of the remote file is written to the local file. During this process the number of bytes read is accumulated in the counter local variable. If the size of the file is known, this number is assigned to the progress bar using our custom setProgressValue() method. We also calculate the percentage of downloading complete with our custom getPercentComplete() method, and display it in the progress bar using our custom setProgressString() method. If the size of file is unknown (i.e. it is less than or equal to 0), we can only display the number of kilobytes currently downloaded at any given time. To obtain this value we simply divide the current byte count, stored in the l! ocal counter variable, by 1024.

The putFile() method uploads the content of a local file to a remote pre-specified URL. If the name of the local file is not specified, a message is printed, using our custom message() method, and we simply return. Otherwise, the size of the local file is determined and used as the maximum value of our progress bar using our custom setMaximum() method (the minimum value is always 0). A FileInputStream is openned to read from the local file, and an OutputStream is retrieved from the FTPClient to write to the remote file. A while loop is set up to perform typical read/write operations until all content of the local file is writt! en to the remote host. During this process the number of bytes written is accumulated in the counter local variable. This number is assigned to the progress bar using our custom setProgressValue() method. As in the getFile() method, we also calculate the percentage of downloading complete with our custom getPercentComplete() method, and display it in the progress bar using our custom setProgressString() method. Since we can always determine the size of a local File object, there is no need to display the progress in terms of kilobytes (as we did in getFile() above).

The message() method takes a String parameter to display in our m_monitor text area. The setProgressValue() and setProgressMaximum() methods assign selected and maximum values to our progress bar respectively. Since each of these methods modifes the state of our progress bar component, and each is called from a custom thread, we wrap their bodies in Runnables and send them to the event-dispatching queue using SwingUtilities.invokeLater().

Unfortunately the FtpClient class does not provide a direct way to determine the size of a remote file, as well as any other available file specifics. The only way we can get any information about files on the remote host using this class is to call the its list() method which returns a TelnetInputStream supplying the printout of the results of an ls -l command. Our getFileSize() method uses this method in an attempt to obtain the length of a remote file specified by a given file name and FTPClient instance. This method captures the printout from the remote server, splits it into lines separated by '\n' characters, and uses a ! StringTokenizer to parse them into tokens. According to the syntax of the ls -l command output, the the length of the file in bytes appears as the 5th token, and the last token should contain the file name. So we go character by character through each line until a line containing a matching file name is found, and the length is returned to the caller. If this does not succeed we return -1 to indicate that the server either does not allow browsing of its content or that an error has occured.

Running the Code

Figure 13.8 shows FTPApp in action. Try running this application and transfering a few files. Start by entering your user name, password, URL containing the host FTP sever, and (optionally) a local file name and path to act as the source or destination of a transfer. Press the "Get" button to download a specified remote file, or press the "Put" button to upload a specified local file to the host. If the required connection is established successfully, you will see the transfer progress updated incrementally in the progress bar.

In figure 13.8 we specify "anonymous" as username and use an email address as password. In our URL text field we specify the remote "tutorial.zip" file (the most recent Java Tutorial) on the "java.sun.com" FTP server in its "docs" directory. In our File text field we specify "tutorial.zip" as the destination file in the current running directory. Clicking on "Get" establishes a connection, changes remote directory to "docs," determines the size of the remote "tutorial.zip" file, and starts retrieving and storing it as a local file in the current running directory. Try performing this transfer and note how smoothly the progress bar updates itself (it can’t hurt to keep a local copy of the Java Tutorial, but be aware that this archive is close to 10 megabytes).

 

Note: In the next chapter we will customize JFileChooser to build a ZIP/JAR archive tool. This can be used to unpackage tutorial.zip if you do not have access to an equivalent tool.