27、Qt文件操作与XML/JSON:QFile、QDir、QFileInfo、XML解析(QXmlStreamReader)、JSON解析(QJsonDocument)

文件操作和序列化格式,是每个桌面应用都绕不开的坎。我做了这么多年Qt开发,几乎每个项目都要跟文件打交道——配置文件、日志文件、数据交换文件,样样都来。今天咱们就把Qt里文件操作和XML/JSON解析这块彻底讲透。

核心知识点速览:QFile读写文件、QDir遍历目录、QFileInfo获取文件属性、QXmlStreamReader解析XML、QJsonDocument处理JSON。这五个类,基本覆盖了日常开发中90%的文件处理场景。

Qt 文件操作与序列化知识体系 文件操作核心类 QFile 文件读写 QDir 目录遍历 QFileInfo 文件属性 QXmlStreamReader QJsonDocument QJsonObject/Array 应用场景:配置文件、日志、数据交换

QFile:最基础的文件读写

QFile是Qt里最直接的文件操作类。说白了,它就是帮你打开文件、读数据、写数据、关文件。用法跟C++标准库的fstream差不多,但跟Qt的信号槽机制配合得更好。

我个人习惯用QFile搭配QTextStream做文本文件读写,或者搭配QDataStream做二进制文件读写。举个例子,写一个简单的配置文件读取:

// 文本文件读取
QFile file("config.ini");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    qWarning() << "文件打开失败:" << file.errorString();
    return;
}

QTextStream in(&file);
while (!in.atEnd()) {
    QString line = in.readLine();
    // 处理每一行
    qDebug() << line;
}
file.close();

小技巧:记得检查open()的返回值。我在项目中遇到过好几次因为路径不对导致文件打不开,结果程序静默失败,排查了半天才发现是文件不存在。所以,养成检查返回值的习惯很重要。

写文件也类似,把OpenMode换成WriteOnly就行。不过要注意,默认是覆盖写,如果想追加内容,加上QIODevice::Append标志。

QDir:目录操作与文件遍历

QDir用来操作目录——创建、删除、重命名、遍历文件列表。你想想看,一个文件管理器软件,核心功能就是靠QDir实现的。

我最常用的几个方法:

  • entryList():获取目录下的文件和子目录列表
  • exists():判断目录是否存在
  • mkpath():创建多级目录
  • removeRecursively():递归删除目录及其内容
// 遍历当前目录下所有.cpp文件
QDir dir(".");
QStringList filters;
filters << "*.cpp" << "*.h";
dir.setNameFilters(filters);

QFileInfoList fileList = dir.entryInfoList();
foreach (const QFileInfo &info, fileList) {
    qDebug() << "文件名:" << info.fileName()
            << "大小:" << info.size()
            << "最后修改:" << info.lastModified();
}

注意:entryList()默认不排序。如果你需要按文件名或时间排序,记得调用setSorting()。我曾经因为没排序,结果文件列表顺序每次都不一样,导致测试用例随机失败,折腾了好久才发现是这个问题。

QFileInfo:文件属性的瑞士军刀

QFileInfo不直接操作文件内容,它专门用来获取文件的元信息。比如文件大小、创建时间、最后修改时间、文件权限、后缀名等等。

嗯,这里要注意一个细节:QFileInfo在构造时不会立即访问磁盘,它是在你第一次调用某个getter方法时才去获取信息。所以如果你频繁修改文件,记得调用refresh()方法更新缓存。

QFileInfo info("/home/user/document.txt");
if (info.exists()) {
    qDebug() << "绝对路径:" << info.absoluteFilePath();
    qDebug() << "基础名:" << info.baseName();      // "document"
    qDebug() << "后缀名:" << info.suffix();          // "txt"
    qDebug() << "完整后缀:" << info.completeSuffix(); // "txt"
    qDebug() << "是否可读:" << info.isReadable();
    qDebug() << "是否目录:" << info.isDir();
}

XML解析:QXmlStreamReader

XML虽然现在不如JSON流行,但在配置文件、文档格式(比如Office Open XML)里还是大量使用。Qt提供了两种XML解析方式:DOM(QDomDocument)和流式(QXmlStreamReader)。

我个人强烈推荐用QXmlStreamReader。为什么?因为它基于流,不会把整个XML文档加载到内存里。你想想看,如果遇到一个几百MB的XML文件,用DOM解析直接内存爆炸。而流式解析可以一边读一边处理,内存占用非常小。

QXmlStreamReader xml;
xml.setDevice(&file);  // file是已经打开的QFile对象

while (!xml.atEnd() && !xml.hasError()) {
    QXmlStreamReader::TokenType token = xml.readNext();
    
    if (token == QXmlStreamReader::StartElement) {
        if (xml.name() == "book") {
            QString id = xml.attributes().value("id").toString();
            qDebug() << "发现书籍, ID:" << id;
        } else if (xml.name() == "title") {
            QString title = xml.readElementText();
            qDebug() << "书名:" << title;
        }
    }
}

if (xml.hasError()) {
    qWarning() << "XML解析错误:" << xml.errorString();
}

避坑指南:我曾经在解析一个包含命名空间的XML时,直接用name()比较死活匹配不上。后来才发现,带命名空间的元素需要用qualifiedName()或者单独处理命名空间URI。记住,name()返回的是不带前缀的本地名,而qualifiedName()返回的是带前缀的完整名。

JSON解析:QJsonDocument

JSON现在是数据交换的事实标准。Qt的JSON支持非常完善,从Qt 5.0开始就内置了QJsonDocument、QJsonObject、QJsonArray、QJsonValue这几个类。

解析JSON的流程很简单:先把文本读进来,用QJsonDocument::fromJson()解析,然后根据顶层是对象还是数组,分别用object()或array()获取。

// 读取JSON文件
QFile file("data.json");
if (!file.open(QIODevice::ReadOnly)) {
    qWarning() << "无法打开文件";
    return;
}

QByteArray rawData = file.readAll();
file.close();

QJsonParseError parseError;
QJsonDocument doc = QJsonDocument::fromJson(rawData, &parseError);

if (parseError.error != QJsonParseError::NoError) {
    qWarning() << "JSON解析错误:" << parseError.errorString();
    return;
}

if (doc.isObject()) {
    QJsonObject obj = doc.object();
    QString name = obj["name"].toString();
    int age = obj["age"].toInt();
    QJsonArray hobbies = obj["hobbies"].toArray();
    
    qDebug() << "姓名:" << name << "年龄:" << age;
    foreach (const QJsonValue &val, hobbies) {
        qDebug() << "爱好:" << val.toString();
    }
}

写JSON也很简单,构造好QJsonObject或QJsonArray,然后调用QJsonDocument的toJson()方法,最后写入文件。

// 构建JSON并写入文件
QJsonObject person;
person["name"] = "张三";
person["age"] = 28;
person["isStudent"] = false;

QJsonArray hobbies;
hobbies.append("编程");
hobbies.append("阅读");
hobbies.append("跑步");
person["hobbies"] = hobbies;

QJsonDocument doc(person);
QFile file("output.json");
if (file.open(QIODevice::WriteOnly)) {
    file.write(doc.toJson(QJsonDocument::Indented));
    file.close();
}

关键对比:XML适合文档型数据(有属性、有嵌套、有注释),JSON适合结构化数据(轻量、易读、数组操作方便)。在实际项目中,我一般用JSON做配置文件,用XML做需要保留格式和注释的文档。

综合示例:文件配置管理器

最后,咱们把今天学的知识串起来,写一个简单的配置管理器。它支持从JSON或XML文件读取配置,也支持写入。

class ConfigManager {
public:
    bool loadFromJson(const QString &filePath) {
        QFile file(filePath);
        if (!file.open(QIODevice::ReadOnly)) return false;
        
        QJsonDocument doc = QJsonDocument::fromJson(file.readAll());
        file.close();
        
        if (doc.isNull()) return false;
        m_data = doc.object();
        return true;
    }
    
    bool loadFromXml(const QString &filePath) {
        QFile file(filePath);
        if (!file.open(QIODevice::ReadOnly)) return false;
        
        QXmlStreamReader xml(&file);
        m_data = QJsonObject();  // 清空
        
        while (!xml.atEnd()) {
            xml.readNext();
            if (xml.isStartElement()) {
                if (xml.name() == "config") continue;
                m_data[xml.name().toString()] = xml.readElementText();
            }
        }
        
        file.close();
        return !xml.hasError();
    }
    
    QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const {
        return m_data.value(key).toVariant(defaultValue);
    }
    
private:
    QJsonObject m_data;
};

这个类虽然简单,但已经能应付不少场景了。你可以在项目里直接拿来用,或者根据需求扩展。记住,文件操作的核心就三点:打开要检查、关闭要及时、路径要小心。把这三点记牢了,文件处理这块基本不会出大问题。