☰
VS+Qt+QGIS二次开发:C++实现地图画点功能全解析
2026/10/8 14:35:37 网站建设 项目流程

简介:面向C++与GIS开发者的VS+QGIS+Qt地图画点实现资源,演示了在Windows10环境下利用Visual Studio、QGIS与Qt构建地图标注应用的核心流程。资源共3个文件,包含2个C++源文件和1个头文件,压缩包仅3KB,属于轻量级示例代码。其内容涵盖QGIS中加载WMS/WMTS网络地图或本地Shapefile、新建标注图层、将经纬度坐标转换为墨卡托投影坐标,以及利用Qt的QPainter在画布上绘制点等关键环节,完整展示了桌面GIS应用从数据获取到图形渲染的链路。由于涉及C++编程、GIS空间参考系统和GUI交互相结合的细节,对需要快速上手GIS开发的初学者尤为友好。已有3620人浏览/学习,通过阅读源码可以理清VS项目配置、外部库链接、坐标转换算法实现以及QPainter绘图对象的配合方式,为后续自行扩展地图缩放、点选交互等功能提供了可复用的参考范式。

1. 用 VS + QGIS + Qt 在界面上画点:为什么我要绕开 QGIS 桌面端

做过 GIS 二次开发的人多少都撞过这堵墙:项目需要一张离线地图承载业务数据,你第一反应是装 QGIS 桌面端,手动加图层、加点,再把截图塞进报告——但客户要的是“在自研程序里点一下鼠标就落一个点”,这个需求 QGIS 桌面端给不了,你得把 QGIS 的地图渲染能力嵌进 Qt 窗口,用 Visual Studio 把 C++ 工程组织起来。这套“VS + QGIS + Qt”的组合,解决的是桌面端无法完成的可编程地图画点:程序启动时加载底图,鼠标点击画布,经纬度坐标换算成屏幕坐标,在指定图层上落点并立刻刷新显示。它适合三类人:用 Qt 做桌面业务系统却临时要加 GIS 能力的人、想从 PyQGIS 转向原生 C++ 开发的工程师、以及需要把地图渲染和业务逻辑耦合在一个进程里的团队。难点集中在三个地方,环境变量匹配、Qt 版本与 QGIS 构建一致性、画点时要做的坐标变换和图层事务操作,后面我会逐个拆开。

2. 环境搭配与构建准备:为什么 QGIS 的 include 目录总是让你头皮发麻

2.1 版本选型:Desktop 安装包不是只有桌面程序

QGIS 的 Windows 安装包除了 QGIS Desktop,还附带一整套开发库,安装时勾选对应组件,你就能在C:\Program Files\QGIS 3.34下看到include、bin、lib三个目录。QGIS 的 C++ 接口依赖 Qt5,所以你的 Qt 版本必须是 5.15 或相近版本,不能上来就装 Qt6,原因后面坑里细说。Visual Studio 选择 2019 或 2022 都可以,但平台工具集要对应上。比如 QGIS 3.34 官方构建用的就是 MSVC 2019,你用 2022 打开也行,只要不把工具集擅自改成 v143 对应的旧版本就行。

下载时注意区分在线安装器和离线包,离线包更省心:装 QGIS 时把Desktop和OSGeo4W相关的全部组件点开,因为开发库头文件和动态链接库分散在好几个子组件里。装完检查一件事——C:\Program Files\QGIS 3.34\bin\qgis.dll在不在,这个文件不出现,后面 CMake 配置必挂。

2.2 用 CMake 把三项粘在一起:一个能用的栗子

Visual Studio 打开 Qt 工程通常用 Qt VS Tools 插件,但 QGIS 库的链接信息用 qmake 管理很折腾,我用 CMake 做胶水层。以下是一个最小 CMakeLists,能同时找到 Qt5 和 QGIS:

cmake_minimum_required(VERSION 3.16) project(MapDotDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_PREFIX_PATH "C:/Qt/Qt5.15.2/msvc2019_64;C:/Program Files/QGIS 3.34") find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(QGIS REQUIRED) add_executable(MapDotDemo main.cpp mapwidget.cpp mapwidget.h) target_link_libraries(MapDotDemo PRIVATE Qt5::Widgets ${QGIS_LIBRARIES} ) target_include_directories(MapDotDemo PRIVATE ${QGIS_INCLUDE_DIRS})

逻辑说明有三层。第一,CMAKE_PREFIX_PATH同时给了 Qt 和 QGIS 的根目录,find_package(QGIS REQUIRED)才会读取 QGIS 自带的QGISConfig.cmake,这个文件在 QGIS 安装目录的cmake子目录下。第二,${QGIS_LIBRARIES}变量会把qgis_core、qgis_gui、qgis_analysis等库全带进来,如果你的工程只用画点功能,后续可以裁剪。第三,CMAKE_AUTOMOC ON必须开,因为 QGIS 的头文件大量包含Q_OBJECT宏,moc 跑不出来编译直接报错。

参数说明:CMAKE_PREFIX_PATH的顺序有讲究,Qt 在前 QGIS 在后,因为 QGIS 的 cmake 文件可能反向引用 Qt 的配置,先找到 Qt 能减少找不到Qt5Config.cmake的概率。如果本机装了多个 Qt 版本,路径一定要精确到msvc2019_64这种带编译器和位数描述的目录。

2.3 三种链接方式的取舍:静态库、动态库和官方开发包

QGIS 官方不提供静态库,只给 DLL 动态链接,所以别指望把整个 QGIS 塞进单个 exe。常见做法有两种。一是直接用官方 OSGeo4W 安装目录下的库,程序运行时把C:\Program Files\QGIS 3.34\bin加入PATH;二是用 vcpkg 编译一套 QGIS 源码,这适合需要定制底层插件的情况,但耗时长,而且 vcpkg 的 QGIS 版本经常落后官方一两个迭代,遇到 bug 不好查。我一般建议前者,理由很简单:官方 bin 目录里的依赖都是配套的,DLL 之间不会打架。画点这个需求非常基础,没必要从源码重造轮子。

3. 在 VS 里构建 QGIS 地图画点的最小工程:三屏联动逻辑拆解

3.1 建立地图画布:QgsMapCanvas 与图层面板

QGIS 的 C++ API 里,画地图只用两个核心类:地图画布QgsMapCanvas和图层树QgsLayerTreeWidget。画布负责渲染,图层树负责管理图层可见性。下面是在 Qt Widget 界面里初始化这两个对象的代码,常用于构造函数里:

#include <QgsMapCanvas.h> #include <QgsLayerTreeWidget.h> #include <QgsMapLayerProxyModel.h> #include <QVBoxLayout> #include <QgsProject.h> // 构造函数中创建画布与图层树 QgsMapCanvas *canvas = new QgsMapCanvas(this); canvas->setCanvasColor(QColor(240, 240, 240)); canvas->setCrs(QgsCoordinateReferenceSystem::fromEpsgId(3857)); canvas->setDestinationCrs(QgsCoordinateReferenceSystem::fromEpsgId(3857)); canvas->setLayers(QList<QgsMapLayer*>()); QgsLayerTreeWidget *treeWidget = new QgsLayerTreeWidget(this); treeWidget->setCanvas(canvas); treeWidget->setModel(new QgsLayerTreeModel(QgsProject::instance()->layerTreeRoot(), this)); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(treeWidget); layout->addWidget(canvas);

逻辑说明分四点。第一,setDestinationCrs决定画布用什么坐标系渲染,这里用 EPSG:3857 是 Web 墨卡托投影,适合全球范围显示,如果你只做某城市的点,改成 EPSG:4547(国家 2000 高斯投影)会更精确。第二,setLayers传空列表是暂不加载任何图层,底图和矢量图层稍后手动添加。第三,QgsLayerTreeModel的构造函数需要 QGIS 全局工程对象QgsProject::instance()的图层树根节点,这是 QGIS 统一管理图层的入口。第四,treeWidget->setCanvas(canvas)内部会建立画布和图层的联动关系,缩放时图层树复选框状态会同步刷新。

参数说明涉及两个坐标系。3857 的优点是全球一套坐标,做 GPS 轨迹、路网等跨区域数据不串位;缺点是面积变形严重,不适合精确量算。4547 这类投影坐标系在市政、测绘行业更常见,但 EPSG 代码跟城市强相关,参数写错了图层会跑到非洲去。初始化时最好读配置文件统一设置,运行中不要随意切换投影。

3.2 鼠标画点:把点击变坐标再变要素

想在鼠标点下的位置落一个点,最核心的是搞清楚三个坐标系的换算:屏幕坐标、地图坐标、要素坐标。QGIS 画布给了transformFromCanvas接口做变换。下面是一个鼠标点击事件处理函数,截获点击位置的经纬度并落点:

#include <QgsMapMouseEvent.h> #include <QgsVectorLayer.h> #include <QgsGeometry.h> #include <QgsFeature.h> #include <QgsPointXY.h> #include <QgsProject.h> #include <QgsRubberBand.h> void MapWidget::mousePressEvent(QMouseEvent *event) { // 只处理左键点击 if (event->button() == Qt::LeftButton) { // 将屏幕点击位置转换为地图坐标(当前画布坐标系) QgsPointXY mapPoint = m_canvas->mapSettings().toMapCoordinates(event->pos()); // 如果底图坐标是3857,想存成WGS84经纬度需要反算 QgsCoordinateReferenceSystem destCrs = QgsCoordinateReferenceSystem::fromEpsgId(4326); QgsCoordinateTransform transform(m_canvas->mapSettings().destinationCrs(), destCrs); QgsPointXY wgsPoint = transform.transform(mapPoint); // 添加到临时点位图层 addPointToLayer(wgsPoint); // 在画布上画一个临时高亮点 QgsRubberBand *band = new QgsRubberBand(m_canvas, QgsWkbTypes::PointGeometry); band->setColor(QColor(255, 0, 0, 150)); band->setIconSize(8); band->setWidth(3); band->addPoint(mapPoint); band->show(); } QWidget::mousePressEvent(event); }

逻辑说明有三层。第一,toMapCoordinates是画布的逆变换,把界面上的像素坐标换成当前画布坐标系下的地图坐标,这个坐标单位依据setDestinationCrs而定。第二,QgsCoordinateTransform负责投影转换,常见的是把 3857 的 Web 墨卡托转成 4326 的经纬度,这样才能存进 PostGIS 或 Shapefile,这条转换链是踩坑重灾区,坐标系不一致会导致点偏移几十公里。第三,QgsRubberBand是临时闪烁画刷,用来反馈用户“点已生效”,它不是正式图层数据,关闭程序即消失。

参数说明:iconSize控制点图标像素大小,width控制外框粗细,setColor的第四个参数是透明度。这些参数纯粹是视觉反馈用的,业务数据落库不受它们影响。如果项目要求点必须落在地图实际数据里,就不要用 RubberBand,直接跳过这一步走下面的图层写操作。

3.3 写进矢量图层:要素的创建与事务提交

上面只是把点画到了屏幕上,要真正让 QGIS 认这个点,需要创建内存矢量图层并提交要素。下面代码展示如何建立一个带 GSSJ 等字段的内存图层并做数据写入:

#include <QgsVectorLayer.h> #include <QgsFeature.h> #include <QgsField.h> #include <QgsGeometry.h> #include <QgsPointXY.h> QgsVectorLayer* createMemoryPointLayer() { QgsVectorLayer *layer = new QgsVectorLayer( QStringLiteral("Point?crs=epsg:4326&field=name:string(50)&field=time:string(30)&field=remark:string(200)"), QStringLiteral("业务标注点"), QStringLiteral("memory")); // 必须将内存图层添加到工程,否则后续无法管理 QgsProject::instance()->addMapLayer(layer); return layer; } bool addPointToLayer(QgsPointXY wgsPoint, const QString &name, const QString &time, const QString &remark) { QgsVectorLayer *layer = m_pointLayer; // 假设 m_pointLayer 一开始用 createMemoryPointLayer() 创建 if (!layer || !layer->isValid()) return false; QgsFeature feat; feat.setGeometry(QgsGeometry::fromPointXY(wgsPoint)); feat.setAttribute(QStringLiteral("name"), name); feat.setAttribute(QStringLiteral("time"), time); feat.setAttribute(QStringLiteral("remark"), remark); QgsVectorLayerEditBuffer *buffer = layer->getEditBuffer(); if (!buffer) { // 未进入编辑状态时,最好用 dataProvider 直接添加要素 QgsFeatureList feats; feats.append(feat); return layer->dataProvider()->addFeatures(feats); } QgsFeatureList feats; feats.append(feat); bool ok = buffer->addFeatures(feats); if (ok) layer->commitChanges(); // 提交并刷新 return ok; }

逻辑说明要点包括:内存图层的 uri 字符串里用crs=epsg:4326指明坐标系,字段定义紧跟其后。m_pointLayer是类成员变量,在初始化时提前创建。假如图层处于编辑状态,要用getEditBuffer()->addFeatures走事务机制,没进编辑状态就直接dataProvider()->addFeatures,两种路径不要混用。提交成功后调用画布刷新m_canvas->refresh(),点就显示出来了。

参数说明里值得强调的是 uri 字符串。Point?crs=epsg:4326&field=name:string(50)这段不是瞎写的,Point代表几何类型,crs=定义坐标系,多个field=拼字段名和类型,中间用&分隔。字段类型关键词必须是 QGIS 支持的骨架类型:string、int、double、date。如果你在 uri 里写了datetime这种类型,图层可能创建失败,isValid()会返回 false。这类问题排查思路很简单——在 QGIS 桌面端同样字符串创建一次就知道对不对。

3.4 点和底图叠加:Z 序与图层顺序的影响

画布上存在多个图层时,顺序决定谁压谁。QgsProject::instance()->layerTreeRoot()->findLayer(layer->id())可以拿到树节点,调用node->setItemVisibilityChecked(true)控制可见性即可。通常顺序是底图光栅在最底层,矢量点位在上面。如果你想让点盖住一切,把点图层往坐标系最末挪;想让底图的某个建筑边界显示在点上方,就把底图调整到点图层的上方——但这样做在视觉上是点被遮住,业务上一般不这么干。我建议开两个图层,一个放业务点另一个放辅助注记,注记在图斑上层。

4. 画点数据的坐标与渲染避坑:5 个让 QGIS 程序瞬间翻车的细节

4.1 坑 1:3857 坐标被当经纬度直接用

现象:画布上点击后,点落在离鼠标十万八千里的位置,或者干脆跑出地球。原因:toMapCoordinates返回的是当前画布坐标系的坐标,如果画布设过setCrs(EPSG:3857),拿到的就是米制坐标,你却把它当成 4326 经纬度传给了fromPointXY。QGIS 不会帮你自动纠错,因为两个数字都在合法范围内,错得毫无提示。解决:在 transfrom 里总是显式从m_canvas->mapSettings().destinationCrs()转到EPSG:4326,宁可多一步也别默认坐标系一致。

4.2 坑 2:Qt6 与 QGIS 库彻底不兼容

现象:程序编译过了,启动时疯狂报无法解析的外部符号或者直接内存崩溃。原因:QGIS 3.34 的官方 Windows 构建依赖 Qt5,它的动态库找的是 Qt5 的符号表,你用 Qt6 的 MSVC 库链接,完整版本匹配度对不上。解决:老老实实装 5.15.2 或 Qt 5.12,并把 Qt 安装路径跟 QGIS 的bin目录同时加入系统PATH。这一点在工程文档里写清楚,防止团队成员上来就装最新的 Qt6 卡一天。

4.3 坑 3:运行时找不到 qgis_core.dll

现象:Visual Studio 的调试器能启动,但一旦脱离开发环境就提示缺少 DLL。原因:QGIS 的库路径没有进PATH,或者 copy 到 exe 目录的文件不全。解决:写一个startup.bat,脚本里先set PATH=C:\Program Files\QGIS 3.34\bin;%PATH%再启动 exe,这是最省事的方式。发布时可以直接打一个压缩包,让目标机器解压后执行批处理启动,比配置环境变量省掉一轮喊叫。

4.4 坑 4:图层创建成功但画布不显示点

现象:addFeatures返回成功,但画布上没有新点。原因:内存图层默认不在工程树里,画布的setLayers列表没刷新,或者图层没调用triggerRepaint()。解决:添加要素后执行三步,layer->updateExtents()、m_canvas->setExtent(layer->extent())、m_canvas->refresh()。三步缺一不可,尤其updateExtents,数据范围变了画布不知道。

4.5 坑 5:坐标转换在高 DPI 屏幕下偏移

现象:同样的点击在 4K 屏上位置整体右移几个像素,放大缩小时误差变大。原因:Qt 的高 DPI 缩放属性和 QGIS 的像素到地图坐标换算之间没有对齐devicePixelRatio。解决:在 main.cpp 里QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)放在创建窗口之前,并检查m_canvas->mapSettings().devicePixelRatio()返回值是否和屏幕一致。不过这类问题多出现在 175% 缩放的 Windows 系统,100% 缩放下几乎看不到。

5. 进阶:让画点不止是画点——坐标解析、属性绑定与增量刷新

做到上面一步,程序已经像个雏形了,但离“好用的地图标注工具”还有三个进阶点要补:一是点数据的外业来源不一定是鼠标点击,可能是 GPS 实时输出或者文本文件里的经纬度对;二是点必须绑定业务属性,否则就是个孤立坐标;三是大量点连续写入时刷新逻辑必须优化。

5.1 批量导入与逐点画线:从 CSV 到图层的管道

外业数据通常是经度,纬度,名称,时间格式的 CSV,文件读取和点位入库可以用一个函数解决:

bool loadCsvToLayer(const QString &csvPath, QgsVectorLayer *layer) { QFile file(csvPath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false; QTextStream stream(&file); stream.setCodec("UTF-8"); QString header = stream.readLine(); // 跳过表头 QgsFeatureList feats; while (!stream.atEnd()) { QString line = stream.readLine(); QStringList parts = line.split(','); if (parts.size() < 2) continue; bool okLon = false, okLat = false; double lon = parts[0].toDouble(&okLon); double lat = parts[1].toDouble(&okLat); if (!okLon || !okLat) continue; QgsFeature feat; feat.setGeometry(QgsGeometry::fromPointXY(QgsPointXY(lon, lat))); feat.setAttribute(QStringLiteral("name"), parts[2]); feat.setAttribute(QStringLiteral("time"), parts[3]); feats.append(feat); } file.close(); return layer->dataProvider()->addFeatures(feats); }

这段代码的重点在于:一次性把要素放进QgsFeatureList再调用addFeatures,比循环每条addFeature快得多。GPS 数据的坐标绝大多数是 WGS84 经纬度,直接用 4326 存;如果 CSV 里的数据是平面 XY 米制坐标,要先明确是哪套投影,再在导入时同步转成经纬度,否则点和底图错位。

5.2 点属性编辑:双击改点的表单元交互

画点不是终点,用户会想选中点看属性甚至改属性。QgsMapCanvas 提供setSelectionMode,结合 QgsIdentifyResult 可以做点选。设计思路是:右键点击画布时,用QgsMapToolIdentify识别最近的点要素,把属性表展示在右侧 QTableView 里。这里要提醒的是,内存图层的属性编辑不需要手动保存,因为数据在内存里;但如果是 Shapefile 文件图层,changeAttributeValue之后必须commitChanges。如果你把点位存到 PostGIS 里,还涉及事务回滚的问题,编辑中途异常就rollBack。

5.3 增量刷新替代全图重绘

连续画几百个点时,每次canvas->refresh()会让画面闪烁并短暂掉帧。QGIS 有QgsMapCanvas::setCacheMode和局部重绘机制,但最简单有效的方式是把画布设置成“手动刷新模式”,大批量添加完要素后统一刷新一次。代码上可以这样做:维护一个计数器,每次画点只加要素不刷新,当计数器超过 50 或一轮操作完成后再refresh。这个技巧在配着QgsRubberBand画临时预览点时尤其有用——临时线跟业务点分开刷,视觉上流畅很多。

5.4 性能观察:什么时候该换数据库

内存图层在几千个点范围内毫无压力,但到了十万级,QGIS 内存图层的选择会变慢。常见做法是在图层初始化时设置QgsVectorLayer的数据源为 Spatialite 文件,这样 QGIS 内部自动走空间索引,点选和范围查询都舒服。再往后,PostGIS + 空间索引是标准方案,但程序要多维护一套数据库连接状态,启动时要判断网络。画点工具在早期阶段用内存图层合理,它的边界是 2 万点以内的交互流畅,超过这个量级再考虑切换数据源——不要一开始就上重武器,后面维护成本高。

6. 验证画点成果的一把尺子:用日志和自动化校验保证数据正确

给你的程序写两个自检点,一个用于开发验证,一个用于交付验收。第一,程序里加一个QgsMessageLog::logMessage,每次添加点要素时记录坐标、字段值和耗时,输出到警告级别的日志面板。开发时开着日志对照地图点击,能看到是否发生了坐标变换错误——如果日志里的经纬度和鼠标视觉位置差得远,就是第 4.1 节的坑。第二,可以做一个静默的自检函数,遍历图层所有要素,验证每条要素的几何类型是 Point 而不是 MultiPoint,同时检查经纬度范围是否在 -180 到 180、-90 到 90 内。这个自检在加载历史点数据时特别关键,能提前发现脏数据而不是等到展示时让用户背锅。

void validateLayer(QgsVectorLayer *layer) { QgsFeatureIterator it = layer->getFeatures(); QgsFeature feat; int badCount = 0; while (it.nextFeature(feat)) { QgsGeometry geom = feat.geometry(); if (geom.isNull() || geom.type() != QgsWkbTypes::PointGeometry) { badCount++; qWarning() << "Feature id" << feat.id() << "has null or non-point geometry"; continue; } QgsPointXY pt = geom.asPoint(); if (pt.x() < -180 || pt.x() > 180 || pt.y() < -90 || pt.y() > 90) { badCount++; qWarning() << "Feature id" << feat.id() << "out of range:" << pt.x() << pt.y(); } } qInfo() << "Validation complete, bad count:" << badCount; }

这段自检代码写在一个validateLayer函数里,运行在加载数据之后、图层显示之前。它不用 QGIS 的依赖也能运行,你可以在应用的“数据体检”菜单里触发。实际交付时我习惯把它做成启动项,自动跑一遍并且把坏数据导出成 CSV 让用户核对,这比用户在界面上肉眼挑毛病省心得多。

写点工具的这几年,我最大的教训是“坐标问题永远是第一优先排查项”,几乎百分之八十的翻车都不是代码逻辑问题,而是坐标系没对上。另一个习惯是每次改动坐标系或数据源配置后,强制自己用 QGIS 桌面端打开同一个数据源对比一次渲染结果,桌面端对就是程序对,桌面端错就是参数错。画点这件事本身不难,难的是你永远不知道用户的电脑里装了什么版本的依赖,所以发布包里带上启动脚本和依赖清单的文档,能替用户省下无数深夜崩溃。希望这些踩过的坑和验证方法能帮到你,让你的地图画点功能一次性顺利落地。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询