by Jennie HallUpdated Jan. 23, 2009In 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<copydata> 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"); } } </copydata>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 ApplicationTo 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 ResourcesSample code for this tip The Java Tutorial About the AuthorJennie Hall is a lead developer working in the financial sector. Software DevelopmentJavaTechnology Industry