Qt Archive 入门教程:创建与解压文件


Qt Archive 入门教程:创建与解压文件

引言

在现代软件开发中,数据的存储和传输是不可或缺的一环。为了有效地管理磁盘空间、减少网络传输时间和带宽消耗,文件压缩和归档技术应运而生。Qt 作为一个功能全面且跨平台的 C++ 应用程序开发框架,在其核心库 QtCore 中提供了对数据压缩和解压缩的基础支持,使得开发者可以方便地在应用程序中集成这些功能。

虽然 Qt Core 本身没有提供一个像 QZipQTar 这样直接用于创建和管理标准归档文件(如 .zip.tar)的高级类库(这通常需要借助第三方库如 QuaZip),但它通过 QIODevice 的接口,结合特定的打开模式标志(Qt::CompressionQt::Uncompression),提供了基于 zlib 库的底层数据流压缩与解压缩能力。这对于处理单个文件或数据流的压缩(通常产生 .gz 类似格式的效果)或者在自定义数据格式中嵌入压缩数据非常有用。

本教程旨在详细介绍如何利用 Qt Core 提供的这些基础压缩/解压缩功能来创建压缩文件和解压文件。我们将深入探讨相关的 Qt 类和概念,并通过具体的代码示例,一步步指导你完成基本的文件压缩与解压缩操作。本文适合对 Qt 和 C++ 有一定基础,并希望了解如何在 Qt 应用中实现数据压缩功能的开发者。

本教程主要内容包括:

  1. Qt 压缩/解压缩功能概述
  2. 环境准备与设置
  3. 核心概念:QIODevice, QFile, Qt::Compression, Qt::Uncompression
  4. 实战:创建压缩文件(数据流压缩)
  5. 实战:解压文件(数据流解压缩)
  6. 结合 QDataStream 进行压缩
  7. 重要注意事项与局限性
  8. 处理大型文件与错误处理
  9. 关于标准归档格式(如 ZIP)的处理
  10. 总结与展望

1. Qt 压缩/解压缩功能概述

Qt 的压缩功能主要基于广泛使用的 zlib 库。zlib 提供了一套无损数据压缩方法,是许多开放标准(如 Gzip, PNG, ZIP 的 Deflate 算法)的基础。Qt 将 zlib 的功能集成到了其 I/O 系统中。

关键点在于 QIODevice 类。QIODevice 是 Qt 中所有输入/输出设备的基类(例如 QFile, QBuffer, QTcpSocket 等)。当你以特定模式打开一个 QIODevice 时,可以指定一个“过滤器”,这个过滤器可以在数据读写时自动进行处理。Qt 提供了两个内置的过滤器标志,用于处理压缩:

  • Qt::Compression: 当以写入模式打开设备时指定此标志,写入设备的数据将在实际写入底层设备(如文件)之前自动被压缩。
  • Qt::Uncompression: 当以读取模式打开设备时指定此标志,从底层设备读取的数据将在返回给应用程序之前自动被解压缩。

需要特别强调的是,这种机制主要作用于单个数据流。当你使用 QFile 配合这些标志时,通常是对整个文件内容进行压缩或解压缩,其效果类似于使用 gzipgunzip 处理单个文件,生成或读取 .gz 文件。它不直接支持创建或解析包含多个文件、目录结构和元数据的标准归档格式(如 .zip.tar)。

2. 环境准备与设置

要学习和实践本教程的内容,你需要:

  1. 安装 Qt 开发环境: 确保你已经安装了 Qt 库和开发工具(Qt Creator IDE 是推荐的选择)。可以从 Qt 官方网站下载在线或离线安装包。在安装过程中,请确保选择了包含 QtCore 模块的套件(通常是默认包含的)。
  2. C++ 编译器: Qt 需要一个兼容的 C++ 编译器(如 GCC, Clang, MSVC)。Qt 安装器通常会帮助配置或提示安装。
  3. 创建一个 Qt 项目: 你可以使用 Qt Creator 创建一个新的 Qt 控制台应用程序(Console Application)或 Widgets/QML 应用程序项目来进行实验。本教程的代码片段主要是核心逻辑,可以方便地集成到任何类型的 Qt 项目中。

在你的项目文件(.pro 文件使用 qmake,或 CMakeLists.txt 文件使用 CMake)中,确保 QtCore 模块被链接。通常,这是默认包含的,但检查一下没有坏处。

qmake (.pro 文件):
qmake
QT += core

CMake (CMakeLists.txt):
cmake
find_package(Qt6 COMPONENTS Core REQUIRED) # 或者 find_package(Qt5...)
target_link_libraries(your_target_name PRIVATE Qt::Core)

3. 核心概念

在我们深入代码之前,先理解几个关键的 Qt 类和概念:

  • QIODevice: 这是 Qt I/O 操作的抽象基类。它定义了如 open(), close(), read(), write(), readAll(), peek(), seek() 等通用 I/O 操作。所有具体的 I/O 设备(文件、内存缓冲区、网络套接字等)都继承自 QIODevice。压缩/解压缩过滤器就是通过 QIODevice::open() 方法的 mode 参数来应用的。

  • QFile: QIODevice 的一个重要子类,专门用于处理本地文件系统中的文件。它提供了打开、读取、写入、关闭文件的具体实现。我们将主要使用 QFile 来演示文件级别的压缩和解压缩。

  • QIODevice::OpenMode: 这是一个枚举类型,用于指定打开设备(如文件)的方式。常见的模式有 QIODevice::ReadOnly, QIODevice::WriteOnly, QIODevice::ReadWrite, QIODevice::Append, QIODevice::Truncate。关键在于,这些模式可以通过按位或(|)操作符与 Qt::CompressionQt::Uncompression 结合使用。

    • QIODevice::WriteOnly | Qt::Compression: 以写模式打开,写入的数据会被压缩。
    • QIODevice::ReadOnly | Qt::Uncompression: 以读模式打开,读取的数据会被自动解压缩。
  • QDataStream: 这个类提供了一个独立于操作系统和硬件的二进制数据流接口。它可以方便地序列化(写入)和反序列化(读取)各种 Qt 数据类型(如 int, double, QString, QList, QMap 等)到 QIODevice。当与配置了压缩/解压缩过滤器的 QIODevice(如 QFile)一起使用时,QDataStream 写入或读取的数据也会被相应地压缩或解压缩。

4. 实战:创建压缩文件(数据流压缩)

让我们从一个简单的例子开始:将一个已存在的文本文件压缩成一个新的文件。假设我们有一个名为 original.txt 的文件,我们想将其压缩为 compressed.dat(或者习惯上命名为 original.txt.gz)。

步骤:

  1. 创建一个 QFile 对象代表源文件 (original.txt),并以只读模式打开它。
  2. 创建一个 QFile 对象代表目标压缩文件 (compressed.dat),并以写入模式结合 Qt::Compression 标志打开它。
  3. 检查两个文件是否都成功打开。
  4. 从源文件读取数据。
  5. 将读取的数据写入目标文件。由于目标文件是以 Qt::Compression 模式打开的,写入的数据会被自动压缩。
  6. 关闭两个文件。

示例代码 (compressFile 函数):

```cpp

include

include

include

include

// 函数:将源文件压缩到目标文件
// sourceFilePath: 源文件路径
// compressedFilePath: 目标压缩文件路径
// compressionLevel: 压缩级别(-1=默认, 0=不压缩, 1=最快, 9=最佳压缩)
// 返回值:成功返回 true,失败返回 false
bool compressFile(const QString &sourceFilePath, const QString &compressedFilePath, int compressionLevel = -1)
{
QFile sourceFile(sourceFilePath);
QFile compressedFile(compressedFilePath);

// 1. 打开源文件(只读)
if (!sourceFile.open(QIODevice::ReadOnly)) {
    qWarning() << "Error: Cannot open source file for reading:" << sourceFile.errorString();
    return false;
}

// 2. 打开目标文件(写入 + 压缩)
// 注意:Qt::Compression 标志本身不接受级别参数,压缩级别是通过 QDataStream 或其他方式间接影响,
// 或者对于 QFile 直接使用时,通常使用 zlib 的默认级别。
// 如果需要精确控制压缩级别,可能需要更底层的 zlib API 或第三方库。
// 对于 QIODevice::open() 的 Qt::Compression,我们通常依赖默认设置。
// 这里我们添加一个注释说明级别控制的局限性。
// int effectiveCompressionLevel = qBound(0, compressionLevel, 9); // 理论上 zlib 支持 0-9
// QFile 似乎没有直接设置压缩级别的 API,它使用 zlib 默认值。
Q_UNUSED(compressionLevel); // 标记 compressionLevel 参数当前未使用

if (!compressedFile.open(QIODevice::WriteOnly | Qt::Compression)) {
    qWarning() << "Error: Cannot open target file for writing with compression:" << compressedFile.errorString();
    sourceFile.close(); // 确保关闭已打开的源文件
    return false;
}

qInfo() << "Starting compression from" << sourceFilePath << "to" << compressedFilePath;

// 3. 读取源文件并写入目标文件(自动压缩)
// 使用 readAll() 适合小文件,对于大文件应分块读写(见后文)
QByteArray data = sourceFile.readAll();
if (sourceFile.error() != QFile::NoError) {
     qWarning() << "Error reading from source file:" << sourceFile.errorString();
     sourceFile.close();
     compressedFile.close();
     return false;
}

qint64 bytesWritten = compressedFile.write(data);
if (bytesWritten == -1 || compressedFile.error() != QFile::NoError) {
    qWarning() << "Error writing compressed data to target file:" << compressedFile.errorString();
    sourceFile.close();
    compressedFile.close();
    // 可能需要删除不完整的压缩文件
    // compressedFile.remove();
    return false;
}

// 4. 关闭文件
sourceFile.close();
// compressedFile.flush(); // 确保所有缓冲数据已写入(通常 close() 会做)
compressedFile.close();

if (compressedFile.error() != QFile::NoError) {
     qWarning() << "Error closing the compressed file:" << compressedFile.errorString();
     // 文件可能已部分写入,但关闭时出错
     return false; // 或者根据情况决定是否算成功
}

qInfo() << "Compression finished successfully. Bytes written (compressed):" << bytesWritten;
return true;

}

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

// 准备一个测试文件 original.txt
QFile testFile("original.txt");
if (testFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
    QTextStream out(&testFile);
    out << "This is a test file for Qt compression tutorial.\n";
    out << "It contains multiple lines of text.\n";
    out << "We will compress this file using Qt::Compression flag.\n";
    testFile.close();
    qInfo() << "Created test file: original.txt";
} else {
    qWarning() << "Failed to create test file.";
    return 1;
}

// 调用压缩函数
QString source = "original.txt";
QString compressed = "compressed.dat"; // 或 "original.txt.gz"
if (compressFile(source, compressed)) {
    qInfo() << "File" << source << "successfully compressed to" << compressed;
} else {
    qWarning() << "File compression failed.";
}

// 可以在这里添加解压缩的调用进行验证

// return a.exec(); // 如果是控制台应用,可能不需要事件循环
return 0;

}
```

代码解释:

  • 我们包含了必要的头文件。
  • compressFile 函数接收源文件路径和目标压缩文件路径。
  • 使用 QFile 分别表示源文件和目标文件。
  • 源文件以 QIODevice::ReadOnly 打开。
  • 目标文件以 QIODevice::WriteOnly | Qt::Compression 打开。这里的 | 是按位或操作符,用于组合模式标志。
  • 进行了错误检查,确保文件都成功打开。
  • 使用 sourceFile.readAll() 读取源文件的全部内容到 QByteArray注意:对于非常大的文件,这会消耗大量内存,后面会讨论更好的方法。
  • 使用 compressedFile.write(data) 将数据写入目标文件。因为打开时指定了 Qt::Compression,Qt 会在内部调用 zlib 进行压缩,然后将压缩后的数据写入文件。
  • 最后,关闭两个文件。关闭目标文件很重要,因为它会确保所有缓冲的压缩数据被刷新到底层文件,并写入必要的压缩尾部信息。
  • main 函数展示了如何创建一个简单的测试文件并调用 compressFile

运行这段代码后,你应该会看到一个 compressed.dat 文件,它的大小通常会比 original.txt 小。你可以尝试用标准的 gunzip 工具(如果你的系统上有)来解压这个 compressed.dat 文件(可能需要先重命名为 .gz 后缀),验证其内容是否与 original.txt 一致。

5. 实战:解压文件(数据流解压缩)

现在,我们来实现反向操作:解压缩之前创建的 compressed.dat 文件,恢复出 original_restored.txt

步骤:

  1. 创建一个 QFile 对象代表压缩文件 (compressed.dat),并以只读模式结合 Qt::Uncompression 标志打开它。
  2. 创建一个 QFile 对象代表解压后的目标文件 (original_restored.txt),并以写入模式打开它。
  3. 检查两个文件是否都成功打开。
  4. 从压缩文件读取数据。由于是以 Qt::Uncompression 模式打开的,读取操作会自动解压缩数据。
  5. 将解压后的数据写入目标文件。
  6. 关闭两个文件。

示例代码 (decompressFile 函数):

```cpp

include

include

include

include

// (接上文的 compressFile 函数和 main 函数)

// 函数:将压缩文件解压到目标文件
// compressedFilePath: 源压缩文件路径
// decompressedFilePath: 目标解压文件路径
// 返回值:成功返回 true,失败返回 false
bool decompressFile(const QString &compressedFilePath, const QString &decompressedFilePath)
{
QFile compressedFile(compressedFilePath);
QFile decompressedFile(decompressedFilePath);

// 1. 打开压缩文件(只读 + 解压缩)
if (!compressedFile.open(QIODevice::ReadOnly | Qt::Uncompression)) {
    qWarning() << "Error: Cannot open compressed file for reading with decompression:" << compressedFile.errorString();
    return false;
}

// 2. 打开目标文件(写入,如果已存在则清空)
if (!decompressedFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
    qWarning() << "Error: Cannot open target file for writing:" << decompressedFile.errorString();
    compressedFile.close(); // 关闭已打开的压缩文件
    return false;
}

qInfo() << "Starting decompression from" << compressedFilePath << "to" << decompressedFilePath;

// 3. 读取压缩文件(自动解压)并写入目标文件
// 同样,使用 readAll() 适合小文件
QByteArray decompressedData = compressedFile.readAll();
 if (compressedFile.error() != QFile::NoError) {
     qWarning() << "Error reading or decompressing from source file:" << compressedFile.errorString();
     compressedFile.close();
     decompressedFile.close();
     return false;
}

qint64 bytesWritten = decompressedFile.write(decompressedData);
if (bytesWritten == -1 || decompressedFile.error() != QFile::NoError) {
    qWarning() << "Error writing decompressed data to target file:" << decompressedFile.errorString();
    compressedFile.close();
    decompressedFile.close();
    // 可能需要删除不完整的解压文件
    // decompressedFile.remove();
    return false;
}

// 4. 关闭文件
compressedFile.close();
decompressedFile.close();

 if (decompressedFile.error() != QFile::NoError) {
     qWarning() << "Error closing the decompressed file:" << decompressedFile.errorString();
     return false;
 }

qInfo() << "Decompression finished successfully. Bytes written (decompressed):" << bytesWritten;
return true;

}

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

// (创建 original.txt 的代码...)

QString source = "original.txt";
QString compressed = "compressed.dat";
QString restored = "original_restored.txt";

// 先压缩
if (compressFile(source, compressed)) {
    qInfo() << "File" << source << "successfully compressed to" << compressed;

    // 然后解压缩
    if (decompressFile(compressed, restored)) {
        qInfo() << "File" << compressed << "successfully decompressed to" << restored;

        // 可选:验证内容是否一致
        QFile f1(source);
        QFile f2(restored);
        if (f1.open(QIODevice::ReadOnly) && f2.open(QIODevice::ReadOnly)) {
            if (f1.readAll() == f2.readAll()) {
                qInfo() << "Verification successful: Original and restored files are identical.";
            } else {
                qWarning() << "Verification failed: Original and restored files differ.";
            }
            f1.close();
            f2.close();
        } else {
            qWarning() << "Could not open files for verification.";
        }

    } else {
        qWarning() << "File decompression failed.";
    }

} else {
    qWarning() << "File compression failed.";
}

return 0;

}
```

代码解释:

  • decompressFile 函数结构与 compressFile 非常相似。
  • 关键区别在于打开压缩文件时使用了 QIODevice::ReadOnly | Qt::Uncompression 模式。
  • 目标文件(用于存放解压后的内容)以普通的 QIODevice::WriteOnly 模式打开(加上 Truncate 确保如果文件已存在则清空内容)。
  • 当调用 compressedFile.readAll() 时,Qt 的 I/O 系统会自动处理 zlib 解压缩,返回的是原始的、未压缩的数据。
  • 然后将这些解压后的数据写入目标文件。
  • main 函数现在演示了先压缩再解压缩,并添加了一个简单的验证步骤,通过读取原始文件和恢复后的文件内容进行比较,确认解压缩是否成功且无误。

运行修改后的 main 函数,你应该能看到 original.txt 被压缩成 compressed.dat,然后 compressed.dat 被解压缩成 original_restored.txt。最后的消息会告诉你验证结果。

6. 结合 QDataStream 进行压缩

QDataStream 可以与配置了压缩/解压缩过滤器的 QIODevice 无缝协作。这使得你可以方便地将复杂的 Qt 数据结构(如 QStringList, QMap, 自定义对象等)序列化到压缩文件中,或从压缩文件中反序列化出来。

示例:将 QStringList 写入压缩文件

```cpp

include

include

include

include

bool saveStringListCompressed(const QString &filePath, const QStringList &list)
{
QFile file(filePath);
// 打开文件用于写入,并启用压缩
if (!file.open(QIODevice::WriteOnly | Qt::Compression)) {
qWarning() << "Failed to open file for compressed writing:" << file.errorString();
return false;
}

QDataStream out(&file);
// 设置版本以确保兼容性
out.setVersion(QDataStream::Qt_5_15); // 或者你使用的 Qt 版本

// 使用 QDataStream 的 << 操作符写入 QStringList
out << list;

// QDataStream 的析构函数(或手动关闭 file)会确保数据被刷新和压缩完成
file.close(); // 显式关闭是个好习惯

if (file.error() != QFile::NoError) {
    qWarning() << "Error during compressed writing or closing:" << file.errorString();
    return false;
}

qInfo() << "QStringList successfully saved and compressed to" << filePath;
return true;

}

bool loadStringListCompressed(const QString &filePath, QStringList &list)
{
QFile file(filePath);
// 打开文件用于读取,并启用解压缩
if (!file.open(QIODevice::ReadOnly | Qt::Uncompression)) {
qWarning() << "Failed to open file for compressed reading:" << file.errorString();
return false;
}

QDataStream in(&file);
in.setVersion(QDataStream::Qt_5_15); // 必须与写入时使用的版本一致

// 使用 QDataStream 的 >> 操作符读取 QStringList
in >> list;

file.close();

if (in.status() != QDataStream::Ok) {
     qWarning() << "Error during compressed reading (QDataStream status not OK)";
     // 检查文件错误可能也有帮助
     if (file.error() != QFile::NoError) {
          qWarning() << "File error:" << file.errorString();
     }
    return false;
}
 if (file.error() != QFile::NoError) {
    qWarning() << "Error closing file after reading:" << file.errorString();
    // 数据可能已读出,但关闭出错
}


qInfo() << "QStringList successfully loaded and decompressed from" << filePath;
return true;

}

// 在 main 或其他地方调用:
// QStringList myData = {"Apple", "Banana", "Cherry"};
// QString compressedDataFile = "list_data.dat.gz";
// if (saveStringListCompressed(compressedDataFile, myData)) {
// QStringList loadedData;
// if (loadStringListCompressed(compressedDataFile, loadedData)) {
// qDebug() << "Loaded data:" << loadedData;
// // 验证 loadedData 是否等于 myData
// }
// }
```

在这个例子中,QDataStream 负责将 QStringList 序列化为字节流,而 QFile 在写入(<< 操作符触发)时自动压缩这些字节流,或在读取(>> 操作符触发)时自动解压缩。开发者只需关注 QDataStream 的接口,压缩/解压缩对 QDataStream 的使用者来说是透明的。

7. 重要注意事项与局限性

  1. 单流处理: 如前所述,Qt::Compression/Qt::Uncompression 是作用于整个 QIODevice 数据流的。它们不理解也不创建像 ZIP 或 TAR 那样的包含多个文件条目、目录结构和元数据的归档格式。如果你用这种方法压缩一个目录,你实际上需要自己遍历目录,将每个文件的内容(可能还有文件名和路径信息)序列化到一个单一的压缩流中,解压时也需要自己解析这个流来重建目录和文件。这通常很复杂,不如使用专门的库。

  2. 格式兼容性: 使用 QFileQt::Compression 产生的文件通常与标准的 gzip 格式兼容(因为它内部使用 zlib)。这意味着你可以用标准的 gunzip 工具解压 Qt 程序创建的压缩文件,反之亦然(只要内容是单一的数据流)。

  3. 压缩级别: Qt 的 QIODevice::open() 接口没有直接提供设置 zlib 压缩级别(0-9)的方法。它通常使用 zlib 的默认压缩级别(Z_DEFAULT_COMPRESSION,通常是 6)。如果你需要更精细地控制压缩速度与压缩率的平衡,或者使用不同的压缩算法(如 bzip2, lzma),你需要使用更底层的 zlib API,或者寻找支持这些功能的第三方 Qt 库(如 KArchive)或 C++ 库。

  4. QDataStream 版本: 当使用 QDataStream 时,务必设置并匹配读写两端的 QDataStream 版本 (setVersion()),以确保数据格式的兼容性,尤其是在跨 Qt 版本或长期存储数据时。

  5. 不是真正的“归档”: 术语“Qt Archive”在本教程中主要指利用 Qt 的压缩功能。严格意义上的“归档”通常指将多个文件打包成一个文件的能力。Qt Core 本身不直接提供这个功能。

8. 处理大型文件与错误处理

处理大型文件

前面示例中使用的 readAll() 方法会将整个文件内容加载到内存中,这对于小文件来说很方便,但对于大文件(几百 MB 或 GB 级别)则会导致内存耗尽。处理大文件时,必须采用分块读写的方式。

分块压缩示例:

```cpp
bool compressFileChunked(const QString &sourceFilePath, const QString &compressedFilePath, qint64 chunkSize = 65536) // 64KB chunk size
{
QFile sourceFile(sourceFilePath);
QFile compressedFile(compressedFilePath);

if (!sourceFile.open(QIODevice::ReadOnly)) { /* ... error handling ... */ return false; }
if (!compressedFile.open(QIODevice::WriteOnly | Qt::Compression)) { /* ... error handling ... */ sourceFile.close(); return false; }

qInfo() << "Starting chunked compression from" << sourceFilePath << "to" << compressedFilePath;
char buffer[chunkSize]; // 使用栈上的缓冲区,或者 QByteArray(chunkSize, 0)
qint64 totalBytesRead = 0;
qint64 totalBytesWritten = 0;

while (!sourceFile.atEnd()) {
    qint64 bytesRead = sourceFile.read(buffer, chunkSize);
    if (bytesRead == -1) {
        qWarning() << "Error reading chunk from source file:" << sourceFile.errorString();
        sourceFile.close();
        compressedFile.close();
        return false;
    }
    if (bytesRead == 0 && !sourceFile.atEnd()) {
        // 可能发生了某种读取问题,但没有报告错误
        qWarning() << "Read 0 bytes but not at end of file, stopping.";
         sourceFile.close();
         compressedFile.close();
        return false;
    }
    if (bytesRead > 0) {
        qint64 bytesWritten = compressedFile.write(buffer, bytesRead);
        if (bytesWritten == -1 || bytesWritten != bytesRead) { // 注意:压缩后写入的字节数通常不等于读取的字节数,write应该返回写入压缩数据的量,但这里检查的是write的调用是否成功以及是否写入了它被告知要处理的数据量(压缩前)对应的压缩结果。更好的检查是看 write 是否返回 -1
             qWarning() << "Error writing compressed chunk to target file:" << compressedFile.errorString();
             sourceFile.close();
             compressedFile.close();
             return false;
        }
         // 也可以检查 compressedFile.bytesWritten() 来跟踪总写入量,但 write 返回值通常足够
         totalBytesWritten += bytesWritten; // 跟踪实际写入的压缩字节数
    }
    totalBytesRead += bytesRead;
    // 可选:报告进度
    // double progress = (double)sourceFile.pos() / sourceFile.size() * 100.0;
    // qDebug() << QString("Compression progress: %1%").arg(progress, 0, 'f', 2);
}

sourceFile.close();
compressedFile.close(); // 确保关闭以刷新缓冲区

if (compressedFile.error() != QFile::NoError) {
    qWarning() << "Error finalizing or closing compressed file:" << compressedFile.errorString();
    return false;
}

qInfo() << "Chunked compression finished. Total bytes read:" << totalBytesRead << "Total bytes written (compressed):" << totalBytesWritten; // 注意:这里 totalBytesWritten 可能会因缓冲而不完全精确,但关闭文件后磁盘上的大小是最终大小。
return true;

}
```

分块解压缩 也遵循类似的逻辑:循环读取压缩文件(数据会自动解压到缓冲区),然后将缓冲区的内容写入目标文件。

错误处理

健壮的程序必须进行充分的错误处理。在使用 QFile 或其他 QIODevice 时,应始终检查:

  • open() 的返回值:确保文件成功打开。如果失败,使用 errorString() 获取错误信息。
  • read()write() 的返回值:检查是否发生 I/O 错误(通常返回 -1)。对于 write(),还要确保写入了预期的数据量(虽然对于压缩流,写入的字节数与原始字节数不同,但 write 本身不应返回 -1 或小于请求处理的数据量对应的结果(除非是错误))。
  • QFile::error()QIODevice::error():在操作后检查设备的状态,QFile::NoError 表示没有错误。
  • QDataStream::status():当使用 QDataStream 时,检查其状态以确保序列化/反序列化操作成功。

在发生错误时,应向用户报告问题(例如通过日志、状态栏消息或对话框),并确保所有已打开的资源(如文件句柄)被正确关闭。有时,可能还需要清理操作(如删除不完整的输出文件)。

9. 关于标准归档格式(如 ZIP)的处理

正如多次强调的,Qt Core 的 Qt::Compression/Qt::Uncompression 机制不直接支持创建、读取或修改标准的 ZIP 或 TAR 归档文件。这些格式有自己的文件结构规范,包含文件头、目录、文件数据以及各种元数据(文件名、时间戳、权限、压缩方法等)。

如果你需要在 Qt 应用程序中处理标准的 .zip 文件(例如,解压用户下载的 ZIP 包,或将程序生成的一组文件打包成 ZIP),你应该使用第三方库

一个非常流行且与 Qt 集成良好的选择是 QuaZip

  • QuaZip: 是一个基于 MiniZip 库(zlib 包的一部分)的 Qt/C++ 包装库。它提供了易于使用的 Qt 风格 API 来创建、读取和修改 ZIP 和 UNZIP 归档文件。
    • 支持添加文件、添加目录、从归档中读取文件列表、按名称提取文件、密码保护等。
    • 通常需要单独下载、编译并集成到你的 Qt 项目中。
    • 官方网站或 GitHub 页面通常包含集成说明。

使用 QuaZip 的基本流程大致如下:

  • 解压:

    1. 创建 JlCompress 对象 (QuaZip 提供的便捷类) 或 QuaZip 对象。
    2. 打开 ZIP 文件。
    3. 获取文件列表 (getFileInfoList())。
    4. 遍历列表或按名称查找,打开归档内的文件 (setCurrentFile(), QuaZipFile 对象)。
    5. 像使用 QFile 一样读取 QuaZipFile 的内容。
    6. 或者使用 JlCompress::extractDir() 一次性解压整个目录。
  • 压缩:

    1. 创建 QuaZip 对象并以写入模式打开目标 ZIP 文件。
    2. 对于要添加的每个文件/目录:
      • 创建 QuaZipFile 对象。
      • 设置文件名(在 ZIP 内部的路径)。
      • 打开 QuaZipFile 用于写入。
      • 将源文件内容写入 QuaZipFile
      • 关闭 QuaZipFile
    3. 关闭 QuaZip 对象以完成归档。
    4. 或者使用 JlCompress::compressDir() / compressFiles() 简化常见任务。

如果你需要处理 .tar, .tar.gz, .tar.bz2 等格式,Qt 的 KArchive 框架(源自 KDE)是一个强大的选择,尽管它可能依赖更多的 KDE Frameworks 库。

10. 总结与展望

本教程详细介绍了如何使用 Qt Core 中 QIODeviceQt::CompressionQt::Uncompression 标志来实现基本的数据流压缩与解压缩。我们学习了:

  • Qt 压缩功能基于 zlib,并通过 QIODevice 过滤器实现。
  • 如何使用 QFile 结合这些标志来压缩和解压缩单个文件(类似于 gzip/gunzip)。
  • 如何将 QDataStream 与压缩的 QIODevice 结合使用,方便地序列化/反序列化 Qt 数据类型到压缩流。
  • 处理大文件时采用分块读写的重要性。
  • 进行健壮错误处理的必要性。
  • 明确了 Qt Core 内建功能的局限性:它不直接支持多文件归档格式如 ZIP。
  • 对于标准的 ZIP 文件操作,推荐使用第三方库如 QuaZip。

虽然 Qt Core 的内建压缩功能相对基础,但对于需要对单一数据流(无论是文件还是内存中的数据)进行快速压缩/解压缩的场景,它提供了一个方便、跨平台且与 Qt I/O 系统紧密集成的解决方案。理解了这些基础知识,你就能在自己的 Qt 应用中更有效地管理数据存储和传输了。

如果你需要更复杂的归档功能,下一步可以研究 QuaZip 或 KArchive 等库,它们将在 Qt Core 提供的基础上,为你打开更广阔的应用天地。祝你在 Qt 开发的道路上不断进步!


THE END