简介本资源是一份面向GIS开发初学者与C桌面应用开发者的技术实践项目聚焦于基于QGIS SDK与Qt框架在Visual Studio环境下开展轻量级GIS桌面工具的二次开发重点解决矢量数据Shapefile/GeoJSON/KML等的加载、解析与基础地图渲染问题。压缩包共10个文件含3个核心cpp源文件如main.cpp、gisTest2.cpp、2个头文件定义图层树菜单交互逻辑、1个说明文档.docx、1个README.md、1个PNG界面截图、1个txt说明文件及.gitignore配置总大小仅59KB结构精炼便于快速理解QGIS嵌入式集成的关键流程。已有102人学习下载适合希望掌握QGIS API调用、Qt UI集成、跨平台GIS应用构建的中级开发者。读者可直接复用项目骨架快速启动具备矢量加载能力的GIS桌面程序并参考代码中LayerTreeView菜单扩展、QgsApplication初始化、矢量图层添加等典型实现模式。1. 项目缘起为什么选择QGISQT在VS里搞二次开发最近在做一个内部工具需求很明确需要一个能加载、查看、简单编辑矢量数据的桌面应用比如加载一些项目区域的边界、道路网再叠加点项目点位信息。团队里没人想从头造轮子去写一个GIS引擎那太费劲了。市面上成熟的GIS桌面软件不少像ArcGIS、QGIS功能是强但要么商业授权贵要么太“重”我们只需要其中一小部分核心功能而且希望界面和逻辑能完全自定义嵌入到我们自己的业务流程里。这时候二次开发就成了最务实的选择。QGIS作为开源GIS的扛把子其核心能力数据格式支持、坐标转换、渲染、基础分析是经过千锤百炼的。而QT是C领域里做跨平台桌面UI的顶级框架生态和稳定性都没得说。我们的主力开发环境又是Visual Studio在Windows平台下VS的调试和开发体验是最好的。所以很自然地技术栈就锚定在了“基于QGIS与QT框架在VisualStudio环境下进行二次开发”。这个组合的优势在于你可以站在QGIS这个“巨人”的肩膀上直接调用其强大的地理数据处理能力同时用QT来构建完全符合业务需求的轻量级界面最终通过Visual Studio这个高效的IDE完成编译、调试和集成。最终产物是一个独立的exe不依赖完整的QGIS桌面环境干净利落。下面我就把这次从环境搭建到跑通第一个功能的完整过程以及中间踩过的坑详细拆解一遍。2. 环境搭建VSQTQGIS SDK的“三位一体”配置这是整个项目最磨人但也最关键的一步。配置不对后面全是徒劳。我们的目标是在Visual Studio中创建一个QT项目并能成功链接QGIS的库和头文件进行开发。2.1 基础环境准备VS与QT的联姻首先确保你有一个较新版本的Visual Studio比如VS2019或VS2022并安装“使用C的桌面开发”工作负载。这是基础不再赘述。接下来是QT。这里有个关键选择必须使用与后续QGIS SDK编译环境相匹配的QT版本。QGIS官方有明确的编译环境说明通常对于Windows会使用特定版本的MSVC编译器搭配特定版本的QT。例如QGIS 3.28 LTR版本可能推荐使用VS2019MSVC 16和QT 5.15.2。你需要去QT官网下载对应版本的离线安装包或者使用QT在线安装器在安装时勾选对应的MSVC组件。安装好后最关键的一步是在Visual Studio中集成QT。这需要安装一个叫“Qt Visual Studio Tools”的扩展。直接在VS的扩展管理器中搜索安装即可。安装完成后在VS的菜单栏会出现“Qt VS Tools”。你需要在这里点击“Qt Versions”添加你刚才安装的QT路径指向包含qmake.exe的目录例如C:\Qt\5.15.2\msvc2019_64。添加成功后VS就能识别并使用这个QT版本来创建和管理项目了。注意很多初次配置的人会在这里卡住表现为创建QT项目时报错。请务必检查1) VS的QT扩展是否安装成功2) 添加的QT版本路径是否正确3) 该QT版本的架构x64/x86是否与你的项目设置一致。我们一般都用64位。2.2 QGIS SDK获取官方构建与自行编译的取舍要调用QGIS你需要它的开发库头文件.h、链接库.lib和运行时依赖DLL。有两个主要途径使用OSGeo4W网络安装器获取官方SDK推荐新手这是最省事的方法。运行OSGeo4W安装程序选择“Advanced Install”在安装类型里除了默认的“Express”包你需要在“Libs”分类下勾选以qgis-dev开头的包例如qgis-dev和qgis-dev-dbg。这个qgis-dev包就包含了QGIS核心库、头文件以及相关的依赖库如GEOS, Proj, GDAL等。安装路径通常是C:\OSGeo4W或C:\OSGeo4W64。安装后所需的include、lib和bin目录就都有了。自行从源码编译QGIS这能让你获得最干净、版本最匹配的SDK但过程极其耗时且容易出错。你需要准备CMake、编译好的依赖库可以通过OSGeo4W获取qgis-dev-deps包然后按照QGIS官方Wiki的指引一步步配置、生成VS工程、编译。除非你有定制QGIS核心代码的硬性需求否则不建议走这条路。对于我们的轻量级工具开发强烈推荐使用第一种方法。它帮你解决了所有复杂的依赖关系。假设我们通过OSGeo4W安装到了D:\OSGeo4W64那么关键的几个路径是头文件目录D:\OSGeo4W64\include库文件目录D:\OSGeo4W64\lib运行时DLL目录D:\OSGeo4W64\bin这个路径需要添加到系统的PATH环境变量或者将DLL复制到你的exe输出目录2.3 Visual Studio项目配置链接与调试的细节在VS里创建一个QT Widgets Application项目后真正的配置工作才开始。你需要告诉VS去哪里找QGIS。包含目录Include Directories在项目属性 - C/C - 常规 - 附加包含目录中添加QGIS的头文件路径。通常不止一个D:\OSGeo4W64\include D:\OSGeo4W64\include\qgis可能还需要添加其依赖库的头文件路径如D:\OSGeo4W64\include\gdal等具体取决于你用了哪些类。一个稳妥的方法是如果编译时报错找不到某个头文件再根据错误信息将其父目录添加进来。库目录Library Directories在项目属性 - 链接器 - 常规 - 附加库目录中添加QGIS的库文件路径D:\OSGeo4W64\lib附加依赖项Additional Dependencies在项目属性 - 链接器 - 输入 - 附加依赖项中添加你需要链接的.lib文件。这里不能一股脑全加要根据你的功能来。最核心的几个通常是qgis_core.lib qgis_gui.lib qgis_app.lib (如果你需要一些高级的应用程序级功能)同样你可能还需要链接GDAL、Proj等库例如gdal_i.lib、proj_6_2.lib。这些库的名字和版本号需要根据你安装的OSGeo4W版本来确定。一个实用的技巧是去D:\OSGeo4W64\lib目录下查看所有以.lib结尾的文件按需添加。预处理器定义Preprocessor Definitions通常需要添加_USE_MATH_DEFINES。如果遇到与QT宏的冲突特别是slots可能还需要添加QT_NO_KEYWORDS。C语言标准设置为C17或更高QGIS新版本需要。运行时库设置为多线程DLL (/MD)以匹配QGIS官方构建的配置。环境变量为了能在调试时正常运行程序必须确保D:\OSGeo4W64\bin在系统的PATH环境变量中或者更简单的方法是在VS项目属性 - 调试 - 环境中添加一行PATHD:\OSGeo4W64\bin;%PATH%。这样在启动调试时系统就能找到所有必需的DLL。配置完成后可以写一个最简单的测试代码例如在main.cpp里包含#include qgsapplication.h并尝试初始化一个QgsApplication对象。如果能编译通过并运行弹出一个空的QT窗口说明基础环境配置成功了。3. 核心功能实现从零构建一个GIS视图器环境配通只是万里长征第一步。接下来我们要实现核心功能一个能加载并显示矢量数据的窗口。这涉及到QT界面与QGIS渲染引擎的融合。3.1 构建基础界面MapCanvas与Layout在QT设计师里我们可以拖拽一个QWidget作为我们的主地图显示区域。但要让这个Widget显示地图我们需要用到QGIS的核心类QgsMapCanvas。首先在UI头文件例如mainwindow.h中我们需要前向声明并包含必要的QGIS头文件#include QMainWindow // 前向声明QGIS类避免在头文件中包含过重的头文件 class QgsMapCanvas; class QgsVectorLayer; namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow(); private: Ui::MainWindow *ui; // 声明QgsMapCanvas指针 QgsMapCanvas *mMapCanvas nullptr; };在源文件mainwindow.cpp的构造函数中我们进行初始化#include mainwindow.h #include ui_mainwindow.h // 包含QGIS核心头文件 #include qgsmapcanvas.h #include qgsapplication.h #include qgsvectorlayer.h #include qgsproject.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-setupUi(this); // 1. 创建QgsMapCanvas对象 mMapCanvas new QgsMapCanvas(this); // 2. 将MapCanvas设置到我们预留的QWidget中假设我们在UI中放了一个QWidget对象名为mapWidget ui-mapWidget-layout()-addWidget(mMapCanvas); // 确保mapWidget有一个布局 // 或者简单替换ui-mapWidget-layout()-addWidget(mMapCanvas); // 3. 设置Canvas的一些基本属性 mMapCanvas-setCanvasColor(Qt::white); mMapCanvas-enableAntiAliasing(true); mMapCanvas-setParallelRenderingEnabled(true); // 此时地图画布已经就位但还没有任何数据。 }这里有个关键点QgsMapCanvas本身是一个QWidget所以它可以像任何QT控件一样被嵌入到布局中。我们通常不会在设计师里直接放置它而是动态创建并放入一个容器Widget。3.2 加载矢量数据理解QgsVectorLayer与数据源URI地图画布准备好了接下来就是喂数据。QGIS支持数十种矢量数据格式其核心抽象是QgsVectorLayer矢量图层。创建一个图层需要两个关键信息数据源路径URI和图层类型Provider。以加载一个Shapefile文件为例bool MainWindow::loadShapefile(const QString filePath) { // 构建数据源URI。对于ShapefileURI就是文件路径。 // 第二个参数是给图层起个名字第三个参数是数据提供者对于本地文件通常是 ogr QgsVectorLayer *layer new QgsVectorLayer(filePath, QFileInfo(filePath).baseName(), ogr); // 检查图层是否创建成功 if (!layer || !layer-isValid()) { QMessageBox::warning(this, tr(Error), tr(Failed to load layer! %1).arg(layer ? layer-error().message() : )); delete layer; return false; } // 将图层添加到当前项目QgsProject中 QgsProject::instance()-addMapLayer(layer); // 将图层设置为地图画布的当前图层如果是第一个图层这也会自动设置画布范围 mMapCanvas-setLayers(QListQgsMapLayer *() layer); mMapCanvas-setExtent(layer-extent()); // 缩放到图层范围 mMapCanvas-refresh(); // 刷新显示 return true; }这里有几个需要深入理解的地方Provider提供者ogr是QGIS中用于处理多种矢量格式Shapefile, GeoJSON, KML, GML等的底层库。对于PostGIS数据库Provider是postgres对于WMS/WFS服务Provider是wms/wfs。这个字符串决定了QGIS用什么驱动去解析你的数据源。数据源URI对于ShapefileURI简单就是文件路径。但对于其他数据源就复杂了。例如加载一个GeoPackage文件中的特定表D:/data/my.gpkg|layernameroads。加载一个WMS服务urlhttp://xxx/wmsformatimage/pnglayerslayer1。务必查阅QGIS官方文档关于数据源URI格式的部分这是加载数据时最常见的错误来源。图层有效性检查layer-isValid()至关重要。如果失败可以通过layer-error().message()获取错误信息这对于调试无法加载的数据非常有用。QgsProject这是一个单例类管理着当前项目中的所有图层、地图视图、布局等。将图层添加到QgsProject中是QGIS框架管理图层生命周期的标准方式。3.3 渲染与样式设置让地图“好看”起来加载进来的图层默认是随机单色渲染通常我们需要设置样式。QGIS的渲染系统非常强大这里介绍最简单的单一符号渲染。void MainWindow::setSimpleStyle(QgsVectorLayer *layer) { if (!layer) return; // 1. 创建一个符号例如一个红色的、1像素宽的边线黄色填充的多边形符号 QgsFillSymbol *symbol QgsFillSymbol::createSimple({ {color, yellow}, {outline_color, red}, {outline_width, 1} }); // 2. 创建一个单一符号渲染器 QgsSingleSymbolRenderer *renderer new QgsSingleSymbolRenderer(symbol); // 3. 将渲染器设置给图层 layer-setRenderer(renderer); // 4. 触发画布刷新 mMapCanvas-refresh(); }对于线图层和点图层使用QgsLineSymbol和QgsMarkerSymbol。createSimple方法接受一个属性映射可以设置颜色、大小、透明度等多种属性。更复杂的渲染如分类、分级、规则则需要构建更复杂的渲染器树这是QGIS样式系统的核心。4. 项目实战构建一个极简GIS工具框架有了加载和显示的基础我们可以搭建一个具备基本功能的工具框架。这个框架将包括菜单、工具栏、图层列表和地图画布。4.1 界面布局与图层管理QgsLayerTreeView一个典型的GIS桌面应用左边是图层列表可以控制图层可见性、顺序右边是地图画布。QGIS提供了QgsLayerTreeView和QgsLayerTreeMapCanvasBridge来轻松实现这个功能。首先在UI设计师里使用QSplitter分割窗口左边放一个QTreeView我们会在代码中将其提升为QgsLayerTreeView右边放我们之前用于容纳QgsMapCanvas的QWidget。在代码中// mainwindow.h #include QgsLayerTreeView.h #include QgsLayerTreeMapCanvasBridge.h class MainWindow : public QMainWindow { // ... private: QgsMapCanvas *mMapCanvas nullptr; QgsLayerTreeView *mLayerTreeView nullptr; QgsLayerTreeMapCanvasBridge *mBridge nullptr; }; // mainwindow.cpp MainWindow::MainWindow(QWidget *parent) : ... { ui-setupUi(this); // 初始化地图画布 mMapCanvas new QgsMapCanvas(this); ui-mapWidget-layout()-addWidget(mMapCanvas); // 初始化图层树视图 mLayerTreeView new QgsLayerTreeView(this); ui-treeWidget-layout()-addWidget(mLayerTreeView); // 假设左边容器叫treeWidget // 关键创建桥梁将图层树与地图画布关联起来 // QgsProject::instance()-layerTreeRoot() 是项目的根节点 mBridge new QgsLayerTreeMapCanvasBridge(QgsProject::instance()-layerTreeRoot(), mMapCanvas, this); // 设置桥梁这样图层树的变化如可见性、顺序会自动同步到画布 mLayerTreeView-setModel(QgsProject::instance()-layerTreeModel()); // 现在当我们通过代码 QgsProject::instance()-addMapLayer(layer) 添加图层时 // 它会自动出现在左边的图层树视图里并且其可见性、顺序可以直接在树上控制。 }通过这个桥梁我们就实现了一个基本的图层管理界面。用户可以通过勾选图层树上的复选框来显示/隐藏图层拖动图层来调整上下叠加顺序这些操作会自动反映在地图画布上。4.2 实现“加载数据”功能接下来我们实现一个通过菜单或工具栏按钮打开文件对话框加载矢量数据的功能。// 在MainWindow类中添加一个槽函数 private slots: void onActionOpenVectorTriggered(); // 在构造函数中连接信号与槽 // 假设我们有一个QAction *ui-actionOpenVector connect(ui-actionOpenVector, QAction::triggered, this, MainWindow::onActionOpenVectorTriggered); // 槽函数实现 void MainWindow::onActionOpenVectorTriggered() { // 弹出文件选择对话框过滤常见的矢量格式 QStringList filters; filters Shapefiles (*.shp) GeoJSON (*.geojson *.json) All files (*.*); QFileDialog dialog(this); dialog.setFileMode(QFileDialog::ExistingFile); dialog.setNameFilters(filters); dialog.setWindowTitle(tr(Open Vector Layer)); if (dialog.exec() ! QDialog::Accepted) return; QStringList selectedFiles dialog.selectedFiles(); if (selectedFiles.isEmpty()) return; QString filePath selectedFiles.first(); loadVectorLayer(filePath); // 调用我们之前写的加载函数 } // 增强版的加载函数支持更多格式 bool MainWindow::loadVectorLayer(const QString filePath) { QString providerKey ogr; // 默认 QString layerName QFileInfo(filePath).baseName(); QString uri filePath; // 简单根据后缀判断实际项目可能需要更复杂的逻辑 if (filePath.endsWith(.gpkg, Qt::CaseInsensitive)) { // GeoPackage: 需要用户选择具体图层这里简化处理加载第一个图层 uri filePath |layername; // 更完善的实现应该弹出对话框让用户选择图层 } QgsVectorLayer *layer new QgsVectorLayer(uri, layerName, providerKey); if (!layer || !layer-isValid()) { QMessageBox::critical(this, tr(Layer Error), tr(Could not load layer!\nProvider: %1\nPath: %2\nError: %3) .arg(providerKey).arg(filePath).arg(layer ? layer-error().message() : Unknown)); delete layer; return false; } QgsProject::instance()-addMapLayer(layer); // 由于有了LayerTreeMapCanvasBridge我们不需要手动设置画布的图层和刷新 // 图层会自动添加到树视图画布会自动更新 // 如果是第一个图层可以自动缩放至其范围 if (QgsProject::instance()-mapLayers().count() 1) { mMapCanvas-setExtent(layer-extent()); mMapCanvas-refresh(); } return true; }4.3 地图导航与基础交互一个基本的GIS视图器需要平移、缩放等导航功能。QGIS的QgsMapCanvas已经内置了这些交互工具我们需要激活它们。// 在MainWindow类中声明工具指针 private: QgsMapToolPan *mPanTool nullptr; QgsMapToolZoomIn *mZoomInTool nullptr; QgsMapToolZoomOut *mZoomOutTool nullptr; // 在构造函数或初始化函数中创建工具并关联到按钮 void MainWindow::initMapTools() { // 创建工具将地图画布作为参数传入 mPanTool new QgsMapToolPan(mMapCanvas); mZoomInTool new QgsMapToolZoomIn(mMapCanvas); mZoomOutTool new QgsMapToolZoomOut(mMapCanvas); // 假设我们有工具栏按钮ui-actionPan, ui-actionZoomIn, ui-actionZoomOut ui-actionPan-setCheckable(true); ui-actionZoomIn-setCheckable(true); ui-actionZoomOut-setCheckable(true); // 将按钮的触发信号连接到设置对应工具的槽函数 connect(ui-actionPan, QAction::triggered, [this]() { mMapCanvas-setMapTool(mPanTool); }); connect(ui-actionZoomIn, QAction::triggered, [this]() { mMapCanvas-setMapTool(mZoomInTool); }); connect(ui-actionZoomOut, QAction::triggered, [this]() { mMapCanvas-setMapTool(mZoomOutTool); }); // 设置一个默认工具 mMapCanvas-setMapTool(mPanTool); ui-actionPan-setChecked(true); }这样用户点击工具栏上的“手形”按钮地图画布就进入平移模式鼠标拖动地图点击“放大镜”按钮进入放大模式鼠标点击或框选放大。这些工具类已经处理了所有的鼠标事件逻辑。5. 编译、部署与避坑指南功能实现后最终我们需要生成一个可以独立分发的应用程序。这一步问题最多。5.1 发布构建解决DLL依赖问题在VS中编译成功生成YourApp.exe直接双击运行很可能会失败提示缺少xxx.dll。这是因为我们的程序动态链接了QGIS及其依赖库。我们需要将必要的DLL复制到exe所在目录。一个相对可靠的方法是将OSGeo4W64\bin目录下所有DLL都复制过去但这会导致发布包非常大可能超过1GB。我们需要精简。可以使用Dependencies原Dependency Walker的替代品或VS自带的dumpbin /dependents YourApp.exe命令来查看exe的直接依赖。但更麻烦的是这些DLL还有它们自己的依赖递归依赖。最实用的方法是使用QGIS官方提供的windeployqt思路的变种。OSGeo4W没有直接提供这样的工具但我们可以手动整理一个最小依赖集。一个基本的清单通常包括qgis_core.dll,qgis_gui.dll,qgis_app.dll如果你链接了Qt5Core.dll,Qt5Gui.dll,Qt5Widgets.dll等QT核心DLL可以通过QT的windeployqt.exe工具自动拷贝GDAL系列DLLgdal.dll,ogr_*.dll等Proj DLLproj_*.dllGEOS DLLgeos_c.dllSQLite DLLsqlite3.dll其他iconv.dll,expat.dll,zlib.dll,libpng16.dll等一个更省事的办法是先全部拷贝OSGeo4W64\bin下的DLL然后运行你的exe如果报错缺少某个DLL再从原目录补。运行起来后可以尝试逐步删除你认为可能不需要的DLL如qgis_3d.dll如果你没用3D功能并测试功能是否正常。这是一个试错过程。对于QT的DLL一定要使用与你编译环境匹配的QT版本的windeployqt.exe工具。在exe输出目录下执行windeployqt YourApp.exe这个命令会自动将exe所需的QT运行时DLL和插件目录如platforms拷贝过来。5.2 常见编译与运行时错误排查LNK2001/LNK2019: 无法解析的外部符号这是最常见的编译错误。原因头文件找到了但链接器找不到对应的库文件.lib。排查检查“附加依赖项”里是否添加了正确的库名注意Debug/Release版本不同Debug库通常以d结尾如qgis_cored.lib。检查“附加库目录”路径是否正确是否包含了该.lib文件所在的目录。确保你链接的库的架构x64/x86与你的项目配置一致。我们全程应使用x64。如果错误符号来自QT检查QT版本是否匹配以及是否在项目属性中正确配置了QT模块通过Qt VS Tools。C1083: 无法打开包括文件编译错误找不到头文件。原因“附加包含目录”配置错误或缺失。排查根据错误信息中缺失的头文件名去OSGeo4W64\include及其子目录下查找确保其父目录已添加到包含路径中。程序启动崩溃或运行时断言失败最常见原因DLL版本不匹配或缺失。特别是Debug版程序链接了Release版的DLL或者反之。排查确保你的程序构建配置Debug/Release与所使用的QGIS SDK、QT的DLL版本一致。通过OSGeo4W安装的SDK通常是Release版本。因此你的项目也应使用Release模式进行最终编译和发布。使用Visual Studio的调试器启动如果崩溃在DLL内部查看调用堆栈往往能定位到是哪个库的问题。地图画布一片灰白不显示数据检查图层是否有效layer-isValid()。检查坐标系数据源的坐标系CRS与地图画布的坐标系是否设置如果画布范围Extent与图层范围相差极大例如图层是经纬度画布默认是米制单位也可能看不到。可以尝试mMapCanvas-setDestinationCrs(layer-crs())和mMapCanvas-setExtent(layer-extent())。检查渲染器图层是否有有效的渲染器默认的随机单色也可能因为颜色太浅看不清。检查图层树桥梁确保QgsLayerTreeMapCanvasBridge已正确创建并关联。5.3 项目结构优化建议当项目逐渐变大为了更好的可维护性建议将QGIS和QT的路径设置为环境变量或属性表不要在每个项目的属性里硬编码D:\OSGeo4W64这样的路径。可以在VS中创建一个“属性表”.props文件里面定义QGIS_DIR、QT_DIR等变量然后在项目属性中导入这个属性表。这样当SDK路径变更时只需修改属性表即可。分离UI逻辑与GIS业务逻辑不要把所有代码都堆在MainWindow里。可以创建专门的类如MapManager来负责图层的加载、管理创建StyleManager来管理样式设置。这样代码更清晰也便于单元测试。合理使用QGIS的插件机制进阶如果你的工具功能模块相对独立可以考虑将其实现为QGIS的C插件。这样它既可以独立运行通过你的主程序也可以被加载到标准的QGIS桌面软件中使用复用性更强。但这需要遵循QGIS插件的开发规范复杂度更高。通过以上步骤一个基于QGIS和QT框架、在Visual Studio环境下开发的轻量级GIS桌面应用就具备了雏形。这个框架实现了核心的数据加载、显示、图层管理和基础导航功能并且解决了从开发到部署的关键技术问题。你可以在此基础上继续添加属性查询、要素编辑、地图打印、插件扩展等更高级的功能逐步构建出一个满足特定业务需求的强大工具。本文还有配套的精品资源点击获取