Evgenii Legotckoi
Evgenii Legotckoi25 мая 2018 г. 12:08

Qt/C++ Урок 080. Скачивание больших по размеру файлов с помощью QNetworkAccessManager

После возникновения вопроса на форуме о скачивании больших файлов с помощью библиотеки Qt, я поднял некоторые свои проекты и подготовил более подробный мануал с использованием подобного функционала. Тем более, что возникшая проблема со скачиванием файлов была связана с редиректами. По умолчанию QNetworkAccessManager не переходит по редиректам для скачивания файлов и получения страниц, поэтому в запросе нужно установить соответствующий атрибут, тогда всё заработает, но давайте рассмотрим всё подробнее.

Приложение будет обладать следующим функционалом.

  • QLineEdit для ввода целевого URL для скачивания
  • QLineEdit для ввода целевого каталога для скачивания в режиме readOnly. Заполнять его будем с помощью QFileDialog.
  • QProgressBar, который будет показывать прогресс загрузки
  • Кнопку для отмены загрузки

Выглядеть наш Downloader будет так


Структура проекта

Проект состоит из

  • FileDownloader.pro - профайл проекта
  • Downloader.h - Заголовочный файл класса для скачивания файлов
  • Downloader.cpp - Файл реализации класса для скачивания файлов
  • Widget.h - Заголовочный файл окна приложения
  • Widget.cpp - Файл реализации окна приложения
  • Widget.ui - Графическая форма окна приложения
  • main.cpp - Файл с главной функцией приложения

FileDownloader.pro, main.cpp, Widget.ui рассматривать не будет, первые два создаются по умолчанию, последний создаётся через графический редактор Qt Designer, посмотрите его в самом проекте, который прикреплён к статье в самом конце.

Widget.h

В заголовочном файле объявлены все необохимые слоты для обработки кнопок интерфейса а также объявлен в стеке сам класс для скачивания файлов

#ifndef WIDGET_H
#define WIDGET_H

#include "Downloader.h"

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

private slots:
    // Слот для старта загрузки
    void onDownloadButtonClicked();

    // Слот для выбора каталога для скачивания
    void onSelectTargetFolderButtonClicked();

    // Слот для отмены загрузки
    void onCancelButtonClicked();

    // Слот для обновления прогресса загрузки
    void onUpdateProgress(qint64 bytesReceived, qint64 bytesTotal);

private:
    Ui::Widget *ui;
    Downloader m_downloader; // Класс для скачивания
};

#endif // WIDGET_H

Widget.cpp

#include "Widget.h"
#include "ui_Widget.h"

#include <QFileDialog>
#include <QStandardPaths>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    // Подключаемся к слотам
    connect(ui->downloadPushButton, &QPushButton::clicked, this, &Widget::onDownloadButtonClicked);
    connect(ui->selectTargetFolderPushButton, &QPushButton::clicked, this, &Widget::onSelectTargetFolderButtonClicked);
    connect(ui->cancelPushButton, &QPushButton::clicked, this, &Widget::onCancelButtonClicked);
    connect(&m_downloader, &Downloader::updateDownloadProgress, this, &Widget::onUpdateProgress);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::onDownloadButtonClicked()
{
    // Запускаем скачивание файла передавая в качестве аргументов
    // путь к каталогу, куда будем закачивать файлы
    // url, где находится файл
    m_downloader.get(ui->targetFolderLineEdit->text(), ui->urlLineEdit->text());
}

void Widget::onSelectTargetFolderButtonClicked()
{
    // Выбор целевого каталога для скачивания
    QString targetFolder = QFileDialog::getExistingDirectory(this,
                                                             tr("Select folder"),
                                                             QStandardPaths::writableLocation(QStandardPaths::DownloadLocation),
                                                             QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
    ui->targetFolderLineEdit->setText(targetFolder);
}

void Widget::onCancelButtonClicked()
{
    // Отмена загрузки
    m_downloader.cancelDownload();
    ui->downloadProgressBar->setMaximum(100);
    ui->downloadProgressBar->setValue(0);
}

void Widget::onUpdateProgress(qint64 bytesReceived, qint64 bytesTotal)
{
    // Обновляем прогресс загрузки
    ui->downloadProgressBar->setMaximum(bytesTotal);
    ui->downloadProgressBar->setValue(bytesReceived);
}

Downloader.h

А теперь посмотрим на класс для скачивания файлов с учётом проверки прогресса скачивания.

Важным моменто является то, что файлы большого размера требуется обрабатывать постепенно, они не могут быть считаны одним запросом. Поэтому нужно обрабатывать сигнал QNetworkReply::readyRead от объекта текущего ответа на запрос. Данный сигнал испускается тогда, когда в буффере содержаться данные, которые мы можем считать.

И только по завершению скачивания QNetworkAccessManager испустит сигнал finished , по которому будет закрыт файл и завершено соединение с удаление объекта текущего ответа на запрос.

#ifndef DOWNLOADER_H
#define DOWNLOADER_H

#include <QNetworkAccessManager>

class QNetworkReply;
class QFile;

class Downloader : public QObject
{
    Q_OBJECT
    using BaseClass = QObject;

public:
    explicit Downloader(QObject* parent = nullptr);

    // Метод для запуска скачиввания
    bool get(const QString& targetFolder, const QUrl& url);

public slots:
    // Метод отмены загрузки
    void cancelDownload();

signals:
    // Сигнал передающий информацию о прогрессе загрузки
    void updateDownloadProgress(qint64 bytesReceived, qint64 bytesTotal);

private slots:
    // Слот для постепенного считывания загружаемых данных
    void onReadyRead();
    // Слот для обработки завершения запроса
    void onReply(QNetworkReply* reply);

private:
    QNetworkReply* m_currentReply {nullptr};    // Текущий обрабатываемый запрос
    QFile* m_file                 {nullptr};    // Текущий файл в который идёт запись
    QNetworkAccessManager m_manager;            // Сетевой менеджер для загрузки файлов
};

#endif // DOWNLOADER_H

Downloader.cpp

#include "Downloader.h"

#include <QNetworkReply>
#include <QNetworkRequest>
#include <QFile>
#include <QDir>

Downloader::Downloader(QObject* parent) :
    BaseClass(parent)
{
    // Подключаемся к сигналу finished
    connect(&m_manager, &QNetworkAccessManager::finished, this, &Downloader::onReply);
}

bool Downloader::get(const QString& targetFolder, const QUrl& url)
{
    if (targetFolder.isEmpty() || url.isEmpty())
    {
        return false;
    }

    // Cоздаём объект класса файла для скачивания
    // здесь имеется целевая директория и имя файла, которое выделяется из URL
    m_file = new QFile(targetFolder + QDir::separator() + url.fileName());
    // Пробуем открыть файл
    if (!m_file->open(QIODevice::WriteOnly))
    {
        delete m_file;
        m_file = nullptr;
        return false;
    }

    // Создаём запрос
    QNetworkRequest request(url);
    // Обязательно разрешаем переходить по редиректам
    request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
    // Запускаем скачивание
    m_currentReply = m_manager.get(request);

    // После чего сразу подключаемся к сигналам о готовности данных к чтению и обновлению прогресса скачивания
    connect(m_currentReply, &QNetworkReply::readyRead, this, &Downloader::onReadyRead);
    connect(m_currentReply, &QNetworkReply::downloadProgress, this, &Downloader::updateDownloadProgress);
    return true;
}

void Downloader::onReadyRead()
{
    // Если есть данные и файл открыт
    if (m_file)
    {
        // записываем их в файл
        m_file->write(m_currentReply->readAll());
    }
}

void Downloader::cancelDownload()
{
    // Отмена запроса
    if (m_currentReply)
    {
        m_currentReply->abort();
    }
}

void Downloader::onReply(QNetworkReply* reply)
{
    // По завершению запроса
    if (reply->error() == QNetworkReply::NoError)
    {
        // сохраням файл
        m_file->flush();
        m_file->close();
    }
    else
    {
        // Или удаляем его в случае ошибки
        m_file->remove();
    }

    delete m_file;
    m_file = nullptr;
    reply->deleteLater();
}

Заключение

Таким образом имеем приложение, которое по заданному URL может скачать необходимый файл и поместить его в целевой каталог.

Ссылка на скачивание проекта Downloader

Рекомендуем хостинг TIMEWEB
Рекомендуем хостинг TIMEWEB
Стабильный хостинг, на котором располагается социальная сеть EVILEG. Для проектов на Django рекомендуем VDS хостинг.

Вам это нравится? Поделитесь в социальных сетях!

f
  • 1 июня 2018 г. 2:08

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

Evgenii Legotckoi
  • 1 июня 2018 г. 20:59

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

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

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

    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
  • 7 июня 2018 г. 10:21

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

Evgenii Legotckoi
  • 7 июня 2018 г. 12:44

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

Evgenii Legotckoi
  • 7 июня 2018 г. 12:46

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

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

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


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

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

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

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

Evgenii Legotckoi
  • 25 сентября 2019 г. 13:52

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

v
  • 29 октября 2019 г. 19:40

Здпавствуйте Евгений.
Прошу совета, вот из-за этой строчки кода:
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
  • 29 октября 2019 г. 19:51

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

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

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

v
  • 29 октября 2019 г. 19:56
  • (ред.)

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

void onReply(QNetworkReply& reply);

void Downloader::onReply(QNetworkReply &reply)
{
    if(reply.error() == QNetworkReply::NoError){
        m_file->flush();
        m_file->close();
    }
    else{
        m_file->remove();
    }

    delete m_file;
    m_file = nullptr;
    reply.deleteLater();
}
v
  • 29 октября 2019 г. 19:57

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

Evgenii Legotckoi
  • 29 октября 2019 г. 20:02
  • (ред.)

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

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

void Downloader::onReply(QNetworkReply &reply)

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

void Downloader::onReply(QNetworkReply *reply)

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

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

v
  • 29 октября 2019 г. 20:08

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

m
  • 9 февраля 2020 г. 20:44

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

void MainWindow::copyToFtp(QString Fname)
{
    ui->progressBar->show();
    fFileFTP =new QFile (Fname);
    QFileInfo fileInfoFTP(fFileFTP->fileName());
    QUrl url("ftp://ftp.ihostfull.com/htdocs/"+fileInfoFTP.completeBaseName()+"."+fileInfoFTP.suffix());
    url.setPort(21);
    url.setPassword("------");
    url.setUserName("-------");
    fFileFTP->open(QIODevice::ReadOnly);
    QNetworkAccessManager manager(0);
    QNetworkReply *reply = manager.put(QNetworkRequest(url),fFileFTP);
    QEventLoop loop;
    QObject::connect(reply,SIGNAL(finished()),&loop,SLOT(quit()));
    loop.exec();
    fFileFTP->close();
}

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

Evgenii Legotckoi
  • 10 февраля 2020 г. 14:02

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

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

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

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

m
  • 10 февраля 2020 г. 14:06

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

Evgenii Legotckoi
  • 10 февраля 2020 г. 14:15
  • (ред.)

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
1
  • 12333
  • 18 июля 2024 г. 12:34

Qt - Тест 001. Сигналы и слоты

  • Результат:63баллов,
  • Очки рейтинга-1
1
  • 12333
  • 18 июля 2024 г. 12:25

C++ - Тест 005. Структуры и Классы

  • Результат:50баллов,
  • Очки рейтинга-4
AM

C++ - Тест 005. Структуры и Классы

  • Результат:33баллов,
  • Очки рейтинга-10
Последние комментарии
d
dblas55 июля 2024 г. 18:02
QML - Урок 016. База данных SQLite и работа с ней в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssr9 февраля 2024 г. 2:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 9:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 18:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 16:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
Сейчас обсуждают на форуме
F
Fynjy22 июля 2024 г. 11:15
при создании qml проекта Kits есть но недоступны для выбора Поставил Qt Creator 11.0.2. Qt 6.4.3 При создании проекта Qml не могу выбрать Kits, они все недоступны, хотя настроены и при создании обычного Qt Widget приложения их можно выбрать. В чем может …
BlinCT
BlinCT25 июня 2024 г. 8:00
Нарисовать кривую в qml Всем привет. Имеется Лист листов с тосками, точки получаны интерполяцией Лагранжа. Вопрос, как этими точками нарисовать кривую? ChartView отпадает сразу, в qt6.7 появился новый элемент…
Evgenii Legotckoi
Evgenii Legotckoi24 июня 2024 г. 22:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
BlinCT
BlinCT5 мая 2024 г. 12:46
Написать свой GraphsView Всем привет. В Qt есть давольно старый обьект дял работы с графиками ChartsView и есть в 6.7 новый но очень сырой и со слабым функционалом GraphsView. По этой причине я хочу написать х…
Evgenii Legotckoi
Evgenii Legotckoi2 мая 2024 г. 21:07
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.

Следите за нами в социальных сетях