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

做上位机和工控软件久了会发现,配置文件看着简单,坑却特别多。

程序写一半突然断电、多线程同时读写、异常退出,都能把配置文件搞坏,轻则参数丢失,重则软件直接起不来。

为了以后新项目移植不用重复造轮子,我把自己一直在用的配置管理类整理了一下,自带线程安全、原子写入、自动备份,INI/JSON/XML 都能用,基本能应对大部分现场稳定运行的需求。


一、为什么要自己写一个配置类?

Qt 自带的 QSettings 其实能用,但有几个硬伤很容易在现场出问题:

  1. 直接写原文件,写一半断电直接损坏,没有备份
  2. 多线程读写不加锁很容易乱
  3. 文件损坏了不会自动恢复,软件直接卡死
  4. 切换格式要改一堆代码,不方便移植

所以我自己封装了一层,保证:

  • 怎么读写都不会把配置写崩
  • 坏了能自动从备份恢复
  • 多线程同时操作也安全
  • 换 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 再替换)

这是防断电、防异常退出最关键的一步。

不直接覆盖原文件,而是:

  1. 写到 .tmp
  2. 写完再替换原文件
    哪怕中途断电、死机,最多坏临时文件,原来的配置完好无损。

3. 自动备份 + 自动恢复

每次写入都会生成 .bak

启动时自动检查:

  • 格式错误
  • 文件为空
  • 文件不存在
    都会自动从备份恢复,现场不会因为配置丢了就打不开软件。

4. 统一接口,方便移植

不管用 INI、JSON 还是 XML,业务代码都不用改,只换一个枚举就行,老项目升级、新项目重构都很方便。


五、适合哪些项目?

我自己主要用在:

  • 医疗设备上位机
  • 工业串口 / 网口控制软件
  • 需要长期挂机运行的客户端
  • 多线程读写配置比较频繁的程序

只要你怕配置文件损坏、怕现场出问题,这个类基本都能扛住。


六、小结

这个 ConfigManager 是我从多个实际项目里抽出来的通用组件,没有花哨结构,就是稳定、抗造、好移植。

以后开新项目直接拖进去,初始化一行,读写两行,不用再纠结配置损坏、线程安全这些破事,可以把精力专心写业务逻辑。

相关推荐
我不是懒洋洋2 小时前
AI的影响8
笔记
资深流水灯工程师2 小时前
FREERTOS的核心内容与核心组件
笔记
xian_wwq3 小时前
【学习笔记】GB/T 20986-2023 详解,10 类网络安全事件分类
笔记·学习·web安全
鱼鳞_3 小时前
Java学习笔记_Day27(Stream流)
java·笔记·学习
_李小白3 小时前
【OSG学习笔记】Day 42: OSG 动态场景安全修改
笔记·学习·安全
Kapibalapikapi3 小时前
思考笔记 | SSL证书过期的影响
笔记·加解密
扣脑壳的FPGAer4 小时前
数字信号处理学习笔记--Chapter 1.3 常系数线性差分方程
笔记·学习·信号处理
丁劲犇4 小时前
改造传统Qt6Widgets程序为多会话MCPServer生产力工具-技巧与实现
qt·ai·agent·并发·mcp·mcpserver·widgets
NULL指向我4 小时前
TMS320F28379D笔记1:主控-从核双核架构认识
笔记·单片机