资讯动态

QGIS二次开发实战:主窗口布局与图层管理器实现

发布时间:2026/10/20 17:26:49 来源:尧图企业网站定制
做GIS桌面端开发十有八九会接触到QGIS。真正要用好它光会装个软件、点点界面远远不够二次开发才是让它从别人的工具变成自己的产品的关键一步。这篇是这个系列的第三篇前面我们已经聊过环境搭建和基础工程结构这一回专门讲两个绕不开的门槛主窗口布局怎么设计图层管理器怎么实现。我见过不少半路转来做QGIS二次开发的同行卡在最开始的地方往往不是算法或者数据处理而是连一个像样的应用框架都搭不起来。界面上就一个光秃秃的画布没有图层列表打开数据乱糟糟堆在一起也没有任何层次关系和操作入口。这其实就是没有把窗口布局和图层管理器这两块地基打好。这篇文章我会以QGIS 3.18.3 Qt 5.11.2 VS2019这套常见组合为例把完整的实现思路和关键代码拆开讲适合正在做GIS平台研发、地图可视化工具开发或想基于QGIS做定制系统的开发者参考。1. 项目背景与整体思路1.1 为什么绕不开窗口布局和图层管理器在做QGIS二次开发的时候我们最终交付的往往是一个完整的桌面应用而不是一堆零散的类库调用。用户打开软件看到的第一眼就是界面菜单栏在哪里、工具栏放什么、地图在中间还是左边、图层列表能不能拖拽浮动。这些都决定了这个应用像不像一个正经的GIS工具。而在所有这些界面元素里图层管理器是最特殊的一个。它可以理解为整个GIS应用的目录页。一个项目可能加载几十个图层矢量、栅格、分组、样式混在一起没有图层树的面板用户根本无法理解数据之间的空间关系和叠加顺序。QGIS原生界面中的图层面板本质上就是一棵树节点对应图层和分组叶子节点对应真正的数据图层每个节点前面还有一个复选框控制可见性。二次开发场景下我们几乎总是需要把这一套交互能力搬进自己的应用里。你可以直接用QGIS的底层类库把它们组装出来而不是重新发明一套。这也是QGIS二次开发比从头写一个GIS引擎要划算得多的地方。1.2 二次开发的两条路怎么选从实际项目经验看基于QGIS做C二次开发通常有两条路线。一条是直接改QGIS源码。把QGIS源码下下来用CMake和VS2019编译然后改它的客户端主程序代码替换掉自带的界面逻辑。这条路适合对QGIS本身做深度定制、要改到底层行为的团队但问题也很明显源码编译过程长之后每次合并上游更新都要解决冲突团队里每个人都要维护复杂的编译环境越往后越累。另一条是把QGIS当成一个SDK用它的公开类库搭建独立应用。我们自己创建QApplication或者QgsApplication自己写主窗口通过API调用QGIS的渲染、数据访问、坐标参考系统等功能。这是绝大多数二次开发项目的现实选择也是这篇文章采用的方式。窗口布局完全由我们自己掌控图层管理器用QgsLayerTreeView等相关类来实现代码结构清晰后续维护成本低。我个人的建议是如果没有特殊到必须修改QGIS内部实现的需求尽量走第二条路。这也是微软系开发者在VC环境下最常见的技术选型。2. 环境组合与工程配置要点2.1 QGIS、Qt、VS2019三者怎么配合标题里的三个版本号代表了一个典型的Windows开发环境VS2019作为IDE和C编译器Qt 5.11.2作为图形界面框架QGIS 3.18.3作为GIS功能库。三者关系可以用一句话说清楚在VS2019里写C代码用Qt的类来创建界面调用QGIS的类来做地图和空间数据的处理。但这里有一个必须说的坑QGIS 3.18官方在CMake配置阶段要求的最低Qt版本其实是5.15.2左右如果用5.11.2直接去编译QGIS 3.18.3的源码大概率会在版本检查这一步被拦下来。实际操作中不少人会用低版本的Qt编译较老版本的QGIS比如QGIS 3.4对应Qt 5.11或者在升级QGIS的同时把Qt一起升级。如果你手里的环境已经是3.18.3加5.11.2那通常意味着你在编译时手动关闭了版本检查或者使用了某个特定发行版的预编译包这时候要特别注意运行时有没有隐藏的兼容问题。我建议在动手写代码之前先用一个小demo验证一下基本库能不能跑起来。创建一个最简单的Qt窗口程序调用QgsApplication初始化逻辑如果这都能正常弹窗且不报加载依赖的错误后面写界面就踏实了。2.2 头文件、库文件、运行时依赖怎么配二次开发工程配置最容易翻车的点有三个头文件路径、库文件路径、运行时DLL查找路径。先说头文件。QGIS安装目录下通常会有include目录里面按照模块拆分了头文件比如qgis_core、qgis_gui、qgis_analysis。在使用图层树相关功能时我们需要把qgis_gui的include路径加进来因为QgsLayerTreeView、QgsLayerTreeModel这些类都归在GUI模块里。另外qgis_core是几乎所有功能的基础必然要包含。在VS2019里配置C/C附加包含目录时建议把QGIS的include和Qt的include都加上避免写代码的时候每个文件都要写一长串相对路径。再说库文件。链接阶段常见的问题是忘记加对应的lib文件比如用了QgsMapCanvas却只链接了qgis_core.lib会直接报一堆无法解析的外部符号。正确做法是根据用到的类模块把qgis_core.lib、qgis_gui.lib都加进附加依赖项如果涉及分析功能还要加qgis_analysis.lib。最后是运行时。程序启动时如果可执行文件旁边找不到QGIS的DLL和Qt的DLL会直接弹窗告诉你找不到某个动态库。这里有个土办法我一直用把所有DLL所在的目录通过系统Path环境变量加进去或者在工程配置里把PATH调试环境变量设置成QGIS安装目录下的bin目录和Qt的bin目录。不要小看这一步很多新手写的程序在自己机器上能跑换一台机器就报错多半就是运行时路径没有处理好。2.3 CMake工程骨架参考VS2019下搭建QGIS工程我习惯用CMake而不是直接建一个空的VS项目。原因是CMake对依赖查找更规范团队成员之间也好复制环境。一个大致可以用的CMakeLists.txt骨架大概长这样cmake_minimum_required(VERSION 3.16) project(QgisDevDemo) set(CMAKE_PREFIX_PATH D:/Qt/Qt5.11.2/5.11.2/msvc2017 D:/QGIS/3.18.3 ) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets ) find_package(Qgis COMPONENTS Core Gui REQUIRED) add_executable(qgis_demo main.cpp MainWindow.cpp MainWindow.h ) target_link_libraries(qgis_demo Qt5::Core Qt5::Gui Qt5::Widgets Qgis::Core Qgis::Gui )这里有两个柔性配置要注意。一个是CMAKE_PREFIX_PATH要指向Qt和QGIS的安装根目录这样find_package才能找到对应的CMake配置文件。另一个是Qt 5.11.2的预编译包大多是msvc2017工具集编出来的VS2019是向下兼容的可以直接用但如果遇到编译错误或者链接错误可以去工具集这边找原因。3. 主窗口布局搭出GIS应用的骨架3.1 核心思路QMainWindow 中央地图画布 停靠面板一个典型的GIS主窗口结构上是高度统一的菜单栏和工具栏在最上面中间大部分区域是地图显示区旁边或者底部是图层面板、属性面板这类辅助窗口。这个布局在Qt里实现起来非常自然因为QMainWindow天生就支持菜单栏、状态栏、工具栏和停靠窗口这套组合。我们只需要把QgsMapCanvas这个地图画布控件设置为QMainWindow的中央部件用QDockWidget把图层管理器包起来然后停靠在左侧或者右侧。DockWidget是Qt里非常好用的面板容器用户可以拖动它改变位置可以把它拖出来变成独立浮动窗口也可以关闭它。对GIS工具来说这种灵活性刚好满足不同用户的使用习惯。地图画布是整个应用的心脏所有图层最终都要在上面绘制。QgsMapCanvas本身不是简单的一个绘制控件它内部管理了地图范围、坐标参考系统、渲染顺序还支持各种地图工具比如缩放、平移、查询。把它放到中央配合四个方向的停靠区域应用的基本骨架就出来了。3.2 菜单、工具栏、状态栏的落实菜单栏用来放全局操作比如打开图层、保存项目、退出程序。工具栏则把高频操作以图标形式放在显眼位置。这里不建议把菜单和工具栏做成完全两份代码更常见的做法是定义一组QAction同时add到菜单和工具栏里实现一处触发、多处入口。状态栏在GIS应用里有一个很有价值的用途实时显示鼠标所在位置的坐标。QgsMapCanvas在鼠标移动时会发出xyCoordinates信号把当前光标的经纬度或者投影坐标传出来。我们只需要连接这个信号把坐标值显示在状态栏上即可。别小看这个功能实际使用中判断数据是否叠加正确、粗略评估距离经常靠它。举个实现片段connect(mMapCanvas, QgsMapCanvas::xyCoordinates, this, [this](const QgsPointXY pt) { statusBar()-showMessage( QStringLiteral(X: %1 Y: %2).arg(pt.x()).arg(pt.y())); });这个简单的联动效果会让整个应用立刻显得完整很多。3.3 布局状态的保存与恢复窗口布局这件事做到能用的门槛很低但做到好用还得考虑用户体验。用户把图层面板从左边拖到右边调整了工具栏的位置退出程序后再打开如果一切恢复原样他会觉得这个软件很听话。QMainWindow提供了saveState和restoreState两个方法可以把当前窗口各个停靠组件的位置状态序列化成字节数组。再把这份数组交给QSettings存到本地配置文件里下次启动时读取并恢复即可。这里有一个特别容易踩的坑恢复布局的前提是各个DockWidget和工具栏必须设置了唯一的objectName。如果没有这个名字状态数据记录不了这个面板是哪个面板恢复的时候就会错乱。我自己就遇到过布局恢复后图层面板跑到右下角的情况排查了半天最后发现就是objectName漏写了。// 保存 QSettings settings; settings.setValue(UI/MainWindowState, this-saveState()); // 恢复 this-restoreState(settings.value(UI/MainWindowState).toByteArray());很多人觉得布局保存是小功能不值得研究。但真做商用软件时用户不会管你功能多强大界面状态乱了他就觉得软件有问题。布局持久化这块尽量早做越早成本越低。4. 图层管理器从建模到联动4.1 图层树的三层模型树、模型、视图图层管理器在QGIS里不是一个简单的控件它由三个层次协作完成。最底层是数据模型QgsLayerTree它描述了图层和分组的层级关系比如根节点下面有分组A分组A里有两个图层中间层是QgsLayerTreeModel它把树的节点转换成Qt标准的模型数据供视图读取最上层是QgsLayerTreeView它负责把模型渲染到界面上处理用户的鼠标交互。这个三层结构和Qt经典的MVC模式完全对应。很多二次开发者在实现图层面板时犯的错误是把图层列表直接用一个QListWidget代替自己手写增删改逻辑。一开始还能跑等到要处理分组嵌套、图层排序、图例符号显示时代码马上失控。QGIS既然提供了成熟的树模型直接复用是性价比最高的选择。使用方式很简单从QgsProject这个全局单例拿到图层树根节点把它交给QgsLayerTreeModel再把模型赋给QgsLayerTreeViewmRootNode QgsProject::instance()-layerTreeRoot(); mLayerTreeModel new QgsLayerTreeModel(mRootNode, this); mLayerTreeView-setModel(mLayerTreeModel);之后凡是往QgsProject里加图层图层树会自动出现对应的节点不需要我们手动插入。4.2 桥接器图层树的开关怎么传到画布图层树里的每个节点前面都有一个复选框用户勾选或者取消勾选本意是控制图层在地图上是否可见。那么这个状态变化是谁来通知地图画布的呢答案是一个叫QgsLayerTreeMapCanvasBridge的桥接器。桥接器做的事情说起来很简单它同时监听图层树的可见性变化信号以及地图画布当前显示图层的需求然后同步两者状态。我们在搭建主窗口时只需要把图层树根节点和画布对象传给它mBridge new QgsLayerTreeMapCanvasBridge( mRootNode, mMapCanvas, this );之后用户勾选图层、取消勾选、调整叠加顺序地图画布都会自动刷新。这个机制极其重要理解它以后很多图层操作没有反映到地图上的问题都能第一时间想到去查桥接器的连接状态。不过也要注意桥接器默认的同步方向是从树到画布。如果我们在代码里直接往画布添加图层而绕过图层树界面和地图就有可能不一致。后面我们会谈到正确加载图层的方式核心原则永远是统一通过QgsProject来管理图层树和画布都从项目层面获得通知这样一致性由框架保证。4.3 右键菜单与常用图层操作图层树的静态展示只是第一步用户还期待右键能弹出操作菜单。QGIS原生图层面板里右键菜单包含缩放至图层、打开属性表、重命名、移除图层、修改样式等。在自己开发的应用里这些操作一般按项目需要裁剪但缩放至图层、重命名、移除图层这三个几乎总是保留的。为图层树添加右键菜单最直接的方式是继承QgsLayerTreeView重写contextMenuEvent事件。不过如果是快速原型也可以像下面这样使用自定义上下文菜单策略代码量更小mLayerTreeView-setContextMenuPolicy(Qt::CustomContextMenu); connect(mLayerTreeView, QWidget::customContextMenuRequested, this, [this](const QPoint pos) { QgsLayerTreeNode *node mLayerTreeView-currentNode(); if (!node || QgsLayerTree::isGroup(node)) return; QMenu menu(this); QAction *actZoom menu.addAction(QStringLiteral(缩放至图层)); QAction *actRemove menu.addAction(QStringLiteral(移除图层)); QAction *actRename menu.addAction(QStringLiteral(重命名)); QAction *result menu.exec(mLayerTreeView-viewport()-mapToGlobal(pos)); if (result actZoom) { QgsLayerTreeLayer *nodeLayer QgsLayerTree::toLayer(node); if (nodeLayer nodeLayer-layer()) { mMapCanvas-setExtent(nodeLayer-layer()-extent()); mMapCanvas-refresh(); } } else if (result actRemove) { QgsLayerTreeLayer *nodeLayer QgsLayerTree::toLayer(node); if (nodeLayer nodeLayer-layer()) { QgsProject::instance()-removeMapLayer(nodeLayer-layer()); } } else if (result actRename) { mLayerTreeView-edit(node); } });注意移除图层这里正确的API是调用QgsProject::removeMapLayer而不是直接从树节点上摘掉。原因是树节点只是图层在项目中的引用真正的图层所有权在项目里。直接删除节点但保留项目中的图层会导致图层还占着资源甚至继续参与渲染。从项目层面移除树和桥接器都会收到通知并自动更新。5. 完整实操从零搭一个带图层面板的GIS窗口5.1 启动入口与QGIS环境初始化开发QGIS桌面应用入口处初始化是一个不可跳过的步骤。QGIS运行时需要知道自己的安装路径从而定位插件、数据提供程序、SRS数据库等资源文件。这段初始化逻辑放在main函数里#include QgsApplication #include MainWindow.h int main(int argc, char *argv[]) { QgsApplication app(argc, argv, true); QgsApplication::setPrefixPath(D:/QGIS/3.18.3/apps/qgis, true); QgsApplication::initQgis(); MainWindow w; w.show(); int ret app.exec(); QgsApplication::exitQgis(); return ret; }注意一个细节QgsApplication是QApplication的子类所以这里不需要再单独创建QApplication。第三个参数传true表示启用GUI支持这是桌面应用的标准选择。setPrefixPath的路径非常关键。如果传入的路径不对初始化时QGIS会找不到各种资源运行阶段最常见的表现是打开矢量图层时报无效数据源或者加载栅格时找不到GDAL驱动。5.2 主窗口骨架代码主窗口的构造函数里我们把前面提到的各个部分串联起来。下面是一个浓缩版的主窗口初始化代码MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { // 中央地图画布 mMapCanvas new QgsMapCanvas(this); mMapCanvas-setCanvasColor(Qt::white); mMapCanvas-enableAntiAliasing(true); setCentralWidget(mMapCanvas); // 图层树相关对象 mRootNode QgsProject::instance()-layerTreeRoot(); mLayerTreeModel new QgsLayerTreeModel(mRootNode, this); mLayerTreeModel-setFlag(QgsLayerTreeModel::AllowNodeChangeVisibility); mLayerTreeModel-setFlag(QgsLayerTreeModel::AllowNodeRename); mLayerTreeModel-setFlag(QgsLayerTreeModel::AllowNodeChangeIcon); mLayerTreeModel-setFlag(QgsLayerTreeModel::ShowLegendAsTree); mLayerTreeView new QgsLayerTreeView(this); mLayerTreeView-setModel(mLayerTreeModel); // 把图层树塞进停靠面板 QDockWidget *dock new QDockWidget(QStringLiteral(图层), this); dock-setObjectName(QStringLiteral(LayersDock)); dock-setWidget(mLayerTreeView); addDockWidget(Qt::LeftDockWidgetArea, dock); // 桥接图层树与地图画布 mBridge new QgsLayerTreeMapCanvasBridge( mRootNode, mMapCanvas, this); // 恢复上次布局 restoreState(mSettings.value(UI/MainWindowState).toByteArray()); }这里setFlag的几个选项值得逐个说。AllowNodeChangeVisibility允许用户通过复选框控制图层可见性不设置这个flag复选框就看不了也点不了。AllowNodeRename允许重命名节点对应前面右键菜单里的重命名操作。ShowLegendAsTree则让模型尝试用图例符号填充节点视觉效果上和原生QGIS更接近。5.3 加载图层并验证联动窗口搭好以后下一步就是验证核心联动逻辑加载一个图层看它是否出现在左侧图层面板里并且地图画布是否自动显示了该图层。加载图层的代码通常放在打开文件的槽函数中。这里以矢量图层为例。注意新建QgsVectorLayer之后不要立刻添加到画布或者树而是统一交给QgsProjectQStringList filePaths; QString file QFileDialog::getOpenFileName( this, QStringLiteral(打开矢量图层), QString(), QStringLiteral(Shapefile (*.shp);;GeoPackage (*.gpkg))); if (file.isEmpty()) return; QgsVectorLayer *layer new QgsVectorLayer( file, QFileInfo(file).completeBaseName(), QStringLiteral(ogr)); if (!layer-isValid()) { QMessageBox::warning(this, QStringLiteral(错误), QStringLiteral(无法加载图层%1).arg(layer-lastError())); return; } QgsProject::instance()-addMapLayer(layer); mMapCanvas-setExtent(layer-extent()); mMapCanvas-refresh();代码跑起来以后如果一切正常左侧图层树应该出现一个新节点地图画布缩放到这个图层的范围并显示内容。此时去图层树里取消勾选画布上的内容会立刻消失。这就是桥接器在起作用。如果不正常优先确认三件事QgsApplication是否正常初始化、桥接器是否被创建、QgsProject的addMapLayer是否成功。这三点出问题都会导致图层树和画布的联动失效。6. 常见问题与排查技巧6.1 版本组合与编译期的雷开头提到过QGIS 3.18.3和Qt 5.11.2存在版本匹配上的隐患。如果CMake配置阶段报Qt版本过低可以在能找到对应依赖的前提下更换Qt版本到5.15LTS或者换一版兼容的QGIS。实际项目中我见过不少直接把CMake版本检查if语句注释掉的强行做法进程能编出来但运行时偶尔出现字体渲染异常或信号槽断链问题排查起来非常隐蔽。另外Qt 5.11.2针对VS2019的适配也要留意。VS2019默认的工具集是v142而Qt 5.11.2的预编译库很可能用的是v141。多数情况下MSVC是向后兼容的直接链接也能跑通但一旦遇到链接错误建议把工程的平台工具集切换到v141再重新编译。6.2 图层树不显示或空白图层树面板打开了但里面什么都没有。这种情况最常见的三个原因QgsProject的根节点是空的、模型没有正确关联到根节点、视图没有设置模型。还有一个容易忽略的原因是初始化顺序不对。必须保证根节点获取和模型创建发生在QgsApplication初始化之后。如果图层面板出现但节点数据是旧的比如程序启动后加载了一个图层面板里却始终是初始状态那就要看看是不是在多个地方创建了QgsLayerTreeModel实例。一个树节点不能同时被两个不同的模型绑定显示这种情况行为会变得不可预期。正确做法是主窗口内始终只有一个模型实例所有对图层树的增删都通过项目层的API来触发。6.3 图层可见性与画布不同步勾选图层面板的复选框地图上的图层没有反应这是桥接器相关问题的一个典型症状。排查思路是这样的首先确认QgsLayerTreeMapCanvasBridge对象是否还活着。如果主窗口里有代码在后续执行中误删了这个对象或者桥接器创建后没有保持引用它被垃圾回收或者析构掉同步自然中断。其次确认图层的可见性是否真的被修改了。可以在勾选复选框后打印QgsLayerTreeLayer节点的isVisible()结果如果节点本身的可见性没有变化应该去查模型标志位是否允许改变可见性也就是AllowNodeChangeVisibility那个flag。如果节点可见性已经是false了但画布还在绘制它问题大概率出在画布缓存没有刷新调用mMapCanvas-refresh()就能解决。6.4 运行时找不到QGIS插件与数据驱动程序编译通过一运行就报无法加载GDAL数据提供程序或者找不到qgis_core插件。这类问题通常和环境变量有关。QGIS运行时通过QgsApplication::setPrefixPath定位插件目录官方安装包结构下数据提供程序插件在apps/qgis/plugins目录里。遇到这种情况我通常会先在启动代码里把当前初始化路径打印出来对比实际目录结构。很多中文路径或者带空格路径的安装目录也会在某些版本的QGIS中引发加载失败这种情况下将QGIS安装到纯英文无空格路径是成本最低的解决方案。还有一个经验64位程序和32位程序混用会引发同样的崩溃。QGIS 3.18默认是64位的Qt也尽量保持64位一致一旦某个依赖用了32位编译版本启动阶段各种诡异崩溃轮流出现。7. 我的几点实操体会窗口布局和图层管理器做完一个QGIS二次开发应用的地基就算打完了。我个人的经验是这个阶段宁可慢一点把每次改动都搞清楚也不要图快直接跳到业务功能。因为后面不管做数据查询、空间分析还是专题制图所有操作几乎都要依托图层面板选中的图层和地图画布当前的范围。这里结构没理清后面每加一个功能都要回头补课。最后分享一个实用的小技巧如果你希望自己的GIS应用切图切得顺手一些可以在图层树模型里加上AllowNodeChangeIcon标志位然后为不同类别的数据源动态修改节点图标矢量用一组图标栅格用另一组。这个细节虽然不起眼但用户在几百个图层里找数据时图标带来的辨识度提升非常明显。后续还可以继续扩展出图例预览、图层组拖拽排序这些能力图层管理这一块做好了整个应用的品相会上一个大台阶。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑