Qt QSettings 实现上位机配置管理

0 阅读5分钟

上位机每次启动时,通常需要恢复上一次使用的串口号、波特率、设备地址和保存目录。

如果这些参数全部写在代码中,每换一台设备就要重新编译;如果只保存在界面控件里,程序关闭后又会丢失。

Qt 提供的 QSettings 可以完成这类配置读写。对于单机上位机,使用 INI 文件保存配置,比较方便查看和排查问题。

一、选择配置文件的保存位置

下面以 Qt 6 为例,使用明确的 INI 文件路径,避免不同平台默认存储方式带来的差异。

先在程序入口设置应用信息:

#include <QApplication>
#include <QCoreApplication>

int main(int argc, char* argv[])
{
    QApplication app(argc, argv);

    QCoreApplication::setOrganizationName("DeviceTools");
    QCoreApplication::setApplicationName("DeviceMonitor");

    // 创建并显示主窗口……

    return app.exec();
}

获取配置文件路径:

#include <QStandardPaths>
#include <QDir>
#include <QString>

QString configFilePath()
{
    const QString directory =
            QStandardPaths::writableLocation(
                QStandardPaths::AppConfigLocation);

    if (directory.isEmpty() || !QDir().mkpath(directory))
        return {};

    return QDir(directory).filePath("settings.ini");
}

使用系统提供的配置目录,可以减少程序安装目录没有写权限的问题。

不建议直接使用:

QSettings settings("settings.ini", QSettings::IniFormat);

这样的相对路径取决于程序当前工作目录。从 IDE、快捷方式和命令行启动时,最终读取的可能不是同一个文件。

二、保存通信参数

先定义设备配置:

struct DeviceConfig {
    QString name = "新设备";
    QString portName;
    int baudRate = 115200;
    bool autoConnect = false;
};

判断波特率是否在当前项目支持的范围内:

bool isSupportedBaudRate(int value)
{
    switch (value) {
    case 9600:
    case 19200:
    case 38400:
    case 57600:
    case 115200:
        return true;
    default:
        return false;
    }
}

这个范围属于项目约定。设备使用其他波特率时,需要相应补充。

保存函数:

#include <QSettings>
#include <QRegularExpression>

bool saveDeviceConfig(const QString& filePath,
                      const QString& deviceId,
                      const DeviceConfig& config)
{
    static const QRegularExpression idPattern(
        "^[A-Za-z0-9_-]+$");

    if (filePath.isEmpty() ||
        !idPattern.match(deviceId).hasMatch() ||
        !isSupportedBaudRate(config.baudRate)) {
        return false;
    }

    QSettings settings(filePath, QSettings::IniFormat);

    settings.beginGroup("devices");
    settings.beginGroup(deviceId);

    settings.setValue("name", config.name);
    settings.setValue("portName", config.portName.trimmed());
    settings.setValue("baudRate", config.baudRate);
    settings.setValue("autoConnect", config.autoConnect);

    settings.endGroup();
    settings.endGroup();

    settings.sync();

    return settings.status() == QSettings::NoError;
}

调用示例:

DeviceConfig config;
config.name = "温度采集模块";
config.portName = "COM3";
config.baudRate = 115200;
config.autoConnect = false;

const QString path = configFilePath();

if (!saveDeviceConfig(path, "sensor_01", config))
    qWarning() << "配置保存失败";

setValue() 不代表数据已经成功写入磁盘。用户点击“保存”后,可以调用 sync(),再检查 status(),避免界面提示保存成功,实际却因权限或磁盘问题没有写入。

三、读取配置与处理默认值

首次运行时没有配置文件,直接使用默认值即可。但还要考虑配置文件被手动修改,出现非法内容的情况。

例如,下面这行配置不是合法波特率:

baudRate=abc

读取时需要同时检查转换结果和业务范围:

DeviceConfig loadDeviceConfig(QSettings& settings,
                              const QString& deviceId)
{
    DeviceConfig config;

    settings.beginGroup("devices");
    settings.beginGroup(deviceId);

    config.name = settings.value(
        "name", config.name).toString();

    config.portName = settings.value(
        "portName", QString()).toString().trimmed();

    bool converted = false;

    const int baudRate = settings.value(
        "baudRate", config.baudRate).toInt(&converted);

    if (converted && isSupportedBaudRate(baudRate))
        config.baudRate = baudRate;

    const QString autoConnect = settings.value(
        "autoConnect", false).toString().trimmed().toLower();

    config.autoConnect =
            autoConnect == "true" || autoConnect == "1";

    settings.endGroup();
    settings.endGroup();

    return config;
}

使用时:

const QString path = configFilePath();

if (path.isEmpty()) {
    qWarning() << "无法创建配置目录";
    return;
}

QSettings settings(path, QSettings::IniFormat);

const DeviceConfig config =
        loadDeviceConfig(settings, "sensor_01");

if (settings.status() != QSettings::NoError)
    qWarning() << "配置文件读取异常";

这里有两种不同情况:

  • 配置项不存在:使用默认值。
  • 配置项存在但不合法:校验失败后回退到默认值。

如果只是写 value("baudRate", 115200).toInt(),遇到 "abc" 时可能得到 0,并不会自动回退成 115200

四、管理多台设备并恢复界面

同一个 INI 文件中,可以按照设备编号分别保存配置:

devices/sensor_01
devices/sensor_02
devices/controller_01

设备编号应保持稳定,显示名称可以修改。不要把“温度传感器”这样的显示名称直接作为唯一标识,否则用户改名后就可能找不到原来的配置。

列出所有设备配置:

settings.beginGroup("devices");
const QStringList deviceIds = settings.childGroups();
settings.endGroup();

for (const QString& deviceId : deviceIds) {
    const DeviceConfig config =
            loadDeviceConfig(settings, deviceId);

    qDebug() << deviceId << config.name << config.portName;
}

恢复界面时,还要避免控件变化信号触发不必要的保存或连接操作:

#include <QSignalBlocker>

{
    const QSignalBlocker blockPort(ui->portComboBox);
    const QSignalBlocker blockBaud(ui->baudRateComboBox);

    const int portIndex =
            ui->portComboBox->findData(config.portName);

    ui->portComboBox->setCurrentIndex(portIndex);

    const int baudIndex =
            ui->baudRateComboBox->findData(config.baudRate);

    ui->baudRateComboBox->setCurrentIndex(baudIndex);
}

这里假设下拉框的用户数据分别保存端口名称和整数波特率。

如果原来的串口已经不存在,应提示用户重新选择,不要悄悄改成列表中的第一个串口。自动连接也应等配置恢复、端口检查完成后,再由明确的启动逻辑执行。

五、配置管理中的几个注意点

不要把高频采样数据写进配置文件。

QSettings 适合通信参数、窗口状态和用户偏好。连续采集记录应使用数据库或数据文件。

不要在每次控件变化时都强制落盘。

可以在点击“应用”时统一保存,或者延迟合并短时间内的多次修改。窗口尺寸等变化频繁的配置,更适合在操作结束或退出时保存。

配置文件不是加密存储。

INI 文件通常可以直接查看。设备密码、访问令牌等内容,不应因为放进 QSettings 就被认为已经安全保存。

配置结构变化时保留迁移空间。

项目迭代后,字段名称和含义可能变化。可以保存一个 configVersion,新版本启动时按版本迁移,避免把旧配置直接解释成新含义。

配置管理的重点是让程序在首次运行、参数缺失和配置异常时,都有明确且可预测的行为。