Reputation: 189
I have a java swing GUI in which I'm trying to implement a countdown timer. I have a series of tasks that each need to run sequentially inside a for-loop. I've been able to successfully use a swing timer to do the countdown timer, but once I try and put it in a loop I can't get it to update without freezing the GUI.
Here's an SSCCE of what I currently have working. The basic problem is this example run three 30 second timers in parallel. What I want to do is run three 30 second timers sequentially or in other words it should wait for one timer to finish before beginning the next. Everything I've done to try and wait for the timer to finish makes the GUI freeze.
import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.security.auth.callback.Callback;
import javax.swing.*;
public class SwingTester extends JFrame {
public SwingTester() {
JPanel panel = new JPanel();
getContentPane().add(panel);
panel.setLayout(new GridLayout(1,3));
setDefaultCloseOperation(EXIT_ON_CLOSE);
// Add the components
jLblTimer = new JLabel("00:30");
jLblSet = new JLabel("Set #1");
jBtnStart = new JButton("Start!");
jBtnStart.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {jBtnStartActionPerformed(evt);}
});
// Add the labels
panel.add(jLblTimer); panel.add(jLblSet); panel.add(jBtnStart);
pack();
}
private void jBtnStartActionPerformed(ActionEvent evt) {
for (int ss=0; ss<3; ss++) {
// Change the set name
jLblSet.setText("Set #" + Integer.toString(ss+1));
// Setup the counter to begin the countdown
counter = 30;
timer = new Timer(1000, startCycle());
timer.start();
// I want to wait here until the timer is done, nothing I've done
// works without freezing the GUI.
}
}
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new SwingTester().setVisible(true);
}
});
}
private Action startCycle() {
return new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
new ClockTask(null).execute();
}
};
}
class ClockTask extends SwingWorker<Void, String> {
private Callback callback;
public ClockTask(Callback callback) {
this.callback = callback;
}
@Override
protected Void doInBackground() throws Exception {
if (counter >= 0) {
int millis = counter*1000;
String time = String.format("%02d:%02d", TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))
);
publish(time); // call publish which will call process() method in EDT
counter--;
} else {
// this.callback.call();
this.callback.notify();
timer.stop();
}
return(null);
}
@Override
protected void process(List<String> times) {
String lastTime = times.get(times.size()-1);
jLblTimer.setText(lastTime); // just update ui with lastTimer, ohters are ignorable
}
@Override
protected void done() {
super.done();
}
// private interface Callback {
// void call();
// }
}
private javax.swing.JLabel jLblTimer;
private javax.swing.JLabel jLblSet;
private javax.swing.JButton jBtnStart;
private javax.swing.Timer timer;
private int counter;
}
I've also tried using the sleep function to just wait until the countdown finishes, but this stops the GUI from updating. Any suggestions are most welcome. As you can probably tell from my code I'm not a programmer.
Upvotes: 2
Views: 2853
Reputation: 8261
Below is how you need to change your SwingWorker
class ClockTask extends SwingWorker<Void, String> {
private Callback callback;
public ClockTask(Callback callback) {
this.callback = callback;
}
@Override
protected Void doInBackground() throws Exception {
if (counter >= 0) {
int millis = counter*1000;
String time = String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))
);
//jLblTimer.setText(time); // dont update UI in backgroudn thread
publish(time); // call publish which will call process() method in EDT
counter--;
} else {
this.callback.call();
}
return(null);
}
/*
* this method will be called in EDT and will not freeze your UI ;)
*/
@Override
protected void process(List<String> times) {
String lastTime = times.get(times.size()-1);
jLblTimer.setText(lastTime); // just update ui with lastTimer, ohters are ignorable
}
@Override
protected void done() {
super.done();
}
private interface Callback {
void call();
}
}
You should spend few more time with the JavaDoc of SwingWorker
Upvotes: 3
Reputation: 5023
There're several ways to do so. Normally you can create an object, pass it to the background work, after the background work finishs, it use the object to tell you the result and let you do the rest of work.
private class ClockTask extends SwingWorker<Void, Void> {
private Callback _callback;
public ClockTask(Callback callback) {
this._callback = callback;
}
@Override
protected Void doInBackground() throws Exception {
if (counter >= 0) {
int millis = counter*1000;
String time = String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(millis),
TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(millis))
);
jLblTimer.setText(time);
counter--;
} else {
this._callback.notify();
}
return(null);
}
}
Upvotes: -2