2017-07-26 3 views
1

Je pratique Swing et j'ai codé une barre de progression de téléchargement pour télécharger une image lorsque l'utilisateur appuie sur le bouton "Démarrer le téléchargement". Le téléchargement fonctionne. Le problème est que dans mon terminal, je peux voir que le même événement (propertyChange) est lancé plusieurs fois, le nombre de fois augmente avec chaque téléchargement ultérieur. J'ai débogué mon code avec des points de contrôle, mais je ne sais toujours pas pourquoi cela se produit.Pourquoi ma barre de progression de téléchargement envoie-t-elle plusieurs fois le même événement?

Pour être plus précis, dans mon terminal je vois quelque chose comme

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

quand j'attends à voir « ... 100% terminé » une seule fois. Le nombre de "... 100% terminé" affiché s'accumule à chaque téléchargement. Je ne suis pas sûr si cela affecte la performance de mon téléchargement, mais je me demande pourquoi cela se passe.

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

Downloader.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); 
      } 
     }); 
    } 
} 

EDIT: J'ai mis à jour mon code pour refléter les changements suggérés. Mais même après avoir fait les changements, le problème persiste. Je vois encore plusieurs invocations de "... 100% achevées", le nombre d'invocations augmentant à chaque téléchargement. Par exemple, je lance l'application et appuyez sur le bouton de téléchargement pour la première fois, je verrai

...100% completed 

J'appuyez à nouveau sur le bouton de téléchargement. Je vois

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

j'appuie sur le bouton de téléchargement à nouveau ...

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

et ainsi de suite. Pourquoi cela arrive-t-il?

+1

Il s'agit probablement d'une erreur d'arrondi. Ne serait-il pas plus logique de faire quelque chose comme '(int) (((double) totalBytesRead/(double) connection.getContentLength()) * 100.0)' – MadProgrammer

+0

Je veux dire qu'il est possible que les derniers octets puissent générer 100 % value dû à la façon dont la division 'int' est arrondie - mais après avoir regardé un peu plus près votre code, je pense que votre calcul est faux – MadProgrammer

+0

Merci d'avoir rattrapé ce @MadProgrammer J'ai édité ma question pour inclure votre correction au calcul et une reformulation de ma question, puisque le problème persiste – briennakh

Répondre

5

Il est possible que, en raison de la façon dont le pourcentage est calculé qu'il rapportera 100% quand il y a encore un peu plus de travail à remplir

Au cours de mes tests j'ai observé ...

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

Donc beaucoup de valeurs ont été répétées avant que le code soit terminé.

Je note quelques problèmes/bizarreries dans votre code de téléchargement, la plupart du temps le fait que vous ignorez complètement la propriété percentCompleted, donc je l'ai changé quelque chose comme ...

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

Pour moi, je changer le code légèrement, au lieu de faire ...

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

Vous devriez bénéficier du support de progression de SwingWorker intégré, par exemple ...

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

Cela signifie que vous devrez ci-joint un PropertyChangeListener au 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(); 
} 

L'effet secondaire à cela, vous savez avoir un moyen pour être informé également lorsque state changements du SwingWorker pour vérifier à voir quand il est DONE

Mise à jour

Bon, après avoir traversé le code, encore une fois, je peux voir que vous ajoutez un nouveau PropertyChangeListener-model CHAQUE FOIS que vous exécutez la SwingWorker

/* 
* 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; 
} 

Parce que le model est un champ d'instance de Controller, cela a un effet cumulatif.

Une solution pourrait être d'ajouter simplement le Downloader en tant qu'auditeur au model, mais cela vous obligerait à vous assurer que toutes les mises à jour effectuées sur l'interface utilisateur sont correctement synchronisées.

Une meilleure, en général, la solution serait d'ajouter le support pour enlever l'auditeur lorsque le travailleur accomplit

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

Et puis dans la SwingWorker de la méthode done, supprimer l'auditeur ...

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

Merci! J'ai mis à jour ma question pour refléter ces changements et que même après avoir fait les changements, le problème persiste ...? – briennakh

+0

@briennakh Comme je l'ai dit un certain nombre de fois, ce n'est pas inattendu – MadProgrammer

+0

Donc, il n'est pas inattendu que mon programme stocke les propertyChanges de tous les téléchargements précédents et les invoque tous dans chaque téléchargement ultérieur? J'essaie juste de comprendre comment cela fonctionne @MadProgrammer – briennakh

2

Comme le montre here et here, SwingWorker maintient deux propriétés liées: state et progress. L'appel de setProgress() garantit que "PropertyChangeListeners est notifié de manière asynchrone sur le thread d'envoi d'événements ". Ajoutez simplement un PropertyChangeListener à votre barre de progression et appelez setProgress() dans votre implémentation de doInBackground(), ou une méthode qu'il appelle comme download(). Commodément, "À des fins de performance, toutes ces invocations sont fusionnées en une invocation avec le dernier argument d'invocation uniquement."