[PyQt5]
Dans un environnement graphique, il y a souvent des activités qui demandent un “certain temps” (plusieurs secondes, quelquefois plus), qui nécessitent de signaler à l'utilisateur que cette activité est en train de se dérouler (et donc que le programme n'est pas planté).
On peut, bien sûr utiliser une barre de progression (QProgressBar) dans sa configuration “chenille”, mais ici on va utiliser une image gif animée. On rencontre souvent cette solution sous forme de roue qui tourne.
Pour créer le throbber qu'on veut, on peut utiliser ce site web: http://www.ajaxload.info/.
On va ici étudier 2 cas: soit on met le throbber dans la page graphique en cours, soit on le met dans une petite fenêtre surgissante qui apparait le temps du déroulement de l'activité et disparait ensuite. L'animation de l'image ne doit être visible que pendant le déroulement de l'activité.
Bien entendu, pour que l'activité en question ne bloque pas le graphique, il faut qu'elle se déroule dans un thread ou dans un processus. Mais il faut qu'on puisse signaler au graphique que le programme ainsi lancé est terminé, de façon à arrêter le throbber: c'est l'intérêt d'utiliser les classes de PyQt (QThread ou QProcess) qui pourront envoyer un signal de fin au graphique. Solution alternative: utiliser un héritage multiple: threading+QObject.
Ici, on va mettre le throbber dans un QLabel qui sera placé dans la page graphique en cours.
L'activité est simulée par une tempo de 5 secondes située dans un thread (classe Calcul dans le code).
On a mis des options qui sont peut-être inutiles dans d'autres cas: couleur de fond jaune et bordure rouge du QLabel: pour s'en passer, il suffit de ne pas mettre les lignes en question.
Voilà la copie d'écran de l'exemple ci-dessous en pleine activité:
Et voilà le code largement documenté:
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Python 3 import sys, os import time from PyQt5.QtCore import (Qt, QSize, pyqtSlot, pyqtSignal, QThread) from PyQt5.QtGui import (QMovie) from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, QGridLayout) ############################################################################# class Calcul(QThread): # crée un nouveau signal pour indiquer la fin du thread finduthread = pyqtSignal() #======================================================================== def __init__(self, parent=None): super(Calcul, self).__init__(parent) #======================================================================== def run(self): # tempo de 5 secondes pour l'exemple time.sleep(5) # émet le signal de fin du thread self.finduthread.emit() ############################################################################# class Fenetre(QWidget): #======================================================================== def __init__(self, parent=None): super(Fenetre, self).__init__(parent) self.resize(400, 300) # crée le bouton qui lancera le thread self.bouton = QPushButton("Lancer le programme!", self) self.bouton.clicked.connect(self.lancementprogramme) # crée un movie avec le throbber sous forme d'image "gif" animée imagif = os.path.abspath("throbbergif.gif") self.movie = QMovie(imagif) self.movie.setScaledSize(QSize(60, 60)) # dimension 60x60 # crée du QLabel pour intégrer le throbber self.label = QLabel() # redimenssionne 80x80 self.label.resize(QSize(80, 80)) # interdit le redimenssionnement à la souris self.label.setFixedSize(self.label.width(), self.label.height()) # option: met un fond jaune et un cadre rouge self.label.setStyleSheet("background-color:yellow; border: 1px solid #ff0000 ;") # le contenu du QLabel sera au milieu (hor. et vert.) self.label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) # positionne les widgets dans la fenêtre posit = QGridLayout() posit.addWidget(self.bouton, 0, 0, 1, 3) posit.addWidget(self.label, 1, 1, 1, 1) self.setLayout(posit) self.thread = None # signale qu'il n'y a aucun thread en cours #======================================================================== @pyqtSlot() def lancementprogramme(self): """slot exécuté lors du clic sur le bouton: lance le thread """ if self.thread != None: return # = il y a déjà un thread en cours: on ne fait rien # affiche le throbber self.label.setMovie(self.movie) # affecte le movie au QLabel self.movie.start() # lance l'animation du throbber # lance le thread self.thread = Calcul() self.thread.finduthread.connect(self.findeprogramme) self.thread.start() #======================================================================== @pyqtSlot() def findeprogramme(self): """slot exécuté lors de la clôture du thread """ # arrête l'affichage du throbber self.movie.stop() # arrête l'animation self.label.clear() # efface le contenu du QLabel # traite la fin du thread self.thread.finduthread.disconnect() # retire le lien avec la méthode self.thread = None # signale qu'il n'y a plus de thread en cours ############################################################################# if __name__ == "__main__": app = QApplication(sys.argv) fen = Fenetre() fen.show() sys.exit(app.exec_())
Ici, on va mettre le throbber dans une fenêtre héritée d'un QLabel (classe ThrobberWindow).
On a ici ajouté quelques astuces purement cosmétiques: un cadre simplifié pour la fenêtre et un fond jaune pour attirer l’œil: on peut bien sûr s'en passer!
Voilà la copie d'écran de l'exemple ci-dessous en pleine activité:
Et voilà le code largement documenté:
#!/usr/bin/python3 # -*- coding: utf-8 -*- # Python 3 import sys, os import time from PyQt5.QtCore import (Qt, QSize, pyqtSlot, pyqtSignal, QThread) from PyQt5.QtGui import (QMovie) from PyQt5.QtWidgets import (QApplication, QWidget, QPushButton, QLabel, QGridLayout) ############################################################################# class Calcul(QThread): # crée un nouveau signal pour indiquer la fin du thread finduthread = pyqtSignal() #======================================================================== def __init__(self, parent=None): super(Calcul, self).__init__(parent) #======================================================================== def run(self): # tempo de 5 secondes pour l'exemple time.sleep(5) # émet le signal de fin du thread self.finduthread.emit() ############################################################################# class ThrobberWindow(QLabel): """petite fenêtre qui affiche un throbber (image gif animée) pour signaler une activité en cours et faire patienter l'utilisateur """ #======================================================================== def __init__(self, parent=None): super(ThrobberWindow, self).__init__(parent) # image gif animée du throbber imagif = os.path.abspath("throbbergif.gif") # configure la fenêtre self.setWindowTitle(" ") # supprime le titre de la fenêtre self.resize(160, 160) # met une fenêtre carrée self.setFixedSize(self.width(), self.height()) # pas de redimenssionnement self.setStyleSheet("background-color:yellow;") # met un fond jaune # le contenu du QLabel sera au milieu (hor. et vert.) self.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) # met seulement un cadre autour de la fenêtre du throbber # NB: mais on peut encore la déplacer à la souris! newflags = Qt.Dialog newflags |= Qt.CustomizeWindowHint # permet la personnalisation newflags &= ~Qt.WindowCloseButtonHint # pas de case de fermeture newflags &= ~Qt.WindowSystemMenuHint # pas de menu de fenêtre newflags &= ~Qt.WindowContextHelpButtonHint # pas de case "?" self.setWindowFlags(newflags) # crée un movie avec le throbber sous forme d'image "gif" animée self.movie = QMovie(imagif) self.movie.setScaledSize(QSize(60, 60)) # fixe dimension 60x60 self.setMovie(self.movie) # affecte le movie au QLabel self.movie.start() # lance l'animation du throbber #======================================================================== def closeEvent(self, event): """méthode exécutée lors de la fermeture de la fenêtre """ self.movie.stop() # arrête l'animation self.clear() # efface le contenu du QLabel event.accept() # accepte la fermeture ############################################################################# class Fenetre(QWidget): """fenêtre pour tester le throbber """ #======================================================================== def __init__(self, parent=None): super(Fenetre, self).__init__(parent) self.resize(400, 300) # créer le bouton self.bouton = QPushButton("Lancer le programme!", self) self.bouton.clicked.connect(self.lancementprogramme) # positionne le widget dans la fenêtre posit = QGridLayout() posit.addWidget(self.bouton, 0, 0) self.setLayout(posit) # pour signaler qu'il n'y a aucun thread en cours self.thread = None #======================================================================== @pyqtSlot() def lancementprogramme(self): """slot exécuté lors du clic sur le bouton: lance le thread """ if self.thread != None: return # il y a déjà un thread en cours: on ne fait rien # crée le throbber self.throbber = ThrobberWindow() # option: met la fenêtre du throbber en mode "modal" self.throbber.setWindowModality(Qt.ApplicationModal) # affiche la petite fenêtre du throbber self.throbber.show() # lance le thread self.thread = Calcul() self.thread.finduthread.connect(self.findeprogramme) self.thread.start() #======================================================================== @pyqtSlot() def findeprogramme(self): """slot exécuté lors de la clôture du thread """ # ferme la fenêtre du throbber self.throbber.close() # traite la fin du thread self.thread.finduthread.disconnect() # retire le lien avec la méthode self.thread = None # indique qu'il n'y a plus de thread en cours ############################################################################# if __name__ == "__main__": app = QApplication(sys.argv) fen = Fenetre() fen.show() sys.exit(app.exec_())
Amusez-vous bien!