简介:本资源面向在 Windows 平台使用 Qt MinGW 开发地理空间数据处理程序的 C++ 开发者,提供已编译好的 GDAL 库,省去从源码编译的繁琐流程,可直接集成到 Qt 工程中调用。压缩包共 133 个文件,约 38.95MB,包含 59 个头文件、28 个 csv 数据表、21 个可执行工具、6 个 wkt 定义文件,以及静态库、动态库、dll、svg、txt 等,覆盖开发所需的头文件、链接库与配套数据资源。已有 4489 人学习下载,说明该预编译方案在实际项目中具备较高参考价值。借助其中的头文件与库文件,读者可在 .pro 中配置 INCLUDEPATH 与 LIBS 后直接读写 TIFF、JPEG、Shapefile 等格式数据,并利用附带工具完成投影转换、裁剪、重采样等操作,快速搭建地理空间数据处理应用,把精力集中在核心业务逻辑上。
1. 为什么我宁愿用别人编译好的 GDAL,也不自己从头编一遍
如果你在 Windows 上做 GIS 相关的桌面开发,大概率绕不开 GDAL 这个库。它能读写的栅格和矢量格式多到离谱,Shapefile、GeoJSON、GeoTIFF、PostGIS、FileGDB 基本都能啃下来。但真正让人头疼的不是 API 怎么调,而是怎么在 Windows 上把它编出来——尤其是 Qt + MinGW 这套组合。我自己第一次尝试用 MSYS2 从源码编 GDAL,光依赖链就折腾了一整个下午,PROJ、GEOS、SQLite3、libtiff、libpng 一个接一个报错,最后卡在某个 C++ 标准库符号找不到的问题上,直接放弃。
后来我拿到一份在 MinGW 环境下已经编译好的 GDAL 库,解压就能用,头文件、静态库、动态库、依赖 DLL 全都在。这篇文章就是把我从拿到这份资源到在 Qt 项目里跑通第一个坐标转换的完整过程拆开讲,包括怎么配.pro文件、怎么处理运行时 DLL 缺失、怎么验证编译出来的库到底能不能用。适合两类人:一是刚接触 Qt + GIS 开发、不想在编译环境上耗时间的新手;二是之前自己编过但踩了坑、想找个稳定替代方案的熟手。
2. 先搞清楚这份 GDAL 库的目录结构和版本匹配逻辑
2.1 拿到压缩包后先看什么
解压之后不要急着往 Qt 项目里塞,先花两分钟把目录结构看清楚。一份标准的 MinGW 编译产物通常长这样:
gdal-mingw/ ├── include/ │ ├── gdal.h │ ├── gdal_priv.h │ ├── ogr_api.h │ ├── ogrsf_frmts.h │ └── ... ├── lib/ │ ├── libgdal.a │ ├── libgdal.dll.a │ └── ... ├── bin/ │ ├── libgdal-XX.dll │ ├── libproj-XX.dll │ ├── libgeos-XX.dll │ └── ... └── share/ └── gdal/ ├── data/ └── ...include放头文件,lib放链接用的.a和.dll.a,bin放运行时需要的动态库,share/gdal/data放 GDAL 运行时要加载的坐标系统和格式定义文件。这四个目录缺一个,后面都可能出问题。
关键是要确认这份库是用哪个版本的 MinGW 编的。Qt 的 MinGW 版本和编译 GDAL 用的 MinGW 版本必须 ABI 兼容,否则链接阶段就会报一堆undefined reference。常见做法是看libgdal.dll.a的文件名或者用objdump看一下:
objdump -p libgdal.dll.a | grep "DLL Name"如果输出里有libgcc_s_seh-1.dll或libgcc_s_dw2-1.dll,说明用的是 SEH 或 DW2 异常模型。Qt 5.12 之后的 MinGW 基本都是 SEH,老版本可能是 DW2。这个对不上,后面必翻车。
2.2 版本号怎么确认
GDAL 的版本号直接决定 API 兼容性。2.x 和 3.x 之间有不少函数签名变了,比如GDALOpenEx的参数、OGRSpatialReference的导入方式。确认版本最直接的办法是看头文件:
// 在任意 .cpp 里加这一行,编译时看输出 #include <gdal.h> #pragma message(GDAL_RELEASE_NAME)或者直接翻gdal.h,搜GDAL_VERSION_MAJOR、GDAL_VERSION_MINOR、GDAL_RELEASE_NAME这三个宏。我手里这份是 3.6.x 的,对应 Qt 5.15 + MinGW 8.1 的 64 位环境,实测能跑。
注意:如果你用的是 Qt 6,MinGW 版本通常是 11.2 或更高,和 Qt 5 时代的 MinGW 8.1 不兼容。拿库之前先确认自己的 Qt 用的是哪套 MinGW。
2.3 依赖 DLL 的清单怎么理
GDAL 不是单独一个 DLL 就能跑的,它依赖 PROJ 做坐标转换、依赖 GEOS 做几何运算、依赖 SQLite3 做空间数据库支持。这些依赖在bin目录里应该都有,但你需要知道哪些是必须随程序分发的。一个笨但有效的办法是用ntldd或者objdump递归查:
objdump -p libgdal-3.dll | grep "DLL Name"输出会列出所有直接依赖。然后对每个依赖再查一遍,直到没有新的 DLL 出现。常见的必须分发的有:
| DLL 名称 | 作用 | 是否必须 |
|---|---|---|
| libgdal-3.dll | GDAL 主库 | 必须 |
| libproj-XX.dll | 坐标转换 | 必须 |
| libgeos_c.dll | 几何运算 | 用到空间查询时必须 |
| libsqlite3-0.dll | SQLite 驱动 | 用到 GPKG/SQLite 时必须 |
| libtiff-5.dll | TIFF 读写 | 读写 GeoTIFF 时必须 |
| libpng16-16.dll | PNG 读写 | 读写 PNG 栅格时必须 |
| zlib1.dll | 压缩库 | 多数格式间接依赖 |
这份清单不是固定的,取决于编译时开了哪些驱动。最稳妥的做法是程序跑起来之后用Process Explorer看它加载了哪些 DLL,缺哪个补哪个。
3. 在 Qt 项目里链接 GDAL:.pro 文件怎么写才不报错
3.1 .pro 文件的最小可用配置
假设你把 GDAL 解压到了D:/libs/gdal-mingw,Qt 项目里.pro文件的核心配置就三块:头文件路径、库路径、链接库名。
# GDAL 根目录,按实际解压位置改 GDAL_ROOT = D:/libs/gdal-mingw # 头文件搜索路径 INCLUDEPATH += $$GDAL_ROOT/include # 库文件搜索路径 LIBS += -L$$GDAL_ROOT/lib # 链接 GDAL 主库,注意顺序:gdal 在前,依赖在后 LIBS += -lgdal这里有个细节:MinGW 下链接库的顺序很重要。如果 GDAL 依赖 PROJ,而你把-lproj写在-lgdal前面,链接器可能会报undefined reference to proj_xxx。正确顺序是被依赖的库放在后面:
LIBS += -lgdal -lproj -lgeos_c -lsqlite3但实际情况是,如果你用的是libgdal.dll.a这种导入库,它内部已经记录了对其他 DLL 的依赖,通常不需要手动再链一遍。我一般先只写-lgdal,编译报缺什么再加什么。
3.2 编译期和运行期的路径要分开处理
.pro里配的LIBS和INCLUDEPATH只影响编译和链接阶段。程序跑起来之后,Windows 加载器会去PATH环境变量和可执行文件所在目录找 DLL。所以你有两个选择:
方案一:把 bin 目录加到 PATH
在 Qt Creator 的“项目 → 运行 → 环境”里加一行:
PATH=D:/libs/gdal-mingw/bin;%PATH%这样调试的时候能跑,但发布的时候用户机器上没有这个 PATH,还是会缺 DLL。
方案二:把 DLL 拷到可执行文件旁边
这是发布时的标准做法。在.pro里加一个自定义构建步骤,编译完自动拷贝:
# 编译完成后把 GDAL 的 DLL 拷到输出目录 win32 { GDAL_BIN = $$GDAL_ROOT/bin DESTDIR_WIN = $$replace(OUT_PWD, /, \\) GDAL_BIN_WIN = $$replace(GDAL_BIN, /, \\) QMAKE_POST_LINK += $$quote(cmd /c copy /y $$GDAL_BIN_WIN\\*.dll $$DESTDIR_WIN$$escape_expand(\\n\\t)) }这段代码的逻辑是:在链接完成后,用copy命令把bin目录下所有 DLL 复制到构建输出目录。$$replace是把正斜杠转成反斜杠,因为 Windows 的copy命令不认正斜杠路径。QMAKE_POST_LINK保证在链接之后执行。
注意:
DESTDIR_WIN用的是OUT_PWD,这是构建目录。如果你改了DESTDIR,要相应调整。
3.3 验证链接是否成功的最小代码
配好之后别急着写业务逻辑,先用一段最小代码验证 GDAL 能不能正常初始化和输出版本号:
#include <gdal.h> #include <cstdio> int main() { // 注册所有驱动,GDAL 3.x 之后可以省略,但显式调用更保险 GDALAllRegister(); // 输出版本信息 printf("GDAL Version: %s\n", GDALVersionInfo("RELEASE_NAME")); // 测试坐标转换能力:WGS84 转 Web Mercator OGRSpatialReference src, dst; src.importFromEPSG(4326); // WGS84 经纬度 dst.importFromEPSG(3857); // Web Mercator OGRCoordinateTransformation *ct = OGRCreateCoordinateTransformation(&src, &dst); if (ct == nullptr) { printf("Coordinate transformation creation failed!\n"); return -1; } double x = 116.397, y = 39.908; // 某个城市的经纬度 if (ct->Transform(1, &x, &y)) { printf("Transformed: %.2f, %.2f\n", x, y); } else { printf("Transform failed!\n"); } OGRCoordinateTransformation::DestroyCT(ct); return 0; }这段代码做了三件事:调用GDALAllRegister()注册驱动、用GDALVersionInfo确认版本、用OGRCreateCoordinateTransformation测试 PROJ 依赖是否正常。如果第三步返回nullptr,说明 PROJ 的数据文件没找到,需要设置PROJ_LIB环境变量指向share/proj目录。
参数说明:importFromEPSG(4326)是 WGS84 地理坐标系,importFromEPSG(3857)是 Web Mercator 投影坐标系。Transform的第一个参数是点数,后面是两个double数组的指针。返回值是int,1 表示成功,0 表示失败。
4. 运行时踩坑记录:DLL 找不到、PROJ 数据缺失、编码乱码
4.1 现象:程序启动直接闪退,事件查看器里报 0xc000007b
原因:这是典型的 32 位/64 位不匹配。你的 Qt 项目是 64 位,但拷过去的某个 DLL 是 32 位的,或者反过来。GDAL 的依赖链很长,只要有一个 DLL 位数不对,加载器就会直接拒绝启动。
解决:用dumpbin /headers或objdump -f检查每个 DLL 的架构。MinGW 下可以用:
objdump -f libgdal-3.dll | grep "file format"输出pe-x86-64是 64 位,pe-i386是 32 位。把所有 DLL 统一成同一个架构。另外注意,Qt 的 MinGW 套件本身也分 32 位和 64 位,确认你用的 Qt 版本和 GDAL 库位数一致。
4.2 现象:坐标转换返回空指针,控制台输出PROJ: proj_create_from_database: Cannot find proj.db
原因:PROJ 6 之后把坐标转换的数据库独立成了proj.db文件,放在share/proj目录下。GDAL 运行时需要知道这个目录在哪。如果没设置PROJ_LIB环境变量,PROJ 会去默认路径找,找不到就返回空。
解决:在main()函数最开头设置环境变量:
#include <cstdlib> int main(int argc, char *argv[]) { // 把 PROJ 数据目录指向 GDAL 的 share/proj qputenv("PROJ_LIB", "D:/libs/gdal-mingw/share/proj"); // 如果 GDAL 的数据目录也需要指定 qputenv("GDAL_DATA", "D:/libs/gdal-mingw/share/gdal"); // ... 后续代码 }用qputenv而不是setenv,因为 Qt 的qputenv在 Windows 上会正确处理宽字符。路径用正斜杠就行,Windows API 能识别。
注意:发布的时候这个路径不能写死。常见做法是把
share目录一起拷到可执行文件旁边,然后用相对路径:
QString appDir = QCoreApplication::applicationDirPath(); qputenv("PROJ_LIB", (appDir + "/share/proj").toLocal8Bit());4.3 现象:读取中文路径的 Shapefile 失败,或者属性表里的中文变成乱码
原因:GDAL 在 Windows 上默认用char*接收文件路径,如果路径里有中文,而你的程序用的是 UTF-8 编码,传进去就会乱。另外 Shapefile 的.dbf属性表默认编码是 LDID/87,中文 Windows 下是 GBK,GDAL 可能按 UTF-8 解析。
解决:两个层面。路径问题用GDALOpenEx的GDAL_OF_VERBOSE_ERROR标志配合宽字符转换:
#include <windows.h> std::string utf8ToGbk(const std::string &utf8Str) { // UTF-8 转 GBK,用于传给 GDAL 的窄字符接口 int wlen = MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, nullptr, 0); std::wstring wstr(wlen, 0); MultiByteToWideChar(CP_UTF8, 0, utf8Str.c_str(), -1, &wstr[0], wlen); int glen = WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, nullptr, 0, nullptr, nullptr); std::string gbkStr(glen, 0); WideCharToMultiByte(CP_ACP, 0, wstr.c_str(), -1, &gbkStr[0], glen, nullptr, nullptr); return gbkStr; }属性表编码问题在打开数据源时指定:
GDALDataset *ds = (GDALDataset*)GDALOpenEx( utf8ToGbk(path).c_str(), GDAL_OF_VECTOR | GDAL_OF_READONLY, nullptr, nullptr, nullptr ); // 设置编码选项 OGRLayer *layer = ds->GetLayer(0); layer->SetIgnoredFields(nullptr); // 清空忽略字段 // 对于 DBF,可以在打开时用 open option 指定编码更彻底的办法是在GDALOpenEx的papszOpenOptions里传ENCODING=GBK:
char **options = nullptr; options = CSLSetNameValue(options, "ENCODING", "GBK"); GDALDataset *ds = (GDALDataset*)GDALOpenEx( path.c_str(), GDAL_OF_VECTOR, nullptr, options, nullptr ); CSLDestroy(options);4.4 现象:编译时报undefined reference to 'GDALOpenEx'但头文件明明包含了
原因:链接顺序问题,或者链接的是静态库libgdal.a而不是导入库libgdal.dll.a。静态库需要把所有依赖库都显式链上,顺序还不能错。
解决:优先用libgdal.dll.a,它只记录 DLL 导出符号,不需要手动链依赖。如果只有静态库,链接顺序按依赖关系从上层到下层排:
LIBS += -lgdal -lproj -lgeos_c -lsqlite3 -ltiff -lpng -lz还不行的话,用-Wl,--verbose看链接器实际找了哪些库:
g++ -Wl,--verbose main.o -o test.exe -L/path/to/lib -lgdal 2>&1 | grep "succeeded"4.5 现象:程序在开发机上跑得好好的,拷到别人机器上就报缺libstdc++-6.dll
原因:MinGW 编译的程序默认动态链接 GCC 运行时库。开发机上装了 MinGW 所以能找到,别人机器上没有。
解决:两个选择。一是把libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll一起拷过去。二是静态链接 GCC 运行时:
QMAKE_LFLAGS += -static-libgcc -static-libstdc++但注意,如果 GDAL 的 DLL 本身是动态链接 GCC 运行时的,你静态链接自己的代码也没用,DLL 加载时还是会去找。所以最稳妥的还是把运行时 DLL 一起分发。
5. 进阶用法:用 GDAL 命令行工具验证库的完整性,以及一个批量坐标转换脚本
5.1 先用命令行工具确认这份库到底带了哪些驱动
编译好的 GDAL 通常会在bin目录里附带几个命令行工具:gdalinfo.exe、gdal_translate.exe、ogr2ogr.exe。这些工具是验证库完整性的最快方式。打开终端,把bin目录加到PATH,然后跑:
gdalinfo --version gdalinfo --formats--formats会列出所有已编译的驱动。如果你只关心矢量,可以过滤一下:
ogrinfo --formats | findstr "ESRI Shapefile"如果输出里有ESRI Shapefile、GeoJSON、GPKG这些常用格式,说明驱动编译得比较全。如果缺了你需要的格式,那这份库可能不适合你的场景,得换一份或者自己补编。
我一般还会跑一个实际的转换测试:
# 把一个 GeoJSON 转成 Shapefile,验证读写链路 ogr2ogr -f "ESRI Shapefile" output.shp input.geojson # 查看生成的 Shapefile 信息 ogrinfo -al -so output.shp这两步能跑通,说明矢量读写、投影处理、文件编码都没大问题。
5.2 批量坐标转换:从 WGS84 到 CGCS2000 的完整脚本
实际项目里经常需要把一批点的经纬度转成平面坐标。下面这个脚本用 GDAL 的 C++ API 做批量转换,输入是一个 CSV 文件,每行是id,lon,lat,输出是转换后的id,x,y。
#include <gdal.h> #include <ogr_spatialref.h> #include <cstdio> #include <fstream> #include <sstream> #include <vector> struct Point { int id; double lon; double lat; }; int main() { // 设置 PROJ 数据路径,按实际位置改 CPLSetConfigOption("PROJ_LIB", "D:/libs/gdal-mingw/share/proj"); // 定义源坐标系和目标坐标系 OGRSpatialReference srcSRS, dstSRS; srcSRS.importFromEPSG(4326); // WGS84 dstSRS.importFromEPSG(4490); // CGCS2000 地理坐标 // 如果要投影坐标,用 EPSG:4547(CGCS2000 3度带) // dstSRS.importFromEPSG(4547); // 创建转换对象 OGRCoordinateTransformation *ct = OGRCreateCoordinateTransformation(&srcSRS, &dstSRS); if (!ct) { printf("Failed to create transformation.\n"); return 1; } // 读取输入 CSV std::ifstream fin("input.csv"); std::ofstream fout("output.csv"); fout << "id,x,y\n"; std::string line; std::vector<Point> points; while (std::getline(fin, line)) { std::stringstream ss(line); std::string token; Point p; std::getline(ss, token, ','); p.id = std::stoi(token); std::getline(ss, token, ','); p.lon = std::stod(token); std::getline(ss, token, ','); p.lat = std::stod(token); points.push_back(p); } // 批量转换 for (auto &p : points) { double x = p.lon, y = p.lat; if (ct->Transform(1, &x, &y)) { fout << p.id << "," << x << "," << y << "\n"; } else { fout << p.id << ",FAILED,FAILED\n"; } } OGRCoordinateTransformation::DestroyCT(ct); printf("Done. %zu points processed.\n", points.size()); return 0; }这段代码的关键点:CPLSetConfigOption设置 PROJ 数据路径,必须在创建转换对象之前调用。importFromEPSG(4490)是 CGCS2000 地理坐标系,如果要平面坐标就换成对应的投影 EPSG 代码。Transform每次处理一个点,批量处理时循环调用即可。如果点数很多(几十万以上),可以考虑用OGRCoordinateTransformation::Transform的数组版本,一次传多个点,减少函数调用开销。
参数说明:CPLSetConfigOption的第一个参数是配置项名称,第二个是值。PROJ_LIB指向包含proj.db的目录。importFromEPSG的参数是 EPSG 数据库里的坐标系编号,可以在 epsg.io 上查。
5.3 一个我踩过的坑:PROJ 数据版本和 GDAL 版本不匹配
有一次我拿到的 GDAL 库是 3.4 编的,但share/proj目录里的proj.db是 PROJ 9.x 的。结果坐标转换时好时坏,某些 EPSG 代码能转,某些直接报错。后来发现是 PROJ 数据库的 schema 版本和 GDAL 期望的不一致。GDAL 3.4 期望 PROJ 8.x 的数据库,PROJ 9 改了表结构。
解决办法很简单:用 GDAL 自带的share/proj目录,不要从别处拷。如果那份库的share/proj缺失或者版本不对,去 PROJ 的官方发布页找对应版本的proj-data包,解压后把proj.db放进去。
从那以后我每次拿到一份编译好的 GDAL,第一件事就是跑gdalinfo --version和projinfo --version(如果有的话),确认两个版本号在同一个大版本区间内。然后再跑一个坐标转换测试,确认proj.db能正常加载。这个习惯帮我省了很多事后排查的时间。
希望这份笔记能帮你在 Qt + MinGW 的环境里少走点弯路。
本文还有配套的精品资源,点击获取