做上位机和工控软件久了会发现,配置文件看着简单,坑却特别多。
程序写一半突然断电、多线程同时读写、异常退出,都能把配置文件搞坏,轻则参数丢失,重则软件直接起不来。
为了以后新项目移植不用重复造轮子,我把自己一直在用的配置管理类整理了一下,自带线程安全、原子写入、自动备份,INI/JSON/XML 都能用,基本能应对大部分现场稳定运行的需求。
一、为什么要自己写一个配置类?
Qt 自带的 QSettings 其实能用,但有几个硬伤很容易在现场出问题:
- 直接写原文件,写一半断电直接损坏,没有备份
- 多线程读写不加锁很容易乱
- 文件损坏了不会自动恢复,软件直接卡死
- 切换格式要改一堆代码,不方便移植
所以我自己封装了一层,保证:
- 怎么读写都不会把配置写崩
- 坏了能自动从备份恢复
- 多线程同时操作也安全
- 换 INI、JSON、XML 几乎不用改业务代码
二、完整代码(可直接复制进项目)
#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>// 配置文件格式,三种常用的都支持enumclassConfigFormat{INI,JSON,XML};classConfigManager:publicQObject{Q_OBJECTpublic:// 单例,全局只用一个实例,避免多处打开文件staticConfigManager*instance(){staticConfigManager*inst=nullptr;if(!inst){inst=newConfigManager();}returninst;}// 初始化:指定文件路径 + 格式// 程序启动时调用一次就行voidinit(constQString&filename,ConfigFormat format){m_filename=filename;m_format=format;checkBackup();// 一启动先检查配置坏没坏}// 读配置,带默认值,没有 key 也不会崩QVariantgetValue(constQString&key,constQVariant&defaultValue=QVariant()){QReadLockerlocker(&m_lock);// 读加锁,允许多线程同时读if(m_format==ConfigFormat::INI){QSettingsset(m_filename,QSettings::IniFormat);returnset.value(key,defaultValue);}elseif(m_format==ConfigFormat::JSON){returngetJsonValue(key,defaultValue);}elseif(m_format==ConfigFormat::XML){returngetXmlValue(key,defaultValue);}returndefaultValue;}// 写配置,自动加锁、原子写入、备份voidsetValue(constQString&key,constQVariant&value){QWriteLockerlocker(&m_lock);// 写加锁,同一时间只能一个线程写if(m_format==ConfigFormat::INI){QSettingsset(m_filename,QSettings::IniFormat);set.setValue(key,value);set.sync();backup();// 写完顺手备个份}elseif(m_format==ConfigFormat::JSON){setJsonValue(key,value);}elseif(m_format==ConfigFormat::XML){setXmlValue(key,value);}}// 检查配置文件是否损坏,坏了自动从 bak 恢复voidcheckBackup(){QFilef(m_filename);// 文件不存在或者解析失败,都视为损坏if(!f.exists()||isFileCorrupted()){QFilefbak(m_filename+".bak");if(fbak.exists()){fbak.copy(m_filename);qInfo()<<"配置文件损坏,已自动从备份恢复:"<<m_filename;}else{qWarning()<<"配置文件不存在,创建一个新的空文件:"<<m_filename;f.open(QIODevice::WriteOnly);f.close();}}}// 手动备份,一般不用自己调,setValue 里会自动调用voidbackup(){QFile::remove(m_filename+".bak");QFile::copy(m_filename,m_filename+".bak");}private:ConfigManager()=default;// 判断文件是不是坏了boolisFileCorrupted(){QFilef(m_filename);if(!f.open(QIODevice::ReadOnly)){returntrue;}boolcorrupted=false;if(m_format==ConfigFormat::JSON){QJsonParseError e;QJsonDocument::fromJson(f.readAll(),&e);corrupted=(e.error!=QJsonParseError::NoError);}elseif(m_format==ConfigFormat::XML){QXmlStreamReaderr(&f);while(!r.atEnd()){r.readNext();}corrupted=r.hasError();}f.close();returncorrupted;}// ==================== JSON 读写 ====================QVariantgetJsonValue(constQString&key,constQVariant&def){QFilef(m_filename);if(!f.open(QIODevice::ReadOnly)){returndef;}QJsonDocument doc=QJsonDocument::fromJson(f.readAll());f.close();returndoc.object().value(key).toVariant(def);}voidsetJsonValue(constQString&key,constQVariant&val){QJsonObject obj;if(QFile(m_filename).exists()){QFilef(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";QFileftmp(tmpName);if(ftmp.open(QIODevice::WriteOnly)){ftmp.write(QJsonDocument(obj).toJson());ftmp.close();QFile::remove(m_filename);QFile::rename(tmpName,m_filename);}backup();}// ==================== XML 读写 ====================QVariantgetXmlValue(constQString&key,constQVariant&def){QFilef(m_filename);if(!f.open(QIODevice::ReadOnly)){returndef;}QXmlStreamReaderr(&f);QString value;while(!r.atEnd()){if(r.readNext()==QXmlStreamReader::StartElement&&r.name()==key){value=r.readElementText();break;}}f.close();returnvalue.isEmpty()?def:value;}voidsetXmlValue(constQString&key,constQVariant&val){QVector<QPair<QString,QString>>nodes;if(QFile(m_filename).exists()){QFilef(m_filename);if(f.open(QIODevice::ReadOnly)){QXmlStreamReaderr(&f);while(!r.atEnd()){if(r.readNext()==QXmlStreamReader::StartElement&&!r.name().isEmpty()){nodes.append({r.name().toString(),r.readElementText()});}}f.close();}}// 存在就更新,不存在就追加boolfound=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";QFileftmp(tmpName);if(ftmp.open(QIODevice::WriteOnly)){QXmlStreamWriterw(&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 一下就行:
// INI 格式ConfigManager::instance()->init("config.ini",ConfigFormat::INI);// JSON 格式// ConfigManager::instance()->init("config.json", ConfigFormat::JSON);2. 读参数
// 读串口配置,没有就用默认值,不会崩溃QString port=ConfigManager::instance()->getValue("Serial/Port","COM1").toString();intbaud=ConfigManager::instance()->getValue("Serial/Baud",115200).toInt();3. 写参数
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 是我从多个实际项目里抽出来的通用组件,没有花哨结构,就是稳定、抗造、好移植。
以后开新项目直接拖进去,初始化一行,读写两行,不用再纠结配置损坏、线程安全这些破事,可以把精力专心写业务逻辑。