Tracking
Progress with Swing
Progress Dialogs Can Help
Prevent Unwanted Actions
By Claude Duguay
Atrieva
Software development is about creating useful programs, and
a large part of that goal involves making sure that your users are
satisfied with what they see. For example, if a process takes a
long time to execute and provides no feedback to the user, the user
might wind up taking several kinds of unexpected actions. For example:
- If there is no indication that something is executing, the
user might think the action that he or she has triggered
is not actually running, leading to a negative impression of
the software.
- If you have provided the ability to cancel the operation,
the user might assume that things are not proceeding as planned
-- and eventually hit the Cancel button, before the action
is complete.
- If it isn't clear that something useful is taking place, the
user might start experimenting by clicking around the program
or trying various keystrokes, sometimes resulting in compounded
problems that you'll eventually have to deal with in other ways.
One company's answer
Providing good user feedback is crucial, but itīs
also important to provide useful information that goes beyond the
simplest approach. For example, the twirling icon found in most
browsers indicates activity. But this simple visual cue may not
be enough. Users also want to know how long it will take for an
action to complete
A progress bar may help, but that may still not be
enough. As an indicator, it's a big improvement over a twirling
icon, but you may need to provide answers to more pressing questions
like: How much longer do I have to wait?
The JFC ProgressMonitor goes a long way to improving
this kind of feedback, providing visual feedback about relative
progress so the user can estimate how long it will take to complete
a lengthy task. The mechanism is easy to use but falls short when
users start asking why the computer can't tell them how much time
remains. In fact, its typically not because this is difficult, but
usually because it wasn't a consideration at design time. If your
needs are simple, ProgressMonitor is well worth a look. If your
users demand the best, this solution will probably appeal to you.
Innovations in backup services
At Atrieva, we've developed an online backup and retrieval service,
founded on the idea that users need immediate access to their files
when there is a problem. Traditional backup approaches make retrieval
a painful process, but we believe the key question is really how
fast you can get to your data and get back to business when there
is a problem. Since most file loss is caused by inadvertent overwriting
or deletion of single files, this is especially important. With
Atrieva, you can be up and running in a few minutes. With other
methods, you may have to wait for hours or days (or sometimes the
media actually fails and the game is just plain over).
Our Atrieva software solution includes both a native Windows C/C++
client and a browser-based JavaTM
technology implementation. Both are independent (though format-compatible),
and our Java solution is evolving toward a more comprehensive, JavaTM
Plug-in successor. This is our chance to use the JFC in production
and, since one of the most demanded features in our products has
always been improved user feedback, our attention was focused on
this very issue. We move a lot of files across the Internet, so
it makes sense that one of our priorities has been improving that
part of our feedback process.
We developed a ProgressDialog class (Figure 1) and a few supporting
classes to help make this an integral part of our development effort.
Since we plan to reuse this dialog box in a number of applications,
we capitalized on the best available features of the Java platform,
and used JFC to produce a set of classes we could count on to provide
user-effective feedback while monitoring progress and displaying
time-related information. The result is a simple-to-use dialog box
that shows the source and target paths, the number of bytes transferred,
throughput, and time remaining.
Figure 1: Progress Dialog
Designing from the Outside-In
Our design is largely modeled on the Netscape download dialog box.
This is familiar to a large number of users and satisfies a number
of our requirements. With JFC, of course, you can flip the look-and-feel
to whatever you like -- and the dialog box looks quite effective
under any of the platforms we've tried.
To make this as easy as possible to develop with, and to promote
reuse, we decided to use a simple interface called ProgressReporter ,
which looks like this:
public interface ProgressReporter
{
public void setSize(int size);
public void setProgress(int current);
public boolean isCancelled();
}
|
The interface is sufficiently generic that it could be used for
any progress reporting. The setSize() method lets us
set the total number of bytes to be counted. The setProgress()
method does the updating and needs to be called by the process being
monitored. We stayed away from using an event or observer model
because of the overhead, but mostly to keep it simple. Finally,
the isCancelled() method can be used by a calling process
to abort if the user pressed the Cancel button. Note that a better
approach might have been to use the JFC's BoundedRangeModel
with a new interface to handle cancellations, but this approach
seemed more complicated than necessary for this solution.
The ProgressReporter interface is used by a pair of
streams that make using the dialog box as non-intrusive as possible.
At Atrieva, we compress and encrypt all user files. When compressing,
we cannot predict the final size, so we must monitor the input stream
from the file to show progress information. When decompressing,
we monitor the output stream. We can use the same ProgressReporter
interface and ProgressDialog implementation to develop
a pair of filter streams that can be wrapped around any InputStream
or OutputStream , typically a FileInputStream
or FileOutputStream or one of the Socket streams.
Figure 2 shows the UML (Universal Modeling Language) diagram for
the classes in this project. The ProgressDialog class
implements the ProgressReporter interface. Both the
ProgressInputStream and ProgressOutputStream
expect to be passed a ProgressReporter in their constructor.
They then support the standard filter input and output stream interfaces.
We throw a UserCancelledException when the user interrupts
a file transfer by pressing the Cancel button.
Figure 2: UML Class Diagram
If you're not familiar with UML notation, the + symbol means public,
- means private and # means protected. Variables are listed first,
with methods and the constructor(s) at the bottom. The variable
or method return type is listed after the colon for each entry.
There is no return type for the constructor(s), but the color is
still shown.
Innovations in backup services
At Atrieva, we've developed an online backup and retrieval service,
founded on the idea that users need immediate access to their files
when there is a problem. Traditional backup approaches make retrieval
a painful process, but we believe the key question is really how
fast you can get to your data and get back to business when there
is a problem. Since most file loss is caused by inadvertent overwriting
or deletion of single files, this is especially important. With
Atrieva, you can be up and running in a few minutes. With other
methods, you may have to wait for hours or days (or sometimes the
media actually fails and the game is just plain over).
Our Atrieva software solution includes both a native Windows C/C++
client and a browser-based JavaTM
technology implementation. Both are independent (though format-compatible),
and our Java solution is evolving toward a more comprehensive, JavaTM
Plug-in successor. This is our chance to use the JFC in production
and, since one of the most demanded features in our products has
always been improved user feedback, our attention was focused on
this very issue. We move a lot of files across the Internet, so
it makes sense that one of our priorities has been improving that
part of our feedback process.
We developed a ProgressDialog class (Figure 1) and a few supporting
classes to help make this an integral part of our development effort.
Since we plan to reuse this dialog box in a number of applications,
we capitalized on the best available features of the Java platform,
and used JFC to produce a set of classes we could count on to provide
user-effective feedback while monitoring progress and displaying
time-related information. The result is a simple-to-use dialog box
that shows the source and target paths, the number of bytes transferred,
throughput, and time remaining.
Figure 1: Progress Dialog
Designing from the Outside-In
Our design is largely modeled on the Netscape download dialog box.
This is familiar to a large number of users and satisfies a number
of our requirements. With JFC, of course, you can flip the look-and-feel
to whatever you like -- and the dialog box looks quite effective
under any of the platforms we've tried.
To make this as easy as possible to develop with, and to promote
reuse, we decided to use a simple interface called ProgressReporter ,
which looks like this:
public interface ProgressReporter
{
public void setSize(int size);
public void setProgress(int current);
public boolean isCancelled();
}
|
The interface is sufficiently generic that it could be used for
any progress reporting. The setSize() method lets us
set the total number of bytes to be counted. The setProgress()
method does the updating and needs to be called by the process being
monitored. We stayed away from using an event or observer model
because of the overhead, but mostly to keep it simple. Finally,
the isCancelled() method can be used by a calling process
to abort if the user pressed the Cancel button. Note that a better
approach might have been to use the JFC's BoundedRangeModel
with a new interface to handle cancellations, but this approach
seemed more complicated than necessary for this solution.
The ProgressReporter interface is used by a pair of
streams that make using the dialog box as non-intrusive as possible.
At Atrieva, we compress and encrypt all user files. When compressing,
we cannot predict the final size, so we must monitor the input stream
from the file to show progress information. When decompressing,
we monitor the output stream. We can use the same ProgressReporter
interface and ProgressDialog implementation to develop
a pair of filter streams that can be wrapped around any InputStream
or OutputStream , typically a FileInputStream
or FileOutputStream or one of the Socket streams.
Figure 2 shows the UML (Universal Modeling Language) diagram for
the classes in this project. The ProgressDialog class
implements the ProgressReporter interface. Both the
ProgressInputStream and ProgressOutputStream
expect to be passed a ProgressReporter in their constructor.
They then support the standard filter input and output stream interfaces.
We throw a UserCancelledException when the user interrupts
a file transfer by pressing the Cancel button.
Figure 2: UML Class Diagram
If you're not familiar with UML notation, the + symbol means public,
- means private and # means protected. Variables are listed first,
with methods and the constructor(s) at the bottom. The variable
or method return type is listed after the colon for each entry.
There is no return type for the constructor(s), but the color is
still shown.
Streams of Progress
The ProgressInputStream extends FilterInputStream
and overrides the read() and skip() methods,
returning false for markSupported() . The constructor
needs a ProgressReporter -- the InputStream
-- to monitor and the size of the file to be monitored. Strictly
speaking, the size should be a long integer but we don't believe
our customers are likely to need to transfer files larger than 2
gigabytes over the internet for a while. If this were being deployed
as a third-party-reusable component, however, you would probably
want to use a long, even though the int data type is typically easier
to work with.
import java.io.IOException;
import java.io.InputStream;
import java.io.FilterInputStream;
public class ProgressInputStream extends FilterInputStream
{
ProgressReporter reporter;
int position = 0;
public ProgressInputStream(ProgressReporter reporter,
InputStream in, int size)
{
super(in);
position = 0;
this.reporter = reporter;
reporter.setSize(size);
reporter.setProgress(position);
}
public int read() throws IOException
{
if (reporter.isCancelled())
throw new UserCancelledException();
int b = in.read();
position++;
reporter.setProgress(position);
return b;
}
public int read(byte[] buf, int off, int len)
throws IOException
{
if (reporter.isCancelled())
throw new UserCancelledException();
int count = in.read(buf, off, len);
position += len;
reporter.setProgress(position);
return count;
}
public long skip(long len) throws IOException
{
if (reporter.isCancelled())
throw new UserCancelledException();
long count = in.skip(len);
position += len;
reporter.setProgress(position);
return count;
}
public boolean markSupported()
{
return false;
}
|
Note that a cancellation produces a UserCancelledException .
Unfortunately, the read and skip methods
cannot be extended to produce additional Exceptions, so the implementation
extends RuntimeException . This is not ideal, but it
functions well and it seems better to deal with cancellations in
this manner, since the exception can be propagated to a higher level
if necessary. We considered using the InterruptedException
but felt it was important to distinguish between an intended user
action and an unintended problem during processing.
public class UserCancelledException extends RuntimeException
{
public UserCancelledException()
{
super();
}
public UserCancelledException(String msg)
{
super(msg);
}
}
|
The ProgressOutputStream extends FilterOutputStream
and overrides the write methods. Similar to the ProgressInputStream ,
the constructor needs a ProgressReporter , the OutputStream
to monitor and the size of the file to be monitored.
import java.io.IOException;
import java.io.OutputStream;
import java.io.FilterOutputStream;
public class ProgressOutputStream extends FilterOutputStream
{
int position = 0;
ProgressReporter reporter;
public ProgressOutputStream(ProgressReporter reporter,
OutputStream out, int size)
{
super(out);
position = 0;
this.reporter = reporter;
reporter.setSize(size);
reporter.setProgress(position);
}
public void write(int b) throws IOException
{
if (reporter.isCancelled())
throw new UserCancelledException();
out.write(b);
position++;
reporter.setProgress(position);
}
public void write(byte[] buf, int off, int len)
throws IOException
{
if (reporter.isCancelled())
throw new UserCancelledException();
out.write(buf, off, len);
position += len;
reporter.setProgress(position);
}
}
|
Both the ProgressInputStream and ProgressOutputStream
are as transparent as possible to the process being monitored.
Dialog with the user
The meat of our solution is the ProgressDialog class
itself. It does all the real work and requires minimal input from
developers. The constructor expects a parent JFrame ,
a frame title, source and target paths to display and the size/length
of the process/stream to monitor.
The elements are laid out using nested panels. Figure 3 shows how
these panels are organized. This layout is not the only way to achieve
the same results, but it was chosen as a way of retaining flexibility.
The JLabel class is used to show the prompt and information. These
are set vertically using a GridLayout in separate panels. By putting
these in separate panels, we can let the BorderLayout manger keep
the prompts aligned to a minimum width while allowing the information
fields to remain resizable, by virtue of their Center location.
The JProgressBar is placed in a panel so that the EmptyBorder can
be used to frame the display, and the JButton is handled in a similar
way to ensure proper spacing.
Figure 3: Dialog Layout
Most of the code in the constructor is there to set up the layout.
The leaf elements are declared as protected instance variables so
they can be accessed elsewhere in the class. You'll see that the
time and number formatters are instantiated in the constructor as
well, so that they can be reused without the extra overhead. Finally,
the setSource() and setTarget() calls
are made after showing the dialog box. This is not a perfect solution,
since it might be desirable to show the dialog box only after doing
additional setup work. However, the setSource() and
setTarget() calls rely on collapsePath()
to make sure that the path is formatted according to the width of
the JLabel fields and this will only work once the
dialog box is displayed. Since the purpose of the dialog box was
to minimize programmer effort, this seems like an acceptable compromise
in a first version effort.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.BorderLayout;
import java.awt.GridLayout;
import java.awt.FontMetrics;
import java.awt.Dimension;
import java.util.Date;
import java.util.TimeZone;
import java.text.SimpleDateFormat;
import java.text.NumberFormat;
import com.sun.java.swing.JLabel;
import com.sun.java.swing.JPanel;
import com.sun.java.swing.JFrame;
import com.sun.java.swing.JDialog;
import com.sun.java.swing.JButton;
import com.sun.java.swing.JProgressBar;
import com.sun.java.swing.border.EmptyBorder;
public class ProgressDialog extends JDialog
implements ProgressReporter, ActionListener
{
public static final int K = 1024;
protected SimpleDateFormat timeFormatter;
protected NumberFormat numFormatter;
protected JProgressBar progress;
protected JButton button;
protected JLabel source, target,
status, timing, percent;
protected boolean cancelled = false;
protected long mark = System.currentTimeMillis();
protected int current, size;
public ProgressDialog(JFrame parent, String title,
String sourcePath, String targetPath, int size)
{
super(parent, title);
timeFormatter = new SimpleDateFormat("HH:mm:ss");
timeFormatter.setTimeZone(
TimeZone.getTimeZone("GMT"));
numFormatter =
NumberFormat.getInstance().getPercentInstance();
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
setSize(375, 191);
JPanel promptPanel = new JPanel();
promptPanel.setPreferredSize(new Dimension(70, 80));
promptPanel.setLayout(new GridLayout(4, 1));
promptPanel.add(new JLabel("Source:"));
promptPanel.add(new JLabel("Target:"));
promptPanel.add(new JLabel("Status:"));
promptPanel.add(new JLabel("Time Left:"));
JPanel infoPanel = new JPanel();
infoPanel.setLayout(new GridLayout(4, 1));
infoPanel.add(source = new JLabel());
infoPanel.add(target = new JLabel());
infoPanel.add(status = new JLabel());
infoPanel.add(timing = new JLabel());
JPanel messagePanel = new JPanel();
messagePanel.setLayout(new BorderLayout());
messagePanel.setBorder(
new EmptyBorder(5, 10, 5, 10));
messagePanel.add("West", promptPanel);
messagePanel.add("Center", infoPanel);
panel.add("North", messagePanel);
progress = new JProgressBar();
progress.setValue(50);
progress.setPreferredSize(new Dimension(300, 20));
JPanel progressPanel = new JPanel();
progressPanel.setLayout(new BorderLayout());
progressPanel.setBorder(
new EmptyBorder(3, 10, 3, 10));
progressPanel.add("Center", progress);
percent = new JLabel("");
percent.setPreferredSize(new Dimension(45, 23));
progressPanel.add("East", percent);
panel.add("Center", progressPanel);
button = new JButton("Cancel");
button.addActionListener(this);
JPanel buttonPanel = new JPanel();
buttonPanel.setBorder(new EmptyBorder(5, 0, 7, 0));
buttonPanel.add(button);
panel.add("South", buttonPanel);
getContentPane().add(panel);
show();
setSource(sourcePath);
setTarget(targetPath);
setSize(size);
}
public void setSource(String name)
{
source.setText(collapsePath(source, name));
}
public void setTarget(String name)
{
target.setText(collapsePath(target, name));
}
public void setSize(int size)
{
this.size = size;
progress.setMaximum(size);
mark = System.currentTimeMillis();
current = 0;
}
public void setProgress(int current)
{
this.current = current;
float per = (float)current / (float)size;
percent.setText(" " + numFormatter.format(per));
progress.setValue(current);
updateStatus();
timing.setText(
formatTime(timeLeftInMillis(current)));
if (cancelled || current >= size)
{
// If completed, give the dialog a chance
// to hit 100% before disposing of it.
try
{
Thread.currentThread().sleep(300);
}
catch (InterruptedException e) {}
setVisible(false);
dispose();
}
}
private String collapsePath(JLabel field, String text)
{
FontMetrics metrics =
getGraphics().getFontMetrics();
int textWidth = metrics.stringWidth(text);
int fieldWidth = field.getSize().width;
if (textWidth > fieldWidth)
{
int center = text.length() / 3;
String left = text.substring(0, center);
String right = text.substring(center + 1);
while(metrics.stringWidth(text) > fieldWidth)
{
left = left.substring(0, left.length() - 1);
right = right.substring(1);
text = left + "..." + right;
}
}
return text;
}
private void updateStatus()
{
String stat = "" + formatSize(current) +
" of " + formatSize(size) + " (at " +
formatSize(bytesPerSecond(current)) + "/sec)";
status.setText(stat);
}
private String formatSize(int size)
{
if (size >= K) return "" + (size / K) + "K";
else return "" + size;
}
private int bytesPerSecond(int current)
{
long elapsed = System.currentTimeMillis() - mark;
double speed = current / elapsed;
return (int)(1000 * speed);
}
public int timeLeftInMillis(int current)
{
long elapsed = System.currentTimeMillis() - mark;
double speed = elapsed / (double)current;
double remaining = size - current;
int expected = (int)(speed * remaining);
return expected + 1;
}
private String formatTime(long millis)
{
return timeFormatter.format(new Date(millis));
}
public boolean isCancelled()
{
return cancelled;
}
public void actionPerformed(ActionEvent event)
{
if (event.getSource() instanceof JButton)
{
cancelled = true;
}
}
}
|
The setSize() method implements the ProgressReporter
interface and sets the total range to monitor. It also marks the
current time in milliseconds so that time-related progress can be
reported. At the same time, the JProgressBar model is updated and
the current position is set to zero (0).
The setProgress()
method implements another ProgressReporter interface
and does most of the real work, much of it by calling internal methods
like updateStatus() , formatSize() , bytesPerSecond() ,
and timeLeftInMillis() , which provide calculations
and formatting for the progress text. The updateStatus()
method builds the status field string and calls on formatSize
and bytesPerSecond to format the information. The bytesPerSecond()
method calls on timeLeftInMillis() to estimate elapsed
and remaining time.
The isCancelled() method implements the final ProgressReporter
interface and works with actionPerformed()
to set the flag if the users presses the Cancel button. The ProgressDialog
is registered as an ActionListener to catch the button
press from JButton .
Conclusion
The JFC provides a solid foundation for user interface design,
making the construction of reusable classes virtually effortless.
This article showed you how to put together a reusable progress
dialog class with easy to use input and output stream filters. The
net effect is a clean user interface element that can ensure users
have a good experience when bytes are transferred over a stream
when the time it takes may be longer than a few seconds. The user
feedback is concise and the implementation is simple and flexible.
The JFC stands well above the crowd when it comes to simple user
interface programming, without sacrificing flexibility. The progress
dialog and supporting classes provide a quick example of how you
can develop components that enrich the user experience and enhance
your program in valuable ways, without requiring much effort.
In this section, The Swing Connection publishes
articles that are written by third-party developers and may contain
code, statements, inferences, and programming techniques that
are not necessarily endorsed by Sun Microsystems, Inc., or by
The Swing Connection.
|