做上位机和工控软件久了会发现,配置文件看着简单,坑却特别多。
程序写一半突然断电、多线程同时读写、异常退出,都能把配置文件搞坏,轻则参数丢失,重则软件直接起不来。
为了以后新项目移植不用重复造轮子,我把自己一直在用的配置管理类整理了一下,自带线程安全、原子写入、自动备份,INI/JSON/XML 都能用,基本能应对大部分现场稳定运行的需求。
一、为什么要自己写一个配置类?
Qt 自带的 QSettings 其实能用,但有几个硬伤很容易在现场出问题:
- 直接写原文件,写一半断电直接损坏,没有备份
- 多线程读写不加锁很容易乱
- 文件损坏了不会自动恢复,软件直接卡死
- 切换格式要改一堆代码,不方便移植
所以我自己封装了一层,保证:
- 怎么读写都不会把配置写崩
- 坏了能自动从备份恢复
- 多线程同时操作也安全
- 换 INI、JSON、XML 几乎不用改业务代码
二、完整代码(可直接复制进项目)
cpp
#include <QObject>
#include <QSettings>
#include <QFile>
#include <QDir>
#include <QReadWriteLock>
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonValue>
#include <QXmlStreamWriter>
#include <QXmlStreamReader>
#include <QVector>
#include <QPair>
// 配置文件格式,三种常用的都支持
enum class ConfigFormat
{
INI,
JSON,
XML
};
class ConfigManager : public QObject
{
Q_OBJECT
public:
// 单例,全局只用一个实例,避免多处打开文件
static ConfigManager* instance()
{
static ConfigManager* inst = nullptr;
if (!inst) {
inst = new ConfigManager();
}
return inst;
}
// 初始化:指定文件路径 + 格式
// 程序启动时调用一次就行
void init(const QString& filename, ConfigFormat format)
{
m_filename = filename;
m_format = format;
checkBackup(); // 一启动先检查配置坏没坏
}
// 读配置,带默认值,没有 key 也不会崩
QVariant getValue(const QString& key, const QVariant& defaultValue = QVariant())
{
QReadLocker locker(&m_lock); // 读加锁,允许多线程同时读
if (m_format == ConfigFormat::INI) {
QSettings set(m_filename, QSettings::IniFormat);
return set.value(key, defaultValue);
}
else if (m_format == ConfigFormat::JSON) {
return getJsonValue(key, defaultValue);
}
else if (m_format == ConfigFormat::XML) {
return getXmlValue(key, defaultValue);
}
return defaultValue;
}
// 写配置,自动加锁、原子写入、备份
void setValue(const QString& key, const QVariant& value)
{
QWriteLocker locker(&m_lock); // 写加锁,同一时间只能一个线程写
if (m_format == ConfigFormat::INI) {
QSettings set(m_filename, QSettings::IniFormat);
set.setValue(key, value);
set.sync();
backup(); // 写完顺手备个份
}
else if (m_format == ConfigFormat::JSON) {
setJsonValue(key, value);
}
else if (m_format == ConfigFormat::XML) {
setXmlValue(key, value);
}
}
// 检查配置文件是否损坏,坏了自动从 bak 恢复
void checkBackup()
{
QFile f(m_filename);
// 文件不存在或者解析失败,都视为损坏
if (!f.exists() || isFileCorrupted()) {
QFile fbak(m_filename + ".bak");
if (fbak.exists()) {
fbak.copy(m_filename);
qInfo() << "配置文件损坏,已自动从备份恢复:" << m_filename;
} else {
qWarning() << "配置文件不存在,创建一个新的空文件:" << m_filename;
f.open(QIODevice::WriteOnly);
f.close();
}
}
}
// 手动备份,一般不用自己调,setValue 里会自动调用
void backup()
{
QFile::remove(m_filename + ".bak");
QFile::copy(m_filename, m_filename + ".bak");
}
private:
ConfigManager() = default;
// 判断文件是不是坏了
bool isFileCorrupted()
{
QFile f(m_filename);
if (!f.open(QIODevice::ReadOnly)) {
return true;
}
bool corrupted = false;
if (m_format == ConfigFormat::JSON) {
QJsonParseError e;
QJsonDocument::fromJson(f.readAll(), &e);
corrupted = (e.error != QJsonParseError::NoError);
}
else if (m_format == ConfigFormat::XML) {
QXmlStreamReader r(&f);
while (!r.atEnd()) {
r.readNext();
}
corrupted = r.hasError();
}
f.close();
return corrupted;
}
// ==================== JSON 读写 ====================
QVariant getJsonValue(const QString& key, const QVariant& def)
{
QFile f(m_filename);
if (!f.open(QIODevice::ReadOnly)) {
return def;
}
QJsonDocument doc = QJsonDocument::fromJson(f.readAll());
f.close();
return doc.object().value(key).toVariant(def);
}
void setJsonValue(const QString& key, const QVariant& val)
{
QJsonObject obj;
if (QFile(m_filename).exists()) {
QFile f(m_filename);
if (f.open(QIODevice::ReadOnly)) {
obj = QJsonDocument::fromJson(f.readAll()).object();
f.close();
}
}
obj.insert(key, QJsonValue::fromVariant(val));
// 关键点:先写临时文件,再替换,防止写一半断电损坏
QString tmpName = m_filename + ".tmp";
QFile ftmp(tmpName);
if (ftmp.open(QIODevice::WriteOnly)) {
ftmp.write(QJsonDocument(obj).toJson());
ftmp.close();
QFile::remove(m_filename);
QFile::rename(tmpName, m_filename);
}
backup();
}
// ==================== XML 读写 ====================
QVariant getXmlValue(const QString& key, const QVariant& def)
{
QFile f(m_filename);
if (!f.open(QIODevice::ReadOnly)) {
return def;
}
QXmlStreamReader r(&f);
QString value;
while (!r.atEnd()) {
if (r.readNext() == QXmlStreamReader::StartElement && r.name() == key) {
value = r.readElementText();
break;
}
}
f.close();
return value.isEmpty() ? def : value;
}
void setXmlValue(const QString& key, const QVariant& val)
{
QVector<QPair<QString, QString>> nodes;
if (QFile(m_filename).exists()) {
QFile f(m_filename);
if (f.open(QIODevice::ReadOnly)) {
QXmlStreamReader r(&f);
while (!r.atEnd()) {
if (r.readNext() == QXmlStreamReader::StartElement && !r.name().isEmpty()) {
nodes.append({r.name().toString(), r.readElementText()});
}
}
f.close();
}
}
// 存在就更新,不存在就追加
bool found = false;
for (auto& node : nodes) {
if (node.first == key) {
node.second = val.toString();
found = true;
break;
}
}
if (!found) {
nodes.append({key, val.toString()});
}
// 同样用临时文件保证安全写入
QString tmpName = m_filename + ".tmp";
QFile ftmp(tmpName);
if (ftmp.open(QIODevice::WriteOnly)) {
QXmlStreamWriter w(&ftmp);
w.setAutoFormatting(true);
w.writeStartDocument();
w.writeStartElement("config");
for (auto& node : nodes) {
w.writeTextElement(node.first, node.second);
}
w.writeEndElement();
w.writeEndDocument();
ftmp.close();
QFile::remove(m_filename);
QFile::rename(tmpName, m_filename);
}
backup();
}
private:
QString m_filename;
ConfigFormat m_format;
QReadWriteLock m_lock; // 读写锁,多线程安全核心
};
三、实际使用示例(非常简单)
1. 程序启动时初始化
在 main 函数里 early init 一下就行:
cpp
// INI 格式
ConfigManager::instance()->init("config.ini", ConfigFormat::INI);
// JSON 格式
// ConfigManager::instance()->init("config.json", ConfigFormat::JSON);
2. 读参数
cpp
// 读串口配置,没有就用默认值,不会崩溃
QString port = ConfigManager::instance()->getValue("Serial/Port", "COM1").toString();
int baud = ConfigManager::instance()->getValue("Serial/Baud", 115200).toInt();
3. 写参数
cpp
ConfigManager::instance()->setValue("Serial/Port", "COM10");
ConfigManager::instance()->setValue("Serial/Baud", 9600);
内部会自动:加锁 → 安全写入 → 备份,不用你管。
四、几个关键设计思路(方便你以后自己改)
1. 读写锁 QReadWriteLock
- 读操作可以并发,效率高
- 写操作独占,不会出现一边读一边写乱掉
上位机多线程、串口线程、UI线程同时操作配置也不会崩。
2. 原子写入(先写 tmp 再替换)
这是防断电、防异常退出最关键的一步。
不直接覆盖原文件,而是:
- 写到
.tmp - 写完再替换原文件
哪怕中途断电、死机,最多坏临时文件,原来的配置完好无损。
3. 自动备份 + 自动恢复
每次写入都会生成 .bak。
启动时自动检查:
- 格式错误
- 文件为空
- 文件不存在
都会自动从备份恢复,现场不会因为配置丢了就打不开软件。
4. 统一接口,方便移植
不管用 INI、JSON 还是 XML,业务代码都不用改,只换一个枚举就行,老项目升级、新项目重构都很方便。
五、适合哪些项目?
我自己主要用在:
- 医疗设备上位机
- 工业串口 / 网口控制软件
- 需要长期挂机运行的客户端
- 多线程读写配置比较频繁的程序
只要你怕配置文件损坏、怕现场出问题,这个类基本都能扛住。
六、小结
这个 ConfigManager 是我从多个实际项目里抽出来的通用组件,没有花哨结构,就是稳定、抗造、好移植。
以后开新项目直接拖进去,初始化一行,读写两行,不用再纠结配置损坏、线程安全这些破事,可以把精力专心写业务逻辑。