SwingWorker not updating progressbar from inside process

572 Views Asked by At

I have a Java 8 Swing app and need to add a time-consuming operation to it when the user clicks a new button. I think its a perfect use case for a SwingWorker although I've never written one before. The full source code and reproducible Swing app is here.

When the user clicks a button, the app must collect information from a few different sources and then start this background operation. It will compute an InputAnalysis and then return that InputAnalysis back to the click handler in the EDT to update the UI. While it works I want it to update a JProgressBar as well so the user sees progress being made. My best attempt thus far:

package com.example.swingworker.suchwow;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.List;

public class MegaApp {

    public static void main(String[] args) {
        new MegaApp().run();
    }

    public void run() {

        SwingUtilities.invokeLater(() -> {

            System.out.println("starting app");

            JFrame.setDefaultLookAndFeelDecorated(true);

            JFrame mainWindow = new JFrame("Some Simple App!");
            mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            mainWindow.setResizable(true);

            mainWindow.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent e) {
                System.out.println("app is shutting down");
                System.exit(0);
                }
            });


            JPanel jPanel = new JPanel();

            JTextField superSecretInfoTextField = new JTextField();
            JButton analyzeButton = new JButton("Analyze");
            JProgressBar progressBar = new JProgressBar();

            superSecretInfoTextField.setPreferredSize(new Dimension(200,
                (int)superSecretInfoTextField.getPreferredSize().getHeight()));

            jPanel.add(superSecretInfoTextField);
            jPanel.add(analyzeButton);
            jPanel.add(progressBar);

            progressBar.setValue(0);

            analyzeButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {

                    // on click, scoop some info from the input and run a time-consuming task.
                    // usually takes 20 - 30 seconds to run, and I'd like to be updating the progress
                    // bar during that time.
                    //
                    // also need to handle cases where the task encounters a POSSIBLE error and needs to
                    // communicate back to the EDT to display a JOPtionPane to the user; and then get the
                    // user's response back and handle it.
                    //
                    // also need to handle the case where the long running task encounters both a checked
                    // and unchecked/unexpected exception
                    String superSecretInfo = superSecretInfoTextField.getText();

                    // here is where we start the long-running task. ideally this needs to go into a SwingWorker
                    // however there is a somewhat complex back-and-forth-communication required. see the analysis
                    // method comments for details
                    try {

                        InputAnalysis analysis = analysisService_analyze(progressBar, superSecretInfo, mainWindow);
                        superSecretInfoTextField.setText(analysis.getSuperSecretAnswer());

                    } catch (IOException ex) {
                        System.out.println(ex.getMessage());
                        JOptionPane.showMessageDialog(
                                mainWindow,
                                "Something went wrong",
                                "Aborted!",
                                JOptionPane.WARNING_MESSAGE);
                    }


                    // comment the above try-catch out and uncomment all the worker code below to switch over
                    // to the async/non-blocking worker based method

//                    MegaWorker analysisWorker = new MegaWorker(mainWindow, progressBar, superSecretInfo);
//                    analysisWorker.addPropertyChangeListener(evt -> {
//
//                        if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
//                            try {
//                                // this is called on the EDT
//                                InputAnalysis asyncAnalysis = analysisWorker.get();
//                                superSecretInfoTextField.setText(asyncAnalysis.getSuperSecretAnswer());
//
//                            } catch (Exception ex) {
//                                System.out.println(ex.getMessage());
//                            }
//                        }
//
//                    });
//
//                    analysisWorker.execute();


                }
            });

            mainWindow.add(jPanel);
            mainWindow.pack();
            mainWindow.setLocationRelativeTo(null);
            mainWindow.setVisible(true);

            System.out.println("application started");

        });

    }

    public InputAnalysis analysisService_analyze(JProgressBar progressBar,
                                                 String superSecretInfo,
                                                 JFrame mainWindow) throws IOException {
        progressBar.setValue(25);

        // simulate a few seconds of processing
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
            throw new RuntimeException("SOMETHIN BLEW UP");
        }

        // now we are ready to analyze the input which itself can take 10 - 15 seconds but
        // we'll mock it up here
        if (superSecretInfo == null || superSecretInfo.isEmpty()) {

            // if the input is null/empty, we'll consider that a "checked exception"; something the
            // REAL code I'm using explicitly has a try-catch for because the libraries I'm using throw
            // them

            throw new IOException("ERMERGERD");

        } else if (superSecretInfo.equals("WELL_WELL_WELL")) {

            // here we'll consider this an unchecked exception
            throw new RuntimeException("DID NOT SEE THIS ONE COMING");

        }

        progressBar.setValue(55);

        // check to see if the input equals "KEY MASTER"; if it does we need to go back to the EDT
        // and prompt the user with a JOptionPane
        if (superSecretInfo.equalsIgnoreCase("KEY MASTER")) {

            int answer = JOptionPane.showConfirmDialog(
                    mainWindow,
                    "We have identified a KEY MASTER scenario. Do you wish to proceed?",
                    "Do you wish to proceed",
                    JOptionPane.YES_NO_OPTION);

            if (answer == JOptionPane.NO_OPTION) {

                // return a partial InputAnalysis and return
                Boolean isFizz = Boolean.TRUE;
                String superSecretAnswer = "HERE IS A PARTIAL ANSWER";
                Integer numDingers = 5;

                return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

            }

        }

        // if we get here, either KEY MASTER was not in the input or they chose to proceed anyway
        Boolean isFizz = superSecretInfo.length() < 5 ? Boolean.TRUE : Boolean.FALSE;
        String superSecretAnswer = "HERE IS A FULL ANSWER";
        Integer numDingers = 15;

        progressBar.setValue(100);

        return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

    }

    public class InputAnalysis {

        private Boolean isFizz;
        private String superSecretAnswer;
        private Integer numDingers;

        public InputAnalysis(Boolean isFizz, String superSecretAnswer, Integer numDingers) {
            this.isFizz = isFizz;
            this.superSecretAnswer = superSecretAnswer;
            this.numDingers = numDingers;
        }

        public Boolean getFizz() {
            return isFizz;
        }

        public void setFizz(Boolean fizz) {
            isFizz = fizz;
        }

        public String getSuperSecretAnswer() {
            return superSecretAnswer;
        }

        public void setSuperSecretAnswer(String superSecretAnswer) {
            this.superSecretAnswer = superSecretAnswer;
        }

        public Integer getNumDingers() {
            return numDingers;
        }

        public void setNumDingers(Integer numDingers) {
            this.numDingers = numDingers;
        }
    }

    public class MegaWorker extends SwingWorker<InputAnalysis,Integer> {

        private JFrame mainWindow;
        private JProgressBar progressBar;
        private String superSecretInfo;

        public MegaWorker(JFrame mainWindow, JProgressBar progressBar, String superSecretInfo) {
            this.mainWindow = mainWindow;
            this.progressBar = progressBar;
            this.superSecretInfo = superSecretInfo;
        }

        @Override
        protected void process(List<Integer> chunks) {

            progressBar.setValue(chunks.size() - 1);

        }

        @Override
        protected InputAnalysis doInBackground() throws Exception {

            publish(25);

            // simulate a few seconds of processing
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
                throw new RuntimeException("SOMETHIN BLEW UP");
            }

            // now we are ready to analyze the input which itself can take 10 - 15 seconds but
            // we'll mock it up here
            if (superSecretInfo == null || superSecretInfo.isEmpty()) {

                // if the input is null/empty, we'll consider that a "checked exception"; something the
                // REAL code I'm using explicitly has a try-catch for because the libraries I'm using throw
                // them

                throw new IOException("ERMERGERD");

            } else if (superSecretInfo.equals("WELL_WELL_WELL")) {

                // here we'll consider this an unchecked exception
                throw new RuntimeException("DID NOT SEE THIS ONE COMING");

            }

            publish(55);

            // check to see if the input equals "KEY MASTER"; if it does we need to go back to the EDT
            // and prompt the user with a JOptionPane
            if (superSecretInfo.equalsIgnoreCase("KEY MASTER")) {

                int answer = JOptionPane.showConfirmDialog(
                        mainWindow,
                        "We have identified a KEY MASTER scenario. Do you wish to proceed?",
                        "Do you wish to proceed",
                        JOptionPane.YES_NO_OPTION);

                if (answer == JOptionPane.NO_OPTION) {

                    // return a partial InputAnalysis and return
                    Boolean isFizz = Boolean.TRUE;
                    String superSecretAnswer = "HERE IS A PARTIAL ANSWER";
                    Integer numDingers = 5;

                    return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

                }

            }

            // if we get here, either KEY MASTER was not in the input or they chose to proceed anyway
            Boolean isFizz = superSecretInfo.length() < 5 ? Boolean.TRUE : Boolean.FALSE;
            String superSecretAnswer = "HERE IS A FULL ANSWER";
            Integer numDingers = 15;

            publish(100);

            return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

        }

    }

}

When I comment-out the try-catch block that houses my analysisService_analyze() call, and uncomment the code for my MegaWorker, the progress bar is still not updated properly.

Not required because all of the necessary code for an SSCCE is provided above, but if you are interested in building and running this code quickly I have prepared this SimpleApp repo on GitHub to save you some time. But not necessary for answering this question, again, all of the code is provided above. 100%.

2

There are 2 best solutions below

7
Hovercraft Full Of Eels On

Not

progressBar.setValue(chunks.size() - 1);

Since the size of the collection is not what you're after. Rather you want the value of the collection to be shown:

for (int item : chucks) {
    progressBar.setValue(item);
}

You also need to call get() on your worker when it has completed its work, either in its done() method or in a PropertyChangeListener that notifies you when the worker's state value is SwingWorker.StateValue.DONE

That same get() call will return the new InputAnalysis object on the EDT and will throw exceptions if any occur during the worker's run, so you can handle them there.

e.g.,

analyzeButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {

        Fizz fizz = fizzService.fetchFromWs(1234);

        // make this guy final
        final Analyzer analyzer = new Analyzer(progressBar, nameTextField.getText(), fizz);

        analyzer.addPropertyChangeListener(evt -> {
            // this is a call-back method and will be called in response to 
            // state changes in the SwingWorker
            if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                try {
                    // this is called on the EDT
                    InputAnalysis analysis = analyzer.get();

                    // do what you want with it here

                } catch (Exception e) {
                    e.printStackTrace();
                }                   
            }
        });

        analyzer.execute();

        // but now, how do I obtain the InputAnalysis instance?!
        // InputAnalysis analysis = null; // analyzer.getSomehow();
    }
}

(code not tested)

Side note: you can do away with the publish/process method pair by simply changing the worker's progress bound field to any value between 0 and 100. Then in the same PropertyChangeListener listen and respond to changes in this property.


For example, sort-of your code, using a skeleton InputAnalysis class:

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.swing.*;

public class SwingWorkerExample {

    private static void createAndShowGui() {
        SwGui mainPanel = new SwGui();

        JFrame frame = new JFrame("SwingWorker Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}
@SuppressWarnings("serial")
class SwGui extends JPanel {
    private JProgressBar progressBar = new JProgressBar(0, 100);
    private JTextArea textArea = new JTextArea(14, 40);
    private StartAction startAction = new StartAction("Start", this);

    public SwGui() {
        JPanel bottomPanel = new JPanel();
        bottomPanel.add(new JButton(startAction));

        progressBar.setStringPainted(true);
        textArea.setFocusable(false);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        setLayout(new BorderLayout());
        add(progressBar, BorderLayout.PAGE_START);
        add(scrollPane);
        add(bottomPanel, BorderLayout.PAGE_END);
    }

    public void appendText(String text) {
        textArea.append(text + "\n");
    }

    public void setProgressValue(int value) {
        progressBar.setValue(value);
    }
}
@SuppressWarnings("serial")
class StartAction extends AbstractAction {
    private SwGui gui;
    private AnalyzerWorker worker;
    private InputAnalysis inputAnalysis;

    public StartAction(String text, SwGui gui) {
        super(text);
        this.gui = gui;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        worker = new AnalyzerWorker();
        setEnabled(false); // turn off button
        gui.appendText("START");
        worker.addPropertyChangeListener(evt -> {
            if (evt.getPropertyName().equals("progress")) {
                int progress = (int) evt.getNewValue();
                gui.setProgressValue(progress);
                gui.appendText(String.format("Percent done: %03d%%", progress));
            } else if (evt.getPropertyName().equals("state")) {
                if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                    setEnabled(true);
                    try {
                        inputAnalysis = worker.get();
                        String analysisText = inputAnalysis.getText();
                        gui.appendText(analysisText);
                    } catch (InterruptedException | ExecutionException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        });
        worker.execute();
    }
}
class InputAnalysis {

    public String getText() {
        return "DONE";
    }

}
class AnalyzerWorker extends SwingWorker<InputAnalysis, Void> {
    private static final int MAX_VALUE = 100;

    @Override
    protected InputAnalysis doInBackground() throws Exception {
        int value = 0;
        setProgress(value);
        while (value < MAX_VALUE) {
            // create random values up to 100 and sleep for random time
            TimeUnit.SECONDS.sleep((long) (2 * Math.random()));
            value += (int) (8 * Math.random());
            value = Math.min(MAX_VALUE, value);
            setProgress(value);
        }

        return new InputAnalysis();
    }
}

Regarding changes to your question and your code:

You look to need a background process that can hold a 2-way "conversation" with the GUI, and to do this, best way I can think of is to create a state machine for your background process, notifying the GUI when the background process changes state, via a property change listener, and allowing the GUI to respond to this change. I will try to show you a code solution, but it may take some time.

0
hotmeatballsoup On

In the end I could not get this working with a SwingWorker, but was able to get it working with a Guava AsyncEventBus.