一、前言说明
从来没有想过,明明已经有个各种js版本的地图组件,为何还要自讨苦吃底层全部实现一遍,其实呢,这也完全是被逼的,因为有客户强烈需求,比如有些客户需要在任意平台实现这个组件,比如安卓上甚至编译成wasm运行在网页上,而之前的js版本的,强烈依赖浏览器控件,而手机端只有qml才有浏览器组件,wasm端qml的浏览器控件也不可用,尽管可以通过quickwidget加载qml进行交互,总归还是很别扭。最核心的一个难点来了,那就是某些国产的机器,性能配置很低,跑浏览器很悬,包括一些开发板,老板只是想用来显示一个地图,上面放两个标注点即可,为了这个功能,引入一个这么大的浏览器控件,得不偿失,毕竟浏览器号称吃内存大户,板子就几百兆内存最多了,哪里够吃的啊,于是迫切需要一个纯qwidget代码绘制的地图组件,说干就干应运而生。通过各种大量的对比测试,性能暴增几十倍不止,尤其是大量的标注点和轨迹运动的时候。
二、效果图

三、相关代码
#include "frmoverlay.h"
#include "ui_frmoverlay.h"
#include "qthelper.h"
#include "maphelper.h"
#include "maputil.h"
#include "overlayhelper.h"
#include "magicfish.h"frmOverlay::frmOverlay(QWidget *parent) : QWidget(parent), ui(new Ui::frmOverlay)
{ui->setupUi(this);this->initForm();this->initConfig();this->loadBoundary();this->loadMap();
}frmOverlay::~frmOverlay()
{delete ui;
}void frmOverlay::initForm()
{point = QPointF(121.424362, 31.175942);ui->cboxPixmap->setCurrentIndex(1);ui->mapWidget->setFlag("frmOverlay");connect(ui->cboxFlag->lineEdit(), SIGNAL(textChanged(QString)), this, SLOT(textChanged(QString)));connect(ui->mapWidget, SIGNAL(mapEvent(int, bool, QPointF)), this, SLOT(mapEvent(int, bool, QPointF)));
}void frmOverlay::initConfig()
{MapHelper::loadTileSource(ui->cboxTileSource, AppConfig::OverlaySource);connect(ui->cboxTileSource, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));connect(ui->cboxTileSource, SIGNAL(currentIndexChanged(int)), this, SLOT(loadMap()));MapHelper::loadTileType(ui->cboxTileType, AppConfig::OverlayType);connect(ui->cboxTileType, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));connect(ui->cboxTileType, SIGNAL(currentIndexChanged(int)), this, SLOT(loadMap()));ui->cboxOffline->setCurrentIndex(AppConfig::OverlayOffline);connect(ui->cboxOffline, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));connect(ui->cboxOffline, SIGNAL(currentIndexChanged(int)), this, SLOT(loadMap()));ui->cboxCache->setCurrentIndex(AppConfig::OverlayCache);connect(ui->cboxCache, SIGNAL(currentIndexChanged(int)), this, SLOT(saveConfig()));connect(ui->cboxCache, SIGNAL(currentIndexChanged(int)), this, SLOT(loadMap()));
}void frmOverlay::saveConfig()
{AppConfig::OverlaySource = ui->cboxTileSource->itemData(ui->cboxTileSource->currentIndex()).toInt();AppConfig::OverlayType = ui->cboxTileType->itemData(ui->cboxTileType->currentIndex()).toInt();AppConfig::OverlayOffline = ui->cboxOffline->currentIndex();AppConfig::OverlayCache = ui->cboxCache->currentIndex();AppConfig::writeConfig();
}void frmOverlay::loadBoundary()
{//加载行政区划边界点文件QString path = QtHelper::appPath() + "/mapboundary";QStringList files = QDir(path).entryList(QStringList() << "*.txt");ui->cboxBoundary->clear();ui->cboxBoundary->blockSignals(true);foreach (QString file, files) {QString name = file;name.replace(".txt", "");ui->cboxBoundary->addItem(name, path + "/" + file);}ui->cboxBoundary->blockSignals(false);
}void frmOverlay::reset()
{index = 1;flag = "overlay1";ui->cboxFlag->clear();ui->cboxFlag->lineEdit()->setText(flag);
}void frmOverlay::loadMap()
{this->reset();int tileSource = ui->cboxTileSource->itemData(ui->cboxTileSource->currentIndex()).toInt();ui->mapWidget->setTileSource(TileSource(tileSource));int tileType = ui->cboxTileType->itemData(ui->cboxTileType->currentIndex()).toInt();ui->mapWidget->setTileType(TileType(tileType));QString offlinePath = TileHelper::getOfflinePath(TileSource(tileSource));ui->mapWidget->setOffline(ui->cboxOffline->currentIndex() == 1);ui->mapWidget->setOfflinePath(offlinePath);QString cachePath = (ui->cboxCache->currentIndex() == 1 ? QtHelper::appPath() + "/mapcache" : "");ui->mapWidget->setCachePath(cachePath);ui->mapWidget->load();
}void frmOverlay::mapEvent(int type, bool right, const QPointF &point)
{if (type == MouseType_Press && !right) {this->point = point;MapHelper::setLngLat(point, ui->txtLng, ui->txtLat);}
}void frmOverlay::addIndex()
{index++;QString text = QString("overlay%1").arg(index);QString item = QString("overlay%1").arg(index - 1);ui->cboxFlag->addItem(item);ui->cboxFlag->lineEdit()->setText(text);
}void frmOverlay::textChanged(const QString &arg1)
{if (!arg1.isEmpty()) {this->flag = arg1;}
}void frmOverlay::on_btnAddLabel_clicked()
{ui->mapWidget->addLabel(flag, point, "#ff0000", "测试文本_" + flag, 2, 20, 0);this->addIndex();
}void frmOverlay::on_btnAddMarker_clicked()
{ui->mapWidget->addMarker(flag, point, ":/mapimage/marker_mini.png", QPixmap());this->addIndex();
}void frmOverlay::on_btnAddPolyline_clicked()
{QVector<QPointF> points = MapHelper::stringToPoints("121.428961,31.249075|121.557167,31.213504|121.469780,31.135397|121.403090,31.198678");qreal degrees = MapUtil::getDegreesPerPixel(ui->mapWidget->getZoom());points = MapHelper::getRandPoints(point, 4, degrees * 150);ui->mapWidget->addPolyline(flag, points, "#A279C5", 3);this->addIndex();
}void frmOverlay::on_btnAddPolygon_clicked()
{QVector<QPointF> points = MapHelper::stringToPoints("121.428961,31.249075|121.557167,31.213504|121.469780,31.135397|121.403090,31.198678");qreal degrees = MapUtil::getDegreesPerPixel(ui->mapWidget->getZoom());points = MapHelper::getRandPoints(point, 3, degrees * 150);ui->mapWidget->addPolygon(flag, points, "#00ff00", 2, QColor(0, 255, 0, 100));this->addIndex();
}void frmOverlay::on_btnAddRectangle_clicked()
{QPointF topLeft = QPointF(121.266260, 31.202138);QPointF bottomRight = QPointF(121.370319, 31.131935);//根据当前缩放对应的距离/随机生成一定像素的尺寸的矩形qreal degrees = MapUtil::getDegreesPerPixel(ui->mapWidget->getZoom());qreal width = (rand() % 70 + 50) * degrees;qreal height = (rand() % 50 + 30) * degrees;topLeft = point;bottomRight = QPointF(topLeft.x() + width, topLeft.y() + height);ui->mapWidget->addRectangle(flag, topLeft, bottomRight, "#008888", 2, QColor(0, 255, 0, 100));this->addIndex();
}void frmOverlay::on_btnAddCircle_clicked()
{//根据当前缩放级别获取合适的半径/这里演示随机生成像素大小int px = rand() % 50 + 30;qreal radius = px * MapUtil::getDegreesPerPixel(ui->mapWidget->getZoom());ui->mapWidget->addCircle(flag, point, radius, "#0000ff", 3, QColor(0, 0, 255, 50));this->addIndex();
}void frmOverlay::on_btnAddWidget_clicked()
{MagicFish *widget = new MagicFish(ui->mapWidget->getMapControl());ui->mapWidget->addWidget(flag, point, widget);this->addIndex();
}void frmOverlay::on_btnUpdateOverlay_clicked()
{//找到对应标识图形OverlayBase *overlay = ui->mapWidget->getOverlay(flag);if (!overlay) {return;}//找到对应图形的类型OverlayType type = overlay->overlayType();if (type == OverlayType_Label) {ui->mapWidget->updateLabel(flag, point, "#00ffff", "旋转文字\n换行文字测试", 2, 50, 30);} else if (type == OverlayType_Marker) {ui->mapWidget->updateMarker(flag, point, ":/mapimage/fly.png", QPixmap(), 45, 2);} else if (type == OverlayType_Polyline) {ui->mapWidget->updatePolyline(flag, QStringList(), QColor(0, 100, 100, 180), 5);} else if (type == OverlayType_Polygon) {ui->mapWidget->updatePolygon(flag, QStringList(), Qt::yellow, 4, QColor(250, 200, 89, 150));} else if (type == OverlayType_Rectangle) {ui->mapWidget->updateRectangle(flag, QPointF(), QPointF(), Qt::red, 3, QColor(255, 0, 89, 150));} else if (type == OverlayType_Circle) {//占据100个固定像素qreal radius = 100 * MapUtil::getDegreesPerPixel(ui->mapWidget->getZoom());//也可以传入距离获取半径//qreal radius = MapUtil::getDegreesFromMeters(15000);ui->mapWidget->updateCircle(flag, point, radius, "#ff0000");} else if (type == OverlayType_Widget) {ui->mapWidget->updateWidget(flag, point);}
}void frmOverlay::on_btnDeleteOverlay_clicked()
{QString flag = ui->cboxFlag->currentText();ui->mapWidget->deleteOverlay(flag, true);//同时移除标识下拉框中对应标识ui->cboxFlag->removeItem(ui->cboxFlag->findText(flag));if (ui->cboxFlag->count() == 0) {this->reset();}
}void frmOverlay::on_btnDeleteRectangle_clicked()
{ui->mapWidget->deleteGroup("rectangle", true);
}void frmOverlay::on_btnDeleteCircle_clicked()
{ui->mapWidget->deleteGroup("circle", true);
}void frmOverlay::on_btnClearOverlay_clicked()
{this->reset();ui->mapWidget->clearOverlay();
}void frmOverlay::on_btnSetOverlayVisible_clicked()
{if (ui->btnSetOverlayVisible->text() == "显示图形") {ui->mapWidget->setOverlayVisible(flag, true);ui->btnSetOverlayVisible->setText("隐藏图形");} else {ui->mapWidget->setOverlayVisible(flag, false);ui->btnSetOverlayVisible->setText("显示图形");}
}void frmOverlay::on_btnSetLayerVisible_clicked()
{if (ui->btnSetLayerVisible->text() == "显示图层") {ui->mapWidget->setLayerVisible(true);ui->btnSetLayerVisible->setText("隐藏图层");} else {ui->mapWidget->setLayerVisible(false);ui->btnSetLayerVisible->setText("显示图层");}
}void frmOverlay::on_btnSetOverlayZIndex_clicked()
{ui->mapWidget->setOverlayZIndex(flag, ui->cboxZIndex->currentText().toInt());
}void frmOverlay::on_btnSavePixmap_clicked()
{//优先行政区划/其次添加的多边形/都没有则指定的一个区域QVector<QPointF> points = ui->mapWidget->getPolygonPoints("boundary");if (points.count() == 0) {points = ui->mapWidget->getPolygonPoints(flag);}if (points.count() == 0) {points << MapHelper::stringToPoints("121.428961,31.249075|121.557167,31.213504|121.469780,31.135397|121.403090,31.198678");}int index = ui->cboxPixmap->currentIndex();QString fileName = QString("%1/mapsnap/%2.jpg").arg(QtHelper::appPath()).arg(STRDATETIME);if (index == 0) {ui->mapWidget->savePixmap(fileName, false);} else if (index == 1) {ui->mapWidget->savePixmap(fileName, true);} else if (index == 2) {ui->mapWidget->savePixmap(fileName, false, points);} else if (index == 3) {//QRectF rect(115.428, 41.0658, 2.09522, -1.61602);QRectF rect(QPointF(115.428, 41.0658), QPointF(117.52322, 39.44978));MapHelper::savePixmap(fileName, rect);}
}void frmOverlay::on_btnAddBoundary_clicked()
{QString fileName = QtHelper::getOpenFileName("*.txt", QtHelper::appPath() + "/mapboundary");if (!fileName.isEmpty()) {QFileInfo file(fileName);ui->cboxBoundary->addItem(file.baseName(), fileName);ui->cboxBoundary->setCurrentIndex(ui->cboxBoundary->count() - 1);}
}void frmOverlay::on_cboxBoundary_currentIndexChanged(int index)
{on_btnClearOverlay_clicked();QString fileName = ui->cboxBoundary->itemData(index).toString();QString points = MapHelper::readFile(fileName);ui->mapWidget->addBoundary("boundary", points, "#ff55ff", 2, QColor(255, 100, 255, 100));
}
四、相关地址
- 国内站点:https://gitee.com/feiyangqingyun
- 国际站点:https://github.com/feiyangqingyun
- 个人作品:https://blog.csdn.net/feiyangqingyun/article/details/97565652
- 文件地址:https://pan.baidu.com/s/1ZxG-oyUKe286LPMPxOrO2A 提取码:o05q 文件名:bin_mapwidget.zip
五、功能特点
- 支持各种地图源,包括天地图、高德地图、腾讯地图、谷歌地图、微软地图等。
- 标准WGS-84地球坐标系,采用默卡托投影,可以拓展其他坐标系和投影规则。
- 支持在线和离线两种场景需求,可以自定义在线瓦片地址格式和离线瓦片地址格式。
- 多线程下载和加载瓦片图片文件,多线程绘制,自动缓存瓦片文件。
- 在线模式下,可以开启是否缓存文件,指定缓存路径,将下载的瓦片文件存放到本地,默认优先从缓存文件查找,如果存在缓存文件则加载缓存文件,不存在则联网下载。
- 可以拖动地图,鼠标滚轮放大和缩小地图,以鼠标所在位置作为缩放中心点,提供缩放控件手动单击进行操作。
- 多图层机制,支持多个瓦片叠加图层和图形绘制图层,全部采用双缓冲技术,所有的图形和瓦片全部绘制到一个图片文件上,最终再将图片文件绘制到地图控件。不可见区域的图层包括覆盖物不会触发绘制,降低CPU占用。
- 预加载机制,默认绘制的图层大小以当前区域往四周放大两倍,这样在鼠标拖动和缩放的时候,不会看到明显的加载过程,体验更佳。
- 内置了多种图形覆盖物,包括坐标点、文本、标注点、折线、多边形、矩形、圆形等,可以设置边框颜色粗细、填充颜色和透明度等参数。
- 标注点支持旋转角度和提示文本,其中提示文本可以设置在标注点的相对位置,标注点图片支持gif动图,可以动态切换静态图和动图。
- 标注点和提示文本可以设置相对位置,位置包括左侧、右侧、上侧、下侧、中间、左上角、右上角、左下角、右下角。标注点默认按照底部居中对齐,一般圆形图标可以设置中心点对齐。
- 标注点提示文本可设置背景颜色,透明度、颜色边框和粗细,支持换行和多行文字。
- 所有的图形可以动态更新前景色、颜色粗细、背景颜色、颜色透明度等。
- 支持删除单个图形、删除一种类型的图形、删除所有图形、隐藏单个或者所有图形等。
- 支持动态绘制各种图形,开启后直接在地图上鼠标按下绘制,鼠标右键结束绘制,非常方便快捷。
- 在对应图形区域鼠标按下,发出图形单击信号,精准识别单击区域,比如折线以鼠标在折线条上作为判断依据,多边形区域以鼠标在整个多边形区域内为准,而不是以矩形区域,包括圆形也是以圆形内部为准。
- 可以动态启动禁用比例尺、十字线、缩放控件、地图拖曳、键盘操作、滚轮缩放、双击放大、鼠标追踪等特性。
- 可以任意指定经纬度区域进行瓦片拼接保存成图片文件,也可以直接对整个可视区域或者缓存区域的地图图片文件保存。支持任意多边形轮廓保存成图片,比如某个行政区的瓦片保存。
- 图形可以动态设置zindex层叠顺序,值越大,越显示在前面,内部维护着一个zindex表,默认按照添加的先后顺序增加,后面添加的显示在前面,主动设置后,按照设置的zindex来绘制。
- 支持将QWidget对象作为覆盖物添加到地图控件中,跟随地图移动位置,极大提高灵活性,比如可以将自定义控件直接作为地图控件的子对象加入进去。
- 内置MarkerMove轨迹移动类,支持历史轨迹数据回放和实时轨迹移动,可设置图标、轨迹线的颜色和粗细、移动速度、移动间隔、平滑移动等,支持多条轨迹线条同时移动。
- 内置MarkerLine航迹规划类,支持动态添加航迹点,显示对应箭头,可以动态拖曳调整航迹点的位置,选中点高亮显示。
- 大量使用按需绘制机制,包括内部提供合理的默认值来触发绘制,也可以手动传入参数指定是否需要立即绘制,比如删除了某个覆盖物,有些频繁的操作可以不指定立即绘制,等操作完成后再统一一起绘制,效率更高。
- 默认开启缓存瓦片机制,所有加载过的瓦片文件都存储在内存中,下次再次绘制直接从内存取出来绘制,既不需要从联网获取,也不需要从缓存文件获取,直接内存取出来绘制,响应迅速效率最高体验最佳。
- 支持批量添加覆盖物,比如几万个标注点和圆形,都是瞬间完成绘制,相比web网页的方式,性能提升百倍以上。
- 支持街道图、卫星图、混合图、路网图等各种图层,可以任意叠加N个图层,甚至杂交不同地图厂家的瓦片文件。
- 纯QWidget绘制,非qml也非web,不依赖qml或者浏览器控件,支持极低性能的嵌入式环境。
- 原创轻量级,5000行代码,架构漂亮,注释详细,拓展方便,容易学习,适合各种初学者和进阶者,方便二次开发。
- 支持任意Qt版本、任意系统、任意编译器,包括嵌入式linux和各种国产电脑环境。古法编程,不含任何AI代码,品质保证。
