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%.
Not
Since the size of the collection is not what you're after. Rather you want the value of the collection to be shown:
You also need to call
get()on your worker when it has completed its work, either in itsdone()method or in a PropertyChangeListener that notifies you when the worker's state value isSwingWorker.StateValue.DONEThat same
get()call will return the newInputAnalysisobject on the EDT and will throw exceptions if any occur during the worker's run, so you can handle them there.e.g.,
(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:
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.