在后台作业时禁用JButton以避免多次单击

时间:2011-12-08 09:34:25

标签: java swing thread-safety jbutton

我需要阻止用户在第一次点击仍然执行时对JButton进行多次点击。

我能够为这个问题找到解决方案,但我并不完全理解为什么它有效。

Bellow我发布了有效的代码(修剪到最小)和无法使用的代码。

在第一个例子中(好的)如果你运行它并多次单击该按钮,则只考虑一个动作和第二个例子(坏)如果你多次单击鼠标就会至少执行两次动作。

第二个(坏)示例根本不使用invokeLater()方法。

行为的差异来自哪里?

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

现在是“错误的”代码

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

在@kleopatra和@BorisPavlović提供的信息后,我创建的代码看起来相当不错。

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

6 个答案:

答案 0 :(得分:8)

你有两个选择

1)JButton#setMultiClickThreshhold

2)你必须将这个想法分解为actionListener或Action

中的两个独立动作
  • 第一。步骤,JButton#setEnabeld(false);
  • 第二。步骤,然后调用其余代码包裹到javax.swing.Action(来自javax.swing.Timer),SwingWorkerRunnable#Thread

答案 1 :(得分:2)

好的,这是使用Action

的代码段
  • 它在执行时禁用了它本身
  • 它产生一个任务,最后再次启用它。注意:为简单起见,这里的任务是由Timer模拟的,真实世界会产生一个SwingWorker来完成后台工作,听取它的属性变化并在接收完成后启用它自己
  • 设置为按钮的操作

代码:

    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 :(得分:1)

还有两种方式。

您可以定义标志。在动作开始时设置它,在结束后重置。检查actionPerformed中的标志。如果inProgress==true什么也不做。

另一种方法是删除侦听器并在操作结束后将其分配回来。

答案 3 :(得分:1)

正确的方法是使用SwingWorker。当用户在将作业提交到SwingWorker之前单击按钮时,按钮的状态应更改为禁用JButton#setEnabled(false)。在SwingWorker完成后,应将按钮的作业状态重置为启用状态。这是Oracle的tutorial on SwingWorker

答案 4 :(得分:1)

经过多年处理这个问题的挫败感,我已经实施了一个我认为最好的解决方案。

首先,为什么没有其他工作:

  1. JButton::setMutliclickThreshold()并非真正的最佳解决方案,因为(正如您所说)无法知道设置阈值的时间。这样做只是为了防止双击快乐的最终用户,因为你必须设置一个任意的阈值。
  2. JButton::setEnabled()是一个明显脆弱的解决方案,只会让生活变得更加困难。
  3. 所以,我创建了SingletonSwingWorker。现在,单身人士被称为反模式,但如果实施得当,他们可以是一个非常强大的。这是代码:

    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();
            }
        }
    }
    

    这将使您能够创建扩展SingletonSwingWorker的类,并保证该类的一个实例一次只能执行。以下是一个示例实现:

    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();
        }
    
    }
    

    在我的SwingWorker实现中,我想加载JProgressBar,因此我始终在运行doInBackground()之前执行此操作。通过此实现,我在JProgressBar方法中加载initAndGo(),我也调用execute(),这必须放在initAndGo()方法中,否则类不会工作即可。

    无论如何,我认为这是一个很好的解决方案,不应该重构代码来重新编写你的应用程序。

    对此解决方案的反馈非常感兴趣。

答案 5 :(得分:0)

请注意,当您在GUI中修改任何内容时,如果您在另一个线程中,则必须使用invokeLater或invokeAndWait在Event Dispatch线程上运行代码。因此,当您尝试从另一个线程修改启用状态时,第二个示例是不正确的,并且它可能导致不可预测的错误。