X

Making Progress With Swing's Progress Monitoring API

Guest Author

by Jennie Hall
Updated Jan. 23, 2009

In this tip, you'll learn how to use Swing's progress indicator support to monitor and report on the progress of long-running operations. It is a good practice to keep users informed as they interact with an application; one way to do this is with a progress bar. A progress bar is an animated image that indicates the degree of completion of a given task. The animation typically looks like a rectangular bar that fills in as the task becomes more complete.

Swing's Progress Monitoring API consists of three classes that enable the use of progress bars. JProgressBar subclasses JComponent and is a graphical component that illustrates the progress of an operation. It can be embedded within other graphical components. ProgressMonitor subclasses Object and is not itself a graphical component. It monitors a task and pops a dialog box with a progress bar in it. ProgressMonitorInputStream is a stream filter with an associated progress monitor. As the stream is read, the progress monitor automatically receives updates on the number of bytes read and displays the percentage of work completed in its dialog box.

The Java Tutorial provides some good rules of thumb that help to determine the appropriate class to use in a given situation. For example, use JProgressBar when you need more than one progress bar or you would like more control over the configuration of the progress bar. If you need a convenient way to cancel the monitored task or to allow the user to dismiss the dialog box while continuing to run the task in the background, ProgressMonitor provides for this. ProgressMonitor also features a modifiable status note in its dialog box that can be updated periodically by your application. The sample application for this tip uses ProgressMonitor.

The Sample Application

The sample application copies files located in a source directory (in) to a destination directory (out). It has a Swing GUI that allows the user to launch the copy operation by clicking the Copy Files button as shown in Figure 1.

Figure 1: Sample Application

Upon the launch of the copy operation, the application creates a progress monitor that keeps track of the amount of work completed and displays this information in a dialog containing a progress bar. The application also writes output regarding the progress of the operation to the console as shown in Figure 2.

Figure 2: Dialog containing progress bar

As shown above, the GUI displays the number of kilobytes copied and the file name of the file currently being copied. The user may cancel the operation at any time by clicking the Cancel button. After the copy operation completes, the GUI appears as shown in Figure 3:

Stepping Through the Sample Application

The sample application consists of a class, ProgressMonitorExample, that extends javax.swing.JPanel and implements java.awt.event.ActionListener and java.beans.PropertyChangeListener. ProgressMonitorExamples main() method tells the event dispatch thread to schedule the execution of a Runnable that creates the application GUI:

public static void main(String[] args) {

// tell the event dispatch thread to schedule the execution

// of this Runnable (which will create the example app GUI) for a later time

SwingUtilities.invokeLater(new Runnable() {

public void run() {

// create example app window

JFrame frame = new JFrame("Progress Monitor Example");

// application will exit on close

frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

frame.setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);


// create example app content pane

// ProgressMonitorExample constructor does additional GUI setup

JComponent contentPane = new ProgressMonitorExample();

contentPane.setOpaque(true);

frame.setContentPane(contentPane);


...

ProgressMonitorExample contains an inner class, CopyFiles, that extends javax.swing.SwingWorker. When the user clicks the Copy Files button, ProgressMonitorExamples actionPerformed() method receives the event, creates a new ProgressMonitor, and starts the file-copying operation on a background thread. Here's the code that creates the ProgressMonitor:

    public void actionPerformed(ActionEvent event) {
// make sure there are files to copy
File srcDir = new File("in");
if (srcDir.exists() && (srcDir.listFiles() != null && srcDir.listFiles().length > 0)) {
// set up the destination directory
File destDir = new File("out");
// create the progress monitor
progressMonitor = new ProgressMonitor(ProgressMonitorExample.this,
"Operation in progress...",
"", 0, 100);
progressMonitor.setProgress(0);

...

ProgressMonitor has a single constructor. The first argument is the parent component to the progress monitor's dialog box. The second argument, of type Object, is displayed on the dialog box. It should be a string, icon, or component. This example supplies the constructor with a string that lets the user know that the requested operation is underway. The third argument is an optional status note that also appears on the dialog box. This status note can be updated periodically as the monitored task runs. Set this value to null if no status note is necessary. The fourth and fifth arguments are the minimum and maximum values for the progress bar in the progress monitor dialog box.

The code below, also excerpted from actionPerformed(), creates a new instance of CopyFiles, adds ProgressMonitorExample as a property change listener on the instance, and executes the instance:

    // schedule the copy files operation for execution on a background thread
operation = new CopyFiles(srcDir, destDir);
// add ProgressMonitorExample as a listener on CopyFiles;
// of specific interest is the bound property progress
operation.addPropertyChangeListener(this);
operation.execute();
// we're running our operation; disable copy button
copyButton.setEnabled(false);

CopyFiles subclasses SwingWorker, so the call to inherited method execute() schedules CopyFiles for execution on a background thread and returns immediately. Time-consuming activities should always run on a background thread rather than the event dispatch thread. This way, the GUI remains responsive.

Although the file-copying operation has begun, the progress monitor dialog box doesn't pop up right away. By default, ProgressMonitor waits for 500 ms before making a decision on whether or not to show the dialog box at all. After this time period has elapsed, if ProgressMonitor determines that the monitored operation has already completed or is likely to complete before the dialog box can be displayed, ProgressMonitor does not pop the dialog box. ProgressMonitors method setMillisToDecideToPopup() controls this setting. setMillisToPopup() sets the estimated amount of time it will take the dialog box to appear; the default value for this property is 2 seconds.

The real work of copying the files occurs in doInBackground(), an abstract method on SwingWorker that CopyFiles overrides. Here's a partial listing:

// perform time-consuming copy task in the worker thread

@Override

public Void doInBackground() {

int progress = 0;

// initialize bound property progress (inherited from SwingWorker)

setProgress(0);

// get the files to be copied from the source directory

File[] files = srcDir.listFiles();

// determine the scope of the task

long totalBytes = calcTotalBytes(files);

long bytesCopied = 0;


while (progress < 100 && !isCancelled()) {

// copy the files to the destination directory

for (File f : files) {

File destFile = new File(destDir, f.getName());

long previousLen = 0;


try {

InputStream in = new FileInputStream(f);

OutputStream out = new FileOutputStream(destFile);

byte[] buf = new byte[1024];

int counter = 0;

int len;


while ((len = in.read(buf)) > 0) {

out.write(buf, 0, len);

counter += len;

bytesCopied += (destFile.length() - previousLen);

previousLen = destFile.length();

if (counter > PROGRESS_CHECKPOINT || bytesCopied == totalBytes) {

// get % complete for the task

progress = (int)((100 \* bytesCopied) / totalBytes);

counter = 0;

CopyData current = new CopyData(progress, f.getName(), getTotalKiloBytes(totalBytes), getKiloBytesCopied(bytesCopied));

// set new value on bound property

// progress and fire property change event

setProgress(progress);


// publish current progress data for copy task

publish(current);

}

}

in.close();

out.close();

} catch (IOException e) {

e.printStackTrace();

}

...

doInBackground() gets any files located in the in directory and copies them one by one to the out directory. Each time a specified number of bytes have been copied, the application calculates what percentage of the total number of bytes has been copied so far, then creates an instance of the inner class CopyData to hold this information along with the total number of kilobytes, the number of kilobytes copied so far, and the filename of the file currently being copied. The application then updates the bound property progress with the calculated percentage, firing a property change event in the process. The call to publish() makes the copy task's current progress data available for processing in the event dispatch thread.

ProgressMonitorExamples propertyChange() method extracts the progress value from the property change event. It then updates the progress monitor animation by calling its setProgress() and passing the progress value. Here's the code:

    // executes in event dispatch thread
public void propertyChange(PropertyChangeEvent event) {
// if the operation is finished or has been canceled by
// the user, take appropriate action
if (progressMonitor.isCanceled()) {
operation.cancel(true);
} else if (event.getPropertyName().equals("progress")) {
// get the % complete from the progress event
// and set it on the progress monitor
int progress = ((Integer)event.getNewValue()).intValue();
progressMonitor.setProgress(progress);
}
}

Notice that ProgressMonitor provides a convenient way to determine if the dialog has been canceled by the user. The sample application responds to a user cancellation by terminating the monitored activity, but in other situations it might be appropriate to allow the user to dismiss the dialog box while the activity continues to run in the background.

By overriding the SwingWorker method process(), CopyFiles can use the progress data made available by the call to publish() to update the GUI. process() executes in the event dispatch thread, so it is safe to update Swing components in this method. Here's the code:

// process copy task progress data in the event dispatch thread

@Override

public void process(List data) {

if(isCancelled()) { return; }

CopyData update = new CopyData(0, "", 0, 0);

for (CopyData d : data) {

// progress updates may be batched, so get the most recent

if (d.getKiloBytesCopied() > update.getKiloBytesCopied()) {

update = d;

}

}


// update the progress monitor's status note with the

// latest progress data from the copy operation, and

// additionally append the note to the console

String progressNote = update.getKiloBytesCopied() + " of "

+ update.getTotalKiloBytes() + " kb copied.";

String fileNameNote = "Now copying " + update.getFileName();


if (update.getProgress() < 100) {

progressMonitor.setNote(progressNote + " " + fileNameNote);

console.append(progressNote + "\\n" + fileNameNote + "\\n");

} else {

progressMonitor.setNote(progressNote);

console.append(progressNote + "\\n");

}

}

As shown above, process() updates the progress monitor's status note with the number of kilobytes copied so far and the filename of the file currently being copied, then appends this information to the console.

When its background operation is finished, CopyFiles sets its own state to done and invokes the done() method in the event dispatch thread. done() invokes the SwingWorker method get(), which returns the final result of the background task. In the case of the sample application, however, there is no final result to be processed. The sample application calls get() to determine whether or not the background task was canceled before completion and responds appropriately:

// perform final updates in the event dispatch thread

@Override

public void done() {

try {

// call get() to tell us whether the operation completed or

// was canceled; we don't do anything with this result

Void result = get();

console.append("Copy operation completed.\\n");

} catch (InterruptedException e) {


} catch (CancellationException e) {

// get() throws CancellationException if background task was canceled

console.append("Copy operation canceled.\\n");

} catch (ExecutionException e) {

console.append("Exception occurred: " + e.getCause());

}

// reset the example app

copyButton.setEnabled(true);

progressMonitor.setProgress(0);

}


Running the Sample Application

To run the sample application, download the sample code and unzip it. The sample application assumes that there are files to copy in the in directory located under the project root, so add some (preferably large) files of your choice to this directory. Launch NetBeans and select File -> Open Project. In the Open Project dialog box, navigate to the directory where you unzipped the sample code and select the folder progressMonitorExample. Select the Open as Main Project check box. Click Open Project Folder. Right-click the progressMonitorExample project and select Build, then right-click the project again and select Run.

References and Resources

Sample code for this tip
The Java Tutorial

About the Author

Jennie Hall is a lead developer working in the financial sector.

Join the discussion

Comments ( 13 )
  • Richard Osbaldeston Tuesday, December 16, 2008

    Notice your code modifies a number of unsynchronised fields in the CopyFiles object from a background thread but at the same time tries to access those same fields from the EDT thread in propertyChange().

    Not a critical failure but is that entirely safe?


  • Richard Osbaldeston Tuesday, December 16, 2008

    Notice your code modifies a number of unsynchronised fields in the CopyFiles object from a background thread but at the same time tries to access those same fields from the EDT thread in propertyChange().

    Not a critical failure but is that entirely safe?


  • osbald Tuesday, December 16, 2008
  • Jennie Hall Tuesday, December 16, 2008

    Hi Richard,

    Thanks for your comments. I agree that some minor modifications would make for a stronger example, so stay tuned for an updated version of the sample application.

    Thanks, Jennie


  • Jennie Hall Friday, January 23, 2009

    The sample application and tip have been updated as mentioned in my previous post.


  • sathish Wednesday, January 28, 2009

    Thank you


  • omar bahert mohmad ahmed bayoumi Wednesday, January 28, 2009

    javascript is a very good program


  • KVRavula Wednesday, January 28, 2009

    Want to know onow the reason for

    math simplicity


  • guest Thursday, January 29, 2009

    thank


  • bhoomika shah Thursday, January 29, 2009

    plz send me the code in java frame work on the Geo-visualization of contour data


  • Marc Wednesday, February 4, 2009

    Hi Jennie, in

    "public Void doInBackground()",

    I think that you mean "void"?

    Cheers

    Marc


  • Marc Wednesday, February 4, 2009

    Hi Jennie, in

    "public Void doInBackground()",

    I think that you mean "void"?

    Cheers

    Marc


  • Infernoz Friday, February 6, 2009

    The ProgressMonitor class is not EDT safe, like much of the high level Swing code, but why resort to the confusing SwingWorker, PropertyChangeEvents, and EDT unsafe access to progressMonitor.isCanceled()?

    I took the source code and rewrote it to provide:

    start time

    time elapsed

    percentage done

    estimated time left

    estimated completion time

    (including fixing time zone and thread safety nasties with SimpleDateFormat)

    .. and added a chaining facility to allow progress of lower level tasks to update the progress for higher level tasks, and all the instances hide EDT concerns from the user code, via internal Runnables on the EDT.

    I have four gripes from my work:

    1. A progress monitor, without a time estimate, is useless for tasks which take a long time, this is a basic requirement.

    2. The SwingWorker is an Event based kludge, and is only required, because Sun do not provide a framework to separate long running code from the EDT, via a concurrent message queue; unlike the Amiga, many years ago, with its MsgPorts and Signals. I personally regard the SwingWorker as pointless, Runnable instances, on the EDT, can be much cheaper and more flexible than Events.

    4. Why on earth does EventQueue.invokeAndWait throw an Exception when the EDT attempts to queue Runnables for itself, just run them already; I had to make a final static utility method, with EventQueue.isDispatchThread(), to work around this pointless Exception.


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.Captcha