实现多线程下载文件,先考虑如何实现动态的添加组件和线程,在这里采用了QListWidget来做为下载项目的容器,比较的简单,将下载组件组合在一个自定义容器类DownLoadItem里,这样可以很方便的创建下载项目。
最终效果:
先说一下组件创建遇到的问题(下载项目重叠):
这个问题可以通过设置样式表来解决:
QListView::item {height:80px;}
然后考虑如何关联线程和组件,可以在DownLoadItem类里创建一个下载进程。
void DownloadItem::initControl()
{
//设置打开文件和打开文件夹不可见
ui.openBtn->setVisible(false);
ui.openFolder->setVisible(false);
m_httpDownload = new HttpDownloadManager();
//连接按钮的信号与槽
//播放暂停按钮
connect(ui.startBtn, &QPushButton::clicked, this, &DownloadItem::onStartBtnClicked);
//删除按钮
connect(ui.deleteBtn, &QPushButton::clicked, this, &DownloadItem::onDeleteBtnCliked);
//打开文件按钮
connect(ui.openBtn, &QPushButton::clicked, this, &DownloadItem::onOpenBtnClicked);
//打开文件夹按钮
connect(ui.openFolder, &QPushButton::clicked, this, &DownloadItem::onOpenFolderBtnClicked);
//连接线程的信号
//更新下载信息
connect(m_httpDownload, &HttpDownloadManager::signalUpdatFileInfo, this, &DownloadItem::onUpdateFileInfo);
//进度
connect(m_httpDownload, &HttpDownloadManager::signalProgressChanged, this, &DownloadItem::onProgressChanged);
//状态改变,完成状态隐藏开始暂停按钮,显示文件,文件夹按钮
connect(m_httpDownload, &HttpDownloadManager::signalStateChanged, this, &DownloadItem::onHttpStateChanged);
//下载速度
connect(m_httpDownload, &HttpDownloadManager::signalSpeedValue, this, &DownloadItem::onSpeedValue);
//设置下载链接,开始下载
m_httpDownload->setUrl(m_url);
m_httpDownload->start();
}
//读取数据
void HttpDownloader::onDatareadReady()
{
const QByteArray&& buffer = m_reply->readAll();
//已经写入的字节
m_file->seek(m_startPoint + m_readySize);
m_file->write(buffer);
//读取大小和速度大小
m_readySize += buffer.size();
m_speedValue += buffer.size();
//向上反馈
emit downloadDatareadReady();
}
然后再考虑下载线程类HttpDownloadManager,该类在在下载时会产生多个子线程HttpDownloader用于文件分块下载,同时该类起到了一个中间者的作用,对HttpDownloader线程的信号进行数据处理,然后再通过信号反馈给DownloadItem类。
HttpDownloadManager::HttpDownloadManager()
: QObject()
{
m_speedTimer = new QTimer(ptr);
//一秒钟一次,方便计算速度
m_speedTimer->setInterval(1000);
m_speedTimer->start();
//将对象移入内置线程
thread = new QThread;
this->moveToThread(thread);
//定时器信号计算下载速度
connect(m_speedTimer, &QTimer::timeout, this, &HttpDownloadManager::onSpeedTimeout);
}
//开始下载
bool HttpDownloadManager::startDownloadUrl(const QString& url)
{
//主线程开启
thread->start();
m_state = DownloadState_e::DWaiting;
HttpDownloadStateChanged(d->m_state);
//获取文件大小
if ((m_totalSize = getFileSize(url)) == -1){
qDebug() << "Get File Size Error";
return false;
}
QString fileName = replaceNoExistFileName(QUrl(url).fileName());
//通知获取到了文件名和文件大小
emit signalUpdatFileInfo(fileName, HttpUtils::bytetoSize(m_totalSize));
//放在临时文件夹
m_filePath = QDir::tempPath() + "/" + fileName;
m_file = new QFile(m_filePath);
if (!d->m_file->open(QFile::WriteOnly))
{
qDebug() << "Can not open file : " + m_file->errorString();
m_file->close();
delete m_file;
m_file = nullptr;
return false;
}
//设置文件大小
m_httpdownloaderlst.clear();
//分块下载,SHARDCOUNT为3
for (int index = 0; index < SHARDCOUNT; index++)
{
qint64 startPoint = d->m_totalSize * index / SHARDCOUNT;
qint64 endPoint = d->m_totalSize * (index + 1) / SHARDCOUNT;
if (index > 0){
startPoint += 1;
}
//开启子线程分块下载
HttpDownloader* httpdownloader = new HttpDownloader(nullptr);
httpdownloader->resetSpeedValue();
//添加到线程列表
m_httpdownloaderlst << httpdownloader;
//子线程下载完成
connect(httpdownloader, SIGNAL(downloadFinished()), this, SLOT(onDownLoadFinished()));
//子线程读取数据
connect(httpdownloader, SIGNAL(downloadDatareadReady()), this, SLOT(onDownloadDatareadReady()), Qt::QueuedConnection);
//下载失败
connect(httpdownloader, SIGNAL(downloadError(int, const QString&)), this, SLOT(onDownloadError(int, const QString&)));
//通知子线程停止下载
connect(this, &HttpDownloadManager::signalpause, httpdownloader, &HttpDownloader::pause);
//通知子线程开始下载
connect(this, &HttpDownloadManager::signalrestart, httpdownloader, &HttpDownloader::restart);
httpdownloader->start(index, url, m_file, startPoint, endPoint, 0);
m_state = DownloadState_e::DDownload;
}
return true;
}
//下载速度
void HttpDownloadManager::onSpeedTimeout()
{
if (m_state == DownloadState_e::DDownload){
qint64 speedsize = 0;
for (const auto iter : m_httpdownloaderlst){
speedsize += iter->getSpeedValue();
iter->resetSpeedValue();
}
//发送信号,界面修改下载速度
emit signalSpeedValue(speedsize);
}
}
最后再来看一下HttpDownloader子线程类
void HttpDownloader::start(int index, const QString &url, QFile *file, qint64 startPoint, qint64 endPoint, qint64 readySize)
{
if (m_state == DownloadState_e::DDownload){
//正在下载中
return;
}
//线程编号
m_index = index;
//下载地址
m_url = url;
//下载的文件
m_file = file;
//开始字节和结束字节
m_startPoint = startPoint;
m_endPoint = endPoint;
//已读入字节
m_readySize = readySize;
//设置下载地址
QNetworkRequest request;
request.setUrl(m_url);
if (endPoint > 0){
QString range = QString("bytes=%1-%2").arg(m_startPoint + m_readySize).arg(endPoint);
request.setRawHeader("Range", range.toUtf8());
}
m_reply = m_manager.get(request);
connect(m_reply, SIGNAL(finished()), this, SLOT(onHttpDownloadFinished()));
connect(m_reply, SIGNAL(readyRead()), this, SLOT(onDatareadReady()));
connect(m_reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(onError(QNetworkReply::NetworkError)));
m_state = DownloadState_e::DDownload;
}
三个类的关系:DownloadItem->HttpDownloadManager->HttpDownloader
(包含一个) (包含多个)
用户的操作通过DownloadItem向下传递,进程的变化通过HttpDownloader向上反馈。
在做这个练习的时候,经常漏写一些代码:
1. 在删除项目的时候,没有先写file.close()函数,导致文件一直无法删除
2. 不用的指针记得删除,否则程序很容易出现异常结束,这个问题遇到了好几次
3. 下载结束后线程记得关闭,调用quit()结束线程
{{ cmt.username }}
{{ cmt.content }}
{{ cmt.commentDate | formatDate('YYYY.MM.DD hh:mm') }}