ARTICLE DETAIL

资讯详情

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

QGIS 3.28 + VS2017 C++二次开发:从零搭建可交互地图工具

QGIS 3.28 + VS2017 C++二次开发:从零搭建可交互地图工具 简介在QGIS插件与工具开发中地图工具是连接用户输入与画布交互的关键环节。这套基于QGIS 3.28与VS2017的二次开发工程面向需要实现自定义地图工具的C与Qt开发者重点演示如何通过继承QgsMapTool基类、重写虚函数和连接信号槽来扩展画布操作能力。资源覆盖平移、点选与要素识别三类典型工具分别对应画布浏览、坐标拾取与要素查询等常见场景读者可直观理解不同工具的触发机制与使用差异。资源共45个文件压缩包约48.84MB包含核心源程序、界面定义、资源文件、工程配置文件以及可执行程序、编译中间文件与调试日志从源码到构建产物一应俱全工程结构清晰既方便在VS2017中直接查看整体框架也能借助调试日志快速定位编译或环境配置问题便于对照学习与二次改造。已有1566人学习下载。借助这份工程可快速掌握QgsMapToolPan、QgsMapToolEmitPoint与QgsMapToolIdentifyFeature的用法理清地图工具与画布之间的协作流程为后续开发点选、绘制、标注等复杂工具提供可复用的框架与排错思路对初学者而言是一条高效的上手路径。 QGIS二次开发这件事很少有一篇真正说清楚从零搭一个地图工具的文章。我第一次拿VS2017去对接QGIS的时候光环境就折腾了两三天各种缺失DLL、版本不匹配、路径不对硬是把一个本应半小时的demo拖成了大型排障现场。所以这篇文章我想以QGIS 3.28 LTR VS2017为例把创建地图工具的全过程按可复现的方式写下来包括CMake工程怎么配、QgsApplication怎么初始化、自定义地图工具怎么写以及最后发布时要注意什么。适合正在接触QGIS C二次开发或者想用Qt QGIS做桌面地图应用的读者。1. 二次开发选型与整体思路1.1 这个组合适合谁先说结论QGIS 3.28 VS2017这个组合最典型的场景是公司内部已经有比较老的C项目不想换工具链但又需要一个能显示地图、能交互的桌面工具。VS2017在2026年看确实不算新但很多传统行业的工控、GIS、测绘软件还在用。你不可能为了让一个地图模块跑起来就把整套老代码全部升级到VS2022那风险太大了。QGIS 3.28是LTRLong Term Release代表长期支持版本修复周期长、API冻结正好适合这类偏保守的集成场景。另外QGIS二次开发常用的是PyQGIS脚本但脚本只能做数据分析和插件做独立桌面工具、定制交互逻辑还是得走C这条路。Qt的C框架配合QGIS的QgsMapCanvas、QgsMapTool体系能实现非常灵活的地图交互这也是QGIS作为GIS框架最难替代的部分。1.2 为什么选QGIS 3.28而不是新版很多刚接触的人会问我QGIS都出到3.36、3.40了为什么不直接上最新版主要原因是稳定性。3.28 LTR从2022年开始维护插件生态、第三方库版本、社区文档都非常成熟。对于二次开发来说API稳定比版本新重要得多。你去搜QgsMapTool、QgsRubberBand的用法网上90%的代码在3.28下都能直接用但换到3.38之后可能就踩到接口变化。再有就是Qt版本。3.28对应Qt 5.15.2这是Qt 5的最后一个商业支持版本兼容性非常好。VS2017的C编译器对Qt 5项目的适配也很成熟踩坑资料一搜一大把。所以不管是从技术风险还是学习成本考虑3.28在VS2017环境下都是最省心的选择。1.3 整体架构一个最小可运行的地图程序做一个QGIS地图工具本质上就是三件事初始化QGIS运行环境QgsApplication在Qt主窗口里塞一个QgsMapCanvas画布给画布挂上各种QgsMapTool工具平移、缩放、点选、绘图我的建议是先搭一个最小骨架跑通了再加功能。很多人一上来就想要一个功能完整的GIS软件结果代码写了一堆编译错误也堆了一堆最后连第一步都没走出去。先把一个能显示shp文件、能放大缩小、能点选要素的demo跑起来剩下的都是在这个框架上做加法。2. 环境搭建依赖文件与工程配置2.1 你需要准备的依赖文件QGIS的C开发不是装一个软件就完事的你需要SDK开发包。我推荐的方案是安装OSGeo4W一个专门用来管理QGIS依赖的软件包管理器。安装的时候需要注意在组件选择页面除了QGIS主程序qgis它会自动选好默认依赖之外强烈建议额外勾选这些开发组件qgis-devC头文件和CMake模块qt5-devQt 5开发头文件与库gdal-dev、geos-dev、proj-dev底层的空间数据读写、几何计算和坐标转换库qgis-rel-dev 或者对应3.28版本的devel包安装完成后关键目录结构大概是这样的C:\OSGeo4W\ ├── apps\ │ ├── qgis\ │ │ ├── include\ QGIS头文件 │ │ ├── lib\ QGIS导入库和DLL │ │ └── lib\cmake\QGIS\ CMake模块 │ └── Qt5\ │ ├── include\ │ └── bin\ └── bin\ GDAL、GEOS等运行库这个路径非常关键后面CMake、环境变量都要用到。我一般会安装到C:\OSGeo4W这个默认路径因为很多文档、脚本、示例代码都默认这个路径能省掉不少麻烦。2.2 创建CMake工程QGIS官方推荐用CMake来构建二次开发项目不建议直接在VS里手动添加头文件目录和库目录。原因很简单QGIS依赖的库太多了手动手工配置必然出错CMake可以通过find_package自动处理。一个最小可用的CMakeLists.txt长这样cmake_minimum_required(VERSION 3.16) project(QgisMapToolDemo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) set(CMAKE_PREFIX_PATH C:/OSGeo4W/apps/qgis C:/OSGeo4W/apps/Qt5 ) find_package(QGIS REQUIRED COMPONENTS Core Gui) find_package(Qt5 REQUIRED COMPONENTS Widgets) add_executable(MapToolDemo main.cpp MainWindow.cpp MainWindow.h MeasureTool.cpp MeasureTool.h ) target_link_libraries(MapToolDemo PRIVATE Qt5::Widgets QGIS::Core QGIS::Gui )这里有几个细节值得说明CMAKE_AUTOMOC ON是必须的。Qt的信号槽机制需要moc预处理器如果不开这个运行时会出现Unknown slot或者诡异崩溃。QGIS::Core和QGIS::Gui是QGIS提供的CMake导入目标分别对应矢量、图层、坐标系管理核心库和画布、地图工具等GUI库。CMAKE_PREFIX_PATH要指向两个地方apps/qgis是QGIS的CMake模块位置apps/Qt5是Qt的位置。2.3 VS2017生成与编译细节在VS2017里我没用传统的.sln文件而是直接用CMake的打开文件夹方式打开工程目录。VS2017对CMake的原生支持已经很好了可以直接生成并调试。如果你更习惯命令行也可以这样cmake .. -G Visual Studio 15 2017 Win64 -A x64这里有一个非常关键的坑QGIS 3.28的官方Windows版本是用MSVC 2019编译的但VS2015/2017/2019的C ABI是二进制兼容的所以VS2017可以正常链接MSVC 2019编译的库。但前提是你必须勾选x64平台并且使用Release配置。QGIS官方发布的库没有Debug版用Debug模式去链接Release库会遇到一连串莫名其妙的链接错误或者运行时崩溃这是我踩过最深的一个坑。所以编译的时候请直接把配置切到Release x64。不用纠结DebugGIS业务要调试可以靠日志靠打印靠逐步定位没必要为了Debug模式去重新编译整个QGIS。3. 写一个能跑起来的地图程序3.1 初始化QGIS运行环境QGIS二次开发和普通Qt程序最大的区别就是首先要初始化QgsApplication。它负责设置QGIS的资源路径、插件路径、坐标参考系统数据库以及各种ProviderOGR、GDAL、PostGIS等的加载。主函数长这样#include QApplication #include qgsapplication.h int main(int argc, char *argv[]) { QgsApplication app(argc, argv, true); QString prefix C:/OSGeo4W/apps/qgis; app.setPrefixPath(prefix, true); app.initQgis(); // 在这里创建主窗口并启动事件循环 int ret app.exec(); app.exitQgis(); return ret; }第三行构造函数里的true表示启用GUI。如果写false那就进入无界面模式很多地图渲染和工具类的方法会直接不可用。setPrefixPath第二个参数传true表示让QGIS自动去默认位置寻找插件、图标、投影定义等资源。如果这个路径不对程序启动时通常会弹一个Unable to load qgis提示框或者干脆在initQgis阶段崩溃。如果你不想硬编码路径也可以用环境变量先设置QGIS_PREFIX_PATH再在代码里用QgsApplication::prefixPath()读取。对于需要分发给别人的工具我建议还是用环境变量方式灵活性更好。3.2 创建主窗口和画布地图窗口的核心组件是QgsMapCanvas。它相当于一个窗户所有地图图层、渲染结果、交互缩放都发生在这个控件里。一个简单的MainWindow类#include QMainWindow #include QgsMapCanvas.h class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget* parent nullptr); private: QgsMapCanvas* m_canvas nullptr; };构造函数里的初始化逻辑#include MainWindow.h #include QVBoxLayout #include QToolBar #include QgsMapCanvas.h #include qgsmapcanvas.h #include QgsMapToolPan.h #include QgsMapToolZoom.h MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { QWidget* central new QWidget(this); QVBoxLayout* layout new QVBoxLayout(central); layout-setContentsMargins(0, 0, 0, 0); m_canvas new QgsMapCanvas(this); layout-addWidget(m_canvas); setCentralWidget(central); // 设置背景色 m_canvas-setCanvasColor(QColor(245, 245, 245)); // 配置坐标参考系统为 Web Mercator方便后续叠加在线底图 m_canvas-setDestinationCrs(QgsCoordinateReferenceSystem::fromEpsgId(3857)); // 设置默认工具平移 QgsMapToolPan* panTool new QgsMapToolPan(m_canvas); m_canvas-setMapTool(panTool); m_canvas-setFocus(); // 添加工具栏按钮 QToolBar* toolbar addToolBar(tr(地图工具)); toolbar-addAction(tr(平移), this, []() { m_canvas-setMapTool(panTool); }); }setDestinationCrs设置的是画布显示时的坐标参考系统。你要显示的数据可能是任意坐标系但画布会通过投影变换实时转成这个坐标系来显示。我这里设为EPSG:3857Web Mercator因为后续无论是加载在线瓦片还是和Web端地图对接都用这个坐标系最方便。3.3 加载矢量图层画布有了下一步往里面塞数据。以加载一个Shapefile为例#include QgsVectorLayer.h #include QgsProject.h #include QMessageBox #include QFileDialog void MainWindow::openShapefile() { QString filePath QFileDialog::getOpenFileName( this, tr(选择Shapefile), , tr(Shapefile (*.shp))); if (filePath.isEmpty()) return; QgsVectorLayer* layer new QgsVectorLayer( filePath, QFileInfo(filePath).baseName(), QStringLiteral(ogr)); if (!layer-isValid()) { QMessageBox::warning(this, tr(错误), tr(图层加载失败)); return; } QgsProject::instance()-addMapLayer(layer); // 让图层铺满整个画布 m_canvas-setExtent(layer-extent()); m_canvas-setLayers({layer}); m_canvas-refresh(); }这里有几个点要说明new QgsVectorLayer的第三个参数是Provider名称ogr就是针对文件数据的通用Provider。只要GDAL支持的格式基本都可以用这个加载比如GeoJSON、gpkg等不用改代码改路径就行。isValid()这个判断非常关键。加载失败时它返回false导致失败的原因大多是文件路径包含中文或空格、文件本身损坏、缺少同行文件。你可以用layer-dataProvider()-error()拿到更具体的错误信息。4. 实现自定义地图工具地图工具是QGIS交互的核心。上面用的是内置的QgsMapToolPan、QgsMapToolZoom但实际项目中你往往需要自己的工具比如画点、画线、测距、属性识别。这一节就说怎么做。4.1 继承QgsMapToolQgsMapTool是一个抽象基类定义了地图交互的接口。你只需要继承它重写canvasPressEvent、canvasMoveEvent、canvasReleaseEvent等方法就能自定义鼠标交互逻辑。一个最基础的点选工具#include QgsMapTool.h #include QgsMapMouseEvent.h class PickPointTool : public QgsMapTool { Q_OBJECT public: PickPointTool(QgsMapCanvas* canvas) : QgsMapTool(canvas) {} void canvasReleaseEvent(QgsMapMouseEvent* e) override; signals: void mouseClicked(const QgsPointXY point); }; void PickPointTool::canvasReleaseEvent(QgsMapMouseEvent* e) { if (e-button() Qt::LeftButton) { QgsPointXY mapPoint e-mapPoint(); emit mouseClicked(mapPoint); } QgsMapTool::canvasReleaseEvent(e); }e-mapPoint()这个方法非常方便它已经帮你把屏幕坐标反算成地图坐标了。很多刚接触QGIS开发的人会手动去写toMapCoordinates其实QgsMapMouseEvent内部已经处理好了。使用这个工具PickPointTool* pickTool new PickPointTool(m_canvas); m_canvas-setMapTool(pickTool);4.2 点选查询点击要素并输出属性光拿到一个坐标没意思我们要的是点哪查哪个要素。可以在上面的点选工具基础上加入缓冲区查询的逻辑void PickPointTool::canvasReleaseEvent(QgsMapMouseEvent* e) { if (e-button() ! Qt::LeftButton) return; QgsPointXY mapPoint e-mapPoint(); // 取出当前画布上的所有矢量图层 QListQgsVectorLayer* layers; const auto mapLayers m_canvas-layers(); for (QgsMapLayer* layer : mapLayers) { if (layer-type() QgsMapLayerType::VectorLayer) { auto* vectorLayer qobject_castQgsVectorLayer*(layer); layers.append(vectorLayer); } } // 建立一个半径0.001的小方框做空间过滤 double tolerance 0.001; QgsRectangle rect(mapPoint.x() - tolerance, mapPoint.y() - tolerance, mapPoint.x() tolerance, mapPoint.y() tolerance); for (QgsVectorLayer* layer : layers) { QgsFeatureRequest req; req.setFilterRect(rect); QgsFeatureIterator it layer-getFeatures(req); QgsFeature feature; if (it.nextFeature(feature)) { QgsAttributes attrs feature.attributes(); qDebug() 查到要素字段数 attrs.count(); // 这里可以把属性表格显示到界面上 break; } } }tolerance这里先写死了一个经验值。注意单位是地图坐标单位如果你的数据是WGS84经纬度0.001大约等于100米左右点选命中范围偏大。更好的做法是用屏幕像素换算地图距离比如取屏幕中心一个4x4像素的矩形再toMapCoordinates转换。这个方法需要查QgsMapCanvas的API上面这种先撑住场景的做法足以演示。4.3 测距工具橡皮筋绘制和长度计算测距工具是展示QGIS交互能力很好的案例它用到了QgsRubberBand橡皮筋图层。橡皮筋就是你画图时看到的临时红色/绿色半透明线条它不属于真实图层数据只是用来做交互预览。测距工具的完整实现#include QgsMapTool.h #include QgsRubberBand.h #include QgsDistanceArea.h #include QgsPointXY.h #include QgsCoordinateReferenceSystem.h class MeasureTool : public QgsMapTool { Q_OBJECT public: MeasureTool(QgsMapCanvas* canvas) : QgsMapTool(canvas) , m_rubberBand(new QgsRubberBand(canvas, QgsWkbTypes::LineGeometry)) { m_rubberBand-setColor(QColor(255, 0, 0, 120)); m_rubberBand-setWidth(2); } void canvasReleaseEvent(QgsMapMouseEvent* e) override { if (e-button() Qt::RightButton) { // 右键结束测距 activate(); return; } QgsPointXY point e-mapPoint(); m_points.append(point); m_rubberBand-addPoint(point); m_rubberBand-update(); if (m_points.size() 2) { QgsDistanceArea da; da.setSourceCrs(canvas()-mapSettings().destinationCrs()); da.setEllipsoid(m_canvas-mapSettings().destinationCrs().isValid() ? QgsProject::instance()-ellipsoid() : QStringLiteral(WGS84)); double total da.measureLine(m_points); emit distanceChanged(total); } } signals: void distanceChanged(double meters); private: QgsRubberBand* m_rubberBand nullptr; QVectorQgsPointXY m_points; };这里重点解释QgsDistanceArea的配置measureLine默认认为你的坐标是平面直角坐标算出来的是图上距离。如果你输入的是经纬度需要调用setSourceCrs设置坐标系同时设置椭球体它内部的测地线算法才会把经纬度换算成真实的地表距离。这个细节很多教程都不会提导致很多人算出来的距离大得离谱就是因为缺了这两步配置。5. 调试、打包与常见问题5.1 DLL环境与路径即使程序编译成功了运行时也有很大概率在加载依赖DLL时崩溃。QGIS的依赖链很长光底层库就有Qt5、GDAL、GEOS、PROJ加上QGIS自己的一堆DLL少一个都不行。经验做法是在启动程序的入口处设置环境变量。可以在main.cpp开头加这样一段#ifdef Q_OS_WIN #include windows.h #endif int main(int argc, char *argv[]) { // 设置QGIS运行库路径 qputenv(PATH, C:/OSGeo4W/bin;C:/OSGeo4W/apps/qgis/bin;C:/OSGeo4W/apps/Qt5/bin; qgetenv(PATH)); // ... 后续初始化 }这样做的好处是即使机器上没有配置系统环境变量程序也能自己找到依赖。缺点是如果你发布给别人路径写死就会出问题。更规范的做法是用一个启动器脚本或者在安装包时设置环境变量。如果程序启动时还是提示缺少DLL最快的排查工具是 Dependencies 把生成的exe拖进去它会列出所有缺失的DLL照着一个个补就行。5.2 常见错误速查表现象可能原因解决办法编译报错找不到QgsApplication.hCMake没找到QGIS路径检查CMAKE_PREFIX_PATH是否指向C:/OSGeo4W/apps/qgis确认安装时勾选了qgis-dev链接错误无法解析的外部符号项目是Debug模式QGIS库是Release切到Release x64重新编译确认运行库是/MT还是/MD统一启动时弹Could not load qgisqgis_core.dll找不到把C:/OSGeo4W/apps/qgis/bin加入PATH确认QgsApplication的prefix路径正确图层加载返回isValid()falseshp路径包含中文或缺失投影文件临时路径全部用英文检查同目录有没有shx、dbf文件使用的API在当前版本不存在QGIS版本不对应检查你的代码和QGIS版本3.28的API和4.x差异较大中文文字显示为方框Qt字体加载问题在QgsApplication初始化后用QFont设置中文字体如Microsoft YaHei5.3 发布工具时的几个建议程序开发完要发给别人用还有几件事不能忘。windeployqt是Qt自带的部署工具它可以把Qt相关的DLL和插件复制到你的exe目录下。但对QGIS来说还需要手工复制apps/qgis/bin下的DLL和apps/qgis/plugins下的插件目录。我通常把exe所在的文件夹命名为Program和data目录平级再把需要的依赖DLL直接丢进去。一个最省事的方案是先拷贝整个OSGeo4W目录到客户机器然后把你的exe放进C:\OSGeo4W\bin目录这样所有依赖都在绝对能跑。缺点是体积大但如果只是内部工具这个方案稳定性最高省去很多无谓的排障时间。客户机器上如果杀毒软件很激进第一次启动可能很慢那是因为它在扫描QGIS目录下几千个小文件正常现象不用慌。最后这个项目还能往哪扩展做完一个能显示地图、能点选、能测距的小工具之后你会发现QGIS的二次开发框架其实已经打开了一扇很大的门。后面可以加属性过滤器QgsAttributeTableDialog一键调出属性表、专题图渲染QgsCategorizedSymbolRenderer按某个字段分类着色、空间查询点击一个面要素选出所有相交的线要素每一步都有官方API可以查。我还想提一个建议不要一上来就去研究QGIS源码那不是大多数人需要的。你需要的是掌握QgsMapCanvas、QgsMapTool、QgsVectorLayer、QgsRubberBand、QgsProject这几个核心类的关系就像掌握了Qt的QWidget和QEvent就能搞定绝大部分界面一样。把基础骨架跑熟再遇到新功能需求你知道这是不是QGIS应该提供的能力然后去官方API文档翻一翻往往很快就能找到对应的类。最后再说一个我在实际开发里特别受用的习惯每次编译成功之后把配置好的工程整个备份一份。因为QGIS依赖的环境变量、CMake缓存、第三方DLL路径任何一个变了都可能让工程突然罢工。备份好一个能跑的版本你的心情会稳定很多。本文还有配套的精品资源点击获取
返回列表