C++(Qt)-GIS开发-简易瓦片地图下载器
Qt-GIS开发-简易瓦片地图下载器
更多精彩内容 |
---|
👉个人内容分类汇总 👈 |
👉GIS开发 👈 |
1、概述
开发环境说明
- 系统:Windows11、Ubuntu20.04
- Qt版本:Qt 5.14.2
- 编译器:MSVC2017-64、GCC/G++64
2、安装openssl
-
qt使用QNetworkReply/https下载瓦片地图需要ssl支持,qt默认是没有ssl库的;
-
使用下列代码打印qt版本支持的ssl版本;
qDebug() << "输出当前QT支持的openSSL版本: " << QSslSocket::sslLibraryBuildVersionString(); qDebug() << "OpenSSL支持情况: " <<QSslSocket::supportsSsl(); qDebug() << "OpenSSL运行时SSL库版本: " << QSslSocket::sslLibraryBuildVersionString();
-
windows可以下载对应版本的openssl,然后进行安装(轻量级就可以);
-
linux可以通过命令行安装,也可以下载源码自己编译。
-
openssl的github仓库
-
openssl官网
-
安装后将openssl/bin文件夹下的libcrypto-1_1-x64.dll、libssl-1_1-x64.dll两个动态库拷贝到qt的编译器路径下,注意区分32和64位
- D:\Qt\Qt5.14.2\5.14.2\msvc2017_64\bin
- D:\Qt\Qt5.14.2\5.14.2\mingw73_64\bin
3、实现效果
- 无需注册、无需key进行瓦片地图下载;
- 地址可能会失效;
- 大量下载可能会限速;
- 仅作为学习使用。
4、主要代码
-
项目文件结构
4.1 算法函数
-
bingformula.h文件
#ifndef BINGFORMULA_H #define BINGFORMULA_H #include <QPoint> #include <QtGlobal> namespace Bing { qreal clip(qreal n, qreal min, qreal max); qreal clipLon(qreal lon); // 裁剪经度范围 qreal clipLat(qreal lat); // 裁剪纬度范围 uint mapSize(int level); // 根据地图级别计算世界地图总宽高(以像素为单位) qreal groundResolution(qreal lat, int level); // 计算地面分辨率 qreal mapScale(qreal lat, int level, int screenDpi); // 计算比例尺 QPoint latLongToPixelXY(qreal lon, qreal lat, int level); // 经纬度转像素 XY坐标 void pixelXYToLatLong(QPoint pos, int level, qreal& lon, qreal& lat); // 像素坐标转WGS-84墨卡托坐标 QPoint pixelXYToTileXY(QPoint pos); // 像素坐标转瓦片编号 QPoint tileXYToPixelXY(QPoint tile); // 瓦片编号转像素坐标 QPoint latLongToTileXY(qreal lon, qreal lat, int level); // 经纬度转瓦片编号 QPointF tileXYToLatLong(QPoint tile, int level); // 瓦片编号转经纬度 QString tileXYToQuadKey(QPoint tile, int level); // 瓦片编号转QuadKey void quadKeyToTileXY(QString quadKey, int& tileX, int& tileY, int& level); // QuadKey转瓦片编号、级别 } // namespace Bing #endif // BINGFORMULA_H
-
bingformula.cpp文件
/******************************************************************** * 文件名: bingformula.cpp * 时间: 2024-04-05 21:36:16 * 开发者: mhf * 邮箱: [email protected] * 说明: 适用于Bing瓦片地图的算法 * ******************************************************************/ #include "bingformula.h" #include <qstring.h> #include <QtMath> static const qreal g_EarthRadius = 6'378'137; // 赤道半径 /** * @brief 限定最小值,最大值范围 * @param n 需要限定的值 * @param min * @param max * @return */ qreal Bing::clip(qreal n, qreal min, qreal max) { n = qMax(n, min); n = qMin(n, max); return n; } /** * @brief 限定经度范围值,防止超限,经度范围[-180, 180] * @param lon 输入的经度 * @return 裁剪后的经度 */ qreal Bing::clipLon(qreal lon) { return clip(lon, -180.0, 180); } /** * @brief 限定纬度范围值,防止超限,经度范围[-85.05112878, 85.05112878] * @param lat 输入的纬度 * @return 裁剪后的纬度 */ qreal Bing::clipLat(qreal lat) { return clip(lat, -85.05112878, 85.05112878); } /** * @brief 根据输入的瓦片级别计算全地图总宽高,适用于墨卡托投影 * @param level 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成) * @return 以像素为单位的地图宽度和高度。 */ uint Bing::mapSize(int level) { uint w = 256; // 第0级别为256*256 return (w << level); } /** * @brief 计算指定纬度、级别的地面分辨率(不同纬度分辨率不同) * @param lat 纬度 * @param level 地图级别 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成) * @return 地面分辨率 单位(米/像素) */ qreal Bing::groundResolution(qreal lat, int level) { lat = clipLat(lat); return qCos(lat * M_PI / 180) * 2 * M_PI * g_EarthRadius / mapSize(level); } /** * @brief 计算地图比例尺,地面分辨率和地图比例尺也随纬度而变化 * @param lat 纬度 * @param level 地图级别 1-23(bing地图没有0级别,最低级别为1,由4块瓦片组成) * @param screenDpi 屏幕分辨率,单位为点/英寸 通常为 96 dpi * @return 地图比例尺 1:N(地图上1厘米表示实际N厘米) */ qreal Bing::mapScale(qreal lat, int level, int screenDpi) { return groundResolution(lat, level) * screenDpi / 0.0254; // 1英寸等于0.0254米 } /** * @brief 将一个点从纬度/经度WGS-84墨卡托坐标(以度为单位)转换为指定细节级别的像素XY坐标。 * @param lon 经度 * @param lat 纬度 * @param level 地图级别 * @return 像素坐标 */ QPoint Bing::latLongToPixelXY(qreal lon, qreal lat, int level) { lon = clipLon(lon); lat = clipLat(lat); qreal x = (lon + 180) / 360; qreal sinLat = qSin(lat * M_PI / 180); qreal y = 0.5 - qLn((1 + sinLat) / (1 - sinLat)) / (4 * M_PI); uint size = mapSize(level); qreal pixelX = x * size + 0.5; pixelX = clip(pixelX, 0, size - 1); qreal pixelY = y * size + 0.5; pixelY = clip(pixelY, 0, size - 1); return QPoint(pixelX, pixelY); } /** * @brief 将像素从指定细节级别的像素XY坐标转换为经纬度WGS-84坐标(以度为单位) * @param pos 像素坐标 * @param level * @param lon * @param lat */ void Bing::pixelXYToLatLong(QPoint pos, int level, qreal& lon, qreal& lat) { uint size = mapSize(level); qreal x = (clip(pos.x(), 0, size - 1) / size) - 0.5; qreal y = 0.5 - (clip(pos.y(), 0, size - 1) / size); lon = x * 360; lat = 90 - (360 * qAtan(qExp(-y * 2 * M_PI)) / M_PI); } /** * @brief 像素坐标转瓦片编号 * @param pos 像素坐标 * @return 瓦片编号 */ QPoint Bing::pixelXYToTileXY(QPoint pos) { int x = pos.x() / 256; int y = pos.y() / 256; return QPoint(x, y); } /** * @brief 瓦片编号转像素坐标 * @param tile 瓦片编号 * @return 像素坐标 */ QPoint Bing::tileXYToPixelXY(QPoint tile) { int x = tile.x() * 256; int y = tile.y() * 256; return QPoint(x, y); } /** * @brief 经纬度转瓦片编号 * @param lon * @param lat * @param level * @return */ QPoint Bing::latLongToTileXY(qreal lon, qreal lat, int level) { return pixelXYToTileXY(latLongToPixelXY(lon, lat, level)); } /** * @brief 瓦片编号转经纬度 * @param tile * @param level * @return 经纬度 x:经度 y纬度 */ QPointF Bing::tileXYToLatLong(QPoint tile, int level) { qreal lon = 0; qreal lat = 0; QPoint pos = tileXYToPixelXY(tile); pixelXYToLatLong(pos, level, lon, lat); return QPointF(lon, lat); } /** * @brief 瓦片编号转 bing请求的QuadKey * @param tile 瓦片编号 * @param level 瓦片级别 * @return */ QString Bing::tileXYToQuadKey(QPoint tile, int level) { QString key; for (int i = level; i > 0; i--) { char digit = '0'; int mask = 1 << (i - 1); if ((tile.x() & mask) != 0) { digit++; } if ((tile.y() & mask) != 0) { digit += 2; } key.append(digit); } return key; } /** * @brief 将一个QuadKey转换为瓦片XY坐标。 * @param quadKey * @param tileX 返回瓦片X编号 * @param tileY 返回瓦片Y编号 * @param level 返回瓦片等级 */ void Bing::quadKeyToTileXY(QString quadKey, int& tileX, int& tileY, int& level) { tileX = 0; tileY = 0; level = quadKey.count(); QByteArray buf = quadKey.toUtf8(); for (int i = level; i > 0; i--) { int mask = 1 << (i - 1); switch (buf.at(i - 1)) { case '0': break; case '1': tileX |= mask; break; case '2': tileY |= mask; break; case '3': tileX |= mask; tileY |= mask; break; default: break; } } }
4.2 瓦片地图下载url拼接
-
mapinput.h
#ifndef MAPINPUT_H #define MAPINPUT_H #include <QWidget> #include "mapStruct.h" namespace Ui { class MapInput; } class MapInput : public QWidget { Q_OBJECT public: explicit MapInput(QWidget *parent = nullptr); ~MapInput(); const QList<ImageInfo> &getInputInfo(); // 获取下载地图所需的输入信息 private: // ArcGis void initArcGis(); void getArcGisMapInfo(); // 高德 void initAMap(); void getAMapInfo(); // Bing地图 void initBing(); void getBingMapInfo(); private: Ui::MapInput *ui; QList<ImageInfo> m_infos; // 保存下载瓦片图片的信息 }; #endif // MAPINPUT_H
-
mapinput.cpp
/******************************************************************** * 文件名: mapinput.cpp * 时间: 2024-01-19 22:22:37 * 开发者: mhf * 邮箱: [email protected] * 说明: 生成地图下载的输入信息 * ******************************************************************/ #include "mapinput.h" #include "bingformula.h" #include "formula.h" #include "ui_mapinput.h" #include <QDebug> MapInput::MapInput(QWidget* parent) : QWidget(parent) , ui(new Ui::MapInput) { ui->setupUi(this); initArcGis(); initAMap(); initBing(); } MapInput::~MapInput() { delete ui; } /** * @brief 填入ArcGis下载地图类型 */ void MapInput::initArcGis() { for (int i = 0; i < 23; i++) { ui->com_z->addItem(QString("%1").arg(i), i); } ui->com_type->addItem("NatGeo_World_Map"); ui->com_type->addItem("USA_Topo_Maps "); ui->com_type->addItem("World_Imagery"); ui->com_type->addItem("World_Physical_Map"); ui->com_type->addItem("World_Shaded_Relief"); ui->com_type->addItem("World_Street_Map"); ui->com_type->addItem("World_Terrain_Base"); ui->com_type->addItem("World_Topo_Map"); ui->com_type->addItem("Canvas/World_Dark_Gray_Base"); ui->com_type->addItem("Canvas/World_Dark_Gray_Reference"); ui->com_type->addItem("Canvas/World_Light_Gray_Base"); ui->com_type->addItem("Canvas/World_Light_Gray_Reference"); ui->com_type->addItem("Elevation/World_Hillshade_Dark"); ui->com_type->addItem("Elevation/World_Hillshade"); ui->com_type->addItem("Ocean/World_Ocean_Base"); ui->com_type->addItem("Ocean/World_Ocean_Reference"); ui->com_type->addItem("Polar/Antarctic_Imagery"); ui->com_type->addItem("Polar/Arctic_Imagery"); ui->com_type->addItem("Polar/Arctic_Ocean_Base"); ui->com_type->addItem("Polar/Arctic_Ocean_Reference"); ui->com_type->addItem("Reference/World_Boundaries_and_Places_Alternate "); ui->com_type->addItem("Reference/World_Boundaries_and_Places"); ui->com_type->addItem("Reference/World_Reference_Overlay"); ui->com_type->addItem("Reference/World_Transportation"); ui->com_type->addItem("Specialty/World_Navigation_Charts"); // 填入下载格式 ui->com_format->addItem("jpg"); ui->com_format->addItem("png"); ui->com_format->addItem("bmp"); } /** * @brief 计算并返回需要下载的瓦片地图信息 * @return */ const QList<ImageInfo>& MapInput::getInputInfo() { m_infos.clear(); // 清除之前的内容 switch (ui->tabWidget->currentIndex()) // 判断是什么类型的地图源 { case 0: // ArcGis { getArcGisMapInfo(); // 计算ArcGis下载信息 break; } case 1: { getAMapInfo(); // 计算高德地图下载信息 break; } case 2: { getBingMapInfo(); // 计算bing地图下载信息 break; } default: break; } qDebug() << "瓦片数:" << m_infos.count(); return m_infos; } /** * @brief 通过输入地图信息计算需要下载的瓦片图信息,下载ArcGIS地图,WGS84坐标系,Web墨卡托投影,z y x输入 */ void MapInput::getArcGisMapInfo() { static QString url = "https://server.arcgisonline.com/arcgis/rest/services/%1/MapServer/tile/%2/%3/%4.%5"; int z = ui->com_z->currentData().toInt(); QString type = ui->com_type->currentText(); QString format = ui->com_format->currentText(); QStringList lt = ui->line_LTGps->text().trimmed().split(','); // 左上角经纬度 QStringList rd = ui->line_RDGps->text().trimmed().split(','); // 右下角经纬度 if (lt.count() != 2 || rd.count() != 2) return; // 判断输入是否正确 int ltX = lonTotile(lt.at(0).toDouble(), z); // 计算左上角瓦片X int ltY = latTotile(lt.at(1).toDouble(), z); // 计算左上角瓦片Y int rdX = lonTotile(rd.at(0).toDouble(), z); // 计算右下角瓦片X int rdY = latTotile(rd.at(1).toDouble(), z); // 计算右下角瓦片Y ImageInfo info; info.z = z; info.format = format; for (int x = ltX; x <= rdX; x++) { info.x = x; for (int y = ltY; y <= rdY; y++) { info.y = y; info.url = url.arg(type).arg(z).arg(y).arg(x).arg(format); m_infos.append(info); } } } /** * @brief 初始化高德地图下载选项信息 */ void MapInput::initAMap() { for (int i = 1; i < 5; i++) { ui->com_amapPrefix->addItem(QString("wprd0%1").arg(i)); } for (int i = 1; i < 5; i++) { ui->com_amapPrefix->addItem(QString("webst0%1").arg(i)); } for (int i = 0; i < 19; i++) { ui->com_amapZ->addItem(QString("%1").arg(i), i); } // 语言设置 ui->com_amapLang->addItem("中文", "zh_cn"); ui->com_amapLang->addItem("英文", "en"); // 地图类型 ui->com_amapStyle->addItem("卫星影像图", 6); ui->com_amapStyle->addItem("矢量路网", 7); ui->com_amapStyle->addItem("影像路网", 8); // 支持png透明背景 ui->com_amapStyle->addItem("卫星+影像路网", 9); // 支持png透明背景 // 图片尺寸,只在7 8生效 ui->com_amapScl->addItem("256x256", 1); ui->com_amapScl->addItem("512x512", 2); // 填入下载格式 ui->com_amapFormat->addItem("jpg"); ui->com_amapFormat->addItem("png"); ui->com_amapFormat->addItem("bmp"); } /** * @brief 计算高德地图瓦片下载信息 */ void MapInput::getAMapInfo() { static QString url = "https://%1.is.autonavi.com/appmaptile?"; int z = ui->com_amapZ->currentData().toInt(); QString format = ui->com_amapFormat->currentText(); QStringList lt = ui->line_LTGps->text().trimmed().split(','); // 左上角经纬度 QStringList rd = ui->line_RDGps->text().trimmed().split(','); // 右下角经纬度 if (lt.count() != 2 || rd.count() != 2) return; // 判断输入是否正确 int ltX = lonTotile(lt.at(0).toDouble(), z); // 计算左上角瓦片X int ltY = latTotile(lt.at(1).toDouble(), z); // 计算左上角瓦片Y int rdX = lonTotile(rd.at(0).toDouble(), z); // 计算右下角瓦片X int rdY = latTotile(rd.at(1).toDouble(), z); // 计算右下角瓦片Y ImageInfo info; info.z = z; info.format = format; int style = ui->com_amapStyle->currentData().toInt(); int count = 1; if (style == 9) { count = 2; // 如果是下载卫星图 + 路网图则循环两次 } for (int i = 0; i < count; i++) { if (count == 2) { if (i == 0) { style = 6; // 第一次下载卫星图 info.format = "jpg"; } else { style = 8; // 第二次下载路网图 info.format = "png"; // 如果同时下载卫星图和路网图则路网图为透明png格式 } } QString tempUrl = url.arg(ui->com_amapPrefix->currentText()); // 设置域名 tempUrl += QString("&style=%1").arg(style); // 设置地图类型 tempUrl += QString("&lang=%1").arg(ui->com_amapLang->currentData().toString()); // 设置语言 tempUrl += QString("&scl=%1").arg(ui->com_amapScl->currentData().toInt()); // 设置图片尺寸,只在7 8生效 tempUrl += QString("<ype=%1").arg(ui->spin_amapLtype->value()); // 设置图片中的信息,只有 7 8有效 for (int x = ltX; x <= rdX; x++) { info.x = x; for (int y = ltY; y <= rdY; y++) { info.url = tempUrl + QString("&x=%1&y=%2&z=%3").arg(x).arg(y).arg(z); info.y = y; m_infos.append(info); } } } } /** * @brief 初始化Bing地图配置 */ void MapInput::initBing() { // 服务器 for (int i = 0; i < 8; i++) { ui->com_bingPrefix->addItem(QString("%1").arg(i)); } // 地图语言 ui->com_bingLang->addItem("中文", "zh-cn"); ui->com_bingLang->addItem("英语", "en-US"); // 地图类型 ui->com_bingType->addItem("卫星地图", "a"); ui->com_bingType->addItem("普通地图", "r"); ui->com_bingType->addItem("混合地图", "h"); ui->com_bingCstl->addItem("默认", "w4c"); ui->com_bingCstl->addItem("白天", "vb"); // 白天道路地图 ui->com_bingCstl->addItem("夜晚", "vbd"); // 夜晚道路图 // 瓦片等级 for (int i = 1; i < 21; i++) { ui->com_bingZ->addItem(QString("%1").arg(i)); } // 填入下载格式 ui->com_bingFormat->addItem("jpg"); ui->com_bingFormat->addItem("png"); ui->com_bingFormat->addItem("bmp"); } /** * @brief 计算Bing地图的下载信息(这些url可能会失效,后续会使用其他方式下载) * https://learn.microsoft.com/en-us/bingmaps/rest-services/directly-accessing-the-bing-maps-tiles */ void MapInput::getBingMapInfo() { //https://r1.tiles.ditu.live.com/tiles/r1321001.png?g=1001&mkt=zh-cn //http://dynamic.t2.tiles.ditu.live.com/comp/ch/r1321001.png?it=G,OS,L&mkt=en-us&cstl=w4c&ur=cn //http://ecn.t{0}.tiles.virtualearth.net/tiles/{1}{2}.png? g={4} //https://t0.dynamic.tiles.ditu.live.com/comp/ch/1320300313132?mkt=zh-CN&ur=CN&it=G,RL&n=z&og=894&cstl=vb //https://t1.dynamic.tiles.ditu.live.com/comp/ch/13203012200201?mkt=zh-CN&ur=cn&it=G,RL&n=z&og=894&cstl=vbd //https://dynamic.t1.tiles.ditu.live.com/comp/ch/1320300313313?it=Z,TF&L&n=z&key=AvquUWQgfy7VPqHn9ergJsp3Q_EiUft0ed70vZsX0_aqPABBdK07OkwrXWoGXsTG&ur=cn&cstl=vbd #define USE_URL 1 #if (USE_URL == 0) // https://r1.tiles.ditu.live.com/tiles/r1321001.png?g=1001&mkt=zh-cn static QString url = "https://r%1.tiles.ditu.live.com/tiles/%2%3.%4?g=1001&mkt=%5"; // 街道图r支持中文 #elif (USE_URL == 1) // http://dynamic.t2.tiles.ditu.live.com/comp/ch/r1321001.png?it=G,OS,L&mkt=en-us&cstl=w4c&ur=cn static QString url = "http://dynamic.t%1.tiles.ditu.live.com/comp/ch/%2%3.%4?it=G,OS,L&mkt=%5&cstl=%6&ur=cn"; #endif int z = ui->com_bingZ->currentText().toInt(); QStringList lt = ui->line_LTGps->text().trimmed().split(','); // 左上角经纬度 QStringList rd = ui->line_RDGps->text().trimmed().split(','); // 右下角经纬度 if (lt.count() != 2 || rd.count() != 2) return; // 判断输入是否正确 int ltX = lonTotile(lt.at(0).toDouble(), z); // 计算左上角瓦片X int ltY = latTotile(lt.at(1).toDouble(), z); // 计算左上角瓦片Y int rdX = lonTotile(rd.at(0).toDouble(), z); // 计算右下角瓦片X int rdY = latTotile(rd.at(1).toDouble(), z); // 计算右下角瓦片Y QString format = ui->com_bingFormat->currentText(); ImageInfo info; info.z = z; info.format = format; int prefix = ui->com_bingPrefix->currentIndex(); QString lang = ui->com_bingLang->currentData().toString(); // 语言 QString type = ui->com_bingType->currentData().toString(); // 类型 QString cstl = ui->com_bingCstl->currentData().toString(); // 样式 QPoint point; for (int x = ltX; x <= rdX; x++) { info.x = x; point.setX(x); for (int y = ltY; y <= rdY; y++) { info.y = y; point.setY(y); QString quadKey = Bing::tileXYToQuadKey(point, z); // 将xy转为quadkey #if (USE_URL == 0) info.url = url.arg(prefix).arg(type).arg(quadKey).arg(format).arg(lang); #elif (USE_URL == 1) info.url = url.arg(prefix).arg(type).arg(quadKey).arg(format).arg(lang).arg(cstl); #endif m_infos.append(info); } } }
4.3 多线程下载
-
downloadthreads.h
#ifndef DOWNLOADTHREADS_H #define DOWNLOADTHREADS_H #include "mapStruct.h" #include <QFutureWatcher> #include <QObject> class DownloadThreads : public QObject { Q_OBJECT public: explicit DownloadThreads(QObject* parent = nullptr); ~DownloadThreads(); // 传入需要下载的瓦片信息 void getImage(QList<ImageInfo> infos); void quit(); // 退出下载 signals: void finished(ImageInfo info); // 返回下载后的瓦片,由于QImage为共享内存,所以传递不需要考虑太多性能 private: QFuture<void> m_future; QList<ImageInfo> m_infos; }; #endif // DOWNLOADTHREADS_H
-
downloadthreads.cpp
/******************************************************************** * 文件名: downloadthreads.cpp * 时间: 2024-03-31 20:32:58 * 开发者: mhf * 邮箱: [email protected] * 说明: 多线程下载瓦片地图 * ******************************************************************/ #include "downloadthreads.h" #include <QtConcurrent> #include <qnetworkaccessmanager.h> #include <qnetworkreply.h> static DownloadThreads* g_this = nullptr; DownloadThreads::DownloadThreads(QObject *parent) : QObject(parent) { g_this = this; // 记录当前 this指针,用于传递信号 } DownloadThreads::~DownloadThreads() { g_this = nullptr; quit(); } /** * @brief 下载瓦片 * @param info * @return */ void getUrl(ImageInfo info) { QNetworkAccessManager manager; QNetworkReply* reply = manager.get(QNetworkRequest(QUrl(info.url))); // 等待返回 QEventLoop loop; QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit); loop.exec(); if(reply->error() == QNetworkReply::NoError) { QByteArray buf = reply->readAll(); info.img.loadFromData(buf); } else { info.count++; if(info.count < 3) { getUrl(info); // 下载失败重新下载 return; } else { qWarning() << "下载失败:" << reply->errorString(); } } if(g_this) { emit g_this->finished(info); // 通过信号将下载后的瓦片传出去 } } /** * @brief 调用线程池下载瓦片 * @param infos */ void DownloadThreads::getImage(QList<ImageInfo> infos) { m_infos = infos; // 这里不能使用infos,因为会在函数退出释放 #if 0 // 由于map使用的是全局线程池,所以可以查看、设置线程数 qDebug() <<QThreadPool::globalInstance()->maxThreadCount(); // 查看最大线程数 QThreadPool::globalInstance()->setMaxThreadCount(1); // 设置最大线程数 #endif m_future = QtConcurrent::map(m_infos, getUrl); } /** * @brief 退出下载 */ void DownloadThreads::quit() { if(m_future.isRunning()) // 判断是否在运行 { m_future.cancel(); // 取消下载 m_future.waitForFinished(); // 等待退出 } }