Désactiver JButton, en tâche de fond, pour éviter les clics multiples

Je dois empêcher l’utilisateur de faire plusieurs clics sur un bouton JButton tant que le premier clic est toujours exécuté.

J’ai pu trouver une solution à ce problème, mais je ne comprends pas tout à fait pourquoi cela fonctionne.

Ci-dessous, j’ai posté le code (ajusté au minimum) qui fonctionne et celui qui ne fonctionne pas.

Dans le premier exemple (bon), si vous l’exécutez et cliquez plusieurs fois sur le bouton, une seule action est considérée comme dans le deuxième exemple (mauvais) si vous cliquez plusieurs fois avec la souris, l’action est exécutée au moins deux fois.

Le deuxième (mauvais) exemple n’utilise simplement pas la méthode invokeLater ().

D’où vient la différence de comportement?

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(Ssortingng[] 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 

Et maintenant le “mauvais” 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(Ssortingng[] 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 

Après les informations fournies par @kleopatra et @Boris Pavlović, voici le code que j’ai créé et qui semble fonctionner assez correctement.

 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(Ssortingng[] 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() { @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().toSsortingng())) { task.setEnabled(true); task.setText("Test"); } } }); worker.execute(); } }); frame.add(task); frame.pack(); frame.setVisible(true); } //end main } //end class 

vous avez deux choix

1) JButton # setMultiClickThreshhold

2) vous devez diviser cette idée en deux actions distinctes dans actionListener ou Action

  • 1er step, JButton # setEnabeld (false);
  • 2ème. étape, puis appelez le rest du code enveloppé dans javax.swing.Action (à partir de et désactivé par javax.swing.Timer ), SwingWorker ou Runnable#Thread

D’accord, voici un extrait de code utilisant une action

  • il se désactive lui même
  • il engendre une tâche à la fin de laquelle il se permet à nouveau. Remarque: pour des raisons de simplicité, la tâche est simulée par un minuteur. Dans la réalité, SwingWorker sera chargé de faire le travail en arrière-plan, en écoutant les modifications de ses propriétés et en se permettant de recevoir un
  • définir comme action du bouton

Le 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); 

Il y a deux autres façons.

Vous pouvez définir un drapeau. Configurez-le quand l’action démarre et réinitialisez-le après la fin. Vérifiez les drapeaux dans l’ actionPerformed . Si inProgress==true ne faites rien.

Une autre méthode consiste à supprimer le programme d’écoute et à le rendre à la fin de l’action.

La bonne façon est d’utiliser un SwingWorker . Lorsque l’utilisateur clique sur le bouton avant de soumettre un travail à SwingWorker l’état du bouton doit être désactivé et désactivé JButton#setEnabled(false) . Une fois que SwingWorker terminé, l’état du travail du bouton doit être réinitialisé pour être activé. Voici le tutoriel d’Oracle sur SwingWorker

Après des années de frustration face à ce problème, j’ai mis en place une solution qui, à mon avis, est la meilleure.

D’abord, pourquoi rien d’autre ne fonctionne:

  1. JButton::setMutliclickThreshold() n’est pas vraiment une solution optimale, car (comme vous l’avez dit), il n’y a aucun moyen de savoir combien de temps pour définir le seuil. Ceci n’est utile que pour éviter le double-clic sur des utilisateurs finaux heureux, car vous devez définir un seuil arbitraire.
  2. JButton::setEnabled() est une solution évidemment fragile qui ne fera que rendre la vie beaucoup plus difficile.

Donc, j’ai créé le SingletonSwingWorker . Maintenant, les singletons sont appelés anti-patterns, mais s’ils sont correctement implémentés, ils peuvent être très puissants. Voici le code:

 public abstract class SingletonSwingWorker extends SwingWorker { abstract void initAndGo(); private static HashMap 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(); } } } 

Cela vous permettra de créer des classes qui étendent SingletonSwingWorker et de garantir qu’une seule instance de cette classe sera exécutable à la fois. Voici un exemple d’implémentation:

 public static void main(Ssortingng[] 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(); } } 

Dans mes implémentations de SwingWorker , j'aime charger une JProgressBar . Je le fais donc toujours avant d'exécuter doInBackground() . Avec cette implémentation, je charge le JProgressBar dans la méthode initAndGo() et j'appelle également execute() , qui doit être placé dans la méthode initAndGo() la classe ne fonctionnera pas .

Quoi qu'il en soit, je pense que c'est une bonne solution et qu'il ne devrait pas être aussi difficile de refactoriser le code pour réajuster vos applications avec.

Très intéressé par les commentaires sur cette solution.

Notez que lorsque vous modifiez quelque chose dans l’interface graphique, votre code doit être exécuté sur le thread Event Dispatch à l’aide de invokeLater ou invokeAndWait si vous vous trouvez dans un autre thread. Donc, le second exemple est incorrect car vous essayez de modifier l’état activé à partir d’un autre thread et cela peut provoquer des bogues imprévisibles.