VS+QGIS+Qt 地图画点:坐标转换与事件处理实战

发布时间:2026/10/11 2:07:29

VS+QGIS+Qt 地图画点:坐标转换与事件处理实战 简介这份资源面向具备一定C基础、希望入门GIS桌面开发的学习者聚焦在Windows 10环境下用Visual Studio、QGIS与Qt实现「地图上画点」这一典型场景。内容围绕环境搭建、项目创建、调用QGIS与Qt接口、加载网络地图、新建图层、经纬度转墨卡托坐标以及标注点绘制等环节展开帮助读者理解C、GIS技术与图形界面如何协同工作。压缩包共3个文件包含2个cpp源文件与1个h头文件整体约3KB源码结构精简便于快速通读核心逻辑。目前已有3621人学习下载适合作为地图标注功能的入门参考也可用于梳理QGIS与Qt整合时的接口调用与坐标转换思路。1. 在 QGIS 和 Qt 之间画一个点为什么你的地图控件总是不响应很多做桌面 GIS 的同行都遇到过这个场景用 VS 建好 Qt 工程把 QGIS 的 Canvas 嵌进窗口地图能加载、能缩放但鼠标点上去就是画不出一个点。代码看着没问题编译也过了运行起来要么没反应要么点一下整个地图闪一下要么点完坐标偏到几百公里外。这个标题——VSQGISQt 实现地图上画点——说的就是这件事在 Windows 上用 Visual Studio 编译 Qt 和 QGIS 的混合工程把 QGIS 的地图画布嵌入 Qt 界面然后响应鼠标事件在正确的地理坐标位置绘制一个点要素。它解决的不是“怎么画一个圆点”这种绘图问题而是坐标转换、事件传递、图层刷新这三件事在 Qt 和 QGIS 两套体系之间怎么对齐。适合已经能跑通 QGIS 二次开发最小工程、但卡在交互绘制这一步的开发者如果你连 QgsMapCanvas 都还没嵌进去建议先把工程骨架搭起来再回来看。2. 环境搭建与最小工程让 QGIS 的 Canvas 在 Qt 窗口里活过来2.1 VSQtQGIS 的工程配置为什么容易翻车这套组合的坑不在代码在环境。VS 管编译器Qt 管界面框架QGIS 管地图渲染三者各自有版本和 ABI 要求。常见做法是用 VS 2019 或 2022 配合 Qt 5.15.x 的 MSVC 版本再配 QGIS 3.x 的二次开发包。注意 QGIS 的二次开发包必须和 Qt 的编译器版本一致Qt 用的是 msvc2019 64bitQGIS 的库也必须是 msvc2019 64bit 编译出来的否则链接阶段就会报一堆 LNK2019。我一般会在系统环境变量里把 QGIS 的 bin 目录加到 PATH 最前面避免运行时找不到 qgis_core.dll 和 qgis_gui.dll。工程属性里需要配三处C/C 附加包含目录指向 QGIS 的 include 目录链接器附加库目录指向 lib 目录附加依赖项里加上 qgis_core.lib 和 qgis_gui.lib。Qt 的模块至少需要 Qt5Core、Qt5Gui、Qt5Widgets如果用到 XML 还要加 Qt5Xml。调试时如果报“无法定位程序输入点”九成是 DLL 版本混了检查 PATH 里有没有旧版 QGIS 的 bin 目录。2.2 初始化 QgsApplication 与嵌入 QgsMapCanvasQGIS 的二次开发必须先用 QgsApplication 替代 QApplication否则图层注册、坐标转换这些底层服务起不来。下面是最小可运行骨架#include QApplication #include qgsapplication.h #include qgsmapcanvas.h #include qgsvectorlayer.h #include qgsproject.h int main(int argc, char *argv[]) { // 必须用 QgsApplicationQApplication 会导致部分 QGIS 服务不可用 QgsApplication app(argc, argv, true); // 设置 QGIS 插件与数据目录指向二次开发包里的对应路径 QgsApplication::setPrefixPath(C:/OSGeo4W/apps/qgis, true); QgsApplication::initQgis(); QgsMapCanvas canvas; canvas.resize(800, 600); // 加载一个矢量图层作为底图这里用 GeoJSON 方便演示 QgsVectorLayer* layer new QgsVectorLayer( C:/data/basemap.geojson, basemap, ogr); if (!layer-isValid()) { return -1; // 图层无效直接退出避免后续空指针 } QgsProject::instance()-addMapLayer(layer); canvas.setLayers(QListQgsMapLayer*() layer); canvas.setExtent(layer-extent()); canvas.show(); return app.exec(); }这段代码的逻辑是先初始化 QGIS 运行时再创建画布加载图层设置图层列表和显示范围。参数上setPrefixPath 的第二个参数 true 表示同时设置插件路径省得后面单独配。setLayers 接收的是 QList顺序决定绘制层级后加的在上层。setExtent 不调用的话画布是空白因为默认范围是无效的。运行起来应该能看到地图但此时鼠标点击还没有任何反应因为事件还没接。2.3 用 .pro 还是 CMake两种构建方式的关键差异VS 里做 Qt 工程可以用 Qt VS Tools 直接建 .pro 工程也可以用 CMake。.pro 的好处是 Qt 的 moc、uic 自动处理QGIS 的库直接写 LIBS 就行。CMake 的好处是跨平台、和 VS 的集成更现代。我一般用 CMake因为 QGIS 官方二次开发包对 CMake 的 find_package 支持更完整。关键片段find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED) find_package(QGIS REQUIRED) add_executable(mapdraw main.cpp) target_link_libraries(mapdraw Qt5::Core Qt5::Gui Qt5::Widgets ${QGIS_LIBRARIES}) target_include_directories(mapdraw PRIVATE ${QGIS_INCLUDE_DIR})注意 QGIS 的 CMake 配置文件在二次开发包的 lib/cmake 目录下需要把该目录加到 CMAKE_PREFIX_PATH。如果 find_package 报找不到手动指定 QGIS_DIR 变量指向那个目录。两种方式没有绝对优劣但团队里统一用一种别混着来否则 DLL 搜索路径会乱。3. 鼠标事件与坐标转换从屏幕像素到地理坐标的那一步3.1 QgsMapCanvas 的事件体系与重写方式QgsMapCanvas 继承自 QGraphicsView鼠标事件默认用于平移和缩放。要画点就得在它上面叠加自己的事件处理。常见做法有两种一是继承 QgsMapCanvas 写一个子类重写 mousePressEvent二是给 canvas 安装事件过滤器。子类方式更干净推荐。class DrawCanvas : public QgsMapCanvas { Q_OBJECT public: explicit DrawCanvas(QWidget* parent nullptr) : QgsMapCanvas(parent) {} protected: void mousePressEvent(QMouseEvent* e) override { if (e-button() Qt::LeftButton) { // 先交给父类处理保留原有的平移缩放逻辑 QgsMapCanvas::mousePressEvent(e); // 把屏幕坐标转成地图坐标 QgsPointXY mapPt mapSettings().screenToMapPoint(e-pos()); emit pointPicked(mapPt); } else { QgsMapCanvas::mousePressEvent(e); } } signals: void pointPicked(QgsPointXY pt); };这里的关键是 screenToMapPoint它内部会考虑当前画布的范围、尺寸、DPI 缩放。注意 e-pos() 在 Qt6 里返回 QPointFQt5 里是 QPoint如果混用会编译不过。emit 一个信号出去让主窗口去处理绘制逻辑这样画布类不依赖具体的图层结构解耦更好。3.2 screenToMapPoint 与坐标偏移为什么点会跑到几百公里外坐标偏移是这套方案里最玄学的问题。现象是点击位置明明在屏幕中央画出来的点却偏到地图角落甚至另一个省。原因通常有三个。第一画布的 DPI 缩放没处理高分屏下 e-pos() 是逻辑像素而 screenToMapPoint 期望的是物理像素两者差一个 devicePixelRatio。第二地图坐标系和图层坐标系不一致比如画布是 EPSG:3857图层是 EPSG:4326直接拿画布坐标去建要素就错了。第三画布范围在点击瞬间正在动画中setExtent 还没生效。解决方式在转换前先确认画布的 destinationCrs 和图层 crs 是否一致不一致就用 QgsCoordinateTransform 转。高分屏下用 e-pos() * devicePixelRatioF() 修正。动画期间可以禁用点击或者用 mapSettings().visibleExtent() 而不是当前 extent。QgsPointXY mapPt mapSettings().screenToMapPoint(e-pos()); QgsCoordinateTransform transform( mapSettings().destinationCrs(), layer-crs(), QgsProject::instance()); QgsPointXY layerPt transform.transform(mapPt);transform 的第三个参数传 QgsProject::instance() 是为了拿到坐标转换上下文缺少它某些动态基准面转换会失败。3.3 构造 QgsFeature 并写入内存图层拿到正确坐标后下一步是建要素。如果只是临时显示用内存图层最快QgsVectorLayer* memLayer new QgsVectorLayer( Point?crsEPSG:4326fieldid:integer, picked, memory); QgsFeature f(memLayer-fields()); f.setGeometry(QgsGeometry::fromPointXY(layerPt)); f.setAttribute(id, 1); memLayer-startEditing(); memLayer-addFeature(f); memLayer-commitChanges(); memLayer-triggerRepaint();内存图层的 URI 里 crs 必须写对field 定义至少给一个字段否则 addFeature 可能静默失败。startEditing 和 commitChanges 成对出现中间 addFeature 返回 bool要检查。triggerRepaint 通知画布重绘不调用的话点不会立刻显示。如果要把点持久化换成 QgsVectorFileWriter 或直接操作数据源但那是另一个话题。4. 避坑与排查画点不显示、坐标错乱、程序崩溃的 5 个真实原因4.1 点画上去了但看不见现象代码没报错图层也加了就是屏幕上没点。原因通常是图层顺序或符号问题。内存图层默认符号是一个很小的圆如果画布范围很大一个点可能只有一像素。解决给图层设置明显的符号或者放大到该点范围。另外检查 canvas.setLayers 是否包含了内存图层很多人只加了底图忘了加绘制层。4.2 点击一次画出多个点现象单击一下地图上出现好几个重叠的点。原因mousePressEvent 被调用了多次可能是事件过滤器重复安装或者子类里既重写了又装了过滤器。解决只保留一种事件接入方式在重写里不要调用 emit 两次。另外检查是否有其他控件把事件透传给了画布。4.3 程序在 addFeature 时崩溃现象点击后程序直接退出无异常提示。原因内存图层的字段定义和 QgsFeature 不匹配或者图层指针为空。解决addFeature 前检查 layer-isValid()QgsFeature 构造时用 layer-fields() 而不是自己手写字段列表。崩溃如果发生在 commitChanges检查是否在 startEditing 之前就调用了 addFeature。4.4 坐标转换结果始终为零现象mapPt 的 x、y 都是 0 或极小值。原因画布的 mapSettings 还没初始化或者 setExtent 没调用导致范围无效。解决确保在鼠标事件触发前画布已经 setLayers 和 setExtent可以在初始化后打印 mapSettings().visibleExtent() 确认范围有效。4.5 高分屏下点击位置偏移现象在 4K 屏上点击位置和实际画点位置差一个固定比例。原因Qt 的高 DPI 缩放和 QGIS 画布的像素计算不一致。解决在 main 里设置 QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)然后用 e-pos() * devicePixelRatioF() 修正坐标。如果还是偏检查 QGIS 的 QgsApplication 是否也开启了高 DPI 支持。5. 进阶技巧把画点做成可编辑、可撤销的交互工具5.1 用 QgsMapTool 替代裸事件重写前面用子类重写 mousePressEvent 能跑通但工程化程度低。QGIS 提供了 QgsMapTool 体系专门用于地图交互工具。继承 QgsMapToolEmitPoint重写 canvasReleaseEvent然后 canvas.setMapTool 激活。好处是工具可以随时切换和 QGIS 自带工具的行为一致而且画布会自动处理工具的生命周期。class PointTool : public QgsMapToolEmitPoint { Q_OBJECT public: PointTool(QgsMapCanvas* c) : QgsMapToolEmitPoint(c) {} void canvasReleaseEvent(QgsMapMouseEvent* e) override { QgsPointXY pt e-mapPoint(); // 已经是地图坐标不用自己转 emit pointAdded(pt); } signals: void pointAdded(QgsPointXY pt); };注意 QgsMapMouseEvent 的 mapPoint() 直接返回地图坐标省去了 screenToMapPoint 那一步也避开了 DPI 的坑。这是比裸重写更稳的做法。5.2 用 QUndoStack 实现撤销重做画点工具一旦上线用户就会要求撤销。Qt 的 QUndoStack 配合 QgsVectorLayer 的编辑缓冲可以做到。每次 addFeature 前记录要素 id撤销时调用 deleteFeature。更规范的做法是封装一个 AddFeatureCommand继承 QUndoCommandredo 里 addFeatureundo 里 deleteFeature。把命令 push 到 QUndoStack界面上绑 CtrlZ 即可。class AddPointCommand : public QUndoCommand { public: AddPointCommand(QgsVectorLayer* l, const QgsFeature f) : mLayer(l), mFeature(f) {} void undo() override { mLayer-deleteFeature(mFeature.id()); mLayer-triggerRepaint(); } void redo() override { mLayer-addFeature(mFeature); mLayer-triggerRepaint(); } private: QgsVectorLayer* mLayer; QgsFeature mFeature; };这里有个细节deleteFeature 后要素 id 会变redo 时不能直接用原来的 id需要重新 addFeature 并更新 id。所以更稳的做法是 undo 时用 deleteFeatures 按 id 删redo 时重新构造要素。这个坑我在实际项目里踩过撤销两次再重做点就丢了。5.3 验证画点是否正确三个自检手段第一打印 mapSettings().destinationCrs().authid() 和图层 crs 的 authid()确认一致。第二在 canvasReleaseEvent 里打印 e-mapPoint() 和 e-originalMapPoint()两者在无旋转时应该相同。第三画完后调用 canvas.zoomToFeatureIds(layer, QgsFeatureIds() f.id())如果能定位到点说明坐标和图层都没问题。这三个手段能覆盖九成的坐标类问题。我自己的习惯是每接一个新的 QGIS 版本先跑一个最小画点 demo确认 screenToMapPoint 和 QgsMapToolEmitPoint 两条路都通再往工程里集成。版本升级时 QGIS 的坐标 API 偶有变动提前验证比事后排查省事得多。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/11 2:02:29

WAND/WOR/WXOR本质是掩码驱动的位级写入指令

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 3:12:35

【单片机课程设计/毕业设计】基于单片机的智能台灯光线人体双感知调光装置设计 基于物联网的智能台灯环境监测与自动调节系统设计(031001)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 3:12:35

【计算机毕业设计单片机案例】基于WIFI的智能台灯光控调光与手机端远程管理系统设计 基于ESP32的智能台灯环境监测与语音控制装置设计(031001)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 3:12:35

【单片机毕业设计】基于ESP32的智能台灯PWM调光与温湿度联动控制装置设计 基于单片机的智能台灯自动光控与闹钟提醒系统设计(031001)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 3:12:35

单片机毕设项目:基于单片机的智能台灯自动调光与环境智能调节系统设计 基于物联网的智能台灯多参数监测与联动控制系统设计(031001)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机,Java、小程序技术领域和毕业项目实战 ✌️…

2026/10/11 3:07:34

MySQL内置函数实战指南:从字符串到窗口函数避开性能坑

干我们这行的,写SQL就像写字一样,MySQL内置函数就是最常用的那套笔画。别小看这几十个函数,用得好,原来要写十几行业务逻辑的查询,一行就能解决;用得不好,线上慢查询一抓一大把,报表…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/11 0:02:13

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/11 0:02:13

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/11 0:02:13

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑