【Qt 开发笔记】能扛住断电、多线程的通用配置类(移植直接用)

张开发
2026/4/11 18:55:22 15 分钟阅读

分享文章

【Qt 开发笔记】能扛住断电、多线程的通用配置类(移植直接用)
做上位机和工控软件久了会发现配置文件看着简单坑却特别多。程序写一半突然断电、多线程同时读写、异常退出都能把配置文件搞坏轻则参数丢失重则软件直接起不来。为了以后新项目移植不用重复造轮子我把自己一直在用的配置管理类整理了一下自带线程安全、原子写入、自动备份INI/JSON/XML 都能用基本能应对大部分现场稳定运行的需求。一、为什么要自己写一个配置类Qt 自带的 QSettings 其实能用但有几个硬伤很容易在现场出问题直接写原文件写一半断电直接损坏没有备份多线程读写不加锁很容易乱文件损坏了不会自动恢复软件直接卡死切换格式要改一堆代码不方便移植所以我自己封装了一层保证怎么读写都不会把配置写崩坏了能自动从备份恢复多线程同时操作也安全换 INI、JSON、XML 几乎不用改业务代码二、完整代码可直接复制进项目#includeQObject#includeQSettings#includeQFile#includeQDir#includeQReadWriteLock#includeQDebug#includeQJsonDocument#includeQJsonObject#includeQJsonValue#includeQXmlStreamWriter#includeQXmlStreamReader#includeQVector#includeQPair// 配置文件格式三种常用的都支持enumclassConfigFormat{INI,JSON,XML};classConfigManager:publicQObject{Q_OBJECTpublic:// 单例全局只用一个实例避免多处打开文件staticConfigManager*instance(){staticConfigManager*instnullptr;if(!inst){instnewConfigManager();}returninst;}// 初始化指定文件路径 格式// 程序启动时调用一次就行voidinit(constQStringfilename,ConfigFormat format){m_filenamefilename;m_formatformat;checkBackup();// 一启动先检查配置坏没坏}// 读配置带默认值没有 key 也不会崩QVariantgetValue(constQStringkey,constQVariantdefaultValueQVariant()){QReadLockerlocker(m_lock);// 读加锁允许多线程同时读if(m_formatConfigFormat::INI){QSettingsset(m_filename,QSettings::IniFormat);returnset.value(key,defaultValue);}elseif(m_formatConfigFormat::JSON){returngetJsonValue(key,defaultValue);}elseif(m_formatConfigFormat::XML){returngetXmlValue(key,defaultValue);}returndefaultValue;}// 写配置自动加锁、原子写入、备份voidsetValue(constQStringkey,constQVariantvalue){QWriteLockerlocker(m_lock);// 写加锁同一时间只能一个线程写if(m_formatConfigFormat::INI){QSettingsset(m_filename,QSettings::IniFormat);set.setValue(key,value);set.sync();backup();// 写完顺手备个份}elseif(m_formatConfigFormat::JSON){setJsonValue(key,value);}elseif(m_formatConfigFormat::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;}boolcorruptedfalse;if(m_formatConfigFormat::JSON){QJsonParseError e;QJsonDocument::fromJson(f.readAll(),e);corrupted(e.error!QJsonParseError::NoError);}elseif(m_formatConfigFormat::XML){QXmlStreamReaderr(f);while(!r.atEnd()){r.readNext();}corruptedr.hasError();}f.close();returncorrupted;}// JSON 读写 QVariantgetJsonValue(constQStringkey,constQVariantdef){QFilef(m_filename);if(!f.open(QIODevice::ReadOnly)){returndef;}QJsonDocument docQJsonDocument::fromJson(f.readAll());f.close();returndoc.object().value(key).toVariant(def);}voidsetJsonValue(constQStringkey,constQVariantval){QJsonObject obj;if(QFile(m_filename).exists()){QFilef(m_filename);if(f.open(QIODevice::ReadOnly)){objQJsonDocument::fromJson(f.readAll()).object();f.close();}}obj.insert(key,QJsonValue::fromVariant(val));// 关键点先写临时文件再替换防止写一半断电损坏QString tmpNamem_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(constQStringkey,constQVariantdef){QFilef(m_filename);if(!f.open(QIODevice::ReadOnly)){returndef;}QXmlStreamReaderr(f);QString value;while(!r.atEnd()){if(r.readNext()QXmlStreamReader::StartElementr.name()key){valuer.readElementText();break;}}f.close();returnvalue.isEmpty()?def:value;}voidsetXmlValue(constQStringkey,constQVariantval){QVectorQPairQString,QStringnodes;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();}}// 存在就更新不存在就追加boolfoundfalse;for(autonode:nodes){if(node.firstkey){node.secondval.toString();foundtrue;break;}}if(!found){nodes.append({key,val.toString()});}// 同样用临时文件保证安全写入QString tmpNamem_filename.tmp;QFileftmp(tmpName);if(ftmp.open(QIODevice::WriteOnly)){QXmlStreamWriterw(ftmp);w.setAutoFormatting(true);w.writeStartDocument();w.writeStartElement(config);for(autonode: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 portConfigManager::instance()-getValue(Serial/Port,COM1).toString();intbaudConfigManager::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 是我从多个实际项目里抽出来的通用组件没有花哨结构就是稳定、抗造、好移植。以后开新项目直接拖进去初始化一行读写两行不用再纠结配置损坏、线程安全这些破事可以把精力专心写业务逻辑。

更多文章