Outils pour utilisateurs

Outils du site


telech_fichier_pyqt4

Télécharger un fichier sur le web

Objectif

On va présenter ici un petit programme PyQt4 du genre “démo” pour télécharger un fichier du web et l'enregistrer sur disque, avec barre de progression et possibilité d'arrêt du téléchargement avant la fin.

Les différents problèmes résolus dans ce code seront principalement:

  • l'utilisation d'un thread de téléchargement (QThread) pour éviter que la fenêtre graphique ne se fige pendant toute la durée du téléchargement
  • la récupération de la progression du téléchargement grâce à urlretrieve du module urllib.
  • la communication par message entre le thread de téléchargement et la fenêtre graphique
  • l'utilisation d'une barre de progression du téléchargement, y compris dans le cas où la taille totale du téléchargement n'est pas disponible
  • la possibilité d'arrêt du téléchargement avant la fin

Code commenté

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 le thread de PyQt4: QtCore.QThread. Son utilisation est quasi identique au Thread du module threading habituel du Python de base.

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:

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:

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!

Le téléchargement

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

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

Et la méthode du thread 'infotelech' recevra 3 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

A noter que, si la taille totale n'est pas connue, totalblocs=-1.

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 2 type 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:

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 2 messages infotelech et fintelech ont été créés pour ce programme (n'existent pas dans la doc de 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 2 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'info 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 2 messages seront émis par:

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

La barre de progression

La barre graphique de progression est définie par:

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

Sa mise à jour des infos de progression sera donc:

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 simple chenille qui signale simplement que le téléchargement est en cours. On fait cette chenille en fixant un minimum et un maximum tous les 2 égaux à zéro:

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, et 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 progressive.

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:

if self.stop:
    raise Abort

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

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.

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 quelque soit la méthode de fermeture choisie (y compris: croix en haut de la fenêtre ou le “fermer” du petit menu système de la fenêtre).

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é avant PyQt4.

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

  • Son adresse web:
    • http://docs.python.org/archives/python-2.7.1-docs-pdf-a4.zip
  • A enregistrer sous (ici dans le même répertoire que le code python):
    • python-2.7.1-docs-pdf-a4.zip


#!/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 quelqu'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_())


Amusez-vous bien!

telech_fichier_pyqt4.txt · Dernière modification: 2011/03/09 15:41 par tyrtamos

Outils de la page