Updating the GUI in real time from SwingWorker

1.3k views Asked by At

Ok, this is a follow-up question to my question from yesterday, "Error handling in SwingWorker."

In consideration of the fact that it might be ok to call SwingUtilities#invokeAndWait() inside of SwingWorker#doInBackground(), I want to push it a step further and ask: might it also be ok to call SwingUtilities#invokeLater() inside the background thread?

Another SSCCE to illustrate:

public class NewClass extends javax.swing.JFrame {

    public NewClass() {
        jScrollPane1 = new javax.swing.JScrollPane();
        atable = new javax.swing.JTable();
        jButton1 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        atable.setModel(new javax.swing.table.DefaultTableModel(
            new Object[][]{
                {null, null},
                {null, null},
                {null, null},
                {null, null}
            },
            new String[]{
                "Title 1", "Title 2"
            }
        ));
        jScrollPane1.setViewportView(atable);

        jButton1.setText("Go");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 380, Short.MAX_VALUE)
                    .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                        .addGap(0, 0, Short.MAX_VALUE)
                        .addComponent(jButton1)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 228, Short.MAX_VALUE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jButton1)
                .addContainerGap())
        );

        pack();
        setLocationByPlatform(true);
        setVisible(true);
    }

    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
        new Task();
    }

    class Task extends javax.swing.SwingWorker<Void, Object[]> {

        DeterminateLoadingFrame dlf;

        public Task() {
            dlf = new DeterminateLoadingFrame();
            atable.setModel(new javax.swing.table.DefaultTableModel(
                new Object[][]{},
                new String[]{
                    "Title 1", "Title 2"
                }
            ));
            dlf.setMaximum(1000);
            execute();
        }

        @Override
        protected void process(java.util.List<Object[]> chunks) {
            for (Object[] row : chunks) {
                ((javax.swing.table.DefaultTableModel) atable.getModel()).addRow(row);
            }
        }

        @Override
        protected Void doInBackground() throws Exception {
            for (int i = 0; i < 1000; i++) {
                final int j = i;
                Object[] row = new Object[2];
                row[0] = "row " + i;
                row[1] = Math.round(Math.random() * 100);
                publish(row);
                javax.swing.SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        dlf.changeText("Processing " + j + " of " + 1000);
                        dlf.setValue(j);
                    }
                });
                Thread.sleep(100);

            }
            return null;

        }

        @Override
        protected void done() {
            if (!isCancelled()) {
                try {
                    get();
                } catch (java.util.concurrent.ExecutionException | InterruptedException e) {
                    e.printStackTrace();
                }
            }
            dlf.dispose();
        }

    }

    class DeterminateLoadingFrame extends javax.swing.JFrame {

        javax.swing.JLabel jLabel1;
        javax.swing.JProgressBar jProgressBar1;

        public DeterminateLoadingFrame() {

            jProgressBar1 = new javax.swing.JProgressBar();
            jLabel1 = new javax.swing.JLabel();

            setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE);
            setTitle("Loading");

            jLabel1.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
            jLabel1.setText("Loading ...");

            javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
            getContentPane().setLayout(layout);
            layout.setHorizontalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                    .addContainerGap()
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING)
                        .addComponent(jLabel1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 182, Short.MAX_VALUE)
                        .addComponent(jProgressBar1, javax.swing.GroupLayout.Alignment.LEADING, javax.swing.GroupLayout.DEFAULT_SIZE, 182, Short.MAX_VALUE))
                    .addContainerGap())
            );
            layout.setVerticalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGroup(layout.createSequentialGroup()
                    .addContainerGap()
                    .addComponent(jLabel1)
                    .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                    .addComponent(jProgressBar1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            );

            pack();
            setAlwaysOnTop(true);
            setLocationRelativeTo(null);
            setVisible(true);
            setAlwaysOnTop(false);
        }

        public void changeText(String message) {
            this.jLabel1.setText(message);
            System.out.println(message);
        }

        public void setMaximum(int max) {
            jProgressBar1.setMaximum(max);
        }

        public void setValue(int n) {
            jProgressBar1.setValue(n);
        }

    }

    public static void main(String args[]) {
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new NewClass();
            }
        });
    }

    private javax.swing.JTable atable;
    private javax.swing.JButton jButton1;
    private javax.swing.JScrollPane jScrollPane1;
}

In this SSCCE, we are updating the DeterminateLoadingFrame to update the user about what is happening, while publishing the results through publish() and process().

This seems as unorthodox as the original question asked yesterday. Yet I am unable to come up with any reason it would be not a good idea. We are adhering to proper threading policies. And updating the GUI in this way would seem (to me) to be incredibly useful and versatile. Much moreso, in fact, than process() and publish().

Further: accomplishing what the SSCCE is illustrating would be harder strictly using process() and publish() because how would you pass the value of i to the process() method?

2

There are 2 answers

1
JB Nizet On

It's correct, but it doesn't benefit from all the work done by the SwingWorker to batch updates and let you avoid using these low-level SwingUtilities methods. It also couples the background process and the UI updates instead of decoupling them elegantly.

How to pass i to the publish method: create a class containing this counter and the row:

private static class Update {
    private int counter;
    private Object[] row;

    // constructor, getters omitted for brevity
}

...

class Task extends SwingWorker<Void, Update> {

    @Override
    protected Void doInBackground() throws Exception {
        for (int i = 0; i < 1000; i++) {
            Object[] row = new Object[2];
            row[0] = "row " + i;
            row[1] = Math.round(Math.random() * 100);
            publish(new Update(i, row);
            Thread.sleep(100);

        }
        return null;
    }

    @Override
    protected void process(java.util.List<Update> chunks) {
        for (Update update : chunks) {
            ((DefaultTableModel) atable.getModel()).addRow(update.getRow());   
        }
        int lastCounter = chunks.get(chunks.size() - 1).getCounter();
        dlf.changeText("Processing " + lastCounter + " of " + 1000);
        dlf.setValue(lastCounter);
    }
    ...
}
2
Hovercraft Full Of Eels On

Another solution is to use SwingWorker's innate SwingPropertyChangeSupport and notify all listeners of changes of pertinent bound properties. You could use the progress bound property that SwingWorkers already have, but you can only use this if your property will go from 0 to 100. If you have one going to a different value (here 1000), you'll have to use your own. Since the PropertyChangeSupport object is a SwingPropertyChangeSupport, then all notifications will be done on the Swing event thread. I like the uncoupling that this provides -- the SwingWorker need know nothing about who is listening to what, or what is done to the information.

e.g.,

class Task extends javax.swing.SwingWorker<Void, Object[]> {

  public static final String ROW_COUNT = "row count";
  DeterminateLoadingFrame dlf;
  private int rowCount = 0;

  public Task() {
     dlf = new DeterminateLoadingFrame();  // and I would pass this in from the GUI
     atable.setModel(new javax.swing.table.DefaultTableModel(
           new Object[][] {}, new String[] { "Title 1", "Title 2" }));
     dlf.setMaximum(1000);
     // execute();  // I reserve this for the calling code.
  }

  public int getRowCount() {
     return rowCount;
  }

  public void setRowCount(int newValue) {
     int oldValue = this.rowCount;
     this.rowCount = newValue;

     firePropertyChange(ROW_COUNT, oldValue, newValue);
  }

  @Override
  protected void process(java.util.List<Object[]> chunks) {
     for (Object[] row : chunks) {
        ((javax.swing.table.DefaultTableModel) atable.getModel())
              .addRow(row);
     }
  }

  @Override
  protected Void doInBackground() throws Exception {
     for (int i = 0; i < 1000; i++) {
        final int j = i;
        Object[] row = new Object[2];
        row[0] = "row " + i;
        row[1] = Math.round(Math.random() * 100);
        publish(row);
        setRowCount(j);
        Thread.sleep(100);

     }
     return null;

  }

  // .....

and

private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
  final Task task = new Task();
  task.addPropertyChangeListener(new PropertyChangeListener() {

     @Override
     public void propertyChange(PropertyChangeEvent evt) {
        if (Task.ROW_COUNT.equals(evt.getPropertyName())) {
           int rowCount = task.getRowCount();
           // do what you need with rowCount here
        }
     }
  });
  task.execute(); // again I usually call this here
}

As an aside:

  • You should use a dialog window such as a JDialog for any secondary windows, not another JFrame.
  • Try to get unnecessary NetBeans-generated code out of your SSCCE. It only serves to distract from the code that is the meat of your question.