QGIS 3.6.1二次开发实战:PyQGIS环境搭建、瓦片接入与批量脚本
简介:这是一份面向QGIS 3.6.1开发者的免编译调试开发包,专供Windows 10环境下使用Visual Studio 2015进行C++插件开发或系统集成的读者使用。包内提供预编译的调试库文件(DLL/LIB)、C++头文件、示例代码、API文档及CMake构建脚本,并附有PDB调试符号,可在VS2015中直接进行源码级断点调试,彻底省去自行编译整个QGIS的繁琐流程。压缩包大小约69.99MB,文件类型以库文件、头文件、示例工程和文档为主,目录结构清晰,便于快速引入现有项目。QGIS本身支持矢量、栅格、地形等多种地理数据格式,开发者可借助这些API实现地图渲染、几何操作、空间分析等定制化任务;而调试版依赖库能让开发者深入追踪核心代码执行路径,快速定位错误来源。目前已有699人学习下载,适合具备C++基础并希望基于QGIS构建自定义功能或插件的中高级开发者。 我是做了快十年GIS开发的人,这几年在生产环境里一直维系着一套基于QGIS 3.6.1的二次开发包。很多人一听“二次开发”就觉得要C++、要编译SDK、要啃一堆源码,其实QGIS这条路没有想象中那么陡:安装版里已经内置了PyQGIS,也就是官方提供的Python绑定库,配合自带解释器和Processing算法框架,日常的图层管理、瓦片叠加、栅格重分类、批量导出shp都能用脚本一条龙完成。这篇内容就以3.6.1为基准,一次讲清环境搭建、开发路线,以及高德、腾讯、百度瓦片接入和重分类这类高频操作,适合正在做GIS项目、想把手工操作转成自动化流程的人参考。
1. 先说清楚:QGIS 3.6.1二次开发包到底是什么
1.1 版本定位:3.6.1在QGIS发布周期中的位置
QGIS的发布节奏是每四个月左右出一个功能版本,每隔几个版本才会出一个LTR(Long Term Release)长期支持版本。3.4是LTR,3.10也是LTR,而3.6.1正好卡在中间,属于常规版本序列里的一个维护版本。它的内置环境是Python 3.7和Qt 5.11,API已经相当成熟,又比3.4那种老LTR多了一些新能力,所以很多做二次开发的人愿意把这一档作为基准线。
如果你去翻历史版本记录会发现,3.6.1发布之后社区很快就迭代到3.8,但实际项目中我并不建议一味追新。插件兼容性是个很现实的问题,特别是一些第三方处理算法插件,在3.8和3.10上经常要等作者适配。而3.6.1这个版本,主流插件基本都跟进过了,踩坑的代价最小。对于一个要长期维护的工具链来说,稳定压倒一切,这几乎是我选型时最重要的依据。
1.2 二次开发的两条路线:PyQGIS与C++插件
所谓“开发包”,并不是一个单独的SDK安装包,而是QGIS安装目录里完整的一套可调用库。它主要由三块构成:一是Python绑定库,存在于apps/qgis/python目录下,里面是qgis.core、qgis.gui、qgis.analysis这些模块;二是C++动态链接库,在lib目录下,比如qgis_core.dll和qgis_gui.dll;三是Processing算法框架,它把几百个空间分析算法统一封装成可调用的处理接口。
绝大多数二次开发场景走Python路线就够了,也就是PyQGIS。它可以直接操作图层、地图画布、符号系统和输出报表,做批量处理脚本非常顺手。C++路线主要用于性能敏感的场景,比如要在QGIS内部做自定义的渲染器或复杂空间索引插件,那就得用CMake去引用C++库,从源码编译整个依赖链,工程量大了不是一星半点。我见过不少团队一开始就想上C++,结果光编依赖就耗了两周,其实需求用PyQGIS半天就完成了,所以先评估清楚再选路线。
2. 环境搭建:把PyQGIS跑起来
2.1 安装方式与目录结构:开发包藏在哪里
Windows环境下,QGIS安装有独立安装包和OSGeo4W网络安装两种方式。做开发我强烈建议用OSGeo4W方式安装,因为它会把Python解释器、Shell环境、头文件和库文件一起整理清楚,后续写脚本、做插件都不用来回配路径。独立安装包更像一个“纯软件”,开发依赖虽然有,但不太方便调试。
装好后,核心目录大概是这样的:
C:\Program Files\QGIS 3.6.1\apps\qgis\python:PyQGIS源码与模块;C:\Program Files\QGIS 3.6.1\bin:QGIS主程序、Python解释器入口;C:\Program Files\QGIS 3.6.1\apps\qgis\plugins:插件安装目录。
如果你用OSGeo4W安装,路径会变成C:\OSGeo4W64\apps\qgis\python之类。开发时记住这几个路径就够了,因为大部分环境问题就是Python找不到这几个目录导致的。另外要提醒一句:3.6.x这个老版本在官网主页已经不在默认下载列表里,需要去下载归档区域找历史版本,或者从已经备份好的安装包直接装,下载完成后注意核对安装程序的哈希值,避免拿到被改过的文件。
2.2 验证PyQGIS能否正常导入
安装完成后,第一步是先验证PyQGIS能不能被Python正常导入。打开OSGeo4W Shell,进入Python交互环境,执行下面的代码:
import sys sys.path.append(r"C:\Program Files\QGIS 3.6.1\apps\qgis\python") from qgis.core import QgsApplication QgsApplication.setPrefixPath(r"C:\Program Files\QGIS 3.6.1", True) QgsApplication.initQgis() print("PyQGIS ok")这里的setPrefixPath是告诉QGIS去哪儿找插件、图标、CRS定义等资源,路径写错的话,后面很多功能会莫名报错。initQgis()用来初始化QGis应用环境,相当于启动一个没有界面的QGIS内核。如果这段代码能跑通,开发环境就算搭好了。值得一提的是,在OSGeo4W Shell里运行Python脚本时,Shell会预先设置好PYTHONPATH和PATH,所以脚本里可以不写sys.path.append,但独立执行脚本时最好还是加上,避免环境变量丢失的坑。
2.3 脚本、控制台、插件,三种开发形态怎么选
PyQGIS开发有三种常见形态,它们的适用场景完全不同。
第一种是QGIS内置的Python控制台,适合做快速实验和调试。在菜单栏的“插件”里打开控制台,直接写代码操作当前打开的图层,所见即所得。第二种是独立脚本,也就是在OSGeo4W Shell里执行python my_script.py,适合批量数据处理、定时任务,不依赖QGIS图形界面。第三种是真正的插件开发,用Plugin Builder生成插件骨架后,把功能打包成带界面的工具,适合交付给不会写脚本的同事用。
我的经验是:自己做实验用控制台,项目里的一次性数据处理用独立脚本,要长期交付给团队用的功能再做成插件。千万别一上来就做插件,因为插件涉及加载机制、资源文件、菜单注册很多东西,迭代效率远不如脚本快。
3. 高频功能实操:瓦片、重分类、shp导出
3.1 矢量数据加载与shp导出
矢量数据是GIS里最常见的点、线、面数据,shp(Shapefile)又是最通用的交换格式之一。很多新手问“qgis导出shp文件最简单方法”,其实界面上两步就能完成:右键图层,选“导出”,再选“要素另存为”,格式选ESRI Shapefile即可。但在二次开发里,我更推荐用代码做这件事:
from qgis.core import QgsVectorLayer, QgsProject, QgsVectorFileWriter layer = QgsVectorLayer(r"D:\data\landuse.shp", "landuse", "ogr") if not layer.isValid(): print("图层加载失败") else: QgsProject.instance().addMapLayer(layer) out_path = r"D:\out\result.shp" QgsVectorFileWriter.writeAsVectorFormat( layer, out_path, "UTF-8", layer.crs(), "ESRI Shapefile" )这里有两个细节值得注意。第一是编码参数,属性表里有中文的话,最好用UTF-8,但如果下游是ArcGIS之类的老软件,可能需要选GBK,否则打开会出现乱码。第二是shp格式本身限制字段名不能超过10个字符,中文字段名尤其容易出问题,导出前先改成英文短字段名,能省很多麻烦。另外,writeAsVectorFormat默认导出全部要素,如果只想把选中要素导出去,需要额外传入要素请求或先复制选择集。
说到矢量数据,顺便纠正个常见错别字:很多人会把“矢量图”写成“适量图”,其实是同一回事,只是输入法惹的祸。矢量数据用坐标点、线和面记录空间对象,和栅格数据那种像元矩阵完全不是一个逻辑,所以处理方式也不同。
3.2 自定义XYZ瓦片:接入高德、腾讯与百度
QGIS 3.6之后,XYZ瓦片底图的接入变得非常轻松。界面操作路径是:图层 -> 添加图层 -> 添加XYZ图层 -> 新建,然后把瓦片URL模板填进去。常用的模板我整理了一张表:
| 来源 | URL模板 | 坐标系 |
|---|---|---|
| 高德矢量 | https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z} | GCJ-02 |
| 高德影像 | https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z} | GCJ-02 |
| 腾讯地图 | https://rt{0-3}.map.gtimg.com/tile?z={z}&x={x}&y={y}&styleid=1 | GCJ-02 |
| 百度地图 | https://maponline0{s}.bdimg.com/tile/?qt=vtile&x={x}&y={y}&z={z}&styles=pl&scaler=2 | BD09 |
在PyQGIS脚本里加载瓦片的方式也很直接:
from qgis.core import QgsRasterLayer, QgsProject url = "type=xyz&url=" + "https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}" rl = QgsRasterLayer(url, "高德", "wms") if rl.isValid(): QgsProject.instance().addMapLayer(rl)这里有一个特别重要的点:高德和腾讯用的是GCJ-02坐标系,百度用的是BD09,它们和GPS常用的WGS84之间存在偏移。如果你把瓦片加载进来后发现和矢量数据对不上,八成不是瓦片坏了,而是坐标系没对齐。做精确分析时,要么把矢量和栅格统一到同一个坐标系,要么用控制点做配准。另外,在线瓦片服务适用于学习和项目验证,正式上线前要确认数据授权范围,局域网离线部署时可以考虑用瓦片下载工具导出MBTiles,这也是“qgis地图下载”相关热词背后真正想解决的问题。
3.3 栅格重分类:两种实现路径
栅格数据和矢量数据是两种完全不同的数据结构,栅格的本质是像元矩阵,每个像元存一个数值。重分类就是按照数值范围把像元重新赋值,比如把高程数据分成低、中、高三类,或者把土地利用类型编号重新映射。
在QGIS 3.6.1里做重分类最直接的方法是菜单栏的“栅格” -> “栅格计算器”。比如要把DEM数据按高程分成三个等级,可以用这个表达式:
if("dem@1" IS NOT NULL, ("dem@1" < 100) * 1 + ("dem@1" >= 100 AND "dem@1" < 200) * 2 + ("dem@1" >= 200) * 3, "dem@1")这个表达式会把小于100的区域赋值为1,100到200之间赋值为2,大于等于200赋值为3,而且专门用if包了一层,让原本的NoData区域保持NoData,不会变成0。很多新手写完表达式觉得没毛病,一看结果整张图都是0,多半就是漏了NoData处理。
另一种路径是Processing工具箱里的SAGA算法,搜索“Reclassify values”就能找到。它支持直接填旧值和新值的映射表,不用写函数表达式,适合重分类规则复杂的场景。注意SAGA算法在中文环境下,输入栅格必须同时有正确的投影和范围信息,否则会报“Invalid grid”之类的错误。
3.4 从界面点击到脚本批量执行
界面操作适合单次处理,但做项目时往往面对几十个文件,这时候就得靠脚本。我在实际工作中经常做的一件事,是把一个目录下所有shp统一转成UTF-8编码并加个前缀:
import glob from qgis.core import QgsVectorLayer, QgsVectorFileWriter for shp in glob.glob(r"D:\data\*.shp"): layer = QgsVectorLayer(shp, "tmp", "ogr") if not layer.isValid(): continue out = shp.replace(".shp", "_out.shp") QgsVectorFileWriter.writeAsVectorFormat(layer, out, "UTF-8", layer.crs(), "ESRI Shapefile") print("导出:", out)如果是重分类批量任务,可以在脚本里调用Processing算法。QGIS 3.6中栅格计算器的Processing算法ID是qgis:rastercalculator,调用时参数值会有差异,第一次跑之前建议先在Processing工具箱里手动执行一次,然后查看“历史”菜单里的命令,拿到精确的参数结构再写脚本。脚本化之后的效率提升非常明显,原来一个下午手工点几十遍的操作,现在两分钟跑完。
4. 实际开发中的高频问题与排查
4.1 模块导入失败:找不到qgis.core
这是新环境最常见的问题。脚本运行时提示ModuleNotFoundError: No module named 'qgis.core',本质是Python没有找到PyQGIS的目录。解决方法有两个:一是像前面说的,在脚本开头用sys.path.append手动加入apps\qgis\python路径;二是用OSGeo4W Shell启动Python,它会自动把环境变量配好。我建议开发环境统一用OSGeo4W Shell,部署环境再考虑独立打包,这样能省掉大量环境匹配的麻烦。
4.2 XYZ瓦片加载后空白或位置偏移
瓦片空白通常有三个原因:URL模板里的{s}通配符用不了、当前网络访问不了该瓦片域名、或者图层CRS设置不对。检查时先把URL复制到浏览器里替换成具体的数字和坐标,如果浏览器能显示图片,说明URL没问题,再回QGIS里排查。位置偏移的问题,根源基本是坐标系不一致。高德、腾讯的GCJ-02和百度的BD09与WGS84之间有系统性偏移,叠加WGS84数据时需要对底图做偏移修正,或者用专门的转换插件,不要指望手动拖动图层能对齐。
4.3 属性表中文乱码与字段丢失
shp导出后中文乱码,十有八九是编码参数选错了。UTF-8导出的文件给QGIS自己用没问题,但给ArcGIS系列用就容易乱,这时选GBK试试。更隐晦的问题是字段名被截断,shp的DBF字段名最长10字节,中文一个字占两三个字节,很容易截断成乱字符。我处理这类问题有一个原则:导出给外部系统的shp,字段名全部用英文,属性值里的中文尽量用代码映射表转换。
4.4 重分类结果全0或NoData区异常
重分类结果全为0,先看原栅格是不是有大量NoData。表达式里如果直接("dem@1" < 100) * 1,NoData参与比较时会得到NULL而不是有效值,有时候QGIS会把它渲染成0,所以结果看起来一片黑。用if("dem@1" IS NOT NULL, 表达式, "dem@1")包一层,能保留NoData。另外,重分类输出格式建议选GeoTIFF,不要选ESRI Grid,后者对NoData的处理在某些情况下会额外占磁盘,还容易写出全0的伪值。
为了让大家排查时有依据,我把最常踩的坑整理成一张速查表:
| 现象 | 原因 | 排查思路 |
|---|---|---|
| 模块导入失败 | PYTHONPATH未包含PyQGIS目录 | 用OSGeo4W Shell或手动追加路径 |
| 瓦片空白 | URL失效、网络受限、通配符错误 | 浏览器测试URL模板 |
| 瓦片偏移 | GCJ-02/BD09与WGS84不一致 | 统一CRS或做坐标转换 |
| shp中文乱码 | 编码参数不匹配 | UTF-8与GBK互相切换 |
| 重分类全0 | 表达式未处理NoData | 用is not null条件包裹 |
5. 这套方案用下来的几点经验
5.1 PyQGIS与Processing的分工
实际项目里,我会把工作分成两类。一类是现成算法能解决的,比如坡度坡向计算、缓冲区分析、影像裁剪,直接调用Processing里的算法,稳定可靠;另一类是自定义逻辑和批量控制流的,比如遍历文件夹、判断条件、动态拼接表达式,这种就用PyQGIS写循环。两者结合的好处是代码量少、出错的概率低,也方便其他同事接手。
5.2 维护开发环境的几条心得
踩过几次坑之后,我现在维护这套3.6.1环境有四个习惯:第一,版本锁死,绝不随手升级,同一套代码在不同QGIS版本上的行为差异比想象中大得多;第二,所有路径尽量用英文,全中文路径在某些GDAL版本下会触发编码问题;第三,复杂操作封装成函数,统一放一个工具模块里,后续调用方便;第四,保留一套干净的OSGeo4W环境,别在系统Python里乱装包,否则依赖冲突会让人怀疑人生。
最后再分享一个实用小技巧:在写PyQGIS脚本时,如果某个处理算法参数搞不清楚,先手动执行一次,再到“处理”菜单里的“历史”里复制Python代码。这个功能相当于把界面操作翻译成了脚本,是我学习Processing算法最快的方式。QGIS 3.6.1这套二次开发包到现在依然能扛住很多生产任务,希望这篇分享能帮你少走一段弯路。
本文还有配套的精品资源,点击获取
