
Qt 中使用 QSettings 存储结构体数据详解
-
- [1、 引言](#1、 引言)
- [2、 为什么不能直接存结构体](#2、 为什么不能直接存结构体)
- [3、 方案一:字段拆分存储](#3、 方案一:字段拆分存储)
- [4、 方案二:QDataStream 二进制序列化](#4、 方案二:QDataStream 二进制序列化)
-
- [4.1 、为结构体重载流操作符](#4.1 、为结构体重载流操作符)
- [4.2、 写入与读取](#4.2、 写入与读取)
- [4.3 、关键细节](#4.3 、关键细节)
- [5、 方案三:QJsonObject / QVariantMap 文本化存储](#5、 方案三:QJsonObject / QVariantMap 文本化存储)
-
- [5.1 、使用 QVariantMap](#5.1 、使用 QVariantMap)
- [5.2 、使用 QJsonObject](#5.2 、使用 QJsonObject)
- [6、 处理嵌套结构体与容器](#6、 处理嵌套结构体与容器)
-
- [6.1 、二进制方案](#6.1 、二进制方案)
- [6.2、 JSON 方案](#6.2、 JSON 方案)
- [7、 版本兼容与数据迁移](#7、 版本兼容与数据迁移)
- 8、工程化封装建议
- 9、方案对比与选型建议
- 10、总结

1、 引言
在 Qt 桌面应用开发中,配置持久化几乎是绕不开的需求:窗口大小、最近打开的文件、用户偏好、服务器地址......这些数据通常用 QSettings 保存。QSettings 用起来很简单,setValue("key", value) 一行就能写入,value("key") 一行就能读出。
但一旦遇到结构体 ,事情就没那么直观了。QSettings 原生只认识 QVariant 支持的类型(int、QString、QStringList、QByteArray、QRect 等),并不认识你自己定义的 struct。于是常见的问题就出现了:
- 直接
setValue("cfg", myStruct)编译不过; - 拆成多个 key 存,字段一多就散乱难维护;
- 用
QDataStream序列化后存QByteArray,但版本升级后字段变了,读旧配置直接错乱; - 结构体里嵌套
QList、QMap,不知道该怎么处理。
本文系统梳理在 Qt 中用 QSettings 存储结构体数据的几种主流方案,从最简单的字段拆分,到 QDataStream 二进制序列化,再到 QJsonObject / QVariantMap 文本化存储,并给出版本兼容 、默认值 、异常处理等工程化细节,最后给出选型建议。
2、 为什么不能直接存结构体
先明确一个前提:QSettings::setValue() 的第二个参数类型是 const QVariant &。也就是说,能被 QSettings 存储的东西,必须是能装进 QVariant 的类型。
cpp
void QSettings::setValue(const QString &key, const QVariant &value);
QVariant 内置支持的类型包括:
| 类别 | 示例类型 |
|---|---|
| 基础类型 | int、double、bool、QString |
| 容器类型 | QStringList、QVariantList、QVariantMap、QVariantHash |
| 几何类型 | QPoint、QSize、QRect、QSizeF |
| 字节类型 | QByteArray |
| 日期时间 | QDate、QTime、QDateTime |
自定义 struct 不在其中。虽然可以通过 Q_DECLARE_METATYPE 让结构体进入 QVariant,但 QSettings 在写入时还需要知道如何把它转成可持久化的文本/二进制形式,否则会写入失败或读回时类型丢失。
因此,存储结构体的本质是:先把结构体转换成 QSettings 认识的形式,读取时再还原。这就引出了下面几种方案。
3、 方案一:字段拆分存储
最朴素的做法,是把结构体的每个字段单独存成一个 key。
cpp
struct AppConfig {
QString serverUrl;
int port = 8080;
bool autoConnect = true;
QStringList recentFiles;
};
写入:
cpp
void saveConfig(const AppConfig &cfg)
{
QSettings settings("MyCompany", "MyApp");
settings.beginGroup("AppConfig");
settings.setValue("serverUrl", cfg.serverUrl);
settings.setValue("port", cfg.port);
settings.setValue("autoConnect", cfg.autoConnect);
settings.setValue("recentFiles", cfg.recentFiles);
settings.endGroup();
}
读取:
cpp
AppConfig loadConfig()
{
QSettings settings("MyCompany", "MyApp");
AppConfig cfg;
settings.beginGroup("AppConfig");
cfg.serverUrl = settings.value("serverUrl", cfg.serverUrl).toString();
cfg.port = settings.value("port", cfg.port).toInt();
cfg.autoConnect = settings.value("autoConnect", cfg.autoConnect).toBool();
cfg.recentFiles = settings.value("recentFiles", cfg.recentFiles).toStringList();
settings.endGroup();
return cfg;
}
优点:
- 实现简单,无序列化开销;
- 配置文件可读性好,用户能直接看懂并手工修改;
- 单个字段缺失时可以用默认值兜底,容错性强。
缺点:
- 字段一多,读写代码冗长,容易漏字段;
- 结构体嵌套时 key 层级会变得很深;
- 增删字段需要同步改两处代码,维护成本高。
适用场景:字段少(5 个以内)、结构稳定、需要用户手工编辑配置文件的场景。
4、 方案二:QDataStream 二进制序列化
Qt 的 QDataStream 天然支持自定义类型的序列化,只要为结构体重载 operator<< 和 operator>>,就能把整个结构体写进 QByteArray,再交给 QSettings 保存。
4.1 、为结构体重载流操作符
cpp
#include <QDataStream>
#include <QString>
#include <QStringList>
struct AppConfig {
QString serverUrl;
int port = 8080;
bool autoConnect = true;
QStringList recentFiles;
};
QDataStream &operator<<(QDataStream &out, const AppConfig &cfg)
{
out << cfg.serverUrl
<< cfg.port
<< cfg.autoConnect
<< cfg.recentFiles;
return out;
}
QDataStream &operator>>(QDataStream &in, AppConfig &cfg)
{
in >> cfg.serverUrl
>> cfg.port
>> cfg.autoConnect
>> cfg.recentFiles;
return in;
}
4.2、 写入与读取
cpp
void saveConfig(const AppConfig &cfg)
{
QByteArray data;
QDataStream out(&data, QIODevice::WriteOnly);
out.setVersion(QDataStream::Qt_6_5); // 固定流版本,保证跨版本兼容
out << cfg;
QSettings settings("MyCompany", "MyApp");
settings.setValue("AppConfig", data);
}
AppConfig loadConfig()
{
QSettings settings("MyCompany", "MyApp");
QByteArray data = settings.value("AppConfig").toByteArray();
AppConfig cfg;
if (data.isEmpty())
return cfg; // 首次运行,返回默认值
QDataStream in(&data, QIODevice::ReadOnly);
in.setVersion(QDataStream::Qt_6_5);
in >> cfg;
return cfg;
}
4.3 、关键细节
- 必须固定
QDataStream版本 。QDataStream的默认版本随 Qt 版本变化,不同版本写入的二进制格式可能不同。显式调用setVersion()并写死一个版本号,才能保证升级 Qt 后仍能读回旧数据。 QSettings会把QByteArray转成文本存储 。在 INI 格式下,二进制数据会被转义成@ByteArray(...)形式,虽然可读性差,但能正确往返。- 字段顺序即协议 。
operator<<和operator>>的字段顺序必须严格一致,否则读出来的数据会错位。
优点:
- 一个 key 存整个结构体,代码简洁;
- 支持任意嵌套类型(
QList、QMap、自定义子结构体); - 性能好,适合数据量较大的场景。
缺点:
- 二进制不可读,用户无法手工修改;
- 版本兼容性差:结构体增删字段后,旧数据无法直接读取,需要额外做版本标记;
- 调试困难,出问题只能靠打印字节。
适用场景:结构体字段多、嵌套深、不需要用户手工编辑、且能接受版本迁移成本的场景。
5、 方案三:QJsonObject / QVariantMap 文本化存储
如果既想要"一个 key 存整个结构体"的简洁,又想要可读性和较好的版本兼容性,可以把结构体转成 QJsonObject 或 QVariantMap,再存进 QSettings。
5.1 、使用 QVariantMap
cpp
QVariantMap toVariantMap(const AppConfig &cfg)
{
QVariantMap map;
map["serverUrl"] = cfg.serverUrl;
map["port"] = cfg.port;
map["autoConnect"] = cfg.autoConnect;
map["recentFiles"] = cfg.recentFiles;
return map;
}
AppConfig fromVariantMap(const QVariantMap &map)
{
AppConfig cfg;
cfg.serverUrl = map.value("serverUrl", cfg.serverUrl).toString();
cfg.port = map.value("port", cfg.port).toInt();
cfg.autoConnect = map.value("autoConnect", cfg.autoConnect).toBool();
cfg.recentFiles = map.value("recentFiles", cfg.recentFiles).toStringList();
return cfg;
}
写入读取:
cpp
void saveConfig(const AppConfig &cfg)
{
QSettings settings("MyCompany", "MyApp");
settings.setValue("AppConfig", toVariantMap(cfg));
}
AppConfig loadConfig()
{
QSettings settings("MyCompany", "MyApp");
return fromVariantMap(settings.value("AppConfig").toMap());
}
5.2 、使用 QJsonObject
QJsonObject 的好处是可以直接序列化成 JSON 字符串,便于日志排查和跨语言交换。
cpp
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>
QJsonObject toJson(const AppConfig &cfg)
{
QJsonObject obj;
obj["serverUrl"] = cfg.serverUrl;
obj["port"] = cfg.port;
obj["autoConnect"] = cfg.autoConnect;
QJsonArray files;
for (const QString &f : cfg.recentFiles)
files.append(f);
obj["recentFiles"] = files;
return obj;
}
AppConfig fromJson(const QJsonObject &obj)
{
AppConfig cfg;
cfg.serverUrl = obj.value("serverUrl").toString(cfg.serverUrl);
cfg.port = obj.value("port").toInt(cfg.port);
cfg.autoConnect = obj.value("autoConnect").toBool(cfg.autoConnect);
const QJsonArray files = obj.value("recentFiles").toArray();
for (const QJsonValue &v : files)
cfg.recentFiles.append(v.toString());
return cfg;
}
存储时可以直接存 QJsonObject(QVariant 支持),也可以存 JSON 字符串:
cpp
QSettings settings("MyCompany", "MyApp");
settings.setValue("AppConfig", toJson(cfg)); // 存 QJsonObject
// 或者存字符串,便于人工查看
QString json = QString::fromUtf8(QJsonDocument(toJson(cfg)).toJson(QJsonDocument::Compact));
settings.setValue("AppConfigJson", json);
优点:
- 一个 key 存整个结构体,代码集中;
- 文本格式可读,便于排查问题;
- 字段按名字读取,增删字段天然兼容:新增字段读不到就用默认值,删除字段直接忽略;
- 便于跨语言、跨平台交换。
缺点:
- 相比二进制方案,序列化开销略大(一般可忽略);
- 需要手写转换函数,字段多时略繁琐;
- 类型信息弱,
int和double容易混淆,需要显式转换。
适用场景:绝大多数业务配置场景,尤其是结构体会随版本演进的场景。
6、 处理嵌套结构体与容器
实际项目中的结构体往往不是扁平的。比如:
cpp
struct Connection {
QString host;
int port = 3306;
};
struct AppConfig {
QString appName;
Connection primary;
QList<Connection> backups;
QMap<QString, QString> extra;
};
6.1 、二进制方案
QDataStream 对嵌套结构体支持很好,只要为每个子结构体都重载流操作符即可:
cpp
QDataStream &operator<<(QDataStream &out, const Connection &c)
{
out << c.host << c.port;
return out;
}
QDataStream &operator>>(QDataStream &in, Connection &c)
{
in >> c.host >> c.port;
return in;
}
QDataStream &operator<<(QDataStream &out, const AppConfig &cfg)
{
out << cfg.appName << cfg.primary << cfg.backups << cfg.extra;
return out;
}
QDataStream &operator>>(QDataStream &in, AppConfig &cfg)
{
in >> cfg.appName >> cfg.primary >> cfg.backups >> cfg.extra;
return in;
}
QList<Connection> 和 QMap<QString, QString> 会被 QDataStream 自动按"长度 + 元素"的方式序列化,无需额外处理。
6.2、 JSON 方案
JSON 方案需要手动处理嵌套:
cpp
QJsonObject connectionToJson(const Connection &c)
{
QJsonObject obj;
obj["host"] = c.host;
obj["port"] = c.port;
return obj;
}
Connection connectionFromJson(const QJsonObject &obj)
{
Connection c;
c.host = obj.value("host").toString(c.host);
c.port = obj.value("port").toInt(c.port);
return c;
}
QJsonObject toJson(const AppConfig &cfg)
{
QJsonObject obj;
obj["appName"] = cfg.appName;
obj["primary"] = connectionToJson(cfg.primary);
QJsonArray backups;
for (const Connection &c : cfg.backups)
backups.append(connectionToJson(c));
obj["backups"] = backups;
QJsonObject extra;
for (auto it = cfg.extra.constBegin(); it != cfg.extra.constEnd(); ++it)
extra[it.key()] = it.value();
obj["extra"] = extra;
return obj;
}
可以看到,嵌套越深,手写转换代码越啰嗦。这也是很多项目最终选择二进制方案的原因。
7、 版本兼容与数据迁移
无论用哪种方案,只要结构体会演进,就必须考虑版本兼容。推荐在存储时额外写入一个版本号:
cpp
static constexpr int kConfigVersion = 2;
void saveConfig(const AppConfig &cfg)
{
QSettings settings("MyCompany", "MyApp");
settings.setValue("AppConfig/version", kConfigVersion);
settings.setValue("AppConfig/data", toJson(cfg));
}
AppConfig loadConfig()
{
QSettings settings("MyCompany", "MyApp");
const int version = settings.value("AppConfig/version", 0).toInt();
const QJsonObject obj = settings.value("AppConfig/data").toJsonObject();
AppConfig cfg;
switch (version) {
case 0:
// 无版本号:可能是首次运行,也可能是旧版本数据
break;
case 1:
cfg = fromJsonV1(obj); // 旧版解析逻辑
migrateV1ToV2(cfg);
break;
case 2:
cfg = fromJson(obj);
break;
default:
qWarning() << "未知配置版本:" << version;
break;
}
return cfg;
}
几个实践要点:
- 版本号单独存一个 key,不要混在数据里,方便在解析前先判断;
- 保留旧版解析函数,不要直接删掉,否则老用户升级后配置全丢;
- 迁移失败要能降级:解析异常时回退到默认配置,而不是让程序崩溃;
- 二进制方案同样需要版本号,因为字段顺序变化后旧数据无法正确解析。
8、工程化封装建议
把读写逻辑散落在各处是维护噩梦。建议为每个需要持久化的结构体提供一个独立的配置类,把序列化细节封装起来。
cpp
class AppConfigStore
{
public:
explicit AppConfigStore(QSettings *settings)
: m_settings(settings) {}
AppConfig load() const
{
const QJsonObject obj = m_settings->value("AppConfig/data").toJsonObject();
return fromJson(obj);
}
void save(const AppConfig &cfg)
{
m_settings->setValue("AppConfig/version", kConfigVersion);
m_settings->setValue("AppConfig/data", toJson(cfg));
m_settings->sync();
}
private:
QSettings *m_settings;
};
配套建议:
- 统一组织名和应用名 :
QSettings settings("MyCompany", "MyApp"),避免不同程序互相覆盖; - 显式调用
sync():QSettings有写入缓存,程序异常退出时可能丢数据,关键配置保存后主动sync(); - 区分用户级与系统级 :
QSettings::UserScope与QSettings::SystemScope按需选择; - 敏感数据不要明文存 :密码、Token 等应加密后再存,
QSettings本身不提供加密; - 提供重置入口 :
settings.remove("AppConfig")一键恢复默认配置,便于排障。
9、方案对比与选型建议
| 维度 | 字段拆分 | QDataStream 二进制 | JSON / QVariantMap |
|---|---|---|---|
| 实现复杂度 | 低 | 中 | 中 |
| 可读性 | 高 | 低 | 高 |
| 嵌套支持 | 差 | 好 | 好(需手写转换) |
| 版本兼容 | 好(字段独立) | 差(需版本号) | 好(按名读取) |
| 性能 | 高 | 最高 | 中 |
| 代码量 | 字段多时爆炸 | 少 | 中等 |
选型建议:
- 字段少、结构稳定 → 字段拆分,简单直接;
- 字段多、嵌套深、数据量大、不需要人工编辑 →
QDataStream二进制,配合版本号; - 结构会演进、需要可读性、需要排查问题 → JSON /
QVariantMap,这是大多数业务配置的最优解。
10、总结
在 Qt 中用 QSettings 存储结构体,核心思路始终是"先转换、再存储、后还原"。三种方案各有取舍:
- 字段拆分胜在简单直观,适合小配置;
QDataStream胜在紧凑高效,适合复杂嵌套,但要自己扛版本兼容;- JSON /
QVariantMap在可读性、扩展性和维护成本之间取得了最好的平衡,是推荐默认选择。
无论选哪种方案,都别忘了三件事:写版本号 、给默认值 、做好异常兜底。做到这三点,配置持久化这块基本就不会再出问题了。
