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