Making Progress With Swing's Progress Monitoring API

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. ProgressMonitorExample's 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, ProgressMonitorExample's 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. ProgressMonitor's 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.

ProgressMonitorExample's 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.

Comments:

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?

Posted by Richard Osbaldeston on December 15, 2008 at 10:45 PM PST #

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?

Posted by Richard Osbaldeston on December 15, 2008 at 10:46 PM PST #

See comment on dzone http://www.dzone.com/links/making_progress_with_swings_progress_monitoring_a.html#66451

Posted by osbald on December 15, 2008 at 11:16 PM PST #

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

Posted by Jennie Hall on December 16, 2008 at 05:08 AM PST #

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

Posted by Jennie Hall on January 23, 2009 at 05:42 AM PST #

Thank you

Posted by sathish on January 28, 2009 at 02:59 AM PST #

javascript is a very good program

Posted by omar bahert mohmad ahmed bayoumi on January 28, 2009 at 08:19 AM PST #

Want to know onow the reason for
math simplicity

Posted by KVRavula on January 28, 2009 at 08:45 AM PST #

thank

Posted by guest on January 28, 2009 at 07:05 PM PST #

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

Posted by bhoomika shah on January 28, 2009 at 10:23 PM PST #

Hi Jennie, in
"public Void doInBackground()",
I think that you mean "void"?

Cheers
Marc

Posted by Marc on February 04, 2009 at 12:29 AM PST #

Hi Jennie, in
"public Void doInBackground()",
I think that you mean "void"?

Cheers
Marc

Posted by Marc on February 04, 2009 at 12:30 AM PST #

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.

Posted by Infernoz on February 06, 2009 at 05:51 AM PST #

Post a Comment:
Comments are closed for this entry.
About

John O'Conner

Search

Categories
Archives
« April 2014
SunMonTueWedThuFriSat
  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today