【问题标题】:Why is my download progress bar firing the same event multiple times?为什么我的下载进度条会多次触发同一个事件?
【发布时间】:2017-07-26 22:16:38
【问题描述】:

我正在练习 Swing,当用户按下“开始下载”按钮时,我编写了一个下载进度条来下载图像。下载有效。问题是在我的终端中,我可以看到同一个事件 (propertyChange) 被多次触发,每次后续下载的次数都会增加。我已经使用检查点调试了我的代码,但我仍然不确定为什么会发生这种情况。

更具体地说,在我的终端中,我看到了类似

...100% completed 
...100% completed 
...100% completed 
...100% completed 
...100% completed 
...100% completed 
...100% completed

当我希望只看到一次“...100% 完成”时。每次下载都会累积显示的“...100% 完成”的数量。我不确定这是否会影响我的下载性能,但我想知道为什么会这样。

ProgressBar.java:

package download_progress_bar;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class ProgressBar {
    private JFrame frame;
    private JPanel gui;
    private JButton button;
    private JProgressBar progressBar;

    public ProgressBar() {
        customizeFrame();
        createMainPanel();
        createProgressBar();
        createButton();
        addComponentsToFrame();
        frame.setVisible(true);
    }

    private void customizeFrame() {
        // Set the look and feel to the cross-platform look and feel
        try {
            UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
        } catch (Exception e) {
            System.err.println("Unsupported look and feel.");
            e.printStackTrace();
        }

        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setResizable(false);
    }

    private void createMainPanel() {
        gui = new JPanel();
        gui.setLayout(new BorderLayout());
    }

    private void createProgressBar() {
        progressBar = new JProgressBar(0, 100);
        progressBar.setStringPainted(true);  // renders a progress string
    }

    private void createButton()  {
        button = new JButton("Start download");
    }

    private void addComponentsToFrame() {
        gui.add(progressBar, BorderLayout.CENTER);
        gui.add(button, BorderLayout.SOUTH);
        frame.add(gui);
        frame.pack();
    }

    // Add passed ActionListener to the button
    void addButtonListener(ActionListener listener) {
        button.addActionListener(listener);
    }

    // Get progress bar
    public JProgressBar getProgressBar() {
        return progressBar;
    }

    // Enable or disable button
    public void turnOnButton(boolean flip) {
        button.setEnabled(flip);
    }
}

下载器.java:

package download_progress_bar;

import java.net.*;
import java.io.*;
import java.beans.*;

public class Downloader {
    private URL url;
    private int percentCompleted;
    private PropertyChangeSupport pcs;

    public Downloader() {
        pcs = new PropertyChangeSupport(this);
    }

    // Set URL object
    public void setURL(String src) throws MalformedURLException {
        url = new URL(src);
    }

    // Add passed PropertyChangeListener to pcs
    public void addListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    public void download() throws IOException {
        // Open connection on URL object
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();

        // Check response code (always do this first)
        int responseCode = connection.getResponseCode();
        System.out.println("response code: " + responseCode);
        if (responseCode == HttpURLConnection.HTTP_OK) {
            // Open input stream from connection
            BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
            // Open output stream for file writing
            BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("cat.jpg"));

            int totalBytesRead = 0;
            //int percentCompleted = 0;
            int i = -1;
            while ((i = in.read()) != -1) {
                out.write(i);
                totalBytesRead++;

                int old = percentCompleted;
                percentCompleted = (int)(((double)totalBytesRead / (double)connection.getContentLength()) * 100.0);
                pcs.firePropertyChange("downloading", old, percentCompleted);

                System.out.println(percentCompleted);  // makes download a bit slower, comment out for speed
            }

            // Close streams
            out.close();
            in.close();
        }
    }
}

Controller.java:

package download_progress_bar;

import java.util.concurrent.ExecutionException;
import javax.swing.*;
import java.awt.event.*;
import java.util.List;
import java.net.*;
import java.io.*;
import java.beans.*;

public class Controller {
    private ProgressBar view;
    private Downloader model;
    private JProgressBar progressBar;
    private SwingWorker<Void, Integer> worker;

    public Controller(ProgressBar theView, Downloader theModel) {
        view = theView;
        model = theModel;
        progressBar = view.getProgressBar();

        // Add button listener to the "Start Download" button
        view.addButtonListener(new ButtonListener());
    }

    class ButtonListener implements ActionListener {
        /**
         * Invoked when user clicks the button.
         */
        public void actionPerformed(ActionEvent evt) {
            view.turnOnButton(false);
            progressBar.setIndeterminate(true);
            // NOTE: Instances of javax.swing.SwingWorker are not reusable, 
            // so we create new instances as needed
            worker = new Worker();
            worker.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent evt) {
                    if (evt.getPropertyName().equals("progress")) {
                        progressBar.setIndeterminate(false);
                        progressBar.setValue(worker.getProgress());
                    }
                }
            });
            worker.execute();
        }
    }

    class Worker extends SwingWorker<Void, Integer> implements PropertyChangeListener {
        /* 
         * Download task. Executed in worker thread.
         */
        @Override
        protected Void doInBackground() throws MalformedURLException {
            model.addListener(this);
            try {
                String src = "https://lh3.googleusercontent.com/l6JAkhvfxbP61_FWN92j4ulDMXJNH3HT1DR6xrE7MtwW-2AxpZl_WLnBzTpWhCuYkbHihgBQ=s640-h400-e365";
                model.setURL(src);
                model.download();
            } catch (IOException ex) {
                System.out.println(ex);
                this.cancel(true);
            }   
            return null;
        }

        /*
         * Executed in event dispatching thread
         */
        @Override
        protected void done() {
            try {
                if (!isCancelled()) {
                    get();  // throws an exception if doInBackground throws one
                    System.out.println("File has been downloaded successfully!");
                }
            } catch (InterruptedException x) {
                x.printStackTrace();
                System.out.println("There was an error in downloading the file.");
            } catch (ExecutionException x) {
                x.printStackTrace();
                System.out.println("There was an error in downloading the file.");
            }

            view.turnOnButton(true);
        }

        /**
         * Invoked in the background thread of Downloader.
         */
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            this.setProgress((int) evt.getNewValue());
            System.out.println("..." + this.getProgress() + "% completed");
        }
    }
}

Main.java:

package download_progress_bar;

import javax.swing.SwingUtilities;

/**
 * Runs the download progress bar application.
 */
public class Main {
    public static void main(String[] args) {
        // Schedule a job for the event-dispatching thread:
        // creating and showing this application's GUI.
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                // Create view
                ProgressBar view = new ProgressBar();
                // NOTE: Should model/controller be created outside invokeLater?
                // Create model
                Downloader model = new Downloader();
                // Create controller
                Controller controller = new Controller(view, model);
            }
        });
    }
}

编辑:我更新了我的代码以反映建议的更改。但即使进行了更改,问题仍然存在。我仍然看到多次调用“...100% 完成”,调用次数随着每次后续下载而增加。比如我运行应用程序,第一次按下下载按钮,我会看到

...100% completed

我再次按下下载按钮。我明白了

...100% completed
...100% completed

我再次按下下载按钮...

...100% completed
...100% completed
...100% completed

等等。为什么会这样?

【问题讨论】:

  • 这很可能是一个舍入错误。做类似(int)(((double)totalBytesRead / (double)connection.getContentLength()) * 100.0)的事情不是更有意义吗?
  • 我的意思是,由于int 除法四舍五入的方式,最后几个字节可能会产生 100% 的值 - 但是仔细查看您的代码,我认为您的计算是错了
  • 感谢@MadProgrammer 我已经编辑了我的问题,包括您对计算的更正和我的问题的改写,因为问题仍然存在
  • 您可以在后台调用setProgress():“出于性能目的,所有这些调用仅与最后一个调用参数合并为一个调用。”
  • 我唯一可以调用setProgress() 的地方是我的propertyChange() 方法,这就是我遇到的问题。它被多次调用,调用次数随着每次后续下载而增加@trashgod

标签: java swing swingworker jprogressbar


【解决方案1】:

有可能,由于百分比的计算方式,当还有更多工作要完成时,它会报告 100%

在测试过程中我观察到...

//...
98
...
99
99
...
100

所以在代码完成之前,很多值都被重复了。

我注意到您的下载代码中存在一些问题/奇怪之处,主要是您完全忽略了 percentCompleted 属性,因此我将其更改为类似...

public void download() throws IOException {
    // Open connection on URL object
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();

    // Check response code (always do this first)
    int responseCode = connection.getResponseCode();
    System.out.println("response code: " + responseCode);
    if (responseCode == HttpURLConnection.HTTP_OK) {
        // Open input stream from connection
        BufferedInputStream in = new BufferedInputStream(connection.getInputStream());
        // Open output stream for file writing
        BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("cat.jpg"));

        int totalBytesRead = 0;
        //int percentCompleted = 0;
        int i = -1;
        while ((i = in.read()) != -1) {
            out.write(i);
            totalBytesRead++;

            int old = percentCompleted;
            percentCompleted = (int) (((double) totalBytesRead / (double) connection.getContentLength()) * 100.0);
            pcs.firePropertyChange("downloading", old, percentCompleted);

            System.out.println(percentCompleted);  // makes download a bit slower, comment out for speed
        }

        // Close streams
        out.close();
        in.close();
    }
}

对我来说,我会稍微更改代码,而不是这样做......

@Override
protected void process(List<Integer> chunks) {
    int percentCompleted = chunks.get(chunks.size() - 1); // only interested in the last value reported each time
    progressBar.setValue(percentCompleted);

    if (percentCompleted > 0) {
        progressBar.setIndeterminate(false);
        progressBar.setString(null);
    }
    System.out.println("..." + percentCompleted + "% completed");
}

/**
 * Invoked when a progress property of "downloading" is received.
 */
@Override
public void propertyChange(PropertyChangeEvent evt) {
    if (evt.getPropertyName().equals("downloading")) {
        publish((Integer) evt.getNewValue());
    }
}

您应该利用SwingWorkers 内置的进度支持,例如...

/**
 * Invoked when a progress property of "downloading" is received.
 */
@Override
public void propertyChange(PropertyChangeEvent evt) {
    setProgress((int)evt.getNewValue());
}

这意味着您需要将PropertyChangeListener 附加到SwingWorker

/**
 * Invoked when user clicks the button.
 */
public void actionPerformed(ActionEvent evt) {
    view.turnOnButton(false);
    progressBar.setIndeterminate(true);
    // NOTE: Instances of javax.swing.SwingWorker are not reusable, 
    // so we create new instances as needed
    worker = new Worker();
    worker.addPropertyChangeListener(new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if ("progress".equals(evt.getPropertyName())) {
                progressBar.setIndeterminate(false);
                progressBar.setValue(worker.getProgress());
            }
        }
    });
    worker.execute();
}

这样做的副作用是,您知道当SwingWorkerstate 更改以检查它何时是DONE 时也可以得到通知

更新

好的,在检查代码之后,我可以看到您每次执行 SwingWorker 时都会向 model 添加一个新的 PropertyChangeListener

/* 
 * Download task. Executed in worker thread.
 */
@Override
protected Void doInBackground() throws MalformedURLException, InterruptedException {
    model.addListener(this); // Add another listener...
    try {
        String src = "https://lh3.googleusercontent.com/l6JAkhvfxbP61_FWN92j4ulDMXJNH3HT1DR6xrE7MtwW-2AxpZl_WLnBzTpWhCuYkbHihgBQ=s640-h400-e365";
        model.setURL(src);
        model.download();
    } catch (IOException ex) {
        System.out.println(ex);
        this.cancel(true);
    }
    return null;
}

因为modelController的实例字段,所以是有累积作用的。

一种解决方案可能是将Downloader 作为侦听器添加到model,但这需要您确保对 UI 执行的任何更新都正确同步。

更好的通用解决方案是添加支持以在工作人员完成后删除侦听器

public class Downloader {
    //...        
    public void removeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }

然后在SwingWorkers done方法中,移除监听器...

/*
 * Executed in event dispatching thread
 */
@Override
protected void done() {
    model.removeListener(this);

【讨论】:

  • 谢谢!我已更新我的问题以反映这些更改,即使在进行更改后,问题仍然存在......?
  • @briennakh 正如我多次说过的,这并不意外
  • 所以我的程序存储所有先前下载的 propertyChanges 并在每次后续下载中调用它们并不意外?我只是想了解这是如何工作的@MadProgrammer
  • 非常感谢!!!我正在为此拉扯头发,而你让我度过了一个美好的夜晚。 @MadProgrammer
【解决方案2】:

herehere 所示,SwingWorker 维护两个绑定属性:stateprogress。调用 setProgress() 确保“PropertyChangeListenersEvent Dispatch Thread 上得到异步通知。”只需将PropertyChangeListener 添加到进度条并在doInBackground() 的实现中调用setProgress(),或者它调用的方法,例如download()。方便的是,“出于性能目的,所有这些调用仅与最后一个调用参数合并为一个调用。”

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-12-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-03
    • 2023-02-02
    • 2014-10-05
    相关资源
    最近更新 更多