Actualizado el 25/03/2022.
En el artículo Tareas en segundo plano con PyQt vimos cómo implementar operaciones pesadas sin que la ventana deje de responder en una aplicación de escritorio de Qt. Los siguientes códigos ─el primero usando hilos, el segundo la librería Twisted─ ilustran cómo implementar la descarga de un archivo vía HTTP mostrando su progreso vía el control QProgressBar
.
Descarga: descarga-con-progreso-pyqt-pyside.zip.
Código 1
from urllib.request import urlopen from PyQt6.QtCore import QThread, pyqtSignal from PyQt6.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton from PyQt6.QtWidgets import QProgressBar class Downloader(QThread): # Señal para que la ventana establezca el valor máximo # de la barra de progreso. setTotalProgress = pyqtSignal(int) # Señal para aumentar el progreso. setCurrentProgress = pyqtSignal(int) # Señal para indicar que el archivo se descargó correctamente. succeeded = pyqtSignal() def __init__(self, url, filename): super().__init__() self._url = url self._filename = filename def run(self): url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" readBytes = 0 chunkSize = 1024 # Abrir la dirección de URL. with urlopen(url) as r: # Avisar a la ventana cuántos bytes serán descargados. self.setTotalProgress.emit(int(r.info()["Content-Length"])) with open(filename, "ab") as f: while True: # Leer una porción del archivo que estamos descargando. chunk = r.read(chunkSize) # Si el resultado es `None` quiere decir que todavía # no se han descargado los datos. Simplemente # seguimos esperando. if chunk is None: continue # Si el resultado es una instancia de `bytes` vacía # quiere decir que el archivo está completo. elif chunk == b"": break # Escribir la porción descargada en el archivo local. f.write(chunk) readBytes += chunkSize # Avisar a la ventana la cantidad de bytes recibidos. self.setCurrentProgress.emit(readBytes) # Si esta línea llega a ejecutarse es porque no ocurrió ninguna # excepción en el código anterior. self.succeeded.emit() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Descarga con progreso en PyQt") self.resize(400, 300) self.label = QLabel("Presione el botón para iniciar la descarga.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Iniciar descarga", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) self.progressBar = QProgressBar(self) self.progressBar.setGeometry(20, 115, 300, 25) def initDownload(self): self.label.setText("Descargando archivo...") # Deshabilitar el botón mientras se descarga el archivo. self.button.setEnabled(False) # Ejecutar la descarga en un nuevo hilo. self.downloader = Downloader( "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe", "python-3.7.2.exe" ) # Conectar las señales que indican el progreso de la descarga # con los métodos correspondientes de la barra de progreso. self.downloader.setTotalProgress.connect(self.progressBar.setMaximum) self.downloader.setCurrentProgress.connect(self.progressBar.setValue) # Qt invocará el método `succeeded` cuando el archivo se haya # descargado correctamente y `downloadFinished()` cuando el hilo # haya terminado. self.downloader.succeeded.connect(self.downloadSucceeded) self.downloader.finished.connect(self.downloadFinished) self.downloader.start() def downloadSucceeded(self): # Establecer el progreso en 100%. self.progressBar.setValue(self.progressBar.maximum()) self.label.setText("¡El archivo se ha descargado!") def downloadFinished(self): # Restablecer el botón. self.button.setEnabled(True) # Eliminar el hilo una vez que fue utilizado. del self.downloader if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec()
from urllib.request import urlopen from PySide6.QtCore import QThread, Signal from PySide6.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton from PySide6.QtWidgets import QProgressBar class Downloader(QThread): # Señal para que la ventana establezca el valor máximo # de la barra de progreso. setTotalProgress = Signal(int) # Señal para aumentar el progreso. setCurrentProgress = Signal(int) # Señal para indicar que el archivo se descargó correctamente. succeeded = Signal() def __init__(self, url, filename): super().__init__() self._url = url self._filename = filename def run(self): url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" readBytes = 0 chunkSize = 1024 # Abrir la dirección de URL. with urlopen(url) as r: # Avisar a la ventana cuántos bytes serán descargados. self.setTotalProgress.emit(int(r.info()["Content-Length"])) with open(filename, "ab") as f: while True: # Leer una porción del archivo que estamos descargando. chunk = r.read(chunkSize) # Si el resultado es `None` quiere decir que todavía # no se han descargado los datos. Simplemente # seguimos esperando. if chunk is None: continue # Si el resultado es una instancia de `bytes` vacía # quiere decir que el archivo está completo. elif chunk == b"": break # Escribir la porción descargada en el archivo local. f.write(chunk) readBytes += chunkSize # Avisar a la ventana la cantidad de bytes recibidos. self.setCurrentProgress.emit(readBytes) # Si esta línea llega a ejecutarse es porque no ocurrió ninguna # excepción en el código anterior. self.succeeded.emit() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Descarga con progreso en Qt (PySide)") self.resize(400, 300) self.label = QLabel("Presione el botón para iniciar la descarga.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Iniciar descarga", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) self.progressBar = QProgressBar(self) self.progressBar.setGeometry(20, 115, 300, 25) def initDownload(self): self.label.setText("Descargando archivo...") # Deshabilitar el botón mientras se descarga el archivo. self.button.setEnabled(False) # Ejecutar la descarga en un nuevo hilo. self.downloader = Downloader( "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe", "python-3.7.2.exe" ) # Conectar las señales que indican el progreso de la descarga # con los métodos correspondientes de la barra de progreso. self.downloader.setTotalProgress.connect(self.progressBar.setMaximum) self.downloader.setCurrentProgress.connect(self.progressBar.setValue) # Qt invocará el método `succeeded` cuando el archivo se haya # descargado correctamente y `downloadFinished()` cuando el hilo # haya terminado. self.downloader.succeeded.connect(self.downloadSucceeded) self.downloader.finished.connect(self.downloadFinished) self.downloader.start() def downloadSucceeded(self): # Establecer el progreso en 100%. self.progressBar.setValue(self.progressBar.maximum()) self.label.setText("¡El archivo se ha descargado!") def downloadFinished(self): # Restablecer el botón. self.button.setEnabled(True) # Eliminar el hilo una vez que fue utilizado. del self.downloader if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec()
from urllib.request import urlopen from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton from PyQt5.QtWidgets import QProgressBar class Downloader(QThread): # Señal para que la ventana establezca el valor máximo # de la barra de progreso. setTotalProgress = pyqtSignal(int) # Señal para aumentar el progreso. setCurrentProgress = pyqtSignal(int) # Señal para indicar que el archivo se descargó correctamente. succeeded = pyqtSignal() def __init__(self, url, filename): super().__init__() self._url = url self._filename = filename def run(self): url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" filename = "python-3.7.2.exe" readBytes = 0 chunkSize = 1024 # Abrir la dirección de URL. with urlopen(url) as r: # Avisar a la ventana cuántos bytes serán descargados. self.setTotalProgress.emit(int(r.info()["Content-Length"])) with open(filename, "ab") as f: while True: # Leer una porción del archivo que estamos descargando. chunk = r.read(chunkSize) # Si el resultado es `None` quiere decir que todavía # no se han descargado los datos. Simplemente # seguimos esperando. if chunk is None: continue # Si el resultado es una instancia de `bytes` vacía # quiere decir que el archivo está completo. elif chunk == b"": break # Escribir la porción descargada en el archivo local. f.write(chunk) readBytes += chunkSize # Avisar a la ventana la cantidad de bytes recibidos. self.setCurrentProgress.emit(readBytes) # Si esta línea llega a ejecutarse es porque no ocurrió ninguna # excepción en el código anterior. self.succeeded.emit() class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Descarga con progreso en PyQt") self.resize(400, 300) self.label = QLabel("Presione el botón para iniciar la descarga.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Iniciar descarga", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) self.progressBar = QProgressBar(self) self.progressBar.setGeometry(20, 115, 300, 25) def initDownload(self): self.label.setText("Descargando archivo...") # Deshabilitar el botón mientras se descarga el archivo. self.button.setEnabled(False) # Ejecutar la descarga en un nuevo hilo. self.downloader = Downloader( "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe", "python-3.7.2.exe" ) # Conectar las señales que indican el progreso de la descarga # con los métodos correspondientes de la barra de progreso. self.downloader.setTotalProgress.connect(self.progressBar.setMaximum) self.downloader.setCurrentProgress.connect(self.progressBar.setValue) # Qt invocará el método `succeeded` cuando el archivo se haya # descargado correctamente y `downloadFinished()` cuando el hilo # haya terminado. self.downloader.succeeded.connect(self.downloadSucceeded) self.downloader.finished.connect(self.downloadFinished) self.downloader.start() def downloadSucceeded(self): # Establecer el progreso en 100%. self.progressBar.setValue(self.progressBar.maximum()) self.label.setText("¡El archivo se ha descargado!") def downloadFinished(self): # Restablecer el botón. self.button.setEnabled(True) # Eliminar el hilo una vez que fue utilizado. del self.downloader if __name__ == "__main__": app = QApplication([]) window = MainWindow() window.show() app.exec_()
Código 2
from PyQt5.QtCore import QCoreApplication from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton from PyQt5.QtWidgets import QProgressBar from twisted.internet.defer import inlineCallbacks class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Descarga con progreso en PyQt") self.resize(400, 300) self.label = QLabel("Presione el botón para iniciar la descarga.", self) self.label.setGeometry(20, 20, 200, 25) self.button = QPushButton("Iniciar descarga", self) self.button.move(20, 60) self.button.pressed.connect(self.initDownload) self.progressBar = QProgressBar(self) self.progressBar.setGeometry(20, 115, 300, 25) def initDownload(self): self.label.setText("Descargando archivo...") # Deshabilitar el botón mientras se descarga el archivo. self.button.setEnabled(False) url = "https://www.python.org/ftp/python/3.7.2/python-3.7.2.exe" # El método `requestSucceeded()` será invocado cuando la conexión # con la dirección de URL se haya establecido correctamente. treq.get(url).addCallback(self.requestSucceeded) def collector(self, chunk): """ Esta función es invocada por Twisted cada vez que se recibe un pedazo (`chunk`) del archivo que estamos descargando. """ self.progressBar.setValue(self.progressBar.value() + len(chunk)) self.f.write(chunk) def requestSucceeded(self, response): # Obtener el tamaño del archivo que vamos a descargar y establecerlo # como el máximo de la barra de progreso. self.progressBar.setMaximum(response.length) # Abrimos el archivo en modo "ab" para ir escribiendo por partes. self.f = open("python-3.7.2.exe", "ab") # Iniciamos la descarga, indicando que `downloadSucceeded` debe # invocarse si la descarga resulta exitosa, y `downloadFinished` # indistintamente si resulta exitosa o fallida. treq.collect(response, self.collector).addCallback( self.downloadSucceeded ).addBoth( self.downloadFinished ) def downloadSucceeded(self, result): self.progressBar.setValue(self.progressBar.maximum()) self.label.setText("¡El archivo se ha descargado!") def downloadFinished(self, result): self.f.close() # Restablecer el botón. self.button.setEnabled(True) def closeEvent(self, event): QCoreApplication.instance().quit() if __name__ == "__main__": app = QApplication([]) import qt5reactor qt5reactor.install() window = MainWindow() window.show() from twisted.internet import reactor import treq import os import certifi # Necesario para conexiones vía HTTPS. os.environ["SSL_CERT_FILE"] = certifi.where() reactor.run()
Esta segunda solución requiere tener instalados los paquetes qt5reactor
, treq
y certifi
:
pip install treq qt5reactor certifi
Solo está disponible para PyQt5.
Curso online 👨💻
¡Ya lanzamos el curso oficial de Recursos Python en Udemy!
Un curso moderno para aprender Python desde cero con programación orientada a objetos, SQL y tkinter
en 2024.
Consultoría 💡
Ofrecemos servicios profesionales de desarrollo y capacitación en Python a personas y empresas. Consultanos por tu proyecto.
JOSE says:
El método 1 me ha sacado de un gran ‘atascazo’, pero…¿se puede poner una especie de join(), para que no devuelva el control hasta que no termine. El fichero que descargo lo proceso a continuación y al hacerlo en otro hilo, se ejecuta antes de que termine de descargar el fichero, Actualmente lo tengo con un sleep pero ¿hay otra forma? Gracias
Recursos Python says:
Hola, José. Poné el código para procesar el archivo dentro de la función
downloadSucceeded()
, que es invocada cuando la descarga finaliza exitosamente. También podés conectar tu propia función a la señal que emite el hilo cuando la descarga finaliza (víaself.downloader.succeeded.connect()
). Unjoin()
no sería conveniente porque bloquearía el bucle principal, que es justamente lo que pretende evitar un hilo.Saludos