Disable JButton during background operation to avoid multiple clicks

I need to stop a user doing a few clicks on JButton while the first click is still executing.

I was able to find a solution for this problem, but I do not understand why it works.

Below I posted a code (cut to a minimum) that works, and one that doesn't work.

In the first example (good), if you run it and click the button several times, only one action is considered as the second example (bad), if you click the mouse button several times, you get an action that is performed at least twice.

The second (bad) example simply does not use the invokeLater () method.

Where is the difference in cames behavior?

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 the information provided by @kleopatra and @Boris Pavlović, here is the code I created that seems to work pretty well.

 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 
+8
java thread-safety swing jbutton
source share
6 answers

you have two choices

1) JButton # setMultiClickThreshhold

2) you need to split this idea into two separate actions inside an actionListener or Action

  • first. step, JButton # setEnabeld (false);
  • second. step, then call the rest of the code wrapped in javax.swing.Action (from and processed by javax.swing.Timer ), SwingWorker or Runnable#Thread
+6
source share

Ok, here is a code snippet using Action

  • disconnect yourself while doing
  • he starts the task, at the end of which it turns on again. Note: for simplicity, here the task is simulated by a timer, the real world will generate SwingWorker to perform background work, listen to its property changes and turn on itself when it is done
  • set as button 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); 
+2
source share

There are two more ways.

You can define a flag. Set it when the action begins, and reset back after completion. Check the flags in actionPerformed . If inProgress==true do nothing.

Another way is to remove the listener and assign it after the action completes.

+1
source share

The correct way is SwingWorker . When the user clicks the button before reassigning the task to SwingWorker , the state of the button should be disabled on JButton#setEnabled(false) . After SwingWorker completes, the button job state must be reset to enable. Here is Oracle SwingWorker

+1
source share

After several years of disappointment with this problem, I implemented a solution that, in my opinion, is the best.

First, why nothing works:

  • JButton::setMutliclickThreshold() is not really the optimal solution because (as you said) there is no way to know how long to set the threshold. This is only useful for double-click protection for happy end users, because you must set an arbitrary threshold.
  • JButton::setEnabled() is obviously a fragile solution that will make life a lot more complicated.

So, I created SingletonSwingWorker . Now singletones are called anti-patterns, but if they execute correctly, they can be 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 allow you to create classes that extend SingletonSwingWorker and ensure that only one instance of this class will be executed at a 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 SwingWorker I like to load a JProgressBar , so I always do this before running doInBackground() . With this implementation, I load a JProgressBar inside the initAndGo() method , and also call execute() , which must be placed in the initAndGo() method or the class will not work .

In any case, I think this is a good solution, and it is not necessary that this code be reorganized to update your applications.

Very interested in feedback on this decision.

+1
source share

Please note that when you change something in the GUI, your code should run in the Event Dispatch thread using invokeLater or invokeAndWait if you are in another thread. Therefore, the second example is incorrect because you are trying to change the enabled state from another thread, and this can cause unpredictable errors.

0
source share

All Articles