Search code examples
javaswinginputstreamprogressmonitor

How to use ProgressMonitorInputStream


I know I must be missing something very obvious, but whenever I try to use the ProgressMonitorInputStream when copying a file, I never get the ProgressDialog popup.

The examples I see don't seem to do much other than wrap their input stream within the ProgressMonitorInputStream.

The docs say:

This creates a progress monitor to monitor the progress of reading the input stream. If it's taking a while, a ProgressDialog will be popped up to inform the user. If the user hits the Cancel button an InterruptedIOException will be thrown on the next read. All the right cleanup is done when the stream is closed.

Here is an extremely simple example I put together that never pops up the dialog with a large file even if I setMillisToPopup() to an insanely small number.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.ProgressMonitorInputStream;
import javax.swing.SwingWorker;

public class ProgressBarDemo extends JFrame {

   private static final long serialVersionUID = 1L;

   private JButton button;

   ProgressBarDemo() 
   {
      button = new JButton("Click me!");
      ButtonActionListener bal = new ButtonActionListener();
      button.addActionListener(bal);

      this.getContentPane().add(button);
   }

   private class ButtonActionListener implements ActionListener 
   {

      @Override
      public void actionPerformed(ActionEvent e) {
         // TODO Auto-generated method stub
         Worker worker = new Worker();
         worker.execute();
         button.setEnabled(false);
      }
   }

   public void go() {

      this.setLocationRelativeTo(null);
      this.setVisible(true);
      this.pack();
      this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }

   private class Worker extends SwingWorker<Void, Void>
   {

      private void copyFile() {

         File file = new File("/Users/mypath/Desktop/WirelessDiagnostics.tar.gz");

         BufferedInputStream bis;
         BufferedOutputStream baos;
         try {
            bis = new BufferedInputStream(new FileInputStream(file));
            ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(
                  ProgressBarDemo.this,
                  "Reading... " + file.getAbsolutePath(),
                  bis);

            pmis.getProgressMonitor().setMillisToPopup(10);
            baos = new BufferedOutputStream(new FileOutputStream("/Users/mypath/Desktop/NewWirelessDiagnostics.tar.gz"));

            byte[] buffer = new byte[2048];
            int nRead = 0;

            while((nRead = pmis.read(buffer)) != -1) {
               baos.write(buffer, 0, nRead);
            }

            pmis.close();
            baos.flush();
            baos.close();
         } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
         } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
         }
      }

      @Override
      protected Void doInBackground() throws Exception {
         // TODO Auto-generated method stub
         copyFile();
         return null;
      }

      @Override
      protected void done() {
         button.setEnabled(true);
      }
   }
}

public class TestProj {

   public static void main(String[] args) {
      ProgressBarDemo demo = new ProgressBarDemo();
      demo.go();
   }
}

Any suggestions?


Solution

  • You are calling copyFile from within the context of the Event Dispatching Thread, this means the EDT is unable to respond to new events or paint requests until after the method returns.

    Try placing the call within it's own Thread context...

    Thread t = new Thread(new Runnable(
        public void run() {
            copyFile();
        }
    ));
    t.start();
    

    Equally, you could use a SwingWorker, it's a little bit of overkill, but you get the benefit of the PropertyChangeListener or it's done method, which could be used to re-enable the JButton, should you want to prevent people from clicking the button while a copy operation is in progress

    See Concurrency in Swing and Worker Threads and SwingWorker for more details

    Updated with example

    Copying a 371mb file, across the local disk...

    Copy

    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.io.BufferedInputStream;
    import java.io.BufferedOutputStream;
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.ProgressMonitorInputStream;
    import javax.swing.SwingWorker;
    
    public class ProgressBarDemo extends JFrame {
    
        private static final long serialVersionUID = 1L;
    
        private JButton button;
    
        ProgressBarDemo() {
            button = new JButton("Click me!");
            ButtonActionListener bal = new ButtonActionListener();
            button.addActionListener(bal);
    
            this.getContentPane().add(button);
        }
    
        public void go() {
    
            this.setLocationRelativeTo(null);
            this.setVisible(true);
            this.pack();
            this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        }
    
        private void copyFile() {
    
            File file = new File("...");
    
            BufferedInputStream bis;
            BufferedOutputStream baos;
            try {
                bis = new BufferedInputStream(new FileInputStream(file));
                ProgressMonitorInputStream pmis = new ProgressMonitorInputStream(
                                this,
                                "Reading... " + file.getAbsolutePath(),
                                bis);
    
                pmis.getProgressMonitor().setMillisToPopup(10);
                baos = new BufferedOutputStream(new FileOutputStream("..."));
    
                byte[] buffer = new byte[2048];
                int nRead = 0;
    
                while ((nRead = pmis.read(buffer)) != -1) {
                    baos.write(buffer, 0, nRead);
                }
    
                pmis.close();
                baos.flush();
                baos.close();
            } catch (FileNotFoundException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IOException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    
        private class ButtonActionListener implements ActionListener {
    
            @Override
            public void actionPerformed(ActionEvent e) {
    
                button.setEnabled(false);
                SwingWorker worker = new SwingWorker() {
    
                    @Override
                    protected Object doInBackground() throws Exception {
                        copyFile();
                        return null;
                    }
    
                    @Override
                    protected void done() {
                        button.setEnabled(true);
                    }
    
                };
    
                worker.execute();
            }
    
        }
    
        public static void main(String[] args) {
            ProgressBarDemo demo = new ProgressBarDemo();
            demo.go();
        }
    }
    

    Remember, there is overhead involved in setting up the window and displaying, which needs to be factored in. The system may "want" to display a window, but by the time the system has set it up and is prepared to display it, the steam may have finished copying...

    Extended Example

    nb: I don't really like the ProgressMonitor API as I've not been able to find where the UI is synchronised with the EDT, this can cause issues in Java 7 & 8

    You could formalise the idea into a self contained worker, for example...

    public class CopyWorker extends SwingWorker {
    
        private File source;
        private File dest;
        private Component parent;
    
        private ProgressMonitorInputStream pmis;
    
        public CopyWorker(Component parent, File source, File dest) {
            this.parent = parent;
            this.source = source;
            this.dest = dest;
        }
    
        @Override
        protected Object doInBackground() throws Exception {
            try (InputStream is = new FileInputStream(source)) {
                try (OutputStream os = new FileOutputStream(dest)) {
                    pmis = new ProgressMonitorInputStream(
                            parent,
                            "Copying...",
                            is);
    
                    pmis.getProgressMonitor().setMillisToPopup(10);
    
                    byte[] buffer = new byte[2048];
                    int nRead = 0;
    
                    while ((nRead = pmis.read(buffer)) != -1) {
                        os.write(buffer, 0, nRead);
                    }
                }
            }
            return null;
        }
    
        @Override
        protected void done() {
            try {
                pmis.close();
            } catch (Exception e) {
            }
        }
    
    }
    

    This attempts to contain the functionality, but also deals with the cleanup of the ProgressMonitorInputStream within the done method, making sure that it's done within the EDT. I'd personally attach a PropertyChangeListener to it and monitor the done property to determine when the worker has completed and examine the return result in order to pick up any exceptions, this gives you the ability to handle the exceptions in your own way and de-couples the worker from your process