简介:Quazip与Zlib是数据压缩领域两个互补的开源库,这套资源面向需要在Visual Studio 2015与Qt 5.10 x86环境下集成压缩功能的Qt开发者,提供编译好的Zlib与Quazip库文件及配套头文件,可直接用于ZIP/GZIP格式的创建、读取与解压。资源共35个文件,以18个头文件、4个lib导入库、4个dll动态库为主,分别用于API声明、编译链接和运行时调用,另有exp、pdb与ilk等导出与调试辅助文件,整体1.8MB,轻量易部署,适合放入Qt工程进行链接调用。已有301人学习下载。对不熟悉nmake/CMake编译流程的开发者而言,省去自行编译和配置的烦琐步骤;对需要排查Zlib与Quazip集成细节的读者,也提供了现成的库结构和常用API调用基础,可在此基础上快速完成文件压缩、解压及流式处理,减少搭建环境的弯路。 先说个很多Qt开发者都会遇到的事:程序要发布了,需要把日志、配置和几个资源文件打包成zip,对方拿到后要在Windows资源管理器里直接双击解压。我当时第一反应是“这不就调个库的事”,结果在“到底用哪个库”上磨了整整一个下午。搜“Qt zip压缩”,资料翻来覆去就是两个名字在眼前转:zlib和QuaZIP。但奇怪的是,大部分文章只给了API用法,没说清楚这两个库的分工边界。结果就是照着抄代码,编译报错一大片,链接出问题又得回头查版本,折腾到怀疑人生。
这篇文章把我这次实操的经验完整写出来,包括QuaZIP和zlib各自负责什么、怎么配合,编译链接阶段最容易翻车的几个细节,以及中文文件名、内存压缩、zip64这类绕不开的实际问题。适合所有用C++/Qt做文件传输、备份、导入导出功能的开发者,尤其是第一次接触QuaZIP的读者,可以省下不少查资料的时间。
1. 两个库各管一段:压缩算法和ZIP容器不是一回事
1.1 zlib的边界:发动机再好,也不会帮你打包
zlib是1995年由Jean-loup Gailly和Mark Adler写出来的DEFLATE压缩算法实现库,地位已经接近“事实标准”。几乎所有编程语言里的zip、gzip、png支持都绕不开它,Qt本身在编译时也经常链着它。但zlib的核心能力很纯粹:它只负责把一段连续的数据流压缩成另一段更小的数据流,或者反向解压。它根本不理解“文件”这个概念,更不关心你压缩的是一个文件还是一个装满文件的目录。
一个真正的ZIP压缩包是什么?它不只是“压缩过的数据”,而是一种容器格式。一个ZIP文件里包含:
- 每个文件条目自己的Local File Header,记录文件名、压缩方式、CRC32校验值、压缩前后大小;
- 文件的实际压缩数据;
- 文件末尾的Central Directory,集中登记所有条目的元数据;
- 最后还有一个End of Central Directory,标记整个包的偏移量。
这些结构之间的偏移量计算、CRC32校验、压缩方法的标志位,全部要由调用方自己组织。单纯用zlib的compress2()或者deflate(),你拿到的只是一团压缩数据,离一个能被WinRAR打开的zip包还差着十万八千里。
网上偶尔能看到有人问“zlib能不能直接生成zip”,结论就是:能,但你要手写ZIP格式规范里的所有字段。我自己试过一次,单文件还好,一旦涉及目录层级、文件名编码、跨平台换行符,字段偏移一个字节没对齐,解压端直接报“文件头损坏”。这种造轮子行为,维护成本远远超过直接使用封装库带来的便利。
1.2 QuaZIP补上的部分:把整套ZIP格式变成Qt的IO抽象
QuaZIP是Czirkos Zoltan维护的开源库,定位就是“基于Qt对ZIP格式的一层完整封装”,底层压缩和解压引擎用的正是zlib。它做的事可以理解为:把ZIP容器格式的解析、生成、条目遍历、元数据管理全部接管过来,然后以Qt程序员最熟悉的QIODevice接口暴露给你。
QuaZIP最核心的设计是:一个ZIP压缩包里的每个条目(entry),都是一个QuaZipFile对象,而QuaZipFile继承自QIODevice。这就意味着,你对一个普通QFile能做的读写操作,对压缩包里的文件也能做;甚至可以直接把QBuffer这类内存设备塞给它作为数据源,实现不落盘的压缩。这种抽象能力,是minizip这种C风格库给不了的。
所以正确的理解方式是:zlib是发动机,QuaZIP是整车。发动机决定压缩比和速度的上限,但驾驶体验、仪表盘逻辑、安全结构这些是整车厂的事。你用QuaZIP时,它内部自动调用zlib去完成每个条目数据的压缩和解压,同时帮你处理好了ZIP的容器规范。而“zlib镜像”这个词,如果翻译成工程语言,其实就是在说“zlib在Qt世界里的对应封装层”——那正是QuaZIP扮演的角色。
2. 编译集成实录:这几个坑我每次换机器都会碰到
2.1 版本矩阵先核对:Qt5还是Qt6,Release还是Debug
QuaZIP对Qt版本非常敏感,因为它直接依赖Qt的QIODevice、QString、QFile等类,而这些类在不同大版本之间接口有差异。GitHub上QuaZIP仓库的master分支对新版Qt跟得比较紧,但很多发行版包管理器里的QuaZIP版本较老,直接在Qt6项目里用会编译失败。
我这次用的组合是Qt 6.5.3 + QuaZIP 1.4,方案是直接从源码编译。先把quazip源码下载下来,用CMake配置:
cmake -S quazip -B build-quazip \ -DQUAZIP_QT_MAJOR_VERSION=6 \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF \ -DCMAKE_PREFIX_PATH=/path/to/Qt/6.5.3/msvc2019_64 cmake --build build-quazip --config Release cmake --install build-quazip有个很容易忽略的点:QUAZIP_QT_MAJOR_VERSION这个宏必须手动指明。如果你不设置,CMake会优先探测Qt5,虽然也能探测到Qt6,但一旦系统里装了多个Qt版本,探测顺序就会随机抽风,导致最终生成的库依赖版本和你项目里的不一致。之后在项目里使用,链接时要把这个宏也定义上,或者通过target_compile_definitions传进去,否则部分内部逻辑会和头文件声明不匹配。
Debug和Release混用也是经典翻车现场。Qt的Debug版和Release版使用了不同的运行时库,QuaZIP如果Debug编译、Release使用,会直接触发“无法解析的外部符号”或者运行期崩溃。我常用的做法是Debug和Release各编译一份,输出目录分开,项目里通过Qt的CONFIG(debug, debug|release)分支来切换库路径,宁可用硬盘多占一点空间,也不在链接阶段浪费时间。
2.2 Windows下zlib的运行时库一致性
QuaZIP依赖zlib,但zlib本身有好几种获取方式:系统自带的、vcpkg装的、Conan装的,或者自己用CMake编的。在Windows上,这里最大的坑就是运行时库不一致。
Visual Studio每个项目都会设置/MD(动态链接到多线程DLL)或/MT(静态链接多线程运行时库)。如果你的zlib是用/MD编出来的,QuaZIP也是/MD,但主程序是/MT,链接时就会冒出无数个“LNK2038: RuntimeLibrary mismatch”错误。解决办法很简单:确保zlib、QuaZIP、你的主项目三者用同一个运行时库模式。
我自己用的zlib是自己编的静态库。zlib源码本身就带CMakeLists,配置命令如下:
cmake -S zlib -B build-zlib \ -DCMAKE_BUILD_TYPE=Release \ -DBUILD_SHARED_LIBS=OFF cmake --build build-zlib --config Release编完之后,把zlib的include目录和生成的zlibstatic.lib路径直接传给QuaZIP的CMake参数ZLIB_INCLUDE_DIR、ZLIB_LIBRARY。QuaZIP的CMake脚本支持通过这两个变量指定zlib位置,省得它去系统环境变量里瞎找。这里有一个非常具体的经验:在Windows下编译QuaZIP时,即使你的zlib存在,find_package(ZLIB)也经常会失败,因为CMake找的是zlib.lib这个文件名,而你编出来的可能是zlibstatic.lib。如果你发现QuaZIP的CMake提示找不到zlib,去build-quazip/CMakeCache.txt里看一眼ZLIB_LIBRARY变量的值,很多时候它指向一个完全不存在的路径。
2.3 静态编译时的链接顺序和宏定义
QuaZIP和zlib都使用静态库时,链接顺序是个讲究活。MSVC和GNU链接器处理符号引用的方式不同,但经验法则是:被依赖的库放在依赖它的库后面。也就是quazip.lib在前,zlibstatic.lib在后。如果你用Qt的pro文件,这样写:
LIBS += -lquazip1-qt6 -lzlibstatic用CMake的话,直接通过target_link_libraries保持顺序即可。如果顺序反了,会出现大量unresolved external symbol错误,而且错误信息里全是deflate、inflate、crc32这些zlib符号,很容易让人误判成zlib没装好。
还有一个几乎必然踩中的宏:QUAZIP_STATIC。静态链接QuaZIP时,如果这个宏没有定义,Windows下的QuaZIP头文件会默认用__declspec(dllimport)修饰所有导出类,结果就是链接报错或者编译报“无法导入”。务必在编译器预定义里加上它。
3. 高频场景的三段代码:打包、解压、读出清单
3.1 打包整个目录:核心是QuaZipFile的写入逻辑
最常用的场景是把某个目录下的若干文件压缩成一个zip包。QuaZIP的写法非常直接:
#include <quazip/quazip.h> #include <quazip/quazipfile.h> bool compressDirToZip(const QString& zipPath, const QStringList& files, const QString& baseDir) { QuaZip zip(zipPath); if (!zip.open(QuaZip::mdCreate)) { return false; } for (const QString& path : files) { QFile srcFile(path); if (!srcFile.open(QIODevice::ReadOnly)) { continue; } // 计算zip包内的相对路径,统一用正斜杠 QString entryName = QDir(baseDir).relativeFilePath(path); entryName.replace('\\', '/'); QuaZipFile outFile(&zip); QuaZipNewInfo info(entryName); // 保留源文件的修改时间,避免解压后时间全变成当前时间 info.setDateTime(QFileInfo(path).lastModified()); if (!outFile.open(QIODevice::WriteOnly, info)) { srcFile.close(); continue; } outFile.write(srcFile.readAll()); outFile.close(); srcFile.close(); } zip.close(); return zip.getZipError() == UNZ_OK; }这段代码里有三个细节值得单独说明。
第一,QuaZipFile的open可以传递一个QuaZipNewInfo对象,用来设置条目名、时间戳、权限位等元数据。很多人只用了两参数的open(QIODevice::WriteOnly),结果发现解压出来的文件时间全部变成了压缩时间,这就是漏了这一步。
第二,写入逻辑是“先open再write,然后close,再处理下一个文件”,不能把多个文件都open着同时写——ZIP格式的结构决定了QuaZIP在写入时必须顺序产生Local File Header和Central Directory条目,多路并发写是没有意义的。
第三,entryName里的分隔符务必统一成/。Windows风格的反斜杠在ZIP规范里虽然名义上允许,但很多解压工具会把它当成普通字符处理,导致解压后出现一个文件名里带反斜杠的“脏文件”。
3.2 解压整个压缩包:遍历条目而不是写死文件名
解压逻辑同样简洁,但要注意QuaZIP的遍历API在Qt新老版本之间的差别:
bool extractZipToDir(const QString& zipPath, const QString& destDir) { QuaZip zip(zipPath); if (!zip.open(QuaZip::mdUnzip)) { return false; } QuaZipFile file(&zip); for (bool more = zip.goToFirstFile(); more; more = zip.goToNextFile()) { if (!file.open(QIODevice::ReadOnly)) { continue; } QString outPath = QDir(destDir).filePath(file.getActualFileName()); QFileInfo info(outPath); QDir().mkpath(info.absolutePath()); QFile outFile(outPath); if (!outFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { file.close(); continue; } outFile.write(file.readAll()); outFile.close(); file.close(); } zip.close(); return zip.getZipError() == UNZ_OK; }需要特别说明的是getActualFileName()和getFileName()的区别。getFileName()返回的是ZIP条目里的原始文件名,也就是我们写压缩包时传入的那个字符串;getActualFileName()则会根据ZIP头里的UTF-8标志位做一次编码转换,返回适合当前系统直接使用的文件名。解压场景下用getActualFileName()是更稳妥的选择,后面讲到中文编码时还会再展开。
3.3 不落盘:把压缩内容直接写进QBuffer
QuaZIP一个容易被忽略的价值是它可以直接把压缩包写入QBuffer,实现全程内存压缩,不碰磁盘。做法很简单:
QByteArray compressToMemory(const QByteArray& input) { QBuffer buffer; buffer.open(QIODevice::WriteOnly); QuaZip zip(&buffer); if (!zip.open(QuaZip::mdCreate)) { return {}; } QuaZipFile outFile(&zip); QuaZipNewInfo info("data.bin"); if (!outFile.open(QIODevice::WriteOnly, info)) { return {}; } outFile.write(input); outFile.close(); zip.close(); return buffer.buffer(); }这里QuaZip构造时传入的不再是路径字符串,而是QBuffer*,QuaZIP内部会通过QIODevice接口把数据写到这个内存设备里。这个能力在做网络传输、内存缓存、配置文件序列化时特别有用,省掉临时文件的创建、权限处理、并发冲突等一系列麻烦。实测下来,几百KB的小文件几乎是瞬间完成,体感和内存拷贝没有区别。
4. 中文文件名不乱码:ZIP编码规范里的一段往事
4.1 为什么中文名解压后会变成乱码
ZIP格式规范诞生于上世纪80年代末,彼时ASCII是绝对主流,规范里对文件名字段的编码方式几乎没有定义。后来各国用户开始用本地编码往ZIP里塞文件名,Windows的压缩工具默认用系统本地代码页(中文系统就是GBK),而macOS和Linux工具则偏好UTF-8。两边都觉得自己没错,但交换文件时就从“乱码偶然发生”变成了“乱码必然发生”。
PKWARE后来在规范里增加了UTF-8标志位:Local File Header和Central Directory的general purpose bit flag的第11位(bit 11)如果设置为1,说明文件名和注释字段使用的是UTF-8编码。这才算给ZIP格式定了一个可选的统一编码方案。问题在于,这个标志位是“可选”的,很多老工具根本不写它,甚至有些工具写了标志位但内容还是本地编码,解压方一旦遇到这种自相矛盾的数据,也只能靠猜。
4.2 QuaZIP里的处理策略:识别标志位,而不是盲目转换
QuaZIP对中文文件名的处理逻辑,可以从写入和读取两个方向来看。
写入时:QuaZipNewInfo构造时会调用一个内部函数,把传入的QString按UTF-8编码写入ZIP头。由于你传入的是QString,它内部就是Unicode,因此只要源数据没问题,中文名写入后一定是合法的UTF-8,且会正确设置bit 11标志位。我实测过用QuaZIP压缩含中文名的文件,再用7-Zip和Windows资源管理器分别解压,中文名都能正确显示。
读取时:解压方调用getActualFileName(),QuaZIP会检查该条目的bit 11标志。如果标志位存在,直接按UTF-8解码成QString;如果标志位不存在,则视为本地编码,在Windows上会尝试用系统本地代码页解码。这个过程全自动,不需要你手工做fromLocal8Bit之类的转换。
实际项目里最容易犯的错误是:写入时手动做了toLocal8Bit转换。比如有开发者为了“兼容Windows”,把QString转成GBK字节再传给QuaZipNewInfo,结果字面变成了乱码。正确做法是根本不要手动转码,一律传QString,QuaZIP自己会按UTF-8处理。
注意:
QuaZip::getFileName()返回的是ZIP里的原始文件名(UTF-8字节解码为QString的结果),而getActualFileName()会额外处理非UTF-8条目。建议在解压场景里统一使用getActualFileName(),规避老工具产出的非标ZIP包。
5. 性能边界、zip64与选型建议
5.1 大文件处理:zip64的坑与读取进度的问题
QuaZIP对zip64的支持在1.0版本之后已经比较完善,可以处理超过4GB的压缩包和超过4GB的单个文件。但这个能力依赖两个前提:一是zlib版本不能太老,二是ZIP条目数上传时QuaZipFile写入时要避免一次性readAll()。
我在处理几个GB的日志压缩时踩过一个性能坑:直接用outFile.write(srcFile.readAll())压缩大文件,内存峰值瞬间吃掉几个GB,电脑直接卡死。正确做法是分块读写:
QByteArray buffer; buffer.resize(64 * 1024); qint64 bytesRead = 0; while ((bytesRead = srcFile.read(buffer.data(), buffer.size())) > 0) { outFile.write(buffer.constData(), bytesRead); }64KB到1MB的块大小之间,压缩率几乎没有差异,但内存占用和磁盘IO压力天差地别。64KB是个稳健的默认值,在机械硬盘、SSD、网络盘上都跑得动。
至于读取时的进度反馈,QuaZIP没有提供直接的字节级进度回调。最简单的方案是遍历条目时,用已解压的字节数除以QuaZipFile::bytesTotal()得到每个条目的百分比,再用“已完成条目数 / 总条目数”得到一个粗粒度的整体进度。对于大多数业务场景够用了,没必要为了精确进度去改库。
5.2 什么时候不需要QuaZIP:和纯zlib、minizip的取舍
有人会在QuaZIP方案和“纯zlib手写”之间犹豫。我的建议是,根据你要做的事来选,而不是根据“哪个库更火”来选:
| 需求场景 | 推荐方案 | 原因 |
|---|---|---|
| Qt项目里打/解zip包,处理文件路径、目录结构 | QuaZIP + zlib | 最省心,QIODevice抽象一致,中文编码处理好 |
| 只需要压缩一段数据流,不需要打包成zip | zlib的compress2/uncompress即可 | QuaZIP引入的是ZIP容器开销,没这个必要 |
| 跨平台非Qt的C/C++项目 | minizip或libzip | 依赖更轻,C接口容易嵌入,但要自己处理编码和文件IO |
| 嵌入式或资源受限环境 | zlib + 手写简化容器 | QuaZIP依赖Qt运行库,体量太大 |
我自己在Qt项目之外处理过一个小工具,没有Qt环境,就只用minizip,因为它在zlib contrib目录里自带,打包和编译都非常轻。但在Qt主项目里,我很明确选QuaZIP,理由不是它性能更强,而是它让“文件打包”这件事彻底融入了Qt的IO模型,团队其他成员接手代码时的学习成本最低。
性能方面,我拿一个包含1000个小文件(平均5KB)的目录,用QuaZIP默认压缩级别(6级)和纯zlib的deflate做对比,压缩耗时差距在3%以内,基本可以视为同一个量级的操作。QuaZIP的额外开销主要在ZIP容器格式的组装和解析上,这部分对总体时间的影响可以忽略。真正的性能瓶颈永远在磁盘IO和文件数量上,文件越多,每个文件头部的处理时间占比就越高。
最后再分享一个小技巧:QuaZIP在打包时如果不显式设置QuaZipNewInfo的时间戳,默认会取QDateTime::currentDateTime(),导致每次构建生成的zip包字节都不一致,对于要做增量发布或文件哈希比对的项目不太友好。建议每次打包前把文档中info.setDateTime设置为源文件的lastModified(),这样同样的输入目录打出的zip包是稳定的,后续做缓存校验会顺利很多。
本文还有配套的精品资源,点击获取