I. Le thread de téléchargement

Comme dit plus haut, ce thread est nécessaire pour que l'interface graphique ne soit pas figée pendant toute la durée du téléchargement.

On utilise ici les threads de PyQt4 : QtCore.QThread. Son utilisation est quasi identique au Thread du module threading habituel de Python.

La méthode la plus sûre est de créer une classe qui hérite de QtCore.QThread. Elle est appelée ici Telecharger. Sa structure est classique :

 
Sélectionnez
class Telecharger(QtCore.QThread):
 
    def __init__(self, source, destination, parent=None):
        super(Telecharger, self).__init__(parent)
        #
        # ici, l'initialisation du thread
        #
 
    def run(self):
        #
        # ici, la partie qui s'exécute en même temps que l'interface graphique
        #

Et le lancement du thread, à partir de la fenêtre graphique est, là aussi, très classique :

 
Sélectionnez
self.telech = Telecharger(source, destination)
self.telech.start()

Bien sûr, il ne faut pas de méthode join(), puisque l'interface graphique doit pouvoir reprendre la main tout de suite.

Remarquez cependant que cette utilisation de QThread n'est pas la seule possible, certains ne la recommandent d'ailleurs pas du tout : les threads sans maux de tête et vous vous y prenez mal....

II. Le téléchargement

Le téléchargement lui-même est lancé par :

 
Sélectionnez
filename, msg = urllib.urlretrieve(self.source, self.destination, reporthook=self.infotelech)

Et la méthode du thread infotelech() recevra trois arguments :

  • telechbloc : le numéro du bloc téléchargé ;
  • taillebloc : la taille de ce bloc en octets ;
  • totalblocs : la taille totale du téléchargement en octets.

Donc, le pourcentage téléchargé sera calculé par telechbloc * taillebloc / totalblocs * 100.

À noter que, si la taille totale n'est pas connue, le nombre total de blocs vaut -1.

III. La communication par message entre le thread et la fenêtre graphique

Le thread ne doit jamais toucher à la partie graphique ! Il faut donc que ce thread donne des infos exclusivement par messages à la partie graphique et ceci pour deux types d'évènements :

  • fournir les infos de progression du téléchargement pour mettre à jour une barre de progression graphique ;
  • signaler l'arrêt normal ou anormal du téléchargement pour signaler à l'utilisateur par une fenêtre graphique la fin de l'opération.

Cette communication par message se fait de la façon suivante.

Juste avant le lancement du thread par start(), on prépare la fenêtre graphique à recevoir des messages de la part du thread et à lancer une méthode à chaque fois :

 
Sélectionnez
self.connect(self.telech, QtCore.SIGNAL("infotelech(PyQt_PyObject)"), self.infotelech)
self.connect(self.telech, QtCore.SIGNAL("fintelech(PyQt_PyObject)"), self.fintelech)

À noter que :

  • les deux messages infotelech et fintelech ont été créés pour ce programme (n'existent pas dans Qt) ;
  • l'argument (PyQt_PyObject) va permettre quelque chose de très intéressant : passer des données Python en même temps que le message.

Les deux méthodes qui seront ainsi lancées à chaque réception de message seront :

  • infotelech qui recevra du thread la progression du téléchargement et qui mettra à jour la barre de progression graphique ;
  • fintelech qui recevra l'information de fin (normale ou non) de téléchargement et qui le signalera à l'utilisateur par une fenêtre de message.

Et, au sein du thread, les deux messages seront émis par :

 
Sélectionnez
# pour transmettre les infos de progression du téléchargement:
self.emit(QtCore.SIGNAL("infotelech(PyQt_PyObject)"), [telechbloc, taillebloc, totalblocs])
 
# pour donner l'info de fin du téléchargement:
self.emit(QtCore.SIGNAL("fintelech(PyQt_PyObject)"), messagefin)

IV. La barre de progression

La barre graphique de progression est définie par :

 
Sélectionnez
self.barre = QtGui.QProgressBar(self)
self.barre.setRange(0, 100)
self.barre.setValue(0)

Sa mise à jour des infos de progression sera donc :

 
Sélectionnez
p = int(telechbloc*taillebloc/totalblocs*100) # pourcentage
self.barre.setValue(p)

Petite particularité : certains téléchargements ne permettent pas de connaitre la taille totale, on ne peut donc pas calculer une progression ! Dans ce cas, la barre de progression affiche une chenille qui signale uniquement que le téléchargement est en cours. On affiche cette chenille en fixant un minimum et un maximum tous les deux égaux à zéro :

 
Sélectionnez
self.barre.reset()
self.barre.setRange(0, 0)

Avec le code présenté, la progression de la barre est un peu saccadée. Cela vient de la grande quantité de messages échangés (un par bloc téléchargé). On pourrait améliorer de la façon suivante : le calcul du pourcentage est fait dans le thread, le message n'est émis que si ce pourcentage est supérieur au pourcentage précédent. Cela complique un peu le code, mais cela rend l'avancement de la barre très progressif.

V. L'arrêt du téléchargement avant la fin

On va profiter que la fonction du téléchargement appelle une méthode pour lui transmettre les informations de progression, pour placer dans cette méthode le test d'une variable et, si nécessaire, un raise qui fera échouer le téléchargement :

 
Sélectionnez
if self.stop:
    raise Abort

Pour faire propre, on a ici créé une exception spécifique appelée Abort :

 
Sélectionnez
class Abort(Exception):
    pass

Et, bien sûr, la fonction de téléchargement est donc dans un try: except Abort: dans la méthode run du thread. La fin de la méthode run termine le thread.

VI. Diverses précautions supplémentaires

Comme c'est un code simplifié, de nombreuses vérifications ne sont pas faites. Il y en a cependant plusieurs, liées au fait que l'utilisateur devrait pouvoir cliquer sur n'importe quoi sans que des dysfonctionnements graves apparaissent :

  • on ne peut pas relancer un téléchargement alors qu'il y en a déjà un de lancé ;
  • on ne peut pas stopper un téléchargement qui n'est pas déjà en cours ;
  • la fermeture de la fenêtre graphique arrête automatiquement le téléchargement s'il y en a un en cours.

Pour le dernier point, on le fait en surchargeant la méthode closeEvent de la classe QtGui.QWidget, qui est déclenchée quelle que soit la méthode de fermeture choisie (y compris la croix en haut de la fenêtre ou le “fermer” du petit menu système de la fenêtre).

VII. Code complet

Voilà le code complet, ici en Python 2.7, que vous pouvez essayer en copier-coller. Il est multiplateforme (au moins Windows-Linux). Vous devez avoir installé PyQt4 avant toute utilisation.

Pour essayer, vous pouvez utiliser le téléchargement de la doc de Python (et la consulter ne fera pas de mal…) :

  • son adresse Web : http://docs.python.org/archives/python-2.7.1-docs-pdf-a4.zip ;
  • à enregistrer sous (ici dans le même répertoire que le code python) python-2.7.1-docs-pdf-a4.zip.
 
Sélectionnez
#!/usr/bin/python
# -*- coding: utf-8 -*-
from __future__ import division
# Python 2.7
 
import sys, os
import urllib
from PyQt4 import QtCore, QtGui
 
#############################################################################
class Abort(Exception):
    """classe d'exception créée pour l'arrêt du téléchargement avant la fin"""
    pass
 
#############################################################################
class Telecharger(QtCore.QThread):
    """Thread de téléchargement"""
 
    #========================================================================
    def __init__(self, source, destination, parent=None):
        super(Telecharger,self).__init__(parent)
        self.source = source
        self.destination = destination
        self.stop = False
 
    #========================================================================
    def run(self):
        # lancement du téléchargement
        try:
            filename, msg = urllib.urlretrieve(self.source, self.destination, 
                                                     reporthook=self.infotelech)
            messagefin = u"Téléchargement terminé\n\n" + unicode(msg)
        except Abort:
            messagefin = u"Téléchargement avorté"
        # fin du thread: émission du message de fin
        self.emit(QtCore.SIGNAL("fintelech(PyQt_PyObject)"), messagefin)
 
    #========================================================================
    def infotelech(self, telechbloc, taillebloc, totalblocs):
        """reçoit les infos de progression du téléchargement"""
        # nécessaire pour stopper le téléchargement avant la fin
        if self.stop:
            raise Abort
        # envoie les infos de progression à la fenêtre graphique
        self.emit(QtCore.SIGNAL("infotelech(PyQt_PyObject)"), 
                                          [telechbloc, taillebloc, totalblocs])
 
    #========================================================================
    def stoptelech(self):
        # permet l'arrêt du téléchargement avant la fin
        self.stop = True
 
#############################################################################
class Fenetre(QtGui.QWidget):
 
    #========================================================================
    def __init__(self, parent=None):
        super(Fenetre,self).__init__(parent)
 
        self.label1 = QtGui.QLabel(u"Adresse web du fichier à télécharger:", self)
        self.fichierweb = QtGui.QLineEdit(self)
 
        self.label2 = QtGui.QLabel(u"Emplacement sur le disque:", self)
        self.fichier = QtGui.QLineEdit(self)
 
        self.label3 = QtGui.QLabel(u"Lancement/arrêt du téléchargement:", self)
 
        self.depart = QtGui.QPushButton(u"Départ", self)
        self.depart.clicked.connect(self.depart_m)
 
        self.stop = QtGui.QPushButton(u"Stop", self)
        self.stop.clicked.connect(self.stop_m)
 
        self.barre = QtGui.QProgressBar(self)
        self.barre.setRange(0, 100)
 
        self.barre.setValue(0)
 
        posit = QtGui.QGridLayout()
        posit.addWidget(self.label1, 0, 0, 1, 2)
        posit.addWidget(self.fichierweb, 1, 0, 1, 2)
        posit.addWidget(self.label2, 2, 0, 1, 2)
        posit.addWidget(self.fichier, 3, 0, 1, 2)
        posit.addWidget(self.label3, 4, 0, 1, 2)
        posit.addWidget(self.depart, 5, 0)
        posit.addWidget(self.stop, 5, 1)
        posit.addWidget(self.barre, 6, 0, 1, 2)
        self.setLayout(posit)        
 
        self.telech = None
 
    #========================================================================
    def depart_m(self):
        if self.telech==None or not self.telech.isRunning():
            # initialisation de la barre de progression
            self.barre.reset()
            self.barre.setRange(0, 100)
            self.barre.setValue(0)
            # démarre le téléchargement
            source = unicode(self.fichierweb.text())
            destination = unicode(self.fichier.text())
            self.telech = Telecharger(source, destination)
            self.connect(self.telech, QtCore.SIGNAL("infotelech(PyQt_PyObject)"), 
                                                            self.infotelech)
            self.connect(self.telech, QtCore.SIGNAL("fintelech(PyQt_PyObject)"), 
                                                            self.fintelech)
            self.telech.start()
 
    #========================================================================
    def infotelech(self, msg):
        """lancé à chaque réception d'info sur la progression du téléchargement"""
        telechbloc, taillebloc, totalblocs = msg
        if totalblocs > 0:
            # on a la taille maxi: on peut mettre à jour la barre de progression
            p = int(telechbloc*taillebloc/totalblocs*100)
            self.barre.setValue(p)
            QtCore.QCoreApplication.processEvents() # force le rafraichissement
        else:
            # taille maxi inconnue: la barre sera une chenille sans progression
            if self.barre.maximum > 0:
                self.barre.reset()
                self.barre.setRange(0, 0)
 
    #========================================================================
    def fintelech(self, msg):
        """Lancé quand le thread se termine (normalement ou pas)"""
        QtGui.QMessageBox.information(self,
            u"Téléchargement",
            msg)
 
    #========================================================================
    def stop_m(self):
        """demande l'arrêt du téléchargement avant la fin"""
        if self.telech!=None and self.telech.isRunning():
            self.telech.stoptelech()
 
    #========================================================================
    def closeEvent(self, event):
        """lancé à la fermeture de la fenêtre quelle qu'en soit la méthode"""
        self.stop_m() # arrête un éventuel téléchargement en cours
        event.accept()
 
#############################################################################
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    fen = Fenetre()
    fen.show()
    sys.exit(app.exec_())

VIII. Remerciements

Merci à dourouc05 et ClaudeLELOUP pour leur relecture !