【问题标题】:Disable JButton, while background job, to avoid multiple clicks在后台作业时禁用 JButton,以避免多次单击
【发布时间】:2012-01-15 18:18:17
【问题描述】:

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

我能够为这个问题提供解决方案,但我不完全理解它为什么会起作用。

下面我发布了有效的代码和无效的代码(修剪到最低限度)。

在第一个示例(好)中,如果您运行它并多次单击按钮,则只考虑一个操作,而对于第二个示例(坏),如果您多次单击鼠标,您的操作至少会执行两次。

第二个(坏的)示例根本没有使用 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 和@Boris Pavlović 提供的信息,这里是我创建的代码,看起来运行得相当不错。

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

【问题讨论】:

  • 无论你做什么,永远不要睡在 EDT
  • 看起来,当“长任务”正在执行时,JButton 仍然会堆积事件,并且一旦“长任务”完成就会执行。
  • 阻止 EDT 时可能会发生各种令人讨厌的事情,没有什么好理解的,因为讨厌也意味着不可预测 - 只是不要做一个快乐的编码员 :-)

标签: java swing thread-safety jbutton


【解决方案1】:

在处理这个问题的困扰多年后,我实施了一个我认为最好的解决方案。

首先,为什么其他方法都不起作用:

  1. JButton::setMutliclickThreshold() 并不是真正的最佳解决方案,因为(正如您所说)没有办法知道设置阈值多长时间。这仅有助于防止双击快乐的最终用户,因为您必须设置任意阈值。
  2. JButton::setEnabled() 显然是一个脆弱的解决方案,只会让生活变得更加困难。

所以,我创建了SingletonSwingWorker。现在,Singleton 被称为反模式,但如果实施得当,它们会非常强大。代码如下:

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() 之前这样做。通过这个实现,我在initAndGo() 方法中加载了JProgressBar,我还调用了execute(),它必须放在initAndGo() 方法中,否则该类将无法工作。

无论如何,我认为这是一个很好的解决方案,重构代码以使用它来调整您的应用程序应该不难。

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

【讨论】:

  • 在我自己的实现中,我可以看到存在一个(小)缺陷:此解决方案假定 SwingWorker::isDone() 始终是确定是否可以再次运行的好方法,但这并不总是情况(在我的情况下)。
【解决方案2】:

好的,这是一个使用 Action 的代码 sn-p

  • 它会在执行时自行禁用
  • 它产生一个任务,在任务结束时再次启用它自己。注意:为了简单起见,这里的任务是由 Timer 模拟的,现实世界会产生一个 SwingWorker 来做后台工作,监听它的属性变化并在接收到 done 时启用自己
  • 设置为按钮的动作

代码:

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

【讨论】:

  • 我真的很想听听您对我刚刚离开的答案的反馈。
【解决方案3】:

你有两个选择

1) JButton#setMultiClickThreshhold

2) 你必须将这个想法拆分为 actionListener 或 Action 中的两个独立动作

  • 第一。一步,JButton#setEnabeld(false);
  • 第二。步骤,然后调用包装到javax.swing.Action 的其余代码(来自javax.swing.Timer 并由javax.swing.Timer 处理)、SwingWorker 或Runnable#Thread

【讨论】:

  • MultiClickThreshold 不正常,因为“长任务”可能比为阈值指定的时间长,因此在“长任务”正在进行时可以触发新的偶数
  • @Alex,真的.. 但是,目标可能只是阻止习惯性双击所有内容的最终用户,因为他们不知道按钮只需要按下一次。也就是在我看来,“setMutliClickThreshold”的主要目标。
【解决方案4】:

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

【讨论】:

  • 但是 task.setEnabled(true);在事件调度线程中执行
  • 是的,我只是对 new Thread() 感到困惑。那么问题和你猜的一样,堆积的事件是在第二种情况下启用按钮之后执行的,而在第一种情况下是在启用按钮之前执行的。
  • 好的,但是为什么在第二种情况下而不是在第一种情况下堆积 - 故障在哪里?
  • 假设您在执行 sleep() 之前单击按钮两次。现在一键还没有处理。在第一种情况下,因为您再次在 invokeLater 中启用按钮,单击将被处理(当按钮被禁用时)并且不会产生任何效果。在第二种情况下,按钮被启用,然后处理挂起的事件。
  • 所以即使按钮被禁用也会处理未决事件...?!?!
【解决方案5】:

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

【讨论】:

  • 正确...除了您不会直接触摸按钮,而是绑定关联的Action的启用
【解决方案6】:

还有两种方法。

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

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

【讨论】:

  • 标志?删除听众?惊讶 ;-) 我们都知道,这很脆弱。 Action 启用的固态模型(正如 OP 基本上想要但还没有完全达到的那样)是要走的路。
  • 同意。基于操作的方式更好,但需要对现有代码进行更多更改。
猜你喜欢
  • 1970-01-01
  • 2016-09-05
  • 2018-06-22
  • 1970-01-01
  • 2019-05-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-07
相关资源
最近更新 更多