Here is an example created from http://www.java2s.com/Code/Java/SWT-JFace-Eclipse/SWTandThread.htm
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
public class MultiThread extends Shell {
Button btnNewButton = new Button(this, SWT.NONE);
/**
* Launch the application.
* #param args
*/
public static void main(String args[]) {
try {
Display display = Display.getDefault();
MultiThread shell = new MultiThread(display);
shell.open();
shell.layout();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Create the shell.
* #param display
*/
public MultiThread(Display display) {
super(display, SWT.SHELL_TRIM);
btnNewButton.addSelectionListener(new SelectionAdapter() {
#Override
public void widgetSelected(SelectionEvent e) {
applicationThread.start();
}
});
btnNewButton.setBounds(86, 47, 68, 23);
btnNewButton.setText("New Button");
createContents();
}
/**
* Create contents of the shell.
*/
protected void createContents() {
setText("SWT Application");
setSize(450, 300);
}
#Override
protected void checkSubclass() {
// Disable the check that prevents subclassing of SWT components
}
final Runnable print = new Runnable() {
public void run() {
System.out.println("Print from thread: \t" + Thread.currentThread().getName());
}
};
final Thread applicationThread = new Thread("currentThread") {
public void run() {
System.out.println("Hello from thread: \t" + Thread.currentThread().getName());
getDisplay().syncExec(print);
System.out.println("Bye from thread: \t" + Thread.currentThread().getName());
}
};
}
My question is: why IllegalThreadStateException occur on second click on button? Is it because second click creates a thread with same name as previous? How can I avoid that?
Thank you in advance!
Use Below code to set threads Name
Thread.currentThread().setName("Hello");
I hope you are starting a thread again without instantiating an object of thread which will result an java.lang.IllegalThreadStateException..
for e.g if you start an thread by instantiating and call start to start executing a thread. Again if you call thread.start will leads to java.lang.IllegalThreadStateException..
The same thing you are trying to achieve in your code.
once your button is clicked you are executing an thread and starting the thread again by calling thread.start()..
For every button click you should start a new thread() by instantiating object of thread.
From the documentation of Thread.start:
throws
IllegalThreadStateException - if the thread was already started.
You are not creating a new thread with the same name, you are calling start() on the same thread again and get what the documentation promised you.
It seems, whoever made that example did not consider the possibility that a user clicks a button more than once…
The problem is that you can't start same thread twice: check javadoc
It is never legal to start a thread more than once. In particular, a
thread may not be restarted once it has completed execution.
Related
Problem: SWT freezes when GUI field is periodically updated.
I would like to have a SWT-based GUI with text field were values are periodically incremented.
Initially I accessed textField from separate Thread what led to throwing exception:
Exception in thread "Thread-0" org.eclipse.swt.SWTException: Invalid
thread access at org.eclipse.swt.SWT.error(SWT.java:4533) at
org.eclipse.swt.SWT.error(SWT.java:4448) at
org.eclipse.swt.SWT.error(SWT.java:4419) at
org.eclipse.swt.widgets.Widget.error(Widget.java:482) at
org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:373) at
org.eclipse.swt.widgets.Text.setText(Text.java:2311) at
regreon.Incrementing.lambda$0(Incrementing.java:62) at
java.lang.Thread.run(Thread.java:745)
After reading SWT documentation (thanks to #marko-topolnik ) - I tried using display.SyncExec(Runnable r) or display.AsyncExec(Runnable r) with runnable that called Thread.sleep in the loop. But this caused the whole thing to freeze.
Here is the code:
package whatever;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.SWT;
public class FreezingGUI {
protected Shell shell;
private Text text;
public static void main(String[] args) {
try {
FreezingGUI window = new FreezingGUI();
window.open();
} catch (Exception e) {
e.printStackTrace();
}
}
public void open() {
Display display = Display.getDefault();
createContents();
shell.open();
shell.layout();
// HOW TO DO THAT???
display.syncExec(() -> {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Integer i = Integer.parseInt(text.getText()) + 1;
text.setText(i.toString());
}
}
}
);
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
}
protected void createContents() {
shell = new Shell();
shell.setSize(450, 300);
shell.setText("SWT Application");
text = new Text(shell, SWT.BORDER);
text.setEditable(false);
text.setText("0");
text.setBounds(30, 32, 78, 26);
}
}
How to avoid freezing and throwing exception?
Any SWT operation which changes a UI object must be run on the SWT User Interface thread.
In your case the text.setText(i.toString()); line is an SWT UI operation and is running in a different thread.
You can use the asyncExec or syncExec methods of Display to run some code in the UI thread. So replace:
text.setText(i.toString());
with
final String newText = i.toString();
Display.getDefault().asyncExec(() -> text.setText(newText));
(this is assuming you are using Java 8).
Using asyncExec will do the UI update asynchronously. Use syncExec instead if you want to pause the thread until the update is done.
If you are using Java 7 or earlier use:
final String newText = i.toString();
Display.getDefault().asyncExec(new Runnable() {
#Override
public void run() {
text.setText(newText);
}
});
Note you should also be checking for the Shell being disposed and stopping your background thread. If you don't do this you will get an error when you close the app. Your code incrementing i is also wrong. This thread works:
new Thread(() -> {
for (int i = 1; true; i++) {
try {
Thread.sleep(1000);
} catch (final InterruptedException e) {
return;
}
if (shell.isDisposed()) // Stop thread when shell is closed
break;
final String newText = Integer.toString(i);
Display.getDefault().asyncExec(() -> text.setText(newText));
}
}).start();
Looking at your code the reason your UI is freezing is because you are executing a runnable on Display.sync that never returns because of the while(true) loop, as a result, other UI threads don't get a chance to execute and the UI freezes. What you need to do is use Displasy.asyncRunnable and instead of having a runnable with while(true) make a scheduler or another thread that sleeps every x seconds that executes the runnable to make the update you want, this way the other UI threads can run preventing your UI from freezing.
i know multithreading a bit but not in vast and i think the problem is of multithreading. I am calling a method to set label's text by invoking a new thread and leaving it blank after a specified time. I am getting the desired output every time but not only the place which i am going to show you by my piece of code. I am expecting that message should be set and disappeared after the specified time and the window should be minimized after that time. But what actually happening is when it is going to the other thread main thread execution starts and goes for sleep for 5 sec and the message is not appearing and after 5 sec window is getting minimized without showing the message which i am setting on the label.
(Main thread)
Validation.setMessageOnLabel("Username and password has been copied", jLabel15,1.5F);
try {
Thread.sleep(5000);
} catch (InterruptedException ex) {
Logger.getLogger(PasswordManager.class.getName()).log(Level.SEVERE, null, ex);
}
setState(ICONIFIED);
validation.java (setMessageOnLabel())
static public void setMessageOnLabel(final String msg, final JLabel label, final float time)
{
new Thread(new Runnable() {
#Override
public void run() {
label.setText(msg);
try {
Thread.sleep((long) (time*1000));
} catch (InterruptedException ex) {
Logger.getLogger(PasswordManager.class.getName()).log(Level.SEVERE, null, ex);
}
label.setText("");
}
}).start();
}
Since you're calling setState() directly, I assume the first code snippet is part of a JFrame. In that case you're most probably sending the event dispatch thread to sleep for 5 seconds and thus prevent screen updates during that time.
Put the sleep into another thread or use a swing worker instead and call setState() on the EDT in the worker's callback method, since setState() is not labelled as thread-safe and calling it on a thread other than the EDT might result in unexpected behavior.
From the linked tutorial:
Some Swing component methods are labelled "thread safe" in the API specification; these can be safely invoked from any thread. All other Swing component methods must be invoked from the event dispatch thread. Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce.
Don't use Thread.sleep(5000);, that block EDT.
For that purposes you can use swing Timer, examine next example:
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.Timer;
public class TestFrame extends JFrame {
private JLabel lbl;
public TestFrame() {
init();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
pack();
setLocationRelativeTo(null);
setVisible(true);
}
private void init() {
lbl = new JLabel(" ");
JButton setText = new JButton("setText");
setText.addActionListener(getActionListener());
add(lbl);
add(setText,BorderLayout.SOUTH);
}
private ActionListener getActionListener() {
return new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
lbl.setText("wait...");
Timer t = new Timer(5000, new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
lbl.setText("");
setState(JFrame.ICONIFIED);
}
});
t.setRepeats(false);
t.start();
}
};
}
public static void main(String args[]) {
new TestFrame();
}
}
When dealing with Swing components you shuld not use threads like that. Launch your own SwingWorker instead.
public class MySwingWorker extends SwingWorker<Object, Object> {
#Override
public Object doInBackground() {
//your code here
//dont forget to repaint changed component or validate parent of it,
//if your text dont shows up.
return null;
}
}
you can also execute your own runnable via SwingUtilites
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
//again your code here...
}
});
I am making a program to check the stock market for a symbol and I got that far, and added a basic gui to it. I am stumped on how to make it check every hour and create a green up arrow if it increased and red down arrow if it decreased.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.select.Elements;
public class QuoteTracker {
JFrame frame;
JPanel mainPanel;
JLabel enterLabel;
JLabel resultLabel;
JTextField text;
JTextField result;
JButton query;
JButton redArrow;
JButton greenArrow;
String url;
public static void main(String[] args) {
new QuoteTracker().buildGui();
}
public class checkingQuote implements Runnable {
#Override
public void run() {
while (true) {
try {
checkQuote(url);
//if increase in value green button
System.out.println("Sleeping");
Thread.sleep(1000 * 60 * 60);
System.out.println("Waking");
} catch (InterruptedException ie) {
ie.printStackTrace();
break;
}
}
}
}
public void checkQuote(String symbol) {
try {
String url = "http://finance.yahoo.com/q?s=" + symbol + "&ql=0";
this.url = url;
Document doc = Jsoup.connect(url).get();
Elements css = doc.select("p > span:first-child > span");
result.setText(css.text());
} catch (IOException e) {
}
}
public void buildGui() {
frame = new JFrame("QuoteTracker");
mainPanel = new JPanel();
enterLabel = new JLabel("enter symbol ");
resultLabel = new JLabel("result ");
text = new JTextField(4);
result = new JTextField(8);
query = new JButton("query");
query.addActionListener(new queryListener());
mainPanel.add(enterLabel);
mainPanel.add(text);
mainPanel.add(query);
mainPanel.add(resultLabel);
mainPanel.add(result);
frame.getContentPane().add(mainPanel);
frame.setSize(300, 400);
frame.setVisible(true);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
class queryListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent ev) {
checkQuote(text.getText());
}
}
}
Do I even need a thread? I've never made one before and tried to add things that made sense. I am thinking I either need a thread or to use java's Timer?
Use SwingWorker to execute long running task in the background while updating the UI based on some results from that long running task. That means, it is actually about two different threads communicating to each other - Worker Threads and Event Dispatch Thread (EDT)
But before that, I want to point some few notes about your code.
Invoke the initialization of your UI in the EDT. That is, instead of just straightly calling new QuoteTracker().buildGui(), call it inside the run method of a Runnable passed to SwingUtilities.invokeLater (like this)
Classes should start in capital letter as per the Java standard.
To apply SwingWorker in you existing code, you can do the following :
First, you must place your checkQuote method in some other class (ideally a service class) then modify your checkQuote method to return the String that is set to the textfield result. Something like this
public Class QuoteService{
public String checkQuote(String symbol) {
try {
String url = "http://finance.yahoo.com/q?s=" + symbol + "&ql=0";
this.url = url;
Document doc = Jsoup.connect(url).get();
Elements css = doc.select("p > span:first-child > span");
return css.text();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
}
You can then make your QuoteTracker class to focus mainly in the UI part of your application. Just create the service object as instance level field so that you can freely call checkQuote method within your the class.
Invoke SwingWorker when the button is clicked.
public void actionPerformed(ActionEvent ev) {
new SwingWorker<Void, String>() {
#Override // this method is done in the Worker Thread
protected Void doInBackground() throws Exception {
while(true){
String res = checkQuote(text.getText());
publish(res); //will call the process method
Thread.sleep(1000 * 60 * 60); //1 hour
}
}
#Override // this method is done in the EDT
protected void process(List<String> resultList){
String res = resultList.get(0);
if(!"".equals(res)){
result.setText(res);
}
}
#Override // this method is done in the EDT. Executed after executing the doInBackground() method
protected void done() {
//... clean up
}
}.execute();
}
Note that done() will be executed after the execution of doInBackground() is finished, which means, in the code I posted, it will never be executed since the while loop used to periodically call checkQuote is infinite. Just modify it so that you can interrupt that loop according to your need
Further Read : Concurrency in Swing
You can use thread and normal while loop in main thread as well, but at the very first , you need to start you thread and that thread must refer your object.
Add following line in public void buildGui() {
Thread t1 = new Thread(new checkingQuote());
t1.start();
This will start you thread, for testing purpose i have modified checkingQuote class
public class checkingQuote implements Runnable {
int count = 0;
#Override
public void run() {
System.out.println("Inside Runner");
while (true) {
try {
count++;
checkQuote(url);
//if increase in value green button
result.setText(""+count);
System.out.println("Sleeping");
Thread.sleep(1000);
System.out.println("Waking");
} catch (InterruptedException ie) {
ie.printStackTrace();
break;
}
}
}
}
I am seeing number change in the text box.... same way you can alter the logic to get and show the quotes.. but you must keep the value for previous quote to compare with the latest code to show green and red notification...
In gui application it is better to use Timer, also you may use ScheduledThreadPoolExecutor. But in the second case notice, that your scheduled tasks may run in non-GUI thread. As you can't invoke ATW/Swing directly from another thread, you should wrap any call to Swing into SwingUtilities.invokeLater() method.
Also notice, that when you do something durable inside GUI thread, the whole GUI becomes unrepsonsive. So, to achieve a better responsiveness, you would query in a separate thread, and expose results to Swing through invokeLater after quotes have checked. So your checkQuote method may be rewritten this way:
public void checkQuote(String symbol) {
try {
final String url = "http://finance.yahoo.com/q?s=" + symbol + "&ql=0";
Document doc = Jsoup.connect(url).get();
Elements css = doc.select("p > span:first-child > span");
final String text = css.text();
SwingUtilities.invokeLater(new Runnable() {
#Override public void run() {
this.url = url;
result.setText(text);
}
}
} catch (IOException e) {
// Don't swallow exceptions
logger.error("Something gone wrong", e);
}
}
public void checkQuote() {
final String symbol = text.getText();
new Thread(new Runnable() {
#Override public void run() {
checkQuote(symbol);
}
}).start();
}
and call it from Timer and from button click listener.
I am trying to create a simple Timer with a Start Stop and Reset Button. I am new to using the Threads and ActionListeners. I have this working, and can get my timer to begin, and my button to change text from start to stop. But after that i am stuck. I need to stop the timer, and then start it again, if i press the start button. Then of course reset turns it back to zero. I do not want to use java.util.Timer, i just want to use threads. How would i get my thread once started, to pause and then resume. I tried using the built in methods, but i could not get it to compile right.
import javax.swing.*;
import java.awt.Font;
import java.lang.String;
import java.awt.*;
public class Timer extends JPanel {
// Here are the fields from below!
private static JLabel label = new JLabel(" 0.00 seconds");
private static javax.swing.JButton start = new javax.swing.JButton("Start");
private static javax.swing.JButton reset = new javax.swing.JButton("reset");
/**
* Here is the Timer method- Begins with JLabel with medium alignment.
*/
public Timer() {
//new Thread(this).start();
//label = new JLabel(" 0.00 Seconds");
//this.label = label;
reset();
}
/**
* Here is the Reset method- pressing this button from below will
* stop the thread and reset the text.
*/
public static void reset() {
label.setFont(new Font("Arial",Font.BOLD,36));
label.setText(" 0.00 Seconds");
}
public static void startStop() {
//start.setText("stop");
//validate();
}
public static void countDown() {
int Msec=0,min=0,sec=0;
while(sec < 60) {
label.setText(min+":"+sec+":"+Msec);
//label.setLayout(new BorderLayout.CENTER);
//label.
Msec++;
if(Msec==60) {
Msec=0;
sec++;
//label.setText(min+":"+sec+":"+Msec);
}
if(sec ==60) {
Msec =0;
sec = 0;
min++;
}
try
{
Thread.sleep(10);
}
catch(Exception e)
{}
}
}
public static void main(String [] args) {
// we need a JFrame to put these elements into
javax.swing.JFrame win = new javax.swing.JFrame("Timer");
// here we are instantating a new timer
final Timer time = new Timer();
//Annonymous inner class
start.addActionListener(new java.awt.event.ActionListener() {
// here is the action performed method to start this.
public void actionPerformed(java.awt.event.ActionEvent e) {
//here we are creating a new thread to run throwable
// every click creates a new Thread ( so it goes faster)
String text = (String)e.getActionCommand();
if (text.equals("Start")){
start.setText("Stop");
}
else{
start.setText("Start");
}
Thread restart = new Thread(new Runnable() {
public void run() {
countDown();
//startStop();
}
});
restart.start();
}
});
//Look at the below abstract actionlistener below to get reset to work
javax.swing.JButton reset = new javax.swing.JButton("reset");
// here we are creating a new annonomys inner class.... check the
// curly braces
reset.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent e) {
Thread restart = new Thread(new Runnable() {
public void run() {
reset();
//Thread.stop();
}
});
restart.destroy();
}
});
//label.setVisible(true);
javax.swing.JPanel tb = new javax.swing.JPanel();
tb.add(reset);
tb.add(start);
//tb.add(circle);
win.add(tb,java.awt.BorderLayout.NORTH);
win.setSize(300,300);
//win.add(tb);
win.add(label,java.awt.BorderLayout.CENTER);
win.setDefaultCloseOperation(javax.swing.JFrame.DISPOSE_ON_CLOSE);
// hiding inside setVisible is borderLayout
win.setVisible(true);
}
}
It is admirable and a great goal that you want to practice and improve with threads, but this really isn't the arena for it. The problem is that Swing is single threaded - only the ui thread should ever update the graphical environment.
For doing operations involving graphics you should use a javax.swing.Timer and javax.swing.SwingWorker, as these are Thread Safe. In one way, you are learning about thread safety here, so you are making progress!
I need to stop user making multiple clicks on a JButton while the first click still execute.
I was able to came with a solution for this issue but I do not completelly understand why it's working.
Bellow I posted the code (trimmed to a minimum) that works and the one that does not work.
In first example (good) if you run it and click the button multiple times only one action is considered as for the second example (bad) if you click the mouse multiple times you get action executed at least twice.
The second (bad) example simply does not use invokeLater() method.
Where the difference in behaviour cames from?
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
public class TestButtonTask {
public static void main(String[] args) {
final JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
final JButton task = new JButton("Test");
task.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
long t = System.currentTimeMillis();
System.out.println("Action received");
task.setText("Working...");
task.setEnabled(false);
SwingUtilities.invokeLater(new Thread() {
#Override
public void run() {
try {
sleep(2 * 1000);
} catch (InterruptedException ex) {
Logger.getLogger(TestButtonTask.class.getName()).log(Level.SEVERE, null, ex);
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
task.setEnabled(true);
task.setText("Test");
}
});
}
});
}
});
frame.add(task);
frame.pack();
frame.setVisible(true);
} //end main
} //end class
And now the "wrong" code
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
public class TestButtonTask {
public static void main(String[] args) {
final JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
final JButton task = new JButton("Test");
task.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
long t = System.currentTimeMillis();
System.out.println("Action received");
task.setText("Working...");
task.setEnabled(false);
SwingUtilities.invokeLater(new Thread() {
#Override
public void run() {
try {
sleep(2 * 1000);
} catch (InterruptedException ex) {
Logger.getLogger(TestButtonTask.class.getName()).log(Level.SEVERE, null, ex);
}
//SwingUtilities.invokeLater(new Runnable() {
//public void run() {
task.setEnabled(true);
task.setText("Test");
//}
//});
}
});
}
});
frame.add(task);
frame.pack();
frame.setVisible(true);
} //end main
} //end class
After info provided by #kleopatra and #Boris Pavlović here is the code I created that seems to work pretty decent.
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.*;
public class TestButtonTask {
public static void main(String[] args) {
final JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
final JButton task = new JButton("Test");
task.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
task.setText("Working...");
task.setEnabled(false);
SwingWorker worker = new SwingWorker<Void, Void>() {
#Override
protected Void doInBackground() throws Exception {
try {
Thread.sleep(3 * 1000);
} catch (InterruptedException ex) {
Logger.getLogger(TestButtonTask.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
};
worker.addPropertyChangeListener(new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println("Event " + evt + " name" + evt.getPropertyName() + " value " + evt.getNewValue());
if ("DONE".equals(evt.getNewValue().toString())) {
task.setEnabled(true);
task.setText("Test");
}
}
});
worker.execute();
}
});
frame.add(task);
frame.pack();
frame.setVisible(true);
} //end main
} //end class
you have two choises
1) JButton#setMultiClickThreshhold
2) you have to split this idea to the two separated actions inside actionListener or Action
1st. step, JButton#setEnabeld(false);
2nd. step, then call rest of code wrapped to the javax.swing.Action (from and dealyed by javax.swing.Timer), SwingWorker or Runnable#Thread
Okay, here's a code snippet using an Action
it disable's itself on performed
it spawns a task, at the end of which is enables itself again. Note: for simplicity here the task is simulated by a Timer, real-world would spawn a SwingWorker to do the background work, listening to its property changes and enable itself on receiving a done
set as the button's action
The code:
Action taskAction = new AbstractAction("Test") {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Action received ");
setEnabled(false);
putValue(NAME, "Working...");
startTask();
}
// simulate starting a task - here we simply use a Timer
// real-world code would spawn a SwingWorker
private void startTask() {
ActionListener l = new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
putValue(NAME, "Test");
setEnabled(true);
}
};
Timer timer = new Timer(2000, l);
timer.setRepeats(false);
timer.start();
}};
JButton task = new JButton(taskAction);
There are two more ways.
You can define a flag. Set it when action start and reset back after the end. Check the flags in the actionPerformed. If inProgress==true just do nothing.
Another way is to remove the listener and assign it back after the action ends.
The right way is using a SwingWorker. When user click the button before submmiting a job to the SwingWorker the state of the button should be changed to disabled JButton#setEnabled(false). After the SwingWorker finished the job state of the button should be reset to enabled. Here's Oracle's tutorial on SwingWorker
After years of dealing with the frustration of this problem, I've implemented a solution that I think is the best.
First, why nothing else works:
JButton::setMutliclickThreshold() is not really an optimal solution, because (as you said) there is no way to know how long to set the threshold. This is only good to guard against double-click happy end-users because you have to set an arbitrary threshold.
JButton::setEnabled() is an obviously fragile solution that will only make life much more difficult.
So, I've created the SingletonSwingWorker. Now, Singletons are called anti-patterns, but if implemented properly, they can be a very powerful. Here is the code:
public abstract class SingletonSwingWorker extends SwingWorker {
abstract void initAndGo();
private static HashMap<Class, SingletonSwingWorker> workers;
public static void runWorker(SingletonSwingWorker newInstance) {
if(workers == null) {
workers = new HashMap<>();
}
if(!workers.containsKey(newInstance.getClass()) || workers.get(newInstance.getClass()).isDone()) {
workers.put(newInstance.getClass(), newInstance);
newInstance.initAndGo();
}
}
}
This will enable you to create classes which extend SingletonSwingWorker and guarantee only one instance of that class will be executable at one time. Here is an example implementation:
public static void main(String[] args) {
final JFrame frame = new JFrame();
JButton button = new JButton("Click");
button.setMultiClickThreshhold(5);
button.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
DisplayText_Task.runWorker(new DisplayText_Task(frame));
}
});
JPanel panel = new JPanel();
panel.add(button);
frame.add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
static class DisplayText_Task extends SingletonSwingWorker {
JFrame dialogOwner;
public DisplayText_Task(JFrame dialogOwner) {
this.dialogOwner = dialogOwner;
}
JDialog loadingDialog;
#Override
void initAndGo() {
loadingDialog = new JDialog(dialogOwner);
JProgressBar jpb = new JProgressBar();
jpb.setIndeterminate(true);
loadingDialog.add(jpb);
loadingDialog.pack();
loadingDialog.setVisible(true);
execute(); // This must be put in the initAndGo() method or no-workie
}
#Override
protected Object doInBackground() throws Exception {
for(int i = 0; i < 100; i++) {
System.out.println(i);
Thread.sleep(200);
}
return null;
}
#Override
protected void done() {
if(!isCancelled()) {
try {
get();
} catch (ExecutionException | InterruptedException e) {
loadingDialog.dispose();
e.printStackTrace();
return;
}
loadingDialog.dispose();
} else
loadingDialog.dispose();
}
}
In my SwingWorker implementations, I like to load a JProgressBar, so I always do that before running doInBackground(). With this implementation, I load the JProgressBar inside the initAndGo() method and I also call execute(), which must be placed in the initAndGo() method or the class will not work.
Anyways, I think this is a good solution and it shouldn't be that hard to refactor code to refit your applications with it.
Very interested in feedback on this solution.
Note that when you are modifying anything in GUI your code must run on Event Dispatch thread using invokeLater or invokeAndWait if you are in another thread. So second example is incorrect as you are trying to modify enabled state from another thread and it can cause unpredictable bugs.