While my code sends http calls I want it to alter the JLabel
on a window (basically showing approximately how much time is left before the http calls end). I have been looking into SwingWorkers
due to another question I asked here, but I'm not sure how I use it. The code I am writing basically has a loop to send the calls, each time timing how long it takes to run the call, calculates the approximate time left and then sends this to the JLabel
(NB the JLabel is in a different instantiated object).
Most SwingWorker
examples show a function continuing in the background that is not affected by the worker thread (e.g. a counter based entirely on time rather than being altered by the code). If this is the case then isn't the alteration of the JLabel
just part of the worker thread as it the code runs through new loop -> calculate time & make call -> alter JLabel
? I'm probably wrong but then how do I have the JLabel altered by the code rather than a independent thread?
One of my issues was that when I initially set my code up there was nothing changing in the JLabel
.
Here is my code:
package transcription.windows;
import javax.swing.*;
import java.awt.*;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
public class PleaseWaitWindow {
private JLabel pleaseWaitJLabel = new JLabel("Please wait");
private GridBagConstraints containerGbc = new GridBagConstraints();
private Container contentPaneContainer = new Container();
private JFrame pleaseWaitJFrame;
public JLabel getPleaseWaitJLabel() {
return pleaseWaitJLabel;
}
public JFrame setPleaseWaitWindow() {
pleaseWaitJFrame = new JFrame();
contentPaneContainer = setContentPane();
pleaseWaitJFrame.setContentPane(contentPaneContainer);
pleaseWaitJFrame.setDefaultCloseOperation(EXIT_ON_CLOSE);
pleaseWaitJFrame.setTitle("");
pleaseWaitJFrame.setSize(350, 150);
pleaseWaitJFrame.setLocationRelativeTo(null);
pleaseWaitJFrame.setVisible(true);
return pleaseWaitJFrame;
}
private Container setContentPane() {
containerGbc.insets.bottom = 1;
containerGbc.insets.top = 2;
containerGbc.insets.right = 1;
containerGbc.insets.left = 1;
containerGbc.weightx = 1;
containerGbc.weighty = 1;
contentPaneContainer.setLayout(new GridBagLayout());
contentPaneContainer.setSize(800, 700);
setPleaseWaitJLabel();
return contentPaneContainer;
}
private void setPleaseWaitJLabel() {
containerGbc.gridx = 2;
containerGbc.gridy = 5;
containerGbc.gridwidth = 2;
containerGbc.gridheight = 1;
contentPaneContainer.add(pleaseWaitJLabel, containerGbc);
}
public void setJLabelDisplay(String displayTime) {
pleaseWaitJLabel.setText(displayTime);
}
public void closeWindow() {
pleaseWaitJFrame.dispose();
}
}
Method that is part of the ServiceUpload class:
public String cuttingLoop(String mpBase64Piece, String jobName, String email) {
Integer numberOfPiecesMinusEnd = (int) Math.ceil(mpBase64Piece.length() / 500000.0);
List<String> base64List = new ArrayList<>();
for (int i = 0; i < numberOfPiecesMinusEnd; i++) {
if (mpBase64Piece.length() >= 500000) {
base64List.add(mpBase64Piece.substring(0, 500000));
mpBase64Piece = mpBase64Piece.substring(500000);
}
}
base64List.add(mpBase64Piece);
pleaseWaitWindow = new PleaseWaitWindow();
pleaseWaitWindow.setPleaseWaitWindow();
for (int n = 0; n < base64List.size(); n++) {
numberOfLoopsLeft = numberOfPiecesMinusEnd - n;
Stopwatch stopwatch = null;
String tag;
Stopwatch.createStarted();
if (base64List.get(n) != null) {
if (n == 0) {
tag = "start";
} else if (n == base64List.size() - 1) {
tag = "end";
} else {
tag = "middle";
}
stopwatch = Stopwatch.createStarted();
response = providerUpload.executeUploadHttp(base64List.get(n), jobName, tag, email);
stopwatch.stop();
}
long oneLoopTime = stopwatch.elapsed(TimeUnit.SECONDS);
pleaseWaitWindow.setJLabelDisplay(numberOfLoopsLeft*oneLoopTime+" seconds remaining");
LOGGER.info("complete");
}
pleaseWaitWindow.closeWindow();
return response;
}
One of my issues was the code did not show the 'JLabel' when a 'SwingWorker' isn't used with the above code.
It's best you split up your code into areas of responsibilities. Let's go with three: 1. the worker (ie the upload); 2. the display (ie the JLabel update); 3. integration of the two (the first two are independent of each other, so you'll need something to tie them together).
Abstracting from the actual work, you can use standard interfaces. The first one is just a Runnable
, ie not taking any parameters and not returning anything. The second one is a Consumer<String>
because it takes a String
(to display) but doesn't return anything. The third will be your main control.
Let's start with the control because that's simple:
Consumer<String> display = createDisplay();
Runnable worker = createWorker();
CompletableFuture.runAsync(worker);
This will start the worker in a separate Thread which is what it sounds like you want.
So here's your uploader:
Consumer<String> display = // tbd, see below
Runnable worker = () -> {
String[] progress = {"start", "middle", "finish"};
for (String pr : progress) {
display.accept(pr);
Thread.sleep(1000); // insert your code here
}
}
Note that this worker actually does depend on the consumer; that is somewhat "unclean", but will do.
Now for the display. Having defined it as a Consumer<String>
, it's abstract enough that we can just print the progress on the console.
Consumer<String> display = s -> System.out.printf("upload status: %s%n", s);
You however want to update a JLabel
; so the consumer would look like
Consumer<String> display = s -> label.setText(s);
// for your code
s -> pleaseWaitWindow.getPleaseWaitLabel().setText(s);
Your actual question
So if you do that, you will notice that your label text doesn't get updated as you expect. That is because the label.setText(s)
gets executed in the thread in which the worker is running; it needs to be inserted in the Swing thread. That's where the SwingWorker
comes in.
The SwingWorker
has a progress
field which is what you can use for your labels; it also has a doInBackground()
which is your actual upload worker thread. So you end up with
class UploadSwingWorker {
public void doInBackground() {
for(int i = 0; i < 3; i++) {
setProgress(i);
Thread.sleep(1000); // again, your upload code
}
}
}
So how does that update your label? The setProgress
raises a PropertyChangeEvent
you can intercept; this done using a PropertyChangeListener
with the signature
void propertyChange(PropertyChangeEvent e)
This is a functional interface, so you can implement this with a lambda, in your case
String[] displays = {"start", "middle", "finish"};
updateLabelListener = e -> {
int index = ((Integer) e.getNewValue()).intValue(); // the progress you set
String value = displays[index];
label.setText(value);
}
and you can add it to the SwingWorker
using
SwingWorker uploadWorker = new UploadSwingWorker();
uploadWorker.addPropertyChangeListener(updateLabelListener);
uploadWorker.execute(); // actually start the worker
Simpler
Note that I myself have never used a SwingWorker
this way. The much simpler way to get around the problem that the GUI is not updated from within your worker thread is to call the GUI update using SwingUtilities.invokeLater()
.
Coming back to the initial Consumer<String>
I brought up, you can do
Consumer<String> display = s -> SwingUtilities.invokeLater(
() -> pleaseWaitWindow.getPleaseWaitLabel().setText(s)
);
and that should do. This allows you to keep your worker in the more abstract Runnable
and use the usual scheduling mechanisms to run it (ExecutorService.submit()
or CompletableFuture.runAsync()
for example), while still allowing to update the GUI on a similarly simple level.