ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

QGIS C++插件开发实战:从环境搭建到自定义地图工具部署

QGIS C++插件开发实战:从环境搭建到自定义地图工具部署 简介面向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相关组件。这里有个容易混淆的点你日常用的是独立安装版的QGISqgis.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\binQt相关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); }这里有个关键点QAction的checkable必须设置为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_LIBRARY和QGIS_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_PLUGINPATHC:\OSGeo4W\apps\qgis\plugins\custom然后把dll放到这个custom目录下。插件管理器里勾选自定义分类时它才会扫描到这个目录。还有一个比较隐蔽的问题是QGIS插件加载后dll会被锁定想重新编译覆盖dll会提示文件被占用。解决办法是把QGIS进程退出后再覆盖或者用任务管理器确认qgis-bin.exe确实结束了。4.3 用VS2017直接调试插件VS2017调试QGIS插件我试过最实用的是附加到进程的方式。在VS2017里配置项目调试属性调试 - 命令填QGIS主程序绝对路径比如C:\OSGeo4W\bin\qgis-bin.exe工作目录填C:\OSGeo4W\bin环境变量添加QGIS_PLUGINPATHC:\OSGeo4W\apps\qgis\plugins\customF5直接启动QGIS加载插件后断点就能命中。这种调试方式最大的好处是能看到QGIS启动时的所有加载日志插件加载失败的原因一目了然。插件如果加载失败常见错误在QGIS的插件对话框里只会提示一句Could not load qgis plugin根本不知道哪里出了问题。我的排查习惯是先看命令行输出或者用DebugView抓调试输出。加载失败的原因十有八九是dll依赖的Qt版本和QGIS自带的Qt版本不一致比如用了msvc2015的Qt忘记把插件放到QGIS_PLUGINPATH指定的目录插件导出的符号不对classFactory函数没写4.4 插件必须导出的两个C函数QGIS插件在加载时会通过动态链接库的导出符号查找两个C语言接口classFactory和name。如果忘了写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墨卡托。此时不能直接把点写进要素必须做坐标转换。转换方式是用QgsCoordinateTransformQgsCoordinateReferenceSystem 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没指对。这种环境类的坑别人一句话就能点破自己摸可能要很久。希望这篇能帮你省下这笔时间。本文还有配套的精品资源点击获取
返回列表