QGIS C++插件开发实战:从环境搭建到自定义地图工具部署
2026/9/20 23:42:01 网站建设 项目流程

简介:面向QGIS二次开发入门与进阶者,这份资源以QGIS 3.28和VS2017为编程环境,聚焦地图工具类的创建与使用。地图工具是鼠标键盘与画布交互的核心接口,通过继承地图工具基类并重写虚函数,可以实现平移、绘制、要素识别等交互功能。资源用实际代码演示了三个典型工具:平移地图工具用于拖动地图;单击取点工具在鼠标点击时发出坐标点,并能连接信号自定义响应;要素识别工具用于识别所选图层上的要素,用户单击地图即可获得该区域的特征。包内含45个文件,以源文件、头文件、工程配置、界面文件、资源文件、可执行程序以及编译日志为主,压缩包约48.84MB,可直接打开工程对照学习。从工程结构到关键接口调用均有清晰展示,便于理解信号槽连接方式与虚函数重写流程,可在此基础上扩展自己的地图工具;已有1566人学习下载,适合希望借助完整示例快速上手二次开发与自定义工具实现的读者。 如果你已经开始在项目里使用QGIS处理数据,早晚会遇到一个坎:坐标采集、要素绘制、地图量算这些高频操作,光靠QGIS自带工具总觉得隔了一层。我自己在项目里就是被逼着从"用QGIS"转向"改QGIS",在QGIS 3.28 LTR版本上用Visual Studio 2017搭了一套C++插件开发环境,把业务需要的自定义地图工具直接嵌进了QGIS主界面。这篇文章就把这套环境的搭建逻辑、地图工具类的实现方式,以及编译部署阶段容易踩的坑完整梳理一遍,给准备入坑QGIS C++二次开发的人做个参考。

1. 为什么用C++写插件,而不是直接用PyQGIS

很多人听到"QGIS二次开发"第一反应是Python。确实,PyQGIS写起来快,加载即生效,不用编译,做数据处理脚本非常顺手。但我在实际项目中最终选择了C++插件,原因很现实。

第一,性能差别在复杂交互场景下很明显。地图工具这种需要高频响应鼠标事件的功能,Python的回调会有可见的延迟,尤其是面对几万个点的矢量层时,每次移动、点击都要做空间计算,C++的QgsMapTool明显更跟手。第二,C++插件可以直接复用QGIS内部的C++ API,比如QgsVectorLayer、QgsFeature、QgsGeometry这些核心类,不用通过SIP绑定绕一层,很多底层能力在PyQGIS里根本没有暴露出来。第三,企业级项目通常要求把功能打包成一个dll分发给其他同事使用,C++插件部署就是一个文件拷贝,不要求目标机器装Python环境。

当然,代价也很直接:编译环境配置复杂、开发周期长、QGIS版本升级时可能需要重新编译适配。所以我的个人建议是,脚本级工具用PyQGIS,要做成正式功能模块、需要深度操作画布交互的,用C++插件。QGIS 3.28是目前最新的LTR(长期支持)版本,API稳定,配套的SDK和依赖也相对固定,拿它作为开发基线是合适的。

2. 环境搭建:QGIS 3.28、VS2017、Qt和CMake的匹配关系

2.1 开发包选型:OSGeo4W SDK是关键

要开发QGIS C++插件,头文件和库文件是必不可少的。最常见的方式是安装OSGeo4W环境时勾选qgis-devel相关组件。这里有个容易混淆的点:你日常用的是独立安装版的QGIS(qgis.org的安装包),但做二次开发最好用OSGeo4W来管理,因为它会把qgis_core、qgis_gui的导入库、头文件和一堆依赖统一放在一个目录下,CMake配置时直接指向这个目录就行。

我的开发机上用的是OSGeo4W 64位版本,选择的包包含qgis、qgis-devel、qt5-devel、cmake。安装完之后,关键路径一般是:

  • QGIS头文件:C:\OSGeo4W\apps\qgis\include
  • 导入库:C:\OSGeo4W\apps\qgis\lib
  • 运行库:C:\OSGeo4W\bin
  • Qt相关:C:\OSGeo4W\apps\Qt5

建议在系统环境变量里加上C:\OSGeo4W\bin,后面调试插件的时候,QGIS才能找到所有依赖dll。

2.2 Qt版本选择与VS2017工具链

QGIS 3.28是基于Qt 5.15.2构建的,我们的插件必须使用同一版本的Qt,否则运行时会有符号冲突或崩溃。Qt 5.15.2官方提供了msvc2017_64和msvc2019_64两套预编译包,VS2017对应的是msvc2017_64,直接用这一套编译插件最安全。

这里要强调一点:QGIS 3.28官方编译用的是MSVC2019/2022工具链,但VS2015、VS2017、VS2019、VS2022这四者的C++运行时是二进制兼容的,因为微软从VS2015起统一了C++运行时库。所以用VS2017配合msvc2017_64的Qt去链接OSGeo4W里msvc2019编译的QGIS库,理论上可以工作,实际我也验证过,能正常加载运行。但如果你遇到奇奇怪怪的内存错误或崩溃,优先检查是不是工具链版本混用导致的,能统一就统一。

另外,VS2017安装时务必勾选"使用C++的桌面开发"工作负载,里面的Windows SDK和MSVC v141编译器都是必须的。CMake方面,OSGeo4W里自带的CMake版本足够用,也可以装一个独立的CMake GUI,方便观察配置选项。

2.3 QGIS插件项目的基本工程结构

一个最小的QGIS C++插件,文件结构大致是这样的:

PointPickPlugin/ ├── CMakeLists.txt ├── pointpickplugin.h ├── pointpickplugin.cpp ├── pointpicktool.h ├── pointpicktool.cpp └── resources.qrc

其中pointpicktool是我们要重点实现的地图工具类,pointpickplugin是插件入口类,负责把工具挂到QGIS界面上。先把这个工程跑起来编译通过,再去填充地图工具的业务逻辑,是效率和心态都最稳的做法。

3. 从零实现一个地图工具:继承QgsMapTool的完整套路

3.1 地图工具的本质

QGIS画布上所有的鼠标交互,本质上都是QgsMapTool的子类在响应事件。内置的"识别要素""测量距离""选择要素"这些工具,清一色继承自QgsMapTool。所以创建自己的地图工具,核心工作就是继承它、重写事件处理方法、然后把它设置为画布当前工具。

我的PointPickTool头文件长这样:

#ifndef POINTPICKTOOL_H #define POINTPICKTOOL_H #include "qgsmaptool.h" #include "qgsmaptoolidentify.h" // 仅用于参考,可不包含 class QgsMapCanvas; class QgsMapMouseEvent; class PointPickTool : public QgsMapTool { Q_OBJECT public: explicit PointPickTool(QgsMapCanvas* canvas); ~PointPickTool() override; void canvasPressEvent(QgsMapMouseEvent* e) override; void canvasMoveEvent(QgsMapMouseEvent* e) override; void canvasReleaseEvent(QgsMapMouseEvent* e) override; void activate() override; void deactivate() override; signals: void pointPicked(const QgsPointXY& pt); private: bool mIsPicking; }; #endif // POINTPICKTOOL_H

构造函数里的第一件事是绑定画布指针,后面所有事件都从这个画布上拿坐标和图层信息。mIsPicking标记当前是否处于拾取状态,防止误触。

3.2 事件响应与坐标转换

QgsMapMouseEvent里已经帮我们做好了屏幕坐标到地图坐标的转换,直接调用e->mapPoint()得到的就是当前投影参照系下的地图坐标。这个转换是很多人容易卡住的地方,纠结半天屏幕坐标和世界坐标怎么换算,其实QGIS在事件分发前就处理完了。

void PointPickTool::canvasPressEvent(QgsMapMouseEvent* e) { if (e->button() != Qt::LeftButton) return; mIsPicking = true; QgsPointXY mapPoint = e->mapPoint(); // 如果打开了捕捉,用捕捉后的点更准确 QgsPointXY snappedPoint = e->snapPoint(); emit pointPicked(snappedPoint); // 也可以直接在这里写业务逻辑,比如新增一个点要素 }

需要注意e->snapPoint()依赖画布当前的捕捉设置,如果没开捕捉,它的返回值就是普通鼠标位置。实际项目里,我用它来做要素节点采集,比直接取鼠标位置精准得多。

activate()deactivate()这两个方法特别容易被忽略。地图工具被激活和释放的时候,负责切换光标、清空临时状态:

void PointPickTool::activate() { QgsMapTool::activate(); mIsPicking = false; } void PointPickTool::deactivate() { QgsMapTool::deactivate(); mIsPicking = false; }

如果工具里有橡皮筋或者临时标注,一定要在deactivate里清掉,不然工具切走后画布上还残留着一堆图形,体验非常糟糕。

3.3 把工具挂到画布上

工具类写好了,要让它在界面上起作用,还需要在插件里实例化并设置到画布。插件类里的核心代码:

PointPickPlugin::PointPickPlugin(QgisInterface* iface) : mIface(iface) { mCanvas = iface->mapCanvas(); mTool = new PointPickTool(mCanvas); } void PointPickPlugin::initGui() { mAction = new QAction(tr("拾取坐标"), this); QIcon icon = QIcon(QStringLiteral(":/icons/point.svg")); mAction->setIcon(icon); mAction->setCheckable(true); connect(mAction, &QAction::triggered, this, &PointPickPlugin::activateTool); mIface->addToolBarIcon(mAction); mIface->addPluginToMenu(tr("自定义工具"), mAction); } void PointPickPlugin::activateTool() { mIface->mapCanvas()->setMapTool(mTool); mAction->setChecked(true); }

这里有个关键点:QActioncheckable必须设置为true,同时监听triggered信号去切换工具。否则在地图工具激活时,工具栏按钮不会保持"按下"状态。QGIS内部会在工具被外部切换时自动刷新工具栏按钮状态,但我们自己代码里最好也在activate()里把按钮checked状态同步一下,保持界面一致。

4. 插件编译与部署:CMake配置、加载机制和调试方法

4.1 CMakeLists.txt 关键配置

QGIS插件的CMakeLists.txt和普通Qt程序差不多,但有几个参数必须写对。下面是一个能直接编译的模板:

cmake_minimum_required(VERSION 3.1) project(PointPickPlugin) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) # 引入Qt find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) # 引入QGIS,这里依赖环境变量 QGIS_INCLUDE_DIR 或 qgis-config.cmake 所在路径 find_package(QGIS REQUIRED) include_directories(${QGIS_INCLUDE_DIRS}) add_library(PointPickPlugin MODULE pointpickplugin.cpp pointpickplugin.h pointpicktool.cpp pointpicktool.h ) target_link_libraries(PointPickPlugin ${QGIS_CORE_LIBRARY} ${QGIS_GUI_LIBRARY} Qt5::Core Qt5::Gui Qt5::Widgets )

重点说明两处:add_library用的是MODULE,意思是编译成动态插件而不是普通共享库,生成的dll不产生配套的导入库。QGIS_CORE_LIBRARYQGIS_GUI_LIBRARY是QGIS提供的CMake变量,分别对应qgis_core和qgis_gui库。OSGeo4W SDK在安装时已经把这些变量的配置文件放好了,CMake能自动找到。

用CMake GUI配置时,指定源码目录后,find_package(QGIS REQUIRED)会自动去C:\OSGeo4W\apps\qgis\cmake下找配置。如果你的SDK装在其他盘,可能需要手动添加QGIS_DIR这个CMake变量指向该路径。

4.2 部署到QGIS插件目录

编译成功后,把生成的PointPickPlugin.dll复制到QGIS的插件目录。OSGeo4W环境下路径是:

C:\Users\<用户名>\AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins

但这是Python插件的目录。C++插件的扫描路径不太一样,需要通过环境变量指定。最省事的方式是在系统环境变量里添加:

QGIS_PLUGINPATH=C:\OSGeo4W\apps\qgis\plugins\custom

然后把dll放到这个custom目录下。插件管理器里勾选"自定义"分类时,它才会扫描到这个目录。还有一个比较隐蔽的问题是:QGIS插件加载后,dll会被锁定,想重新编译覆盖dll会提示"文件被占用"。解决办法是把QGIS进程退出后再覆盖,或者用任务管理器确认qgis-bin.exe确实结束了。

4.3 用VS2017直接调试插件

VS2017调试QGIS插件,我试过最实用的是"附加到进程"的方式。在VS2017里配置项目调试属性:

  1. 调试 -> 命令:填QGIS主程序绝对路径,比如C:\OSGeo4W\bin\qgis-bin.exe
  2. 工作目录:填C:\OSGeo4W\bin
  3. 环境变量:添加QGIS_PLUGINPATH=C:\OSGeo4W\apps\qgis\plugins\custom

F5直接启动QGIS,加载插件后断点就能命中。这种调试方式最大的好处是能看到QGIS启动时的所有加载日志,插件加载失败的原因一目了然。

插件如果加载失败,常见错误在QGIS的"插件"对话框里只会提示一句"Could not load qgis plugin",根本不知道哪里出了问题。我的排查习惯是先看命令行输出,或者用DebugView抓调试输出。加载失败的原因,十有八九是:

  • dll依赖的Qt版本和QGIS自带的Qt版本不一致(比如用了msvc2015的Qt)
  • 忘记把插件放到QGIS_PLUGINPATH指定的目录
  • 插件导出的符号不对,classFactory函数没写

4.4 插件必须导出的两个C函数

QGIS插件在加载时,会通过动态链接库的导出符号查找两个C语言接口:classFactoryname。如果忘了写,dll能编译出来,但QGIS加载时会直接报错。

extern "C" QGISPLUGINEXPORT QgisPlugin* classFactory(QgisInterface* iface) { return new PointPickPlugin(iface); } extern "C" QGISPLUGINEXPORT const QString* name() { return new QString("PointPickPlugin"); }

关于QgisPlugin这个类:在QGIS 3.x早期版本里,插件基类就是QgisPlugin,里面有个initGui()虚函数和unload()虚函数。如果你在QGIS 3.28里打开插件时报找不到QgisPlugin头文件,注意确认包含路径里是否有qgisgui.h。QGIS 3.28的插件基类已经改名为QgsPluginInterface,不过classFactory的签名形式没变,只是返回类型由QgsPluginInterface*

5. 实际开发中必须避开的坑(附经验判断)

5.1 插件升级后界面无变化

这是新人最常问的问题。改了代码、重新编译、覆盖了dll,重新打开QGIS后发现界面上还是旧的功能。原因是QGIS会缓存插件状态,有些情况下dll会被QGIS进程持有,覆盖不生效,所以改动代码后最好完全退出QGIS再覆盖。如果还是不行,检查一下插件管理器里该插件是否处于"已启用"状态。

5.2 事件穿透问题

地图工具有时候会出现"工具响应了,但底层图层也在响应"的情况。例如 QgsMapTool 是连接到画布的事件过滤器,画布会先处理一部分事件,或者当前工具没有调用e->ignore()e->accept(),事件继续向底层的工具或图层传递。正确做法是:在事件处理里主动调用e->ignore(),告诉画布这个事件已经被处理,不需要再传播。

void PointPickTool::canvasPressEvent(QgsMapMouseEvent* e) { e->ignore(); // 阻止事件继续冒泡 // ... 你的处理逻辑 }

5.3 坐标系问题

自己写地图工具时,拿到的mapPoint()是当前地图画布投影坐标系下的坐标。但业务数据往往是另一种坐标系,比如数据是WGS84经纬度,画布却是Web墨卡托。此时不能直接把点写进要素,必须做坐标转换。

转换方式是用QgsCoordinateTransform

QgsCoordinateReferenceSystem srcCrs = mCanvas->mapSettings().destinationCrs(); QgsCoordinateReferenceSystem dstCrs("EPSG:4326"); QgsCoordinateTransform trans(srcCrs, dstCrs, QgsProject::instance()); QgsPointXY projectedPt = trans.transform(mapPoint);

而且要注意,QgsMapTool里的坐标转换时机很关键:如果在地图工具事件里拿到的坐标是投影坐标,那在事件处理内部就要转换完再发射信号或写数据,不要拖到后续的槽函数里再做,因为那时画布坐标系可能已经变了。

5.4 资源文件的使用

插件里的图标、提示文本可以打包进qrc文件,编译器会把它编译到dll里。但资源文件名如果以/开头,加载时路径也要以/开头,这个和普通Qt程序一致。我自己习惯把所有图标打成资源,这样分发插件时只需要一个dll,不存在外部依赖,部署省很多心。

5.5 多版本QGIS兼容性

QGIS从3.0到3.28,冒烟过程中QgisPlugin基类有变化,QgsMapTool的接口基本稳定,但QgisInterface有些方法在不同小版本里有增加。如果插件要兼容从3.16到3.28的多个QGIS版本,编译时就要注意别用太新的API。我的原则是:线上项目锁定一个LTR版本,不要跟着小版本跑,否则每次大版本更新都要重新适配编译。

6. 后续扩展:从地图工具到完整功能模块

地图工具只是QGIS二次开发的一个切入面,但它把整个C++插件开发的主链路占全了:环境搭建、类继承、事件处理、编译部署、调试排错。把这套流程跑通之后,再往里面加功能区就比较顺手了。

比如,你可以在地图工具里结合QgsVectorLayer做要素的实时创建,鼠标点一下就往图层里插入一个点要素并触发刷新;也可以结合QgsRubberBand画临时图形,实现框选、多边形圈选这类交互。这些后续扩展都建立在掌握QgsMapTool的基础上。

我个人的建议是,从QgsMapTool开始,先做一个只有"点击输出坐标"功能的最小工具,把编译加载调试整条链路跑通,再逐步增加业务逻辑。不要一开始就想做一个功能齐全的插件,那样环境问题和代码问题混在一起,很难定位。拿我自己来说,第一次在VS2017里编译QGIS插件,卡在CMake配置上就耗了两天,后来发现就是QGIS_DIR没指对。这种环境类的坑,别人一句话就能点破,自己摸可能要很久。希望这篇能帮你省下这笔时间。

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

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

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

立即咨询