#include "application_settings_loader.h" #include "domain/project_limits.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kCurrentConfigurationVersion = 1; struct ParsedEntry { QString value; int line = 0; }; std::string toUtf8(const QString &value) { const QByteArray bytes = value.toUtf8(); return std::string(bytes.constData(), static_cast(bytes.size())); } QString defaultFileContents() { return QStringLiteral( "[Config]\n" "Version=1\n" "\n" "[ProjectLimits]\n" "MaxHmiPages=32\n" "MaxHmiControlsPerPage=128\n" "MaxAlarmDefinitions=256\n" "MaxControlLogics=32\n" "MaxRungsPerLogic=256\n" "MaxOutputMessages=1000\n" "\n" "[HmiDefaults]\n" "DefaultPageWidth=800\n" "DefaultPageHeight=400\n" "\n" "[PlcDefaults]\n" "PortName=COM3\n" "ServerAddress=1\n" "BaudRate=9600\n" "DataBits=8\n" "Parity=2\n" "StopBits=1\n"); } ApplicationSettingsLoadResult failureResult( const QString &file_path, const QString &reason) { ApplicationSettingsLoadResult result; result.warningRequired = true; result.warningMessage = toUtf8( QStringLiteral("应用配置无法使用,本次已全部采用代码默认值。\n%1") .arg(reason)); result.messages.push_back(toUtf8( QStringLiteral("应用配置错误:%1(%2)") .arg(reason, QDir::toNativeSeparators(file_path)))); result.messages.push_back( "应用配置:本次运行全部使用代码默认值"); return result; } bool containsValue(std::initializer_list values, int value) { for (int candidate : values) { if (candidate == value) { return true; } } return false; } bool readInteger( const QMap &entries, const QString &path, qint64 minimum, qint64 maximum, qint64 default_value, qint64 *value, QStringList *notices, QStringList *errors, bool required = false) { const auto entry = entries.constFind(path); if (entry == entries.cend()) { if (required) { errors->append(QStringLiteral("缺少必填字段 %1").arg(path)); } else { notices->append( QStringLiteral("配置字段 %1 缺失,使用代码默认值 %2") .arg(path) .arg(default_value)); } *value = default_value; return !required; } bool converted = false; const qint64 parsed = entry->value.toLongLong(&converted, 10); if (!converted || parsed < minimum || parsed > maximum) { errors->append( QStringLiteral("第 %1 行字段 %2 的值“%3”无效,允许范围为 %4~%5") .arg(entry->line) .arg(path, entry->value) .arg(minimum) .arg(maximum)); return false; } *value = parsed; return true; } void addUnsupportedValueError( const QMap &entries, const QString &path, const QString &allowed, QStringList *errors) { const ParsedEntry &entry = entries[path]; errors->append( QStringLiteral("第 %1 行字段 %2 的值“%3”无效,只允许 %4") .arg(entry.line) .arg(path, entry.value, allowed)); } } // namespace QString ApplicationSettingsLoader::defaultFilePath() { return QDir(QCoreApplication::applicationDirPath()).filePath( QStringLiteral("config/application.ini")); } ApplicationSettingsLoadResult ApplicationSettingsLoader::load( const QString &file_path) { if (!QFileInfo::exists(file_path)) { const QString directory = QFileInfo(file_path).absolutePath(); if (!QDir().mkpath(directory)) { return failureResult( file_path, QStringLiteral("无法创建配置目录 %1,可能没有写入权限") .arg(QDir::toNativeSeparators(directory))); } QSaveFile file(file_path); if (!file.open(QIODevice::WriteOnly)) { return failureResult(file_path, file.errorString()); } const QByteArray data = defaultFileContents().toUtf8(); if (file.write(data) != data.size()) { const QString reason = file.errorString(); file.cancelWriting(); return failureResult(file_path, reason); } if (!file.commit()) { return failureResult(file_path, file.errorString()); } ApplicationSettingsLoadResult result; result.messages.push_back(toUtf8( QStringLiteral("应用配置:已创建默认配置文件 %1") .arg(QDir::toNativeSeparators(file_path)))); result.messages.push_back( "应用配置:首次运行使用代码默认值,修改后请重启程序"); return result; } QFile file(file_path); if (!file.open(QIODevice::ReadOnly)) { return failureResult(file_path, file.errorString()); } const QByteArray data = file.readAll(); if (file.error() != QFileDevice::NoError) { return failureResult(file_path, file.errorString()); } QTextCodec::ConverterState converter_state; const QString text = QTextCodec::codecForName("UTF-8")->toUnicode( data.constData(), data.size(), &converter_state); if (converter_state.invalidChars > 0) { return failureResult(file_path, QStringLiteral("配置文件不是有效的 UTF-8 文本")); } const QSet known_fields{ QStringLiteral("Config.Version"), QStringLiteral("ProjectLimits.MaxHmiPages"), QStringLiteral("ProjectLimits.MaxHmiControlsPerPage"), QStringLiteral("ProjectLimits.MaxAlarmDefinitions"), QStringLiteral("ProjectLimits.MaxControlLogics"), QStringLiteral("ProjectLimits.MaxRungsPerLogic"), QStringLiteral("ProjectLimits.MaxOutputMessages"), QStringLiteral("HmiDefaults.DefaultPageWidth"), QStringLiteral("HmiDefaults.DefaultPageHeight"), QStringLiteral("PlcDefaults.PortName"), QStringLiteral("PlcDefaults.ServerAddress"), QStringLiteral("PlcDefaults.BaudRate"), QStringLiteral("PlcDefaults.DataBits"), QStringLiteral("PlcDefaults.Parity"), QStringLiteral("PlcDefaults.StopBits")}; QMap entries; QStringList notices; QStringList errors; QString current_section; QString normalized_text = text; if (!normalized_text.isEmpty() && normalized_text.front() == QChar::ByteOrderMark) { normalized_text.remove(0, 1); } const QStringList lines = normalized_text.split(QLatin1Char('\n')); for (int index = 0; index < lines.size(); ++index) { QString line = lines.at(index); if (line.endsWith(QLatin1Char('\r'))) { line.chop(1); } const QString trimmed = line.trimmed(); const int line_number = index + 1; if (trimmed.isEmpty() || trimmed.startsWith(QLatin1Char(';')) || trimmed.startsWith(QLatin1Char('#'))) { continue; } if (trimmed.startsWith(QLatin1Char('['))) { if (!trimmed.endsWith(QLatin1Char(']')) || trimmed.size() < 3) { errors.append( QStringLiteral("第 %1 行分区格式无效:%2") .arg(line_number) .arg(trimmed)); current_section.clear(); continue; } current_section = trimmed.mid(1, trimmed.size() - 2).trimmed(); if (current_section.isEmpty()) { errors.append(QStringLiteral("第 %1 行分区名称不能为空").arg(line_number)); } continue; } const int separator = trimmed.indexOf(QLatin1Char('=')); if (separator <= 0 || current_section.isEmpty()) { errors.append( QStringLiteral("第 %1 行字段格式无效:%2") .arg(line_number) .arg(trimmed)); continue; } const QString key = trimmed.left(separator).trimmed(); const QString value = trimmed.mid(separator + 1).trimmed(); if (key.isEmpty()) { errors.append(QStringLiteral("第 %1 行字段名称不能为空").arg(line_number)); continue; } const QString path = current_section + QLatin1Char('.') + key; if (entries.contains(path)) { errors.append( QStringLiteral("第 %1 行字段 %2 重复,首次出现在第 %3 行") .arg(line_number) .arg(path) .arg(entries.value(path).line)); continue; } entries.insert(path, {value, line_number}); if (!known_fields.contains(path)) { notices.append( QStringLiteral("第 %1 行未知字段 %2 已忽略") .arg(line_number) .arg(path)); } } ApplicationSettings candidate; qint64 parsed = 0; if (readInteger( entries, QStringLiteral("Config.Version"), kCurrentConfigurationVersion, kCurrentConfigurationVersion, kCurrentConfigurationVersion, &parsed, ¬ices, &errors, true)) { candidate.version = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxHmiPages"), 1, ProjectLimits::kMaximumHmiPages, ProjectLimits::kMaximumHmiPages, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumHmiPages = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxHmiControlsPerPage"), 1, ProjectLimits::kMaximumHmiControlsPerPage, ProjectLimits::kMaximumHmiControlsPerPage, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumHmiControlsPerPage = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxAlarmDefinitions"), 0, ProjectLimits::kMaximumAlarmDefinitions, ProjectLimits::kMaximumAlarmDefinitions, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumAlarmDefinitions = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxControlLogics"), 1, ProjectLimits::kMaximumControlLogics, ProjectLimits::kMaximumControlLogics, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumControlLogics = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxRungsPerLogic"), 0, ProjectLimits::kMaximumRungsPerLogic, ProjectLimits::kMaximumRungsPerLogic, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumRungsPerLogic = static_cast(parsed); } if (readInteger( entries, QStringLiteral("ProjectLimits.MaxOutputMessages"), 1, ProjectLimits::kMaximumOutputMessages, ProjectLimits::kMaximumOutputMessages, &parsed, ¬ices, &errors)) { candidate.projectLimits.maximumOutputMessages = static_cast(parsed); } if (readInteger( entries, QStringLiteral("HmiDefaults.DefaultPageWidth"), ProjectLimits::kMinimumHmiPageWidth, ProjectLimits::kMaximumHmiPageWidth, ProjectLimits::kDefaultHmiPageWidth, &parsed, ¬ices, &errors)) { candidate.hmiDefaults.pageWidth = static_cast(parsed); } if (readInteger( entries, QStringLiteral("HmiDefaults.DefaultPageHeight"), ProjectLimits::kMinimumHmiPageHeight, ProjectLimits::kMaximumHmiPageHeight, ProjectLimits::kDefaultHmiPageHeight, &parsed, ¬ices, &errors)) { candidate.hmiDefaults.pageHeight = static_cast(parsed); } const QString port_path = QStringLiteral("PlcDefaults.PortName"); const auto port_entry = entries.constFind(port_path); if (port_entry == entries.cend()) { notices.append( QStringLiteral("配置字段 %1 缺失,使用代码默认值 COM3") .arg(port_path)); } else { const QByteArray port_bytes = port_entry->value.toUtf8(); if (port_entry->value.trimmed().isEmpty() || port_bytes.size() > static_cast(ProjectLimits::kMaximumIdBytes)) { errors.append( QStringLiteral("第 %1 行字段 %2 的值“%3”无效,端口名称不能为空且不能超过 128 个 UTF-8 字节") .arg(port_entry->line) .arg(port_path, port_entry->value)); } else { candidate.plcDefaults.portName = toUtf8(port_entry->value.trimmed()); } } if (readInteger( entries, QStringLiteral("PlcDefaults.ServerAddress"), ProjectLimits::kMinimumPlcServerAddress, ProjectLimits::kMaximumPlcServerAddress, candidate.plcDefaults.serverAddress, &parsed, ¬ices, &errors)) { candidate.plcDefaults.serverAddress = static_cast(parsed); } int baud_rate = candidate.plcDefaults.baudRate; if (readInteger( entries, QStringLiteral("PlcDefaults.BaudRate"), 9600, 115200, candidate.plcDefaults.baudRate, &parsed, ¬ices, &errors)) { baud_rate = static_cast(parsed); if (!containsValue({9600, 19200, 38400, 57600, 115200}, baud_rate)) { addUnsupportedValueError( entries, QStringLiteral("PlcDefaults.BaudRate"), QStringLiteral("9600、19200、38400、57600 或 115200"), &errors); } else { candidate.plcDefaults.baudRate = baud_rate; } } if (readInteger( entries, QStringLiteral("PlcDefaults.DataBits"), 7, 8, candidate.plcDefaults.dataBits, &parsed, ¬ices, &errors)) { candidate.plcDefaults.dataBits = static_cast(parsed); } int parity = candidate.plcDefaults.parity; if (readInteger( entries, QStringLiteral("PlcDefaults.Parity"), 0, 3, candidate.plcDefaults.parity, &parsed, ¬ices, &errors)) { parity = static_cast(parsed); if (!containsValue({0, 2, 3}, parity)) { addUnsupportedValueError( entries, QStringLiteral("PlcDefaults.Parity"), QStringLiteral("0、2 或 3"), &errors); } else { candidate.plcDefaults.parity = parity; } } if (readInteger( entries, QStringLiteral("PlcDefaults.StopBits"), 1, 2, candidate.plcDefaults.stopBits, &parsed, ¬ices, &errors)) { candidate.plcDefaults.stopBits = static_cast(parsed); } if (!errors.isEmpty()) { ApplicationSettingsLoadResult result; result.warningRequired = true; result.warningMessage = "应用配置存在严重错误,本次已全部采用代码默认值。详细原因请查看底部输出面板。"; for (const QString &error : errors) { result.messages.push_back(toUtf8(QStringLiteral("应用配置错误:") + error)); } for (const QString ¬ice : notices) { result.messages.push_back(toUtf8(QStringLiteral("应用配置提示:") + notice)); } result.messages.push_back( "应用配置:整份配置不生效,本次运行全部使用代码默认值"); return result; } ApplicationSettingsLoadResult result; result.settings = candidate; result.messages.push_back(toUtf8( QStringLiteral("应用配置:已加载 %1,修改后需重启程序") .arg(QDir::toNativeSeparators(file_path)))); for (const QString ¬ice : notices) { result.messages.push_back(toUtf8(QStringLiteral("应用配置提示:") + notice)); } return result; }