☰
QGIS C++二次开发:VS2019+Qt5.15.2构建独立GIS桌面应用
2026/10/3 4:30:33 网站建设 项目流程

简介:本资源是一份面向GIS开发初学者与C++桌面应用开发者的技术实践项目,聚焦QGIS二次开发核心能力训练,解决轻量级GIS桌面工具从零构建的工程落地问题,适用于自然资源管理、城市规划等领域的定制化空间数据处理需求。压缩包共10个文件,含3个cpp源文件(实现主窗口、图层树菜单及核心逻辑)、2个h头文件(定义类接口)、1个README.md与1个说明.txt(提供项目结构与编译指引),辅以界面截图png、附赠文档docx及gitignore配置,整体仅59KB,精简实用。已有102人学习下载,资源结构清晰、模块职责分明,读者可直接复用图层树右键菜单扩展机制、矢量数据(Shapefile/GeoJSON等)加载流程及QT+QGIS混合框架集成方案,并掌握VisualStudio环境下跨平台GIS应用的调试要点与性能优化思路。

1. 基于QGIS与QT框架在VisualStudio环境下进行二次开发实现地理信息系统桌面应用:不是插件、不是脚本,而是真正可独立分发的.exe桌面GIS工具

你有没有试过——在QGIS里写完一个Python插件,功能很全,但一换电脑就报错“ModuleNotFoundError: No module named 'qgis'”?或者用PyQt写了个带地图的窗口,结果底图加载失败、矢量渲染卡顿、坐标系乱套,最后发现是PROJ库版本冲突?这不是玄学,是QGIS原生C++ API + Qt Widgets + VS编译链没对齐的真实翻车现场。这个资源包,就是一套绕过Python插件沙箱、不依赖QGIS安装环境、直接用Visual Studio 2019/2022编译生成独立.exe文件的轻量级GIS桌面应用开发骨架。它把QGIS核心(qgis_core、qgis_gui)作为静态/动态链接库嵌入Qt项目,用Qt Designer设计界面,用VS做构建和调试,最终产出一个双击即用、带矢量加载、图层控制、简单空间查询能力的原生Windows GIS工具。适合需要交付给非技术人员、要求离线运行、或需深度集成业务逻辑(如测绘内业质检、国土数据预处理、应急指挥前端)的中小团队。新手能照着跑通第一个shp加载窗口,熟手能快速切入自定义图层渲染器或坐标转换模块——前提是,你得先让Qt和QGIS的ABI对上。


2. QGIS C++ SDK与Qt版本选型:为什么必须用Qt 5.15.2 + QGIS 3.28 LTS + VS2019组合

2.1 QGIS二开不是“装个插件就行”,本质是C++ ABI兼容性工程

QGIS官方不提供“一键SDK包”,其C++ API暴露在qgis_core.dll、qgis_gui.dll中,这些DLL由QGIS源码编译生成,严格绑定其构建时的Qt版本、编译器、运行时库(MSVCRT)。比如QGIS 3.28官方Windows安装包由Qt 5.15.2 + MSVC 2019 x64编译;若你在VS2022中用Qt 6.5创建项目,链接QGIS 3.28的DLL,必然触发fatal: cannot mix incompatible qt library (version ex50601)——这个错误代码ex50601,就是Qt 5.15.2的ABI签名。网上很多教程让你“下载Qt官网任意版本”,结果卡在第一步。真实路径只有一条:QGIS版本 → 其构建日志 → 锁定Qt版本 → 锁定VS版本 → 锁定CMake Generator。

2.2 为什么是Qt 5.15.2而不是更新的5.15.3或6.x?

Qt 5.15.2是Qt 5系列最后一个LTS(长期支持)版本,也是QGIS 3.28及之前所有稳定版的官方构建基线。Qt 5.15.3虽小版本更新,但内部QMetaObject布局微调,导致QGIS DLL中导出的QgsMapCanvas类虚函数表偏移错位,VS链接时看似成功,运行时new QgsMapCanvas()直接崩溃。Qt 6.x则彻底废弃QPainter绘图栈,改用QRhi,而QGIS 3.x全部基于Qt 5的QPainter重绘机制,强行桥接等于重写整个渲染管线——这已超出“二次开发”范畴,属于重构。因此,本项目明确限定:Qt 5.15.2 MinGW 7.3.0 或 MSVC 2019 16.11 x64(二者不可混用),对应VS2019 16.11.22及以上。

2.3 Visual Studio版本选择:不是越新越好,而是要匹配QGIS的CRT

QGIS 3.28 Windows安装包使用/MDd(Debug)和/MD(Release)链接MSVCRT,即动态链接Microsoft C Runtime。VS2019默认CRT版本为v142(对应MSVC 14.29),与QGIS构建时的v142完全一致。若用VS2022(默认v143),即使强制指定-DMSVC_VERSION=142,其生成的.lib导入库仍含v143符号,链接时提示LNK2001: unresolved external symbol __imp_??0QgsApplication@QGIS_CORE@@QEAA@XZ——这是典型的CRT不匹配。解决方案只有两个:降级VS到2019,或在VS2022中安装**“Desktop development with C++”工作负载下的“CMake tools for Visual Studio”组件,并在CMakeLists.txt中显式指定set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreadedDLL")**。本资源包采用前者,确保零配置开箱即用。

2.4 QGIS开发包获取:不要从官网下载安装包,要拿Build Cache

QGIS官网下载的是运行时安装包(含GUI、插件、Python),不含开发头文件(.h)和导入库(.lib)。你需要的是QGIS Build Cache——即CI系统编译QGIS时生成的中间产物。访问https://github.com/qgis/QGIS/releases/tag/release-3_28_0,下拉至“Assets”,找到qgis-3.28.0-src.zip(源码)和关键的qgis-3.28.0-win64-dev.7z(开发包)。后者解压后包含:

  • include/:所有qgis_core.h、qgis_gui.h等头文件
  • lib/:qgis_core.lib、qgis_gui.lib(用于VS链接)
  • bin/:qgis_core.dll、qgis_gui.dll(运行时必需,必须与exe同目录)
  • plugins/:libqgis_app.dll等插件(本项目暂不用,但保留结构)

提示:qgis-3.28.0-win64-dev.7z体积约180MB,解压后include目录有327个头文件,lib目录含21个.lib。不要试图用dumpbin /exports qgis_core.dll > exports.txt自己生成.lib——QGIS大量使用Q_DECL_EXPORT宏控制符号导出,手动处理极易遗漏QgsVectorLayer::addFeature等关键函数。


3. Visual Studio项目结构搭建:从空解决方案到可加载SHP的主窗口

3.1 创建Qt VS Tools项目:拒绝手工配置Include路径

安装Qt VS Tools插件(v2.10+,支持Qt 5.15.2),在VS2019中:
File → New → Project → Qt Widget Application,命名QgisDesktopApp,路径设为D:\gisdev\QgisDesktopApp。关键设置:

  • Qt Version:选择已安装的Qt 5.15.2 MSVC2019 64-bit(必须与QGIS Dev包一致)
  • Target Platform:x64(QGIS 3.28仅提供64位DLL)
  • Project Settings → General → Configuration Type:Application (.exe)
  • Project Settings → C/C++ → General → Additional Include Directories:添加D:\qgis-dev\include(QGIS头文件路径)
  • Project Settings → Linker → General → Additional Library Directories:添加D:\qgis-dev\lib(QGIS .lib路径)
  • Project Settings → Linker → Input → Additional Dependencies:填入qgis_core.lib;qgis_gui.lib;qtmain.lib

注意:qtmain.lib是Qt WinMain入口封装库,若缺失会导致LNK2019: unresolved external symbol WinMain。此库位于Qt\5.15.2\msvc2019_64\lib\qtmain.lib,VS Qt Tools会自动添加,但手动配置时务必确认。

3.2 初始化QGIS应用:比Qt QApplication多三行关键代码

main.cpp不能直接QApplication app(argc, argv),必须先初始化QGIS环境:

#include <QApplication> #include <QMainWindow> #include "qgsapplication.h" // QGIS核心头文件 #include "qgsmapcanvas.h" int main(int argc, char *argv[]) { // 1. 设置QGIS数据路径(必须!否则找不到投影定义、符号库) QString qgisDataPath = "D:/qgis-dev/share/qgis"; // 指向QGIS安装目录的share子目录 QgsApplication::setPrefixPath(qgisDataPath, true); // 2. 创建QGIS应用实例(传入argc/argv,但不接管GUI事件循环) QgsApplication qgisApp(argc, argv, true); // 第三个参数true表示GUI模式 // 3. 初始化QGIS(加载PROJ、GDAL、SIP等依赖) qgisApp.initQgis(); // 4. 启动Qt应用(此时QGIS已就绪) QApplication app(argc, argv); QMainWindow window; window.setWindowTitle("QGIS Desktop Lite"); // 5. 创建地图画布并设为中心部件 QgsMapCanvas *canvas = new QgsMapCanvas(&window); canvas->setCanvasColor(Qt::white); canvas->enableAntiAliasing(true); window.setCentralWidget(canvas); // 6. 加载一个SHP示例(绝对路径!相对路径在VS调试中易失效) QgsVectorLayer *layer = new QgsVectorLayer("D:/data/test.shp", "test_layer", "ogr"); if (layer->isValid()) { QgsProject::instance()->addMapLayer(layer); canvas->setLayers({layer}); canvas->zoomToFullExtent(); } window.show(); int result = app.exec(); // Qt事件循环 qgisApp.exitQgis(); // 退出前释放QGIS资源 return result; }

关键点说明:

  • setPrefixPath必须指向QGIS安装目录的share/qgis(含resources、projection等子目录),不是Dev包路径。若无QGIS安装,需从qgis-3.28.0-win64-dev.7z中提取share目录并复制到指定位置。
  • QgsApplication构造函数第三个参数为true,表示启用GUI模式(否则无法创建QgsMapCanvas)。
  • initQgis()必须在QApplication构造之后、app.exec()之前调用,顺序错则PROJ初始化失败,坐标系识别为Unknown CRS。
  • QgsProject::instance()->addMapLayer()是QGIS 3.x标准图层管理方式,替代旧版QgsMapLayerRegistry::instance()->addMapLayer()。

3.3 Qt Designer界面集成:用.ui文件驱动主窗口,而非硬编码

右键项目 →Add → New Item → Qt → Qt Designer Form Class,命名为MainWindow.ui。拖入QgsMapCanvas控件(需先注册自定义控件):

  1. 在MainWindow.ui空白处右键 →Promote to...
  2. Promoted class name填QgsMapCanvas
  3. Header file填qgsmapcanvas.h
  4. Global include勾选
  5. 点击Add→Promote

生成的ui_mainwindow.h会自动包含#include "qgsmapcanvas.h",并在setupUi()中创建QgsMapCanvas* canvas成员。此时MainWindow.cpp中只需:

#include "mainwindow.h" #include "ui_mainwindow.h" #include "qgsvectorlayer.h" #include "qgsproject.h" MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); // ui->canvas 是QgsMapCanvas指针,已由UI文件创建 ui->canvas->setCanvasColor(Qt::white); // 加载SHP(此处用相对路径,因VS调试工作目录可设) QgsVectorLayer *layer = new QgsVectorLayer("../data/test.shp", "test", "ogr"); if (layer->isValid()) { QgsProject::instance()->addMapLayer(layer); ui->canvas->setLayers({layer}); ui->canvas->zoomToFullExtent(); } }

提示:VS调试时,Working Directory需设为$(ProjectDir)(项目根目录),否则../data/test.shp无法定位。可在Project Properties → Configuration Properties → Debugging → Working Directory中设置。


4. 矢量数据加载与基础交互:从SHP读取到属性表弹窗的完整链路

4.1 OGR驱动加载SHP:为什么不用QgsVectorLayer::setDataProvider?

QgsVectorLayer构造函数第三个参数是providerKey,"ogr"是GDAL/OGR驱动标识符。QGIS内部通过QgsOgrProvider封装GDAL,支持SHP、GeoJSON、GDB等多种格式。关键在于SHP必须包含.shx和.dbf文件,否则isValid()返回false。常见错误:只复制.shp文件,漏掉索引和属性表。验证方法:用ogrinfo -so test.shp检查是否输出Geometry: Polygon和Feature Count: 123。

4.2 坐标系自动识别与强制设置:避免“地图歪斜”的血泪经验

SHP若无.prj文件,QGIS默认用WGS84(EPSG:4326),但实际数据可能是CGCS2000(EPSG:4490)或地方坐标系。正确做法:

QgsVectorLayer *layer = new QgsVectorLayer("D:/data/test.shp", "test", "ogr"); if (layer->isValid()) { // 方案1:从.prj文件自动识别(推荐) layer->setCrs(QgsCoordinateReferenceSystem::fromOgcWmsCrs("EPSG:4490")); // 方案2:若.prj缺失,手动指定(必须!否则渲染错位) layer->setCrs(QgsCoordinateReferenceSystem::fromEpsgId(4490)); QgsProject::instance()->addMapLayer(layer); ui->canvas->setLayers({layer}); }

fromEpsgId(4490)比fromOgcWmsCrs("EPSG:4490")更可靠,后者依赖proj.db数据库查询,而fromEpsgId直接查内置CRS表。

4.3 属性表弹窗实现:复用QGIS原生对话框,非自制TableView

QGIS提供QgsAttributeTableDialog,可直接显示图层属性:

// 在MainWindow中添加槽函数 void MainWindow::showAttributeTable() { QgsVectorLayer *layer = qobject_cast<QgsVectorLayer*>(QgsProject::instance()->mapLayers().begin().value()); if (layer && layer->type() == QgsMapLayer::VectorLayer) { QgsAttributeTableDialog *dialog = new QgsAttributeTableDialog(layer, this); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->show(); } } // 连接到菜单栏Action connect(ui->actionAttribute_Table, &QAction::triggered, this, &MainWindow::showAttributeTable);

注意:QgsAttributeTableDialog依赖QgsGui::enableAutoGeometryRestore(),需在main()中调用QgsGui::enableAutoGeometryRestore(),否则窗口位置记忆失效。

4.4 空间查询基础:点选要素并高亮显示

QgsMapToolIdentify是QGIS标准识别工具,但需继承自QgsMapTool以定制行为:

class IdentifyTool : public QgsMapTool { Q_OBJECT public: IdentifyTool(QgsMapCanvas *canvas) : QgsMapTool(canvas) {} protected: void canvasReleaseEvent(QMouseEvent *e) override { QgsPointXY point = mCanvas->getCoordinateTransform()->toMapCoordinates(e->x(), e->y()); QList<QgsMapToolIdentify::IdentifyResult> results; QgsMapToolIdentify *identifyTool = new QgsMapToolIdentify(mCanvas); results = identifyTool->identify(point.x(), point.y(), QgsMapToolIdentify::TopDownStopAtFirst, {QgsMapLayer::VectorLayer}); if (!results.isEmpty()) { QgsVectorLayer *layer = qobject_cast<QgsVectorLayer*>(results[0].mLayer); QgsFeature feature = results[0].mFeature; // 高亮选中要素(临时图层) QgsHighlight *highlight = new QgsHighlight(mCanvas, feature.geometry(), layer); highlight->setColor(Qt::red); highlight->setWidth(3); highlight->show(); // 弹窗显示属性 QMessageBox::information(this, "Feature Info", QString("ID: %1\nName: %2").arg(feature.id()).arg(feature.attribute("name").toString())); } } };

提示:QgsHighlight需手动delete,否则内存泄漏。生产环境应存入QList<QgsHighlight*>并在图层变更时清理。


5. 避坑指南:五个让开发者凌晨三点还在查dumpbin的致命问题

5.1 现象:程序启动闪退,事件查看器显示“应用程序无法正常启动(0xc000007b)”

原因:32位/64位混用。QGIS 3.28 Dev包是纯64位,但VS项目配置为Win32(即x86)。0xc000007b是Windows加载器检测到PE头架构不匹配的错误码。
解决:Project Properties → Configuration Manager → Active solution platform → New → x64,确保所有项目(包括Qt库、QGIS lib)均为x64。

5.2 现象:QgsMapCanvas显示空白,控制台无报错,但canvas->extent()返回0,0,0,0

原因:QgsProject::instance()未初始化或setLayers()前未调用canvas->setExtent()。QGIS 3.x要求图层添加到Project后,再通过setLayers()触发画布重绘。
解决:严格按顺序执行:

  1. QgsProject::instance()->addMapLayer(layer)
  2. canvas->setLayers({layer})
  3. canvas->zoomToFullExtent()或canvas->setExtent(layer->extent())

5.3 现象:加载SHP后中文字段名显示为“????”,属性值乱码

原因:GDAL默认用ISO-8859-1解码.dbf,而国内SHP多用GBK。QGIS 3.16+支持OGR_ENABLE_PARTIAL_REPROJECTION,但需显式设置编码。
解决:在main()中initQgis()后添加:

QgsSettings settings; settings.setValue("/gdal/encoding", "GBK"); // 强制GDAL用GBK读.dbf

或在SHP路径后加编码参数:"D:/data/test.shp|encoding=GBK"

5.4 现象:Qt Designer中Promote的QgsMapCanvas编译报错“undefined reference tovtable for QgsMapCanvas”

原因:QgsMapCanvas是QObject派生类,需在头文件中声明Q_OBJECT宏,且该头文件必须被moc工具处理。但qgsmapcanvas.h是QGIS头文件,未被moc扫描。
解决:不直接PromoteQgsMapCanvas,改用QWidget占位,然后在MainWindow.cpp中new QgsMapCanvas并setParent():

// MainWindow.h 中声明 private: QgsMapCanvas *mCanvas; // MainWindow.cpp 构造函数中 mCanvas = new QgsMapCanvas(this); mCanvas->setParent(ui->widgetCanvas); // widgetCanvas是UI中的QWidget容器 ui->widgetCanvas->layout()->addWidget(mCanvas);

5.5 现象:VS调试时断点无效,提示“当前不会命中断点,尚未为文档加载任何符号”

原因:QGIS DLL未加载调试符号(.pdb)。QGIS Dev包不含PDB,需自行编译或下载Debug版本。
解决:

  1. 下载qgis-3.28.0-win64-dev-debug.7z(若存在)
  2. 或在VS中Debug → Options → Debugging → Symbols,添加QGIS PDB服务器(需QGIS官方提供)
  3. 实用替代方案:在关键位置加qDebug() << "here";,配合Output窗口查看输出,比断点更可靠。

6. 进阶技巧:打包成单文件.exe并嵌入天地图底图URL(国内可用方案)

6.1 使用windeployqt + 自定义脚本打包:摆脱QGIS安装依赖

windeployqt只能处理Qt依赖,无法复制QGIS DLL。需编写批处理package.bat:

@echo off set APP_DIR=D:\gisdev\QgisDesktopApp\x64\Release set QGIS_DEV=D:\qgis-dev set OUT_DIR=D:\gisdev\QgisDesktopApp\dist mkdir %OUT_DIR% copy %APP_DIR%\QgisDesktopApp.exe %OUT_DIR% copy %QGIS_DEV%\bin\qgis_core.dll %OUT_DIR% copy %QGIS_DEV%\bin\qgis_gui.dll %OUT_DIR% copy %QGIS_DEV%\bin\Qt5Core.dll %OUT_DIR% copy %QGIS_DEV%\bin\Qt5Gui.dll %OUT_DIR% copy %QGIS_DEV%\bin\Qt5Widgets.dll %OUT_DIR% copy %QGIS_DEV%\bin\Qt5Network.dll %OUT_DIR% copy %QGIS_DEV%\bin\Qt5Svg.dll %OUT_DIR% copy %QGIS_DEV%\bin\libwinpthread-1.dll %OUT_DIR% :: 复制QGIS资源(必需!否则报错“Could not find style ‘default’”) xcopy /E /I %QGIS_DEV%\share\qgis %OUT_DIR%\share\qgis :: 复制GDAL数据(必需!否则坐标系识别失败) xcopy /E /I %QGIS_DEV%\share\gdal %OUT_DIR%\share\gdal echo Packaging done. Run %OUT_DIR%\QgisDesktopApp.exe pause

6.2 天地图底图集成:用QgsXyzConnection而非WMS(规避跨域与证书问题)

天地图Web服务(http://t0.tianditu.gov.cn/vec_w/wmts?...)是WMTS协议,但QGIS C++ API对WMTS支持有限。更稳方案是XYZ瓦片:

// 创建天地图矢量底图 QgsRasterLayer *tdtVec = new QgsRasterLayer( "type=xyz&url=https://t0.tianditu.gov.cn/vec_w/wmts?service=WMTS&request=GetTile&version=1.0.0&layer=vec&style=default&format=tiles&tileMatrixSet=w&TileMatrix={zoom}&TileRow={y}&TileCol={x}&tk=your_token", "Tianditu Vector", "wms" ); // 但WMS在C++中需QgsWmsProvider,复杂度高。改用XYZ(QGIS 3.16+支持) QgsRasterLayer *tdtVecXYZ = new QgsRasterLayer( "type=xyz&url=https://t0.tianditu.gov.cn/vec_w/tile/{z}/{y}/{x}.png&zmax=18&zmin=0&crs=EPSG:3857", "Tianditu Vector XYZ", "wms" );

关键参数说明:

  • type=xyz:强制使用XYZ瓦片提供器
  • url:天地图官方XYZ模板,{z}/{y}/{x}为瓦片坐标占位符
  • zmax/zmin:缩放级别范围,天地图公开服务为0-18
  • crs=EPSG:3857:必须指定,否则QGIS用默认WGS84导致偏移

注意:天地图需申请tk(token),免费配额足够测试。URL中tk=your_token替换为实际token,否则返回403。

6.3 离线PROJ数据库:防止“Unknown CRS”错误的后悔药

QGIS依赖proj.db查找坐标系,该文件位于share/proj/。若打包时漏掉,所有坐标系显示为Unknown CRS。验证方法:运行QgsCoordinateReferenceSystem::fromEpsgId(4326).isValid()返回false。
补救措施:在main()中initQgis()前,强制设置PROJ数据路径:

qputenv("PROJ_LIB", "D:/qgis-dev/share/proj"); // 必须是绝对路径 QgsApplication::setPrefixPath("D:/qgis-dev/share/qgis", true);

从那以后我每次打包新版本,都强制走一遍dumpbin /dependents QgisDesktopApp.exe,确认qgis_core.dll、qgis_gui.dll、Qt5Core.dll等关键DLL都在dist目录,且share子目录结构完整。少一个proj.db,用户打开就报错,而你收到的反馈只会是“软件打不开”,没人告诉你缺了哪个文件。希望帮到你。

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

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

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

立即咨询