Evgenii Legotckoi
May 25, 2018, 12:08 p.m.

Qt/C++ Tutorial 080. Downloading large files with QNetworkAccessManager

After the question appeared on the forum about downloading large files using the Qt library, I raised some of my projects and prepared a more detailed manual using this functionality. Moreover, the problem with downloading files was related to redirects. By default, QNetworkAccessManager does not switch to redirects for downloading files and retrieving pages, so you need to set the appropriate attribute in the request, then everything will work, but let's take a closer look.

The application will have the following functionality.

  • QLineEdit for entering the destination URL for downloading
    QLineEdit to enter the target directory for download in readOnly mode. We will fill it with QFileDialog.
    QProgressBar, which will show the progress of the download
    Button to cancel download

Look our downloader will be so


Project Structure

The project consists of

  • FileDownloader.pro - project profile
  • Downloader.h - Header file for downloading files
  • Downloader.cpp - Class implementation file for downloading files
  • Widget.h - Application window header file
  • Widget.cpp - Application window implementation file
  • Widget.ui - Graphical form of the application window
  • main.cpp - The file with the main application function

FileDownloader.pro, main.cpp, Widget.ui will not be considered, the first two are created by default, the latter is created through the Qt Designer graphic editor, look at it in the project itself, which is attached to the article at the very end.

Widget.h

In the header file, all the unimaginable slots for processing the interface buttons are declared and also the class for downloading files is declared on the stack

  1. #ifndef WIDGET_H
  2. #define WIDGET_H
  3.  
  4. #include "Downloader.h"
  5.  
  6. #include <QWidget>
  7.  
  8. namespace Ui {
  9. class Widget;
  10. }
  11.  
  12. class Widget : public QWidget
  13. {
  14. Q_OBJECT
  15.  
  16. public:
  17. explicit Widget(QWidget *parent = nullptr);
  18. ~Widget();
  19.  
  20. private slots:
  21. // Slot for download start
  22. void onDownloadButtonClicked();
  23.  
  24. // Slot for selecting the download directory
  25. void onSelectTargetFolderButtonClicked();
  26.  
  27. // Slot for canceling the download
  28. void onCancelButtonClicked();
  29.  
  30. // Slot for updating download progress
  31. void onUpdateProgress(qint64 bytesReceived, qint64 bytesTotal);
  32.  
  33. private:
  34. Ui::Widget *ui;
  35. Downloader m_downloader; // Download Class
  36. };
  37.  
  38. #endif // WIDGET_H

Widget.cpp

  1. #include "Widget.h"
  2. #include "ui_Widget.h"
  3.  
  4. #include <QFileDialog>
  5. #include <QStandardPaths>
  6.  
  7. Widget::Widget(QWidget *parent) :
  8. QWidget(parent),
  9. ui(new Ui::Widget)
  10. {
  11. ui->setupUi(this);
  12. // Connect to slots
  13. connect(ui->downloadPushButton, &QPushButton::clicked, this, &Widget::onDownloadButtonClicked);
  14. connect(ui->selectTargetFolderPushButton, &QPushButton::clicked, this, &Widget::onSelectTargetFolderButtonClicked);
  15. connect(ui->cancelPushButton, &QPushButton::clicked, this, &Widget::onCancelButtonClicked);
  16. connect(&m_downloader, &Downloader::updateDownloadProgress, this, &Widget::onUpdateProgress);
  17. }
  18.  
  19. Widget::~Widget()
  20. {
  21. delete ui;
  22. }
  23.  
  24. void Widget::onDownloadButtonClicked()
  25. {
  26. // We start downloading the file by passing the
  27.   // path to the directory where we will upload files,
  28.   // url, where the file is located
  29. m_downloader.get(ui->targetFolderLineEdit->text(), ui->urlLineEdit->text());
  30. }
  31.  
  32. void Widget::onSelectTargetFolderButtonClicked()
  33. {
  34. // Selecting the destination directory for downloading
  35. QString targetFolder = QFileDialog::getExistingDirectory(this,
  36. tr("Select folder"),
  37. QStandardPaths::writableLocation(QStandardPaths::DownloadLocation),
  38. QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
  39. ui->targetFolderLineEdit->setText(targetFolder);
  40. }
  41.  
  42. void Widget::onCancelButtonClicked()
  43. {
  44. // Cancel upload
  45. m_downloader.cancelDownload();
  46. ui->downloadProgressBar->setMaximum(100);
  47. ui->downloadProgressBar->setValue(0);
  48. }
  49.  
  50. void Widget::onUpdateProgress(qint64 bytesReceived, qint64 bytesTotal)
  51. {
  52. // Updating upload progress
  53. ui->downloadProgressBar->setMaximum(bytesTotal);
  54. ui->downloadProgressBar->setValue(bytesReceived);
  55. }

Downloader.h

And now look at the class for downloading files, taking into account the progress of the download.

An important point is that large files need to be processed gradually, they can not be read by one query. Therefore, you need to handle the QNetworkReply::readyRead signal from the object of the current response to the request. This signal is emitted when the buffer contains data that we can assume.

And only after the download is completed QNetworkAccessManager will issue a finished signal, which will close the file and complete the connection with the removal of the object of the current response to the request.

  1. #ifndef DOWNLOADER_H
  2. #define DOWNLOADER_H
  3.  
  4. #include <QNetworkAccessManager>
  5.  
  6. class QNetworkReply;
  7. class QFile;
  8.  
  9. class Downloader : public QObject
  10. {
  11. Q_OBJECT
  12. using BaseClass = QObject;
  13.  
  14. public:
  15. explicit Downloader(QObject* parent = nullptr);
  16.  
  17. // Method for starting the download
  18. bool get(const QString& targetFolder, const QUrl& url);
  19.  
  20. public slots:
  21. // Method of canceling the load
  22. void cancelDownload();
  23.  
  24. signals:
  25. // A signal that sends information about the progress of the download
  26. void updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);
  27.  
  28. private slots:
  29. // Slot for gradual reading of downloaded data
  30. void onReadyRead();
  31. // Slot for processing request completion
  32. void onReply(QNetworkReply* reply);
  33.  
  34. private:
  35. QNetworkReply* m_currentReply {nullptr}; // Current request being processed
  36. QFile* m_file {nullptr}; // The current file to which the entry is being written
  37. QNetworkAccessManager m_manager; // Network manager for downloading files
  38. };
  39.  
  40. #endif // DOWNLOADER_H

Downloader.cpp

  1. #include "Downloader.h"
  2.  
  3. #include <QNetworkReply>
  4. #include <QNetworkRequest>
  5. #include <QFile>
  6. #include <QDir>
  7.  
  8. Downloader::Downloader(QObject* parent) :
  9. BaseClass(parent)
  10. {
  11. // Connect to the finished signal
  12. connect(&m_manager, &QNetworkAccessManager::finished, this, &Downloader::onReply);
  13. }
  14.  
  15. bool Downloader::get(const QString& targetFolder, const QUrl& url)
  16. {
  17. if (targetFolder.isEmpty() || url.isEmpty())
  18. {
  19. return false;
  20. }
  21.  
  22. // Create object of file class for download
  23. // Here there is a target directory and the name of the file that is allocated from the URL
  24. m_file = new QFile(targetFolder + QDir::separator() + url.fileName());
  25. // Trying to open the file
  26. if (!m_file->open(QIODevice::WriteOnly))
  27. {
  28. delete m_file;
  29. m_file = nullptr;
  30. return false;
  31. }
  32.  
  33. // Creating a request
  34. QNetworkRequest request(url);
  35. // Allow to go on redirects
  36. request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
  37. // Running the download
  38. m_currentReply = m_manager.get(request);
  39.  
  40. // After that, we immediately connect to signals about readiness of data to read and update the progress of downloading
  41. connect(m_currentReply, &QNetworkReply::readyRead, this, &Downloader::onReadyRead);
  42. connect(m_currentReply, &QNetworkReply::downloadProgress, this, &Downloader::updateDownloadProgress);
  43. return true;
  44. }
  45.  
  46. void Downloader::onReadyRead()
  47. {
  48. // If there is data and the file is open
  49. if (m_file)
  50. {
  51. // write them to a file
  52. m_file->write(m_currentReply->readAll());
  53. }
  54. }
  55.  
  56. void Downloader::cancelDownload()
  57. {
  58. // Cancel request
  59. if (m_currentReply)
  60. {
  61. m_currentReply->abort();
  62. }
  63. }
  64.  
  65. void Downloader::onReply(QNetworkReply* reply)
  66. {
  67. // by completion of the request
  68. if (reply->error() == QNetworkReply::NoError)
  69. {
  70. // save file
  71. m_file->flush();
  72. m_file->close();
  73. }
  74. else
  75. {
  76. // Or delete it in case of error
  77. m_file->remove();
  78. }
  79.  
  80. delete m_file;
  81. m_file = nullptr;
  82. reply->deleteLater();
  83. }

Conclusion

Thus, we have an application that can download the required file for a given URL and place it in the target directory.

Link to download the project Downloader

Do you like it? Share on social networks!

f
  • June 1, 2018, 2:08 a.m.

не могу понять как обработать ошибку некорректной ссылки?
Пример: "ftp://cddis.gsfc.nasa.gov/pub/slr/data/npt_crd/gracea/2010/gracea_20100101.npt.Z"

Evgenii Legotckoi
  • June 1, 2018, 8:59 p.m.

У вас скорее всего ошибка превышения интервала ожидания - QNetworkReply::TimeoutError

Гляньте вот эту статью про описание ошибок QNetworkAccessManager
Там в конце статьи есть пример обработки ошибок с выводом в qDebug()
f
  • June 6, 2018, 10:42 p.m.

у меня никак не получается обработать ошибку некорректной ссылки(

    connect(&m_downloader.manager, &QNetworkAccessManager::finished, this, &Widget::onResult);
    connect(m_downloader.currentReply, static_cast<void (QNetworkReply::*)(QNetworkReply::NetworkError)>(&QNetworkReply::error), this, &Widget::errorSlot);
R
  • June 7, 2018, 10:21 a.m.

Здравствуйте. В вашем примере не обновляется прогресс-бар. А если точнее - он выставляет 100% после закачки, и всё. Во время самой закачки просто 0%. Сомневаюсь что так задумано, но всё же, как сделать чтобы обновлялся каждый кусок?

Evgenii Legotckoi
  • June 7, 2018, 12:44 p.m.

Какого размера пробуете скчивать файлы и на какой скорости у вас работает интернет.
Есть ещё один неприятный момент в том, что не на всех платформах правильно передаётся информация о прогрессе закачки.
Так что какую используете операционную систему?

Evgenii Legotckoi
  • June 7, 2018, 12:46 p.m.

А почему вы не сделаете обработку ошибок в слоте onResult.

Кстати, по поводу кастов перегруженных сигналов и слотов. Посмотрите на использование Overload шаблона , ппроще писать перегруженные сигнал/слотовые соединения будет.
R
  • June 7, 2018, 1:28 p.m.

Проблема была в ресурсе, с которого скачиваю. Пытался тянуть с google disc - а там ограничение 25мб на прямые ссылки. Поэтому он и втупал.


Можете подсказать какие-нибудь ресурсы, на которых есть возможность обновлять файл (контроль версий), и при этом ссылка на него останется та же. Ну и без ограничений как у гугла, буду очень благодарен.
Evgenii Legotckoi
  • June 7, 2018, 1:32 p.m.

ну.. хз.. Github может быть, там вам и контроль версии и всё остальное, вроде и ссылки на метки прямые.

Ну или поднимите свой онлайн ресурс ))
Если честно, понятия не имею, не было такой необходимости, а если и была, то объодился ресурсами своего сайта.
F
  • Sept. 20, 2019, 9:25 p.m.

вызываю метод get у m_downloader в другом методе и приложение начинает вылетать. В чем ошибка?

Evgenii Legotckoi
  • Sept. 25, 2019, 1:52 p.m.

Это дебажить нужно, может быть что угодно.

v
  • Oct. 29, 2019, 7:40 p.m.

Здпавствуйте Евгений.
Прошу совета, вот из-за этой строчки кода:
connect(&m_manager, &QNetworkAccessManager::finished, this, &Downloader::onReply);

Выдает такую ошибку: D:\Qt\5.10.1\mingw53_32\include\QtCore\qglobal.h:762: ошибка: static assertion failed: Signal and slot arguments are not compatible.
#define Q_STATIC_ASSERT_X(Condition, Message) static_assert(bool(Condition), Message)
Что это может быть?

Evgenii Legotckoi
  • Oct. 29, 2019, 7:51 p.m.

Добрый день.
Вы полностью скопировали тот код? Или самостоятельно писали свой вариант, посматривая в эту статью?

Выглядит так, что у вас несовместимый сигнал со слотом.

Покажите сигнатуру метода Downloader::onReply

v
  • Oct. 29, 2019, 7:56 p.m.
  • (edited)

Писал свой вариант, вот сигнатура метода Downloader::onReply:

  1. void onReply(QNetworkReply& reply);
  2.  
  3. void Downloader::onReply(QNetworkReply &reply)
  4. {
  5. if(reply.error() == QNetworkReply::NoError){
  6. m_file->flush();
  7. m_file->close();
  8. }
  9. else{
  10. m_file->remove();
  11. }
  12.  
  13. delete m_file;
  14. m_file = nullptr;
  15. reply.deleteLater();
  16. }
v
  • Oct. 29, 2019, 7:57 p.m.

Точнее будет сказать что я набивал руками Ваш код)))

Evgenii Legotckoi
  • Oct. 29, 2019, 8:02 p.m.
  • (edited)

Понятно )) Вы допустили ошибку.

Вот ваша строчка

  1. void Downloader::onReply(QNetworkReply &reply)

Вот моя строчка

  1. void Downloader::onReply(QNetworkReply *reply)

В вашем случае используется ссылка, а в моём случае указатель. Это разные вещи.

Используйте, пожалуйста, диалог вставки программного кода. Это кнопка с символом <> в редакторе.
Дело в том, что в редакторе комментариев используется markdown синтаксис, поэтому нужна специальная разметка кода. Диалог добавляет её автоматически.

v
  • Oct. 29, 2019, 8:08 p.m.

))) Спасибо Евгений! Впреть буду внимательнее)))

m
  • Feb. 9, 2020, 8:44 p.m.

Дня доброго.
Прочитал ваш урок. Вроде все понял. Но взялся реализовывать этот принцип в своем проекте(загрузка файлов на фтп) и уперся что не могу понять как реализовать progressbar.
вот код:

  1. void MainWindow::copyToFtp(QString Fname)
  2. {
  3. ui->progressBar->show();
  4. fFileFTP =new QFile (Fname);
  5. QFileInfo fileInfoFTP(fFileFTP->fileName());
  6. QUrl url("ftp://ftp.ihostfull.com/htdocs/"+fileInfoFTP.completeBaseName()+"."+fileInfoFTP.suffix());
  7. url.setPort(21);
  8. url.setPassword("------");
  9. url.setUserName("-------");
  10. fFileFTP->open(QIODevice::ReadOnly);
  11. QNetworkAccessManager manager(0);
  12. QNetworkReply *reply = manager.put(QNetworkRequest(url),fFileFTP);
  13. QEventLoop loop;
  14. QObject::connect(reply,SIGNAL(finished()),&loop,SLOT(quit()));
  15. loop.exec();
  16. fFileFTP->close();
  17. }

Как сюда интегрировать progressBar?
Сделайте поправку что новичек в програмированнии.

Evgenii Legotckoi
  • Feb. 10, 2020, 2:02 p.m.

Реализуйте сначала правильно закачку по ftp. А потом уже внедряйте прогресс бар.

Ошибки, которые сразу бросаются в глаза

  • QNetworkAccessManager manager(0); - объявлено локально внутри метода, в моём примере на тсеке класса. Конечно правильно, что вы петлёй тормозите выход из метода, но при этом полностью уничтожаете преимущество ассинхронности QNetworkAccessManager
  • QObject::connect(reply,SIGNAL(finished()),&loop,SLOT(quit())); - Использование устревшего синтаксиса сигналов и слотов. В этой статье используется новый синтаксис.

Вообще ваш код сам по себе работает? Если работает, то сначала реализуйте как в статье, чтобы без QEventLoop было. Поскольку этой петлёй вы морозите GUI, скорее всего поэтому ничего и не работает.

m
  • Feb. 10, 2020, 2:06 p.m.

Код сам по себе работает. Файлы на фтп грузяться.

Evgenii Legotckoi
  • Feb. 10, 2020, 2:15 p.m.
  • (edited)

QEventLoop скорее всего морозит GUI, поскольку тормозит выполнение всего остального программного кода в приложении, ожидая выполнения работы QNetworkAccessManager. Вам нужно переписать так, как показано в примере. Тем более, что QNetworkAccessManager может выполнять несколько запросов одновременно и асинхронно. Нет нужды создавать объект QNetworkAccessManager в каждом слоте. Тем более, что более одного при таком подходе вы не создадите. GUI просто не будет реагировать на событие мыши, пока не выполнится загрузка.

Comments

Only authorized users can post comments.
Please, Log in or Sign up
  • Last comments
  • AK
    April 1, 2025, 11:41 a.m.
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    March 9, 2025, 9:02 p.m.
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    March 9, 2025, 4:14 p.m.
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…
  • ИМ
    Nov. 22, 2024, 9:51 p.m.
    Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
  • Evgenii Legotckoi
    Oct. 31, 2024, 11:37 p.m.
    Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup