Qt5 进阶【12】JSON/XML 数据协议处理:与后端/配置文件的对接

目标读者:已经会用 Qt 写常规桌面程序,开始需要对接后端 REST 接口、管理本地配置/存档文件,但对 JSON/XML 解析还停留在"能跑就行"阶段的 C++/Qt 开发者。

示例环境:Qt 5.12/5.15 + Qt Creator + CMake(你习惯 qmake 也完全没问题,改一下工程文件即可)。


一、问题背景:乱 parse 的代价,通常要到线上才看见

1. "能跑就行"的 JSON 解析,迟早出事

和后端约定好一个 JSON 接口之后,最常见的写法大概是下面这样:

cpp 复制代码
QJsonDocument doc = QJsonDocument::fromJson(replyData);
QJsonObject obj = doc.object();

QString name = obj["name"].toString();
int age = obj["age"].toInt();
bool ok = obj["ok"].toBool();

刚开始看起来一切正常:

  • 接口字段和类型都和文档一致;
  • 测试环境数据完全受控。

但等项目跑到一定时间,接口字段被人悄悄加了一两个,或者某个字段类型从 int 改成了 string,或者上线环境返回了错误页 HTML,这种"啥检查都不做"的解析方式就会开始报错甚至崩溃:

  • 字段缺失:obj["age"] 返回默认值 0,逻辑以为这是合法数据;
  • 类型错误:obj["ok"].toBool()"yes" 变成 0,业务突然全失败;
  • 整个响应不是 JSON:fromJson() 解析失败,doc.isObject() 是 false,你还在调用 doc.object()

这些问题的共同点其实只有一句话:没有把"网络数据永远不可信"当回事

2. 配置文件一会儿 JSON,一会儿 XML,到底该用哪个?

配置/存档文件这块,更常见的场景是混用:

  • UI 层小配置、偏好、窗口位置,用 INI(QSettings 默认格式);
  • 业务配置有时是前端写的 JSON,有时是后端导出的 XML;
  • 某些老项目甚至还在用自定义文本格式。

结果就是:配置读写逻辑散落在各处,哪里需要就地 new 一个 QSettings 或直接手撸文本解析,久而久之:

  • 你自己都搞不清楚哪些配置在哪个文件里;
  • 想统一迁移格式(比如从 INI 迁到 JSON)变成了噩梦;
  • 稍微一升级版本,老配置立刻不兼容。

3. 大 JSON/XML 一次 readAll,内存曲线肉眼可见

还有一种"看不见"的坑是大文件处理:

cpp 复制代码
QFile file(path);
if (file.open(QIODevice::ReadOnly)) {
    QByteArray data = file.readAll();
    QJsonDocument doc = QJsonDocument::fromJson(data);
    // ...
}

对几 KB 的配置文件无所谓,但一旦你要处理的是:

  • 几十 MB 的服务器返回 JSON;
  • 上百 MB 的 XML 日志/报表;

一次性 readAll() + 构造整棵 JSON/XML 树,内存占用会极速飙升,甚至直接被系统干掉。

这一期我们就围绕这些真实问题,从 Qt 的 JSON/XML 能力 出发,搭一套"接口与配置中心"的小工程,顺手把健壮解析、流式读写和配置版本管理一起讲透。


二、核心知识点:Qt 里到底该怎么"对话" JSON/XML

1. QJsonDocument / QJsonObject / QJsonArray:JSON 三件套

1.1 基本结构和常用操作

整体关系可以简单理解为:

  • QJsonDocument:表示整个 JSON 文档,根可以是对象或数组;
  • QJsonObject:键值对集合,对应 { ... }
  • QJsonArray:有序数组,对应 [ ... ]

典型读写流程:

cpp 复制代码
// 从字节数组解析
QByteArray raw = reply->readAll();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error != QJsonParseError::NoError) {
    qWarning() << "JSON 解析失败:" << err.errorString();
    return;
}

if (!doc.isObject()) {
    qWarning() << "预期 JSON 对象,结果不是";
    return;
}

QJsonObject obj = doc.object();

// 安全取字段
if (obj.contains("name") && obj["name"].isString()) {
    QString name = obj["name"].toString();
}

构造一个 JSON 对象:

cpp 复制代码
QJsonObject user;
user["id"] = 123;
user["name"] = "ZhangSan";
user["age"] = 25;

QJsonArray scores;
scores.append(90);
scores.append(85);
scores.append(95);
user["scores"] = scores;

QJsonDocument doc(user);
QByteArray jsonBytes = doc.toJson(QJsonDocument::Indented);

要点:

  • 所有数字在 JSON 里都是"数值型",Qt 统一当 double 处理,toInt() 会做向下取整;
  • contains() + isXxx() 是稳解析的第一步;
  • QJsonValue 还能 isNull() / isUndefined(),可用于区分"字段没给"和"显式给 null"。
1.2 面向对象封装:fromJson / toJson

不要在业务代码里到处写 obj["xxx"],更不要混杂在 UI 控件逻辑里。

合理的做法是,每个业务实体写一个小类,把 JSON 读写逻辑藏进去:

cpp 复制代码
class User {
public:
    int id = 0;
    QString name;
    int age = 0;
    bool enabled = true;
    QList<int> scores;

    bool fromJson(const QJsonObject &obj);
    QJsonObject toJson() const;
};

实现示例(简化):

cpp 复制代码
bool User::fromJson(const QJsonObject &obj)
{
    if (!obj.contains("id") || !obj["id"].isDouble())
        return false;
    if (!obj.contains("name") || !obj["name"].isString())
        return false;

    id = obj["id"].toInt();
    name = obj["name"].toString();
    age = obj.value("age").toInt(0);       // 不存在就用默认 0
    enabled = obj.value("enabled").toBool(true);

    scores.clear();
    auto arr = obj.value("scores").toArray();
    for (const QJsonValue &v : arr) {
        if (v.isDouble())
            scores.append(v.toInt());
    }
    return true;
}

QJsonObject User::toJson() const
{
    QJsonObject obj;
    obj["id"] = id;
    obj["name"] = name;
    obj["age"] = age;
    obj["enabled"] = enabled;

    QJsonArray arr;
    for (int s : scores) arr.append(s);
    obj["scores"] = arr;
    return obj;
}

之后 UI 代码只和 User 打交道,就不用管 JSON 字段怎么命名、类型怎么变了。


2. QXmlStreamReader / QXmlStreamWriter:大 XML 的正确打开方式

2.1 流式读取:QXmlStreamReader

流式读取的好处:

  • 不会一次性把整棵 DOM 树放内存,适合大文件;
  • 更接近"边读边处理"的数据管道;

典型模式:

cpp 复制代码
QFile file("menu.xml");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
    return;

QXmlStreamReader reader(&file);

while (!reader.atEnd()) {
    auto token = reader.readNext();

    if (token == QXmlStreamReader::StartElement) {
        if (reader.name() == "item") {
            auto attrs = reader.attributes();
            QString text = attrs.value("text").toString();
            QString action = attrs.value("action").toString();
            // ...
        }
    } else if (token == QXmlStreamReader::EndElement) {
        // 根据 name 判定退出层级
    }
}

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

关键点:

  • readNext() 驱动状态机,自己维护当前所在元素;
  • attributes() 只在 StartElement 里调用,读完一个元素后继续往下读;
  • 对于嵌套结构,常常需要写递归解析函数(如 parseMenu(reader) 再内部读 item/submenu)。
2.2 流式写入:QXmlStreamWriter

类似地,写入时也尽量留在流式层面:

cpp 复制代码
QFile file("menu.xml");
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
    return;

QXmlStreamWriter writer(&file);
writer.setAutoFormatting(true);  // 便于人类阅读
writer.writeStartDocument("1.0", true);

writer.writeStartElement("menu");
writer.writeStartElement("item");
writer.writeAttribute("text", "新建");
writer.writeAttribute("shortcut", "Ctrl+N");
writer.writeAttribute("action", "new");
writer.writeEndElement(); // item

writer.writeEndElement(); // menu
writer.writeEndDocument();

写入大 XML 列表时,循环写入子元素,每一个条目处理完就扔掉中间状态,不做 DOM 累积。


3. 错误处理:给自己定一套"健壮 JSON 解析模板"

通用步骤

  1. 解析阶段

    • fromJson(raw, &err) → 检查 err;
    • 确认 doc.isObject()doc.isArray()
  2. 结构检查阶段

    • 对关键字段 contains()
    • 类型 isString() / isDouble() / isArray() / isObject()
    • 不要对 QJsonValue::type() 做复杂判断,直接用 isXxx() 更直观。
  3. 业务校验阶段

    • 数字范围(如年龄不能为负);
    • 枚举取值(status 必须在"ok/pending/error"之内);
    • 列表长度约束。

可以抽出一个小工具函数:

cpp 复制代码
template<typename TFunc>
bool checkField(const QJsonObject &obj,
                const QString &key,
                QJsonValue::Type expectedType,
                TFunc handler)
{
    if (!obj.contains(key)) {
        qWarning() << "JSON 缺少字段:" << key;
        return false;
    }
    const QJsonValue v = obj.value(key);
    if (v.type() != expectedType) {
        qWarning() << "JSON 字段类型不符:" << key
                   << "期望" << expectedType << "实际" << v.type();
        return false;
    }
    handler(v);
    return true;
}

使用示例:

cpp 复制代码
User u;
checkField(obj, "id", QJsonValue::Double, [&](const QJsonValue &v){
    u.id = v.toInt();
});
checkField(obj, "name", QJsonValue::String, [&](const QJsonValue &v){
    u.name = v.toString();
});

4. JSON / XML / INI:各自的用武之地

简单总结一下:

需求场景 JSON XML INI
REST 接口 强烈推荐 过得去 不适用
小型配置(偏好、窗口) 可以 过度 非常合适
复杂结构配置(层级多) 很合适 很合适 不合适
很大的结构化数据 一般(需流式) 适合(流式好) 不适合
人类可读 一般 很好
生态与文档 非常丰富 历史悠久 限于配置领域

粗暴一点的建议:

  • 网络协议优先 JSON
  • 简单配置优先 INI(QSettings)​
  • 需要与其他语言/老系统兼容且有大文件处理需求时用 XML

三、代码实战:接口模拟与配置中心(ProtocolDemo 工程)

下面这个工程叫 ProtocolDemo,它做三件事:

  1. 接口模拟 :用 ApiClient 构造一段 JSON,按"网络响应"的逻辑解析为 User 对象列表,并展示到表格中;
  2. 配置中心:当前主窗口的一些状态(比如配置格式选项)序列化到 JSON 或 INI;
  3. XML 菜单:从 XML 文件加载一组菜单项,动态生成菜单栏。

目录结构这次不写了,这里直接进入各模块设计和关键代码。完整工程代码可以找我要,后期会上传到资源。

1. User:面向对象 JSON 映射

上面已经展示了一个 User 的实现思路,这里重点强调几点实践要点:

  • 所有字段默认值在类定义时给出,确保部分字段缺失时仍然有合理状态;
  • fromJson() 返回 bool,外部可以根据失败与否做回退;
  • toJson() 只负责数据结构转换,不做业务逻辑。

在工程中,我们会在 ApiClient 里组合使用它,把后端返回的数据"翻译"成 QList<User> 再给 UI 使用。

2. ApiClient:模拟 REST API + 健壮 JSON 解析

ApiClient 的职责:

  • 提供 fetchUsers() 接口,模拟发起一个 HTTP GET 请求;
  • 提供 loadExampleData(),直接使用本地构造的 JSON,方便离线测试;
  • 对响应数据做严格 JSON 检查,填充 QList<User>
  • 通过 dataLoaded()errorOccurred() 信号把结果抛给 UI。

在真实项目中,把 createExampleJson() 换成网络请求即可。

你可以在 MainWindow 里这么用:

cpp 复制代码
connect(fetchBtn, &QPushButton::clicked,
        this, &MainWindow::onFetchUsers);

void MainWindow::onFetchUsers()
{
    m_apiClient->loadExampleData(); // 或 m_apiClient->fetchUsers();
}

void MainWindow::onDataLoaded()
{
    auto users = m_apiClient->users();
    populateTable(users);
}

3. XmlDataParser:用流式解析搭建菜单栏

XmlDataParser 做的事情,是把一份类似下面的 XML:

cpp 复制代码
<menu>
    <item text="新建" shortcut="Ctrl+N" action="new"/>
    <item text="打开" shortcut="Ctrl+O" action="open"/>
    <separator/>
    <submenu text="工具">
        <item text="选项" action="options"/>
    </submenu>
    <separator/>
    <item text="退出" shortcut="Ctrl+Q" action="quit"/>
</menu>

解析成 Qt 的 QMenu + QAction

cpp 复制代码
QMenu *menu = m_xmlParser->loadMenuFromXml("menu.xml");
menuBar()->addMenu(menu);
connect(menu, &QMenu::triggered, this, &MainWindow::onMenuTriggered);

优势:

  • 菜单结构完全由 XML 描述,不需要在 C++ 里硬编码所有菜单项;
  • 将来业务要支持"用户自定义快捷操作列表",只要出一份 XML 编辑器或者手动配文件即可。

4. ConfigManager:JSON / INI 配置统一入口

ConfigManager 的设计思想:

  • 对外屏蔽具体存储格式,统一用 QVariantMap 表达配置;
  • saveConfig(section, map, path, format) / loadConfig(...) 提供通用读写;
  • 自己内部决定用 JSON 还是 INI 或其他格式;
  • 给某些常见场景提供快捷函数,比如"保存窗口状态/列宽/用户偏好"。

同时,它还负责写入版本号:

cpp 复制代码
config["version"] = 1;
config["timestamp"] = QDateTime::currentDateTime().toString(Qt::ISODate);

以后版本升级时,只要检查这个 version 字段,就能做到兼容性判断。

5. MainWindow:把这些"协议能力"拼成一个可视小工具

MainWindow 做的事情:

  • 顶部一块表格(QTableWidget)显示用户列表;
  • 中间一排按钮:模拟接口请求、保存配置、加载配置;
  • 底部一块日志窗口,打印解析过程中的错误和提示;
  • 菜单栏用 XML 配置动态生成。

整体就像一个"协议实验台",你随时可以换掉 ApiClient 的实现、扩展 XmlDataParser 支持更多元素、给 ConfigManager 引入 YAML/TOML 等新格式,而主窗口基本不用动。


四、实战中的坑与优化:写给"已经踩过坑"的你

坑 1:obj["field"].toString() 不检查字段存在和类型

问题

cpp 复制代码
QString name = obj["name"].toString();   // name 不存在 → 返回 ""
int age = obj["age"].toInt();            // age 是 "abc" → 返回 0

你根本没法区分"字段缺失/类型错误"和"真的为 0 或空串"。

建议

  • 对于关键字段,务必 contains() + isXxx()
  • 对于可选字段,用 value(key).toInt(default),并标清默认值含义;
  • 所有解析失败路径要有日志而不是静默吞掉。

坑 2:大 JSON/XML 一次性读入内存

症状

  • 程序处理一个 100MB 的日志 JSON 时,瞬间占用几百 MB 内存;
  • Linux 低内存环境下直接被 OOM Killer 干掉。

应对

  • 大 XML:用 QXmlStreamReader/QXmlStreamWriter 逐节点处理,而不是 DOM;
  • 大 JSON:如有可能,让后端改成分页/分块接口,前端只处理当前页;
    如果必须处理大 JSON,可以考虑流式解析库(Qt 原生对流式 JSON 支持不强,实际中可以引入第三方),或者改用行分隔 JSON(NDJSON)。

坑 3:配置无版本号,升级时用户一片哀嚎

场景

  • V1.0 的配置里某个字段含义是"是否启用功能 A";
  • V2.0 把该字段语义改成"启动模式",但文件名没变,字段名也没变;
  • 老用户升级后,配置被照单全收,结果行为完全不符合预期。

解决方案

  • 每一个持久化配置文件必须写 version 字段;
  • 每当你想改字段含义时,version += 1,并在加载时判断:
    • 比较版本号,小版本按兼容策略迁移(加默认字段、调整值范围......);
    • 高版本可提示"此配置由更高版本生成,可能不兼容"。

坑 4:到处 new QSettings / 手动 parse JSON,完全没有"配置中心"

问题

  • 今天在 A 模块里写了一份 JSON 配置,明天在 B 模块里又写了一份 INI;
  • 谁都可以随时写文件、改字段,没人知道现在到底有多少种配置格式。

建议

  • 定义一个/少数几个"序列化/配置中心"类(例如本文的 ConfigManager);
  • 所有和配置相关的读写,都必须通过它们走;
  • 禁止其他模块直接 QSettings("xxx.ini")QFile("config.json")
  • 字段名、默认值、版本号全部集中维护。

坑 5:在 paintEvent / QGraphicsItem::paint 里做 JSON/XML 解析

这个在前几期的图形视图/多媒体里也提过,再强调一次:绘制路径必须尽量轻量

  • JSON/XML 解析、文件 IO、网络请求属于"重操作",必须在单独逻辑里做完,把结果缓存到内存结构中;
  • paint 只读内存结构直接画,最多做一点简单计算;
  • 如果你发现图形界面一堆卡顿,就检查是不是不小心把解析或持久化逻辑放到了绘制路径上。

五、小结:给自己定一套"协议与配置"的工程规范

结合这一期和前几期的内容,最后给出一份可以直接落在项目里的"JSON/XML 协议与配置处理守则":

  1. 所有 JSON 解析必须经过三个步骤

    • 解析:检查 QJsonParseError
    • 结构:确认根是 object/array,关键字段 contains + isXxx
    • 业务:做范围校验、枚举取值校验。
  2. 业务代码只操作实体类,不直接碰 QJsonObject/QXmlStreamReader

    • 每个实体类提供 fromJson/toJsonfromXml/toXml(视项目需要);
    • 协议字段变化时,只要改一处映射代码。
  3. 大 XML 一律流式读写,禁止 DOM 方式处理大文件

    • QXmlStreamReader/QXmlStreamWriter 是大 XML 的首选;
    • 不要对动辄几十 MB 的文件做 readAll() + QDomDocument
  4. 配置必须集中管理、必须有版本号

    • 通过统一的 ConfigManager/SettingsCenter 类访问;
    • 配置文件里必须有 version 字段,升级前后要有迁移逻辑。
  5. 协议/配置 IO 从 UI 和绘制逻辑中剥离

    • API 调用、JSON/XML 解析、持久化统一放在"数据访问层"或"服务层";
    • UI 通过信号槽拿到数据对象列表,专心刷新视图。
  6. 选择格式时优先考虑"团队能力 + 生态"

    • REST 接口:默认 JSON;
    • 小配置:优先 QSettings/INI,复杂才上 JSON;
    • 必须兼容其他系统、对"结构验证"要求高时考虑 XML + XSD。

把这一套规范真正落在项目里之后,你会发现:

  • 新增一个接口字段再也不是"惊心动魄的大手术",只是改几行映射代码;
  • 配置文件不再散落各处,调试/排查都能一眼看懂;
  • JSON/XML 不再是"玄学崩溃源",而是可控的协议边界。

最后,你可以把上面的 ProtocolDemo 工程当成一个"小骨架",直接放进自己的项目仓库,作为接口调试和配置管理的蓝本,后续只需要不断往里面填实体类和业务逻辑即可。

相关推荐
艾莉丝努力练剑5 小时前
【QT】信号与槽
linux·开发语言·c++·人工智能·windows·qt·qt5
轩情吖5 小时前
Qt的窗口(二)
开发语言·c++·qt·qdialog·对话框·桌面级开发
雨季6665 小时前
Flutter 三端应用实战:OpenHarmony “极简安全文本对齐调节器”
开发语言·前端·javascript·安全·flutter·交互
草莓熊Lotso5 小时前
脉脉独家【AI创作者xAMA第二期】| 从拼图游戏到AI设计革命
android·开发语言·c++·人工智能·脉脉
高-老师5 小时前
基于R语言的贝叶斯网络模型的实践技术应用;R语言实现Bayesian Network分析的基本流程
开发语言·r语言·贝叶斯网络
William_cl6 小时前
C# ASP.NET路由系统全解析:传统路由 vs 属性路由,避坑 + 实战一网打尽
开发语言·c#·asp.net
一起养小猫8 小时前
Flutter for OpenHarmony 实战:打造天气预报应用
开发语言·网络·jvm·数据库·flutter·harmonyos
安全二次方security²8 小时前
CUDA C++编程指南(7.25)——C++语言扩展之DPX
c++·人工智能·nvidia·cuda·dpx·cuda c++编程指南
xyq20248 小时前
Java 抽象类
开发语言