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 :
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.
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 :
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 :
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 :
# 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 :
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 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 :
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 :
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.
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.
#!/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 !