
简介围绕地图上的点标记这一功能这份源码示例展示了使用VS、QGIS与Qt三套工具在Windows 10环境下构建桌面GIS应用的整体思路。代码实现了从网络地图服务获取数据、新建图层、输入经纬度并转换为墨卡托投影坐标、再到画布上绘制点的完整链路。压缩包内共有三个文件即两个C源文件和一个头文件全部代码仅3KB结构精炼没有多余工程配置适合直接阅读或移植目前已有三千六百一十八人学习下载。通过分析源码可以掌握C环境中调用GIS和界面库的方法了解WMS、WMTS或本地矢量数据的加载方式以及坐标转换和图像对象绘制的关键技术源码中的函数划分清晰注释简洁便于定位网络请求、图层创建、坐标换算与图形绘制等核心模块同时也可作为一个基础框架进一步扩展为支持多点标注、图元交互或其他投影转换的完整应用对入门桌面地图开发或准备相关项目设计具有实际参考价值。1. 地图上画点这件事为什么是 VS QGIS Qt 而不是 WebView“vsqgisqt实现地图上画点”这句话差不多是我被问过最多次的桌面 GIS 需求之一。它不是让你打开 QGIS 桌面版手动添点而是要在 Visual Studio 里写 C 程序用 Qt 搭界面把 QGIS 的渲染引擎嵌进来当地图内核让用户在自己的程序窗口里点一下地图就落一个点。这套组合能解决 Web 地图套壳方案最痛的三件事离线与内网部署方便、Shapefile 和 GeoPackage 读写可控、投影和缩放不用自己造轮子。适合做管线巡检标记、外业采点、室内地图标注这类桌面工具的团队。我下面会按环境搭建、画布接入、画点链路、高频踩坑的顺序把整条路线讲透尽量让你照着能复现。2. 环境搭建先解决 Qt 与 QGIS 库的“门当户对”2.1 为什么必须选 MSVC 版 Qt而不是 MinGW 版QGIS 在 Windows 上的官方构建OSGeo4W是用 MSVC 编译的它导出的 qgis_core.lib、qgis_gui.lib 只认同一套 MSVC 运行时。如果你图省事装了 MinGW 版 Qt链接阶段会出现一堆 LNK2019、LNK2001 符号找不到的错误。这是环境搭建里第一个也是最容易让人放弃的翻车点和代码本身没有任何关系。我一般建议直接选 Qt 5.15.2 或 5.12.12 的 msvc2019_64也有 msvc2017_64安装包对应到 Visual Studio 2022 或 2019 都能用。判断标准很简单看 OSGeo4W 安装完以后C:\OSGeo4W\apps 下面带的是 Qt5 还是 Qt6 目录带哪个就用哪个版本的 Qt。如果新版 QGIS 已经切到 Qt6那就老老实实用 Qt 6.x 的 MSVC 包强行混用 Qt5 会在程序启动时直接闪退。安装 Qt 时有个容易被忽略的细节组件列表里要勾上 MSVC 对应的“Qt WebEngine”模块吗不需要。我们只是用 Qt Widgets 和 GUI 基础模块安装时把 Qt 5.15.2 下的 “MSVC 2019 64-bit” 以及 “Additional Libraries” 勾上就够没必要为了一个画点工具拖上整个 WebEngine体积大且容易引入调试符号冲突。2.2 VS2022 Qt VS Tools 的最小配置流程Visual Studio 里开发 Qt 程序的常见做法是装官方扩展 “Qt Visual Studio Tools”装好后 VS 才能识别 .ui 文件和生成 moc 代码。步骤如下安装 VS 时务必勾选“使用 C 的桌面开发”工作负载否则装扩展也编译不了 C 工程。在 VS 菜单栏点“扩展” → “管理扩展”搜索 “Qt Visual Studio Tools”下载安装后重启 VS。菜单栏出现 “Qt VS Tools” 后打开它下面的 “Qt Versions”把 Qt 安装目录添加进去例如 C:\Qt\5.15.2\msvc2019_64。新建项目时搜索 “Qt Widgets Application”VS 会生成带 .ui、main.cpp 的完整骨架。第 3 步最容易被忽略添加 Qt 版本时扩展会去路径下找 bin\qmake.exe 和 bin\qtpaths.exe如果这两个文件缺失版本号会显示 not found后面新建工程也会失败。我遇到过装完 Qt 但没勾选 “Qt Debug Symbols” 导致目录不完整的情况处理办法是重新运行 Qt 安装包把 MSVC 组件补全再添加路径。参数上如果是 VS2022建议 Qt 版本选 5.15.2 的 msvc2019_64它用的是 v142 工具集VS2022 默认能兼容打开如果非要追求最新 Qt6要留意扩展对 Qt6 的 CMake 预设支持还不算太顺手新手别在第一步就给自己上难度。2.3 include / lib / PATH 三件套把 QGIS 库链进工程QGIS 本身是绿色运行库安装 OSGeo4W 时你需要勾选 qgis 以及带 -dev 后缀的开发包才会在 C:\OSGeo4W\apps\qgis 下生成 include 和 lib 目录。之后就是在 VS 工程属性里配置三处C/C → 常规 → 附加包含目录C:\OSGeo4W\apps\qgis\include链接器 → 常规 → 附加库目录C:\OSGeo4W\apps\qgis\lib链接器 → 输入 → 附加依赖项qgis_core.lib;qgis_gui.lib这步做完只是过了编译运行时还需要 DLL。QGIS 的 qgis_core.dll 依赖 GEOS、PROJ、GDAL 这一整套 OSGeo 生态库最省事的做法是调试期间把 C:\OSGeo4W\bin 加到系统 PATH 最前面让程序启动时能找到全部依赖。我不建议一上来就把 DLL 手工拷进 exe 目录因为 QGIS 的库之间存在内部版本关联拷贝漏一个就是运行时玄学崩溃调试期用 PATH打包期再考虑固定目录。提示如果你发现 qgis_core.lib 在 lib 目录里找不到说明 -dev 开发包没装上。回到 OSGeo4W 安装界面补选 qgis-dev 组件不需要重装整个 QGIS。2.4 验证编一个能弹出地图画布的最小程序环境配完先别急着写业务逻辑用一个最小 main.cpp 验证库能不能用#include QApplication #include qgsapplication.h #include qgsmapcanvas.h int main(int argc, char *argv[]) { // 第三个参数 true 表示启用 GUI 插件QGIS 的渲染功能才会初始化 QgsApplication app(argc, argv, true); // 指向 OSGeo4W 里的 QGIS 安装前缀第二个参数 true 会把路径写入 qgis 配置 QgsApplication::setPrefixPath(QStringLiteral(C:/OSGeo4W/apps/qgis), true); // 初始化 QGIS 核心包括图层类型注册、坐标系数据库加载 QgsApplication::initQgis(); // 创建地图画布并显示 QgsMapCanvas canvas; canvas.setCanvasColor(Qt::white); canvas.show(); return app.exec(); }这段代码的逻辑是QgsApplication 继承自 QApplication它额外管理 QGIS 的插件目录、坐标系和渲染后端setPrefixPath 告诉它去哪找 QGIS 的插件和资源initQgis 完成内部注册。值得说明的是第二个参数 true 代表把前缀路径写进 QGIS 全局配置团队多人共用一台开发机时会互相干扰建议改成 false每次启动显式指定路径即可。编译运行如果弹出白色窗口说明 VS、Qt、QGIS 三者的链接关系已经打通可以进入下一步。3. 让地图画布在 Qt 窗口里“活”过来QgsMapCanvas 接入与底图 URL3.1 两种嵌法UI 文件提升 vs 纯代码创建接入画布的第一件事是把 QgsMapCanvas 塞进你的窗口。我用过两种方式各有适用场景第一种是 Qt Designer 界面设计里的“提升法”。在 .ui 文件里放一个普通 QWidget右键“提升为”提升类名填 QgsMapCanvas头文件 qgsmapcanvas.h生成代码后这个控件就变成了地图画布。优点是布局调整直观适合界面控件多的工具缺点是你需要记着在窗口构造函数里把提升后的实例存成成员指针否则后面没法操作画布。第二种是纯代码创建适合界面比较单一的程序// 在窗口构造函数中直接创建并放入布局 m_canvas new QgsMapCanvas(this); // 关闭 QGIS 自动聚焦到选中物的行为画点工具里这个体验更可控 m_canvas-setFocus(); ui-centralLayout-addWidget(m_canvas);我平时偏向第二种因为 QgsMapCanvas 的参数很多比如 setCanvasColor、setRenderFlag、setLayers集中写在构造函数里比散落在 .ui 提升代码里好维护。而且当你需要动态切换底图时纯代码方式改起来不用动 .ui 文件。3.2 初始化 QGIS 运行环境与加载国内可用的底图 URL嵌入画布之后程序里要有一次正式的 QGIS 初始化这不能省// 在 main() 里完成确保 QgsApplication 在整个生命周期内只初始化一次 QgsApplication app(argc, argv, true); QgsApplication::initQgis();回到程序主窗口构造函数里加载底图。国内可用的在线瓦片服务不少踩过一圈后我常用高德路网图作为默认底图速度和稳定性都还行但它的坐标系是 GCJ02后面画点坐标要配套处理#include qgsrasterlayer.h #include qgsproject.h #include qgscrs.h // 高德路网瓦片 URL注意 写成 花括号写成 %7B %7D这是 QGIS 数据源 URI 的转义规则 QgsRasterLayer *base new QgsRasterLayer( typexyzurlhttps://webrd01.is.autonavi.com/appmaptile ?langzh_cnsize1scale1style7 x%7Bx%7Dy%7By%7Dz%7Bz%7D, 高德路网, wms); if (!base-isValid()) { qWarning() 底图加载失败检查 URL 转义; return; } QgsProject::instance()-addMapLayer(base); // 画布使用 Web 墨卡托投影这是瓦片服务默认的切片坐标系 QgsCoordinateReferenceSystem crs; crs.createFromUserInput(EPSG:3857); m_canvas-mapSettings().setDestinationCrs(crs); m_canvas-setExtent(base-extent()); m_canvas-setLayers(QListQgsMapLayer *{base});代码里最关键的是 URL 的转义在 QGIS 的数据源 URI 中参数分隔符 必须写成 坐标占位符 {x} {y} {z} 必须写成 %7Bx%7D %7By%7D %7Bz%7D否则解析器会把 URL 截断成 style7 单独一个参数瓦片请求直接 404。另外 EPSG:3857 是 Web 墨卡托适合在线瓦片如果后面你要在 EPSG:4326 经纬度图层上画点画布 CRS 就得切成 4326这个切换要看具体业务。3.3 画布自带的交互工具与画点模式的切换地图画布默认没有平移缩放能力你需要显式把工具挂上去。QgsMapCanvas 提供了 setMapTool 接口挂 QgsMapToolPan 和 QgsMapToolZoom 是标准做法#include qgsmaptoolpan.h #include qgsmaptoolzoom.h // 浏览模式左键拖拽平移、滚轮缩放 m_canvas-setMapTool(new QgsMapToolPan(m_canvas));但这里有一个新手普遍踩的坑如果后面你用事件过滤器去实现“点一下画一个点”画点模式下会同时触发 QgsMapToolPan 的拖拽逻辑导致鼠标按下瞬间既平移又画点。我一般用处理办法是定义一个 m_drawMode 布尔标志位画点模式下不装 Pan 工具或者更干脆一点不依赖 QgsMapTool而是给画布装事件过滤器#include QMouseEvent #include QEvent // 窗口构造函数里 m_canvas-installEventFilter(this); bool MapCanvasWidget::eventFilter(QObject *watched, QEvent *ev) { if (watched m_canvas ev-type() QEvent::MouseButtonPress) { QMouseEvent *me static_castQMouseEvent *(ev); // 左键画点右键在后面的进阶里用作撤销 if (me-button() Qt::LeftButton m_drawMode) { // 画点核心逻辑下一节展开 return true; // 事件已经被消费不会触发底层工具 } } return QWidget::eventFilter(watched, ev); // 未处理的事件继续走默认流程 }事件过滤器的优势在于浏览模式下 return false事件正常传给平移工具画点模式下 return true左键事件被拦截地图不会乱动。这样就不需要频繁地 setMapTool 去切换工具状态代码量反而更少。注意 QMouseEvent 的 pos() 是相对于画布控件的坐标后续做坐标转换要使用它。4. 画点核心链路屏幕坐标 → 地图坐标 → 要素落盘4.1 先分清三种画点需求再决定落盘策略同样是“地图上画点”背后有三种需求选错了存储策略后期会很被动临时标绘点只是为了当前会话演示程序关掉就丢。这种直接画在 QgsRubberBand 上就行不建图层。会话记忆用户画完点后不马上保存程序退出前可以导出。我一般用内存图层 QgsVectorLayer(memory) 承载保存时再写文件。持久存储点要长期保存可能需要用 GIS 软件打开复核。这部分直接写 Shapefile 或 GeoPackage。绝大多数桌面采点工具对不上第一类需求因为点一旦多起来RubberBand 上只有图形没有属性无法筛选导出。我会默认按第二类做画点时既画 RubberBand 预览又把要素写进内存图层最后统一提供“导出”按钮落盘。这样用户体验和数据结构都不吃亏。4.2 画点前的视觉反馈配置 QgsRubberBand 十字光标地图上画点最怕用户不知道点落在哪尤其底图是卫星影像时。QgsRubberBand 是 QGIS 为画布提供的临时几何带画点前先配一个十字光标#include qgsrubberband.h // 放在窗口初始化里 m_band new QgsRubberBand(m_canvas, QgsWkbTypes::PointGeometry); // 半透明红的十字标记避免遮住底图上的道路和建筑 m_band-setColor(QColor(255, 50, 50, 180)); m_band-setWidth(3); // ICON_CROSS 表示画的是十字线比默认方块更精确 m_band-setIconType(QgsRubberBand::ICON_CROSS); m_band-setIconSize(12); // 十字的显示尺寸按底图比例调整请注意 QgsRubberBand 的构造函数第一个参数是画布指针第二个参数是几何类型PointGeometry 对应点。setWidth 控制线的粗细3 像素在 4K 屏上仍然清晰setIconSize 我调过 8 到 1612 在大部分场景比较折中。如果底图偏灰可以把颜色改成高饱和的青色更容易被看到。4.3 坐标转换toMapCoordinates 与 GCJ02 纠偏这是画点链路里最容易出错的一环。事件过滤器里拿到的是控件坐标像素必须先转成地图坐标。QgsMapCanvas 的 mapSettings() 提供了现成的转换方法// 在 eventFilter 左键分支里 QgsPointXY mapPt m_canvas-mapSettings().toMapCoordinates(me-pos());toMapCoordinates 输入控件坐标输出画布当前 CRS 下的地图坐标。刚才我们把画布 CRS 设成了 EPSG:3857所以 mapPt 是墨卡托米制坐标。如果你之后要把点以经纬度形式存数据库还需要再转一步#include qgscoordinatetransform.h // 从画布 CRS 转到 WGS84 经纬度 QgsCoordinateReferenceSystem srcCrs m_canvas-mapSettings().destinationCrs(); QgsCoordinateReferenceSystem dstCrs; dstCrs.createFromUserInput(EPSG:4326); QgsCoordinateTransform trans(srcCrs, dstCrs, QgsProject::instance()); QgsPointXY lonLat trans.transform(mapPt);如果你的底图是高德瓦片还埋伏着第二个坐标陷阱高德底图本身是 GCJ02 火星坐标而普通 GPS 采出来的是 WGS84两者在大多数城市有几十到几百米的偏差。我自己的做法是加载高德底图时把 WGS84 经纬度做一个近似纠偏再画进图层保证点和底图能对上。那段算法网上能搜到标准版注意别用只做“一次线性平移”的简化版误差还有二三十米。如果你是加载 ArcGIS 世界影像这类 WGS84 底图这步直接跳过。4.4 要素落库从内存图层到 Shapefile 落盘坐标转换完成后把点写进内存图层。我习惯在窗口初始化时就建好图层#include qgsvectorlayer.h #include qgsfeature.h #include qgsgeometry.h #include qgsfield.h // 内存点图层字段 name 用于保存点名称 m_layer new QgsVectorLayer( Point?crsepsg:4326fieldname:string(50), 采集点, memory); // 清理缓存后再添加避免残留几何 m_layer-updateFields();画点时创建一个 QgsFeature设置几何和属性交给 dataProvider 写入QgsFeature feat; feat.setAttribute(name, QStringLiteral(待命名点_%1).arg(m_pointCount)); feat.setGeometry(QgsGeometry::fromPointXY(lonLat)); // lonLat 为 WGS84 坐标 m_layer-dataProvider()-addFeatures(QListQgsFeature() feat); m_layer-updateExtents(); m_canvas-refresh();dataProvider()-addFeatures 对内存图层是即时写入不需要 startEditing/commitChanges 那套事务流程这在交互式画点时更干脆。但要注意如果之后你要支持撤销得把返回值里的 feature id 存下来这个在第 6 章细说。最后一个动作是导出。把内存图层保存成 Shapefile我的写法是走 saveAs 而不是老版 QgsVectorFileWriter因为 QGIS 3.10 以后 saveAs 接口更稳定#include qgsvectorlayersaveasdialog.h QString errMsg; QgsVectorLayer::SaveLayerOptions opts; opts.setDriverName(ESRI Shapefile); // 中文字段名和中文属性值必须指定 UTF-8否则在 ArcGIS 里会乱码 opts.setEncoding(QStringLiteral(UTF-8)); bool ok m_layer-saveAs(D:/work/采集点.shp, opts, errMsg); if (!ok) { qWarning() 保存失败 errMsg; }这里有两个边界坑Shapefile 的字段名最长 10 个字符中文字段名经常触发异常建议 schema 里直接用拼音或数字命名字段另外 saveAs 的第二个参数是 options如果漏了编码配置默认会按系统 ANSI 编码写 DBF到别的机器上属性栏就是一堆问号。5. 避坑手册QGIS 库集成最容易翻车的 5 个场景5.1 启动即崩qt.qpa.plugin: could not find the Qt platform plugin现象程序编译通过双击 exe 后弹窗或控制台直接报类似 could not find the Qt platform plugin windows然后进程退出。有时候在交叉编译场景下报的是 linuxfb道理相同。原因Qt 程序启动时需要加载 platforms/qwindows.dll 这个平台插件它不会自动从 Qt 安装目录里找而是按 exe 所在目录的相对路径推导。最常见的情形是你把 exe 拷到了别的目录或者 VS 的调试工作目录没配置。解决把 Qt 安装目录下 plugins\platforms 整个文件夹复制到 exe 旁边或者代码里在 QApplication 创建前设置环境变量qputenv(QT_QPA_PLATFORM_PLUGIN_PATH, C:/Qt/5.15.2/msvc2019_64/plugins/platforms);我一般调试期用环境变量发布打包时用目录拷贝因为后者对目标机器没有任何先决条件。5.2 崩溃在 QgsApplication::initQgis()DLL 依赖链断裂现象程序跑到了 initQgis() 附近直接崩溃事件查看器里指向 qgis_core.dll 或 gdal.dll 的加载失败。原因PATH 里没加 C:\OSGeo4W\binqgis_core.dll 启动时找不到它的依赖还有一个隐藏原因是你用 Debug 配置编译程序但 QGIS 提供的是 Release 库Debug 版会去找 debug CRT 符号同样崩。解决调试期把 C:\OSGeo4W\bin 放到系统 PATH 的靠前位置VS 里确保解决方案配置是 Release。如果你非要 Debug 调试只能自己用源码编译一套 QGIS Debug 库成本极高我在实际项目里都是日志定位问题没为这个折腾过 Debug 库。5.3 高德底图空白或 404URL 转义与 User-Agent 校验现象底图图层 isValid() 返回 false或者画布上显示为空白在浏览器里打开同样的 URL 却能出图。原因一个是 QGIS 数据源 URI 转义问题 没写成 花括号没转成 %7B另一个是部分瓦片服务会校验浏览器 User-AgentQGIS 的 XYZ 请求 UA 被服务器拒绝。解决先把完整 URL 复制到浏览器里打开如果浏览器能出图说明服务正常问题在转义不能出图则考虑换数据源。高德的 webrd 子域对 UA 校验比较宽松我通常在 QGIS 项目里直接用高德遇到 403 时换成 ArcGIS 世界影像服务它的桌面端兼容性更好。5.4 画的点和高德底图对不上WGS84 与 GCJ02 坐标系偏差现象图层叠加后点落在了道路东侧或南侧几十米底图越放大偏差越明显。原因高德、腾讯这类国内图商为了合规在 WGS84 经纬度上加了非线性偏移也就是 GCJ02 火星坐标。你把 GPS 的 WGS84 坐标直接画上去自然对不齐。解决在写要素前做 WGS84→GCJ02 纠偏算法在 GIS 社区有公开实现就是一段带 double 计算和边界判断的数学函数把它封装成 coordinate_to_gcj02 工具函数即可。纠偏后还剩大约 2 到 3 米的残留误差对采点标注来说完全能接受。5.5 部署到别的机器就闪退DLL 没有随 exe 带走现象开发机一切正常把生成目录复制到一台干净 Windows 机器上双击闪退或者报缺少 DLL。原因开发机 PATH 里有 OSGeo4W\bin程序在开发机上能跑但依赖的 qgis_core.dll、proj_9.dll、geos.dll 等几十个文件一个都没进 exe 目录。解决发布前先跑 windeployqt 把 Qt 相关库打全再把 C:\OSGeo4W\apps\qgis 和 C:\OSGeo4W\bin 里与 QGIS 相关的 DLL 拷进发布目录。我现在项目里用的是 CMake 后处理脚本把 OSGeo4W 的 bin 目录整体同步到输出文件夹虽然粗暴但不会漏。文件体积会到两三百兆这是桌面 GIS 工具的常态用户也接受。6. 画点功能补丁右键撤回、属性输入与落盘自检画点功能能用之后有两处体验补丁我觉得很值得做。第一个是右键撤销用户点错位置是必然的没有撤销就得靠图层编辑工具删实在太难用。实现思路是给 QgsVectorLayer 的 dataProvider()-addFeatures 返回值留个栈每次画点把返回的 feature id 压栈右键时弹出来删除// 成员变量 QStackQgsFeatureId m_addedIds; // 画点时记录 id QListQgsFeature newFeats; if (m_layer-dataProvider()-addFeatures(newFeats, QgsFeatureSink::FastInsert)) { m_addedIds.push(newFeats.first().id()); } // eventFilter 里右键分支 if (me-button() Qt::RightButton !m_addedIds.isEmpty()) { QgsFeatureId fid m_addedIds.pop(); m_layer-dataProvider()-deleteFeatures({fid}); m_canvas-refresh(); }这里用 FastInsert 是为了画点时跳过属性检查提升连续采点的手感右键删除后记得 m_band 也要 reset否则预览十字会残留。第二个补丁是画点时让用户输入属性。很多工具直接画完点什么都不问导出后每个点都是空属性后面无从筛選。我一般会用 QInputDialog 在按下左键后弹一个小输入框#include QInputDialog QString name QInputDialog::getText(this, 点属性, 输入点名称); if (!name.isEmpty()) { feat.setAttribute(name, name); }注意 QInputDialog 是模态的画点时地图会短暂卡住连续采点场景下体验略有牺牲但能保证每个点都有名称值得保留。如果嫌卡可以把输入窗口改成非模态采完一批再一次补改属性。落盘之后的自检也很重要。我现在的习惯是导出 Shapefile 后立刻关掉程序用 QGIS 桌面版重新打开这个 shp 文件检查三件事点位置是否和底图对齐、属性表中文是否乱码、图标是否统一。这个习惯帮我抓出过很多次编码配置漏写的问题。另外一个进阶点是标注显示如果出图时要同时显示点序号和名称QGIS 的图层标注表达式里可以用 format_string 拼出类似分式的多行标注效果不过需要单独研究 QgsPalLayerSettings 的多行格式配置这里先不展开。画点这个功能的坑基本就这些。从环境搭建到落盘自检整体走下来大概一天工期但八成时间会花在 DLL 路径和坐标纠偏上。如果你也遇到和我一样的启动闪退先看 PATH再看坐标系最后才怀疑自己的画点代码这个排查顺序能帮你省下不少时间。希望帮到你。本文还有配套的精品资源点击获取