【问题标题】:SwingWorker not updating progressbar from inside processSwingWorker 没有从进程内部更新进度条
【发布时间】:2020-04-29 18:01:42
【问题描述】:

我有一个 Java 8 Swing 应用程序,当用户单击一个新按钮时,需要向它添加一个耗时的操作。我认为它是SwingWorker 的完美用例,尽管我以前从未写过。完整源代码和reproducible Swing app is here

当用户点击一个按钮时,应用程序必须从几个不同的来源收集信息,然后开始这个后台操作。它将计算一个InputAnalysis,然后将该InputAnalysis 返回到 EDT 中的单击处理程序以更新 UI。虽然它有效,但我希望它也更新JProgressBar,以便用户看到正在取得的进展。迄今为止我最好的尝试:

package com.example.swingworker.suchwow;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import java.util.List;

public class MegaApp {

    public static void main(String[] args) {
        new MegaApp().run();
    }

    public void run() {

        SwingUtilities.invokeLater(() -> {

            System.out.println("starting app");

            JFrame.setDefaultLookAndFeelDecorated(true);

            JFrame mainWindow = new JFrame("Some Simple App!");
            mainWindow.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            mainWindow.setResizable(true);

            mainWindow.addWindowListener(new WindowAdapter() {
                @Override
                public void windowClosing(WindowEvent e) {
                System.out.println("app is shutting down");
                System.exit(0);
                }
            });


            JPanel jPanel = new JPanel();

            JTextField superSecretInfoTextField = new JTextField();
            JButton analyzeButton = new JButton("Analyze");
            JProgressBar progressBar = new JProgressBar();

            superSecretInfoTextField.setPreferredSize(new Dimension(200,
                (int)superSecretInfoTextField.getPreferredSize().getHeight()));

            jPanel.add(superSecretInfoTextField);
            jPanel.add(analyzeButton);
            jPanel.add(progressBar);

            progressBar.setValue(0);

            analyzeButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {

                    // on click, scoop some info from the input and run a time-consuming task.
                    // usually takes 20 - 30 seconds to run, and I'd like to be updating the progress
                    // bar during that time.
                    //
                    // also need to handle cases where the task encounters a POSSIBLE error and needs to
                    // communicate back to the EDT to display a JOPtionPane to the user; and then get the
                    // user's response back and handle it.
                    //
                    // also need to handle the case where the long running task encounters both a checked
                    // and unchecked/unexpected exception
                    String superSecretInfo = superSecretInfoTextField.getText();

                    // here is where we start the long-running task. ideally this needs to go into a SwingWorker
                    // however there is a somewhat complex back-and-forth-communication required. see the analysis
                    // method comments for details
                    try {

                        InputAnalysis analysis = analysisService_analyze(progressBar, superSecretInfo, mainWindow);
                        superSecretInfoTextField.setText(analysis.getSuperSecretAnswer());

                    } catch (IOException ex) {
                        System.out.println(ex.getMessage());
                        JOptionPane.showMessageDialog(
                                mainWindow,
                                "Something went wrong",
                                "Aborted!",
                                JOptionPane.WARNING_MESSAGE);
                    }


                    // comment the above try-catch out and uncomment all the worker code below to switch over
                    // to the async/non-blocking worker based method

//                    MegaWorker analysisWorker = new MegaWorker(mainWindow, progressBar, superSecretInfo);
//                    analysisWorker.addPropertyChangeListener(evt -> {
//
//                        if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
//                            try {
//                                // this is called on the EDT
//                                InputAnalysis asyncAnalysis = analysisWorker.get();
//                                superSecretInfoTextField.setText(asyncAnalysis.getSuperSecretAnswer());
//
//                            } catch (Exception ex) {
//                                System.out.println(ex.getMessage());
//                            }
//                        }
//
//                    });
//
//                    analysisWorker.execute();


                }
            });

            mainWindow.add(jPanel);
            mainWindow.pack();
            mainWindow.setLocationRelativeTo(null);
            mainWindow.setVisible(true);

            System.out.println("application started");

        });

    }

    public InputAnalysis analysisService_analyze(JProgressBar progressBar,
                                                 String superSecretInfo,
                                                 JFrame mainWindow) throws IOException {
        progressBar.setValue(25);

        // simulate a few seconds of processing
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            System.out.println(e.getMessage());
            throw new RuntimeException("SOMETHIN BLEW UP");
        }

        // now we are ready to analyze the input which itself can take 10 - 15 seconds but
        // we'll mock it up here
        if (superSecretInfo == null || superSecretInfo.isEmpty()) {

            // if the input is null/empty, we'll consider that a "checked exception"; something the
            // REAL code I'm using explicitly has a try-catch for because the libraries I'm using throw
            // them

            throw new IOException("ERMERGERD");

        } else if (superSecretInfo.equals("WELL_WELL_WELL")) {

            // here we'll consider this an unchecked exception
            throw new RuntimeException("DID NOT SEE THIS ONE COMING");

        }

        progressBar.setValue(55);

        // check to see if the input equals "KEY MASTER"; if it does we need to go back to the EDT
        // and prompt the user with a JOptionPane
        if (superSecretInfo.equalsIgnoreCase("KEY MASTER")) {

            int answer = JOptionPane.showConfirmDialog(
                    mainWindow,
                    "We have identified a KEY MASTER scenario. Do you wish to proceed?",
                    "Do you wish to proceed",
                    JOptionPane.YES_NO_OPTION);

            if (answer == JOptionPane.NO_OPTION) {

                // return a partial InputAnalysis and return
                Boolean isFizz = Boolean.TRUE;
                String superSecretAnswer = "HERE IS A PARTIAL ANSWER";
                Integer numDingers = 5;

                return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

            }

        }

        // if we get here, either KEY MASTER was not in the input or they chose to proceed anyway
        Boolean isFizz = superSecretInfo.length() < 5 ? Boolean.TRUE : Boolean.FALSE;
        String superSecretAnswer = "HERE IS A FULL ANSWER";
        Integer numDingers = 15;

        progressBar.setValue(100);

        return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

    }

    public class InputAnalysis {

        private Boolean isFizz;
        private String superSecretAnswer;
        private Integer numDingers;

        public InputAnalysis(Boolean isFizz, String superSecretAnswer, Integer numDingers) {
            this.isFizz = isFizz;
            this.superSecretAnswer = superSecretAnswer;
            this.numDingers = numDingers;
        }

        public Boolean getFizz() {
            return isFizz;
        }

        public void setFizz(Boolean fizz) {
            isFizz = fizz;
        }

        public String getSuperSecretAnswer() {
            return superSecretAnswer;
        }

        public void setSuperSecretAnswer(String superSecretAnswer) {
            this.superSecretAnswer = superSecretAnswer;
        }

        public Integer getNumDingers() {
            return numDingers;
        }

        public void setNumDingers(Integer numDingers) {
            this.numDingers = numDingers;
        }
    }

    public class MegaWorker extends SwingWorker<InputAnalysis,Integer> {

        private JFrame mainWindow;
        private JProgressBar progressBar;
        private String superSecretInfo;

        public MegaWorker(JFrame mainWindow, JProgressBar progressBar, String superSecretInfo) {
            this.mainWindow = mainWindow;
            this.progressBar = progressBar;
            this.superSecretInfo = superSecretInfo;
        }

        @Override
        protected void process(List<Integer> chunks) {

            progressBar.setValue(chunks.size() - 1);

        }

        @Override
        protected InputAnalysis doInBackground() throws Exception {

            publish(25);

            // simulate a few seconds of processing
            try {
                Thread.sleep(5 * 1000);
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
                throw new RuntimeException("SOMETHIN BLEW UP");
            }

            // now we are ready to analyze the input which itself can take 10 - 15 seconds but
            // we'll mock it up here
            if (superSecretInfo == null || superSecretInfo.isEmpty()) {

                // if the input is null/empty, we'll consider that a "checked exception"; something the
                // REAL code I'm using explicitly has a try-catch for because the libraries I'm using throw
                // them

                throw new IOException("ERMERGERD");

            } else if (superSecretInfo.equals("WELL_WELL_WELL")) {

                // here we'll consider this an unchecked exception
                throw new RuntimeException("DID NOT SEE THIS ONE COMING");

            }

            publish(55);

            // check to see if the input equals "KEY MASTER"; if it does we need to go back to the EDT
            // and prompt the user with a JOptionPane
            if (superSecretInfo.equalsIgnoreCase("KEY MASTER")) {

                int answer = JOptionPane.showConfirmDialog(
                        mainWindow,
                        "We have identified a KEY MASTER scenario. Do you wish to proceed?",
                        "Do you wish to proceed",
                        JOptionPane.YES_NO_OPTION);

                if (answer == JOptionPane.NO_OPTION) {

                    // return a partial InputAnalysis and return
                    Boolean isFizz = Boolean.TRUE;
                    String superSecretAnswer = "HERE IS A PARTIAL ANSWER";
                    Integer numDingers = 5;

                    return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

                }

            }

            // if we get here, either KEY MASTER was not in the input or they chose to proceed anyway
            Boolean isFizz = superSecretInfo.length() < 5 ? Boolean.TRUE : Boolean.FALSE;
            String superSecretAnswer = "HERE IS A FULL ANSWER";
            Integer numDingers = 15;

            publish(100);

            return new InputAnalysis(isFizz, superSecretAnswer, numDingers);

        }

    }

}

当我注释掉包含我的analysisService_analyze() 调用的try-catch 块并取消注释我的MegaWorker 的代码时,进度条仍未正确更新。

不需要,因为上面提供了 SSCCE 的所有必要代码,但如果您有兴趣快速构建和运行此代码,我已在 GitHub 上准备了此 SimpleApp repo 以保存你有时间。但不是回答这个问题所必需的,上面提供了所有代码。 100%。

【问题讨论】:

  • OK @HovercraftFullOfEels 我已经按照您的要求进行了更改。所有的源代码都在那里,我已经剥离了所有 3rd 方库依赖项。要运行代码,您只需将其粘贴到 Java 源文件中并在系统上安装有效的 JRE。如果您对如何运行/重现问题有任何疑问,请告诉我!
  • 好多了,谢谢你和 1+。您看起来需要一个可以与 GUI 进行双向“对话”的后台进程,为此,我能想到的最好方法是为您的后台进程创建一个状态机,在后台进程时通知 GUI通过属性更改侦听器更改状态,并允许 GUI 响应此更改。我将尝试向您展示代码解决方案,但可能需要一些时间。
  • 完美,谢谢@HovercraftFullOfEels 我期待看到你能做的任何事!

标签: java swing swingworker edt


【解决方案1】:

不是

progressBar.setValue(chunks.size() - 1);

因为集合的大小不是您所追求的。相反,您希望显示集合的值:

for (int item : chucks) {
    progressBar.setValue(item);
}

您还需要在您的工作人员完成其工作时调用get(),无论是在其done() 方法中,还是在当工作人员的状态值为SwingWorker.StateValue.DONE 时通知您的PropertyChangeListener 中

同样的 get() 调用将在 EDT 上返回新的 InputAnalysis 对象,如果在工作进程运行期间发生任何异常,将抛出异常,因此您可以在那里处理它们。

例如,

analyzeButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {

        Fizz fizz = fizzService.fetchFromWs(1234);

        // make this guy final
        final Analyzer analyzer = new Analyzer(progressBar, nameTextField.getText(), fizz);

        analyzer.addPropertyChangeListener(evt -> {
            // this is a call-back method and will be called in response to 
            // state changes in the SwingWorker
            if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                try {
                    // this is called on the EDT
                    InputAnalysis analysis = analyzer.get();

                    // do what you want with it here

                } catch (Exception e) {
                    e.printStackTrace();
                }                   
            }
        });

        analyzer.execute();

        // but now, how do I obtain the InputAnalysis instance?!
        // InputAnalysis analysis = null; // analyzer.getSomehow();
    }
}

(代码未测试)

附注:您可以通过简单地将工作进程的进度绑定字段更改为 0 到 100 之间的任何值来取消发布/处理方法对。然后在同一个 PropertyChangeListener 中侦听并响应此属性的更改。


例如,使用框架 InputAnalysis 类对您的代码进行排序:

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.swing.*;

public class SwingWorkerExample {

    private static void createAndShowGui() {
        SwGui mainPanel = new SwGui();

        JFrame frame = new JFrame("SwingWorker Example");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}
@SuppressWarnings("serial")
class SwGui extends JPanel {
    private JProgressBar progressBar = new JProgressBar(0, 100);
    private JTextArea textArea = new JTextArea(14, 40);
    private StartAction startAction = new StartAction("Start", this);

    public SwGui() {
        JPanel bottomPanel = new JPanel();
        bottomPanel.add(new JButton(startAction));

        progressBar.setStringPainted(true);
        textArea.setFocusable(false);
        JScrollPane scrollPane = new JScrollPane(textArea);
        scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);

        setLayout(new BorderLayout());
        add(progressBar, BorderLayout.PAGE_START);
        add(scrollPane);
        add(bottomPanel, BorderLayout.PAGE_END);
    }

    public void appendText(String text) {
        textArea.append(text + "\n");
    }

    public void setProgressValue(int value) {
        progressBar.setValue(value);
    }
}
@SuppressWarnings("serial")
class StartAction extends AbstractAction {
    private SwGui gui;
    private AnalyzerWorker worker;
    private InputAnalysis inputAnalysis;

    public StartAction(String text, SwGui gui) {
        super(text);
        this.gui = gui;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        worker = new AnalyzerWorker();
        setEnabled(false); // turn off button
        gui.appendText("START");
        worker.addPropertyChangeListener(evt -> {
            if (evt.getPropertyName().equals("progress")) {
                int progress = (int) evt.getNewValue();
                gui.setProgressValue(progress);
                gui.appendText(String.format("Percent done: %03d%%", progress));
            } else if (evt.getPropertyName().equals("state")) {
                if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                    setEnabled(true);
                    try {
                        inputAnalysis = worker.get();
                        String analysisText = inputAnalysis.getText();
                        gui.appendText(analysisText);
                    } catch (InterruptedException | ExecutionException e1) {
                        e1.printStackTrace();
                    }
                }
            }
        });
        worker.execute();
    }
}
class InputAnalysis {

    public String getText() {
        return "DONE";
    }

}
class AnalyzerWorker extends SwingWorker<InputAnalysis, Void> {
    private static final int MAX_VALUE = 100;

    @Override
    protected InputAnalysis doInBackground() throws Exception {
        int value = 0;
        setProgress(value);
        while (value < MAX_VALUE) {
            // create random values up to 100 and sleep for random time
            TimeUnit.SECONDS.sleep((long) (2 * Math.random()));
            value += (int) (8 * Math.random());
            value = Math.min(MAX_VALUE, value);
            setProgress(value);
        }

        return new InputAnalysis();
    }
}

关于您的问题和代码的更改:

您看起来需要一个可以与 GUI 进行双向“对话”的后台进程,为此,我能想到的最好方法是为您的后台进程创建一个状态机,在何时通知 GUI后台进程通过属性更改侦听器更改状态,并允许 GUI 响应此更改。我将尝试向您展示代码解决方案,但这可能需要一些时间。

【讨论】:

  • @MartijnPieters:问题代码现在好多了,问题已经重新打开了
  • 很高兴听到这个消息!我已经清理了 cmets,请随意将剩余的标记为“不再需要”。
  • 嗨@HovercraftFullOfEels 抱歉不要太出风头,有大约 270k 的代表,我猜你在这里是一个非常忙碌的人!但是只是想知道您是否认为有任何机会可以使用您之前提出的状态机更新您的代码示例!如果没有,绝对不用担心,只是希望能留在你的雷达上!再次感谢!
  • 我很忙,因为我是最近几天待命的医生
  • 是的,就像我说的那样,不要急于求成,只是觉得你很忙,这样的问题可能会从你的雷达上消失。不过,感谢您跟进我!
【解决方案2】:

最后我无法使用 SwingWorker 进行此操作,但 能够使用 Guava AsyncEventBus 使其工作。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多