Descargar archivo con barra de progreso en PyQt/PySide

Descargar archivo con barra de progreso en PyQt/PySide

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.

Descarga de archivo con barra de progreso en PyQt

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.

2 comentarios.

  1. 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ía self.downloader.succeeded.connect()). Un join() no sería conveniente porque bloquearía el bucle principal, que es justamente lo que pretende evitar un hilo.

      Saludos

Deja una respuesta