Descargar archivo con barra de progreso en PyQt

Descargar archivo con barra de progreso en PyQt



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.zip.

Descarga de archivo con barra de progreso en PyQt

Código 1

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#   Copyright (C) 2019 Recursos Python
#   https://recursospython.com/
#

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

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#   Copyright (C) 2019 Recursos Python
#   https://recursospython.com/
#

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 solución requiere tener instalados los paquetes qt5reactor y treq:

pip install treq qt5reactor



Deja un comentario