一、需求分析与技术选型
核心需求 :创建一个可视化工具,支持INI配置文件的创建、编辑、保存和加载,需处理多种数据类型(布尔值、整数、浮点数、枚举、字符串)。
技术选型 :
- 框架 :Qt 5/6(推荐5.15+,提供完善的UI组件和文件操作API)
- 语言 :C++17(现代C++特性提升开发效率)
- 构建工具 :Qt Creator(集成开发环境,简化UI设计)
- 编码标准 :UTF-8(解决中文显示问题)
二、项目初始化,创建Qt Widgets项目
js
使用Qt Creator创建新项目:
1. 选择"Qt Widgets Application"
2. 项目名称:config_editor
3. 基类选择QWidget,类名ConfigEditor
4. 勾选"Generate form"
三、配置项目文件(.pro)
js
QT += core gui widgets
CONFIG += c++17
QMAKE_CXXFLAGS += /utf-8 # 解决中文编译乱码
SOURCES += main.cpp configeditor.cpp
HEADERS += configeditor.h
FORMS += configeditor.ui
四、实现入口函数(main.cpp)
js
#include "configeditor.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
ConfigEditor w;
w.show();
return a.exec();
}
五、核心数据模型设计(configeditor.h)
5.1. 头文件基础结构
js
#ifndef CONFIGEDITOR_H // 头文件保护宏,防止重复包含
#define CONFIGEDITOR_H
#include <QWidget> // 基础窗口部件类
#include <QMap> // 键值对容器,用于存储枚举选项和控件映射
#include <QVariant> // 通用数据类型,支持多类型参数值存储
#include <QList> // 动态数组容器,用于存储参数列表
#include <QString> // 字符串类,用于处理文本数据
- 头文件保护宏 : CONFIGEDITOR_H 确保文件只被编译一次,避免重复定义错误
- Qt库依赖 :包含Qt核心GUI组件和数据结构,为跨平台界面开发提供支持
5.2. 核心数据模型: Parameter 结构体
js
// 参数结构体定义(与项目代码完全一致)
struct Parameter {
QString name; // 参数名(INI键)
QString type; // 参数类型(bool/int/double/enum/string)
QVariant value; // 当前值
QString comment; // 中文描述(用户选中的当前行)
QMap<QString, QVariant> options; // 枚举选项(文本→值映射)
};
- name :INI文件中的键名(如 "AutoSave" ),用于文件IO和控件标识
- type :参数类型字符串(而非枚举),决定UI控件类型和数据验证规则
- value :使用 QVariant 存储多类型值(bool/int/字符串等),实现类型安全存储
- comment :中文描述(用户当前选中行),作为UI标签提示用户参数含义
- options :仅枚举类型使用,存储下拉选项(如 {"启用": true, "禁用": false} ) 设计亮点 :通过字符串类型标识和 QVariant 实现灵活的多类型参数系统,无需修改代码即可扩展新参数类型。
5.3. 主窗口类: ConfigEditor
继承自 QWidget ,是应用程序的主界面和控制器,协调数据模型与UI交互。
(1) 类声明与信号槽
js
class ConfigEditor : public QWidget
{
Q_OBJECT // Qt元对象系统宏,支持信号槽机制
public:
explicit ConfigEditor(QWidget *parent = nullptr);
~ConfigEditor() override;
private slots:
void on_btnSave_clicked(); // 保存按钮点击事件
void on_btnOpen_clicked(); // 打开按钮点击事件
- 构造函数 :初始化窗口、参数系统和UI控件
- 析构函数 :释放动态分配的资源(如UI指针)
- 私有槽函数 :响应按钮点击事件,实现文件保存/加载功能
(2) 核心业务方法
js
private:
void initParameters(); // 初始化参数定义
void createControls(); // 动态创建UI控件
bool saveIniFile(const QString& filePath); // 保存INI文件
bool loadIniFile(const QString& filePath); // 加载INI文件
void updateParametersFromControls(); // 从控件同步数据到参数
void updateControlsFromParameters(); // 从参数同步数据到控件
- initParameters() :定义所有配置项元数据(如日志级别、波特率等),是数据驱动设计的核心
- createControls() :根据 parameters 列表动态生成对应UI控件(复选框/文本框/下拉框等)
- 文件IO方法 :处理UTF-8编码的INI文件读写,确保中文正常显示
- 数据同步方法 :维护UI控件与参数列表间的双向数据一致性
(2) 成员变量
js
QList<Parameter> parameters; // 参数列表(核心数据存储)
QMap<QString, QWidget*> controlMap; // 参数名→控件映射表
Ui::ConfigEditor *ui; // UI表单指针(由uic生成)
- parameters :存储所有配置参数的列表,是应用程序的状态核心
- controlMap :建立参数名到控件的快速查找,实现高效数据同步
- ui :指向由 configeditor.ui 生成的界面元素(如按钮、布局容器)
5.4. 完整代码
js
#ifndef CONFIGEDITOR_H
#define CONFIGEDITOR_H
#include <QWidget>
#include <QMap>
#include <QVariant>
#include <QList>
#include <QString>
// 参数结构体定义(与项目代码完全一致)
struct Parameter {
QString name; // 参数名(INI键)
QString type; // 参数类型(bool/int/double/enum/string)
QVariant value; // 当前值
QString comment; // 中文描述(用户选中的当前行)
QMap<QString, QVariant> options; // 枚举选项(文本→值映射)
};
class ConfigEditor : public QWidget
{
Q_OBJECT
public:
explicit ConfigEditor(QWidget *parent = nullptr);
~ConfigEditor() override;
private slots:
void on_btnSave_clicked(); // 保存按钮点击事件
void on_btnOpen_clicked(); // 打开按钮点击事件
private:
void initParameters(); // 初始化参数定义
void createControls(); // 动态创建UI控件
bool saveIniFile(const QString& filePath); // 保存INI文件
bool loadIniFile(const QString& filePath); // 加载INI文件
void updateParametersFromControls(); // 从控件同步数据到参数
void updateControlsFromParameters(); // 从参数同步数据到控件
QList<Parameter> parameters; // 参数列表(核心数据存储)
QMap<QString, QWidget*> controlMap; // 参数名→控件映射表
Ui::ConfigEditor *ui; // UI表单指针(由uic生成)
};
#endif // CONFIGEDITOR_H
六、实现ConfigEditor核心功能(configeditor.cpp)
6.1. 构造函数 ConfigEditor::ConfigEditor
js
ConfigEditor::ConfigEditor(QWidget *parent) :
QWidget(parent),
ui(new Ui::ConfigEditor)
{
ui->setupUi(this); // 初始化UI表单
setWindowTitle("INI配置工具"); // 设置窗口标题
resize(900, 700); // 窗口初始大小
// 全局字体设置(解决中文显示问题)
QFont globalFont("SimSun", 12);
this->setFont(globalFont);
initParameters(); // 初始化参数定义
createControls(); // 动态创建UI控件
// 信号槽防重复连接处理
disconnect(ui->btnSave, SIGNAL(clicked()), this, SLOT(on_btnSave_clicked()));
connect(ui->btnSave, &QPushButton::clicked, this, &ConfigEditor::on_btnSave_clicked);
disconnect(ui->btnOpen, SIGNAL(clicked()), this, SLOT(on_btnOpen_clicked()));
connect(ui->btnOpen, &QPushButton::clicked, this, &ConfigEditor::on_btnOpen_clicked);
}
核心功能 :
- 完成UI初始化与窗口设置
- 配置全局字体(宋体12号)确保中文正常显示
- 调用 initParameters() 加载参数元数据
- 调用 createControls() 动态生成控件
- 信号槽连接前先断开已有连接,解决Qt可能的重复连接问题
6.2. 析构函数 ConfigEditor::~ConfigEditor
js
ConfigEditor::~ConfigEditor()
{
delete ui; // 释放UI指针资源
}
核心功能 :释放由 ui 指针指向的动态分配UI资源,防止内存泄漏。
6.3. 参数初始化 ConfigEditor::initParameters
js
void ConfigEditor::initParameters()
{
parameters = { // 初始化参数列表(完整版本)
{"isDebug", "bool", 1, "调试模式开关", {}},
{"nRotate", "enum", -1, "旋转角度", {{"不旋转", -1}, {"90度", 0}}},
// ... 其他50+参数定义 ...
{"revReworkIP", "string", "192.168.60.80", "复判接收IP", {}}
};
}
定义义所有配置参数的元数据,包括:
- name :INI文件中的键名
- type :参数类型(bool/int/double/enum/string)
- value :默认值
- comment :中文描述
- options :枚举类型的键值对选项
- 项目实际包含50+参数,覆盖系统设置、路径配置、网络参数等
6.4. 动态控件创建 ConfigEditor::createControls
js
void ConfigEditor::createControls()
{
QFormLayout *layout = new QFormLayout(ui->scrollAreaWidgetContents);
layout->setAlignment(Qt::AlignCenter); // 布局居中对齐
layout->setSpacing(12); // 控件间距
layout->setContentsMargins(20, 20, 20, 20);
QFont labelFont("SimSun", 12); // 标签字体
QFont controlFont("SimSun", 12); // 控件字体
for (const auto& param : parameters) {
QLabel *label = new QLabel(param.name + "\n(" + param.comment + ")");
label->setFont(labelFont);
label->setWordWrap(true); // 标签自动换行
QWidget *control = nullptr;
// 根据参数类型创建对应控件
if (param.type == "bool") {
QCheckBox *checkBox = new QCheckBox;
checkBox->setChecked(param.value.toInt() == 1);
control = checkBox;
} else if (param.type == "int") {
QSpinBox *spinBox = new QSpinBox;
spinBox->setRange(-1000000, 100000000);
spinBox->setValue(param.value.toInt());
control = spinBox;
// ... double/enum/string类型控件创建 ...
}
if (control) {
layout->addRow(label, control);
controlMap[param.name] = control; // 建立参数名→控件映射
}
}
}
- 创建表单布局并设置样式(居中对齐、间距、边距)
- 根据参数类型动态生成对应UI控件:
- bool → QCheckBox (勾选状态对应1/0值)
- int → QSpinBox (设置大范围数值限制)
- double → QDoubleSpinBox (支持小数输入)
- enum → QComboBox (加载预设选项)
- string → QLineEdit (文本输入)
- 通过 controlMap 建立参数名与控件的映射关系,支持后续数据同步
6.5. INI文件保存 ConfigEditor::saveIniFile
js
bool ConfigEditor::saveIniFile(const QString& filePath)
{
updateParametersFromControls(); // 先从控件同步数据
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return false;
QTextStream out(&file);
out.setCodec("UTF-8"); // 设置UTF-8编码防中文乱码
out << "[System]\n"; // 写入节标题
// 循环写入参数,最后一个参数不添加换行符
for (int i = 0; i < parameters.size(); ++i) {
const auto& param = parameters[i];
out << param.name << "=" << param.value.toString();
if (!param.comment.isEmpty()) {
out << " ; " << param.comment; // 添加注释
}
if (i != parameters.size() - 1) out << "\n"; // 避免末尾空行
}
file.close();
return true;
}
核心功能 :
- 调用 updateParametersFromControls() 确保数据最新
- 使用 QTextStream 并显式设置UTF-8编码
- 严格遵循INI格式:
- 以 [System] 为节标题
- 键值对格式 name=value
- 尾部添加 ; 注释
- 避免文件末尾产生多余空行
- 返回布尔值指示保存成功与否
6.6. INI文件加载 ConfigEditor::loadIniFile
js
bool ConfigEditor::loadIniFile(const QString& filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) return false;
QTextStream in(&file);
in.setCodec("UTF-8"); // UTF-8编码读取
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith(';')) continue; // 跳过空行和注释
int eqIndex = line.indexOf('=');
if (eqIndex <= 0) continue; // 跳过无效行
QString key = line.left(eqIndex).trimmed();
QString value = line.mid(eqIndex + 1).trimmed();
// 移除行内注释
int commentIndex = value.indexOf(';');
if (commentIndex != -1) value = value.left(commentIndex).trimmed();
// 更新参数值(根据类型转换)
for (auto& param : parameters) {
if (param.name == key) {
if (param.type == "bool") param.value = value.toInt();
else if (param.type == "int") param.value = value.toInt();
// ... 其他类型转换 ...
break;
}
}
}
file.close();
updateControlsFromParameters(); // 更新控件显示
return true;
}
核心功能 :
- 读取并解析INI文件,支持UTF-8编码
- 跳过空行和注释行,提取有效键值对
- 根据参数类型进行值转换(字符串→目标类型)
- 调用 updateControlsFromParameters() 刷新UI显示
6.7. 数据同步:控件→参数 ConfigEditor::updateParametersFromControls
js
void ConfigEditor::updateParametersFromControls()
{
for (auto& param : parameters) {
if (controlMap.contains(param.name)) {
QWidget *control = controlMap[param.name];
if (param.type == "bool") {
QCheckBox *checkBox = qobject_cast<QCheckBox*>(control);
if (checkBox) param.value = checkBox->isChecked() ? 1 : 0;
} else if (param.type == "int") {
QSpinBox *spinBox = qobject_cast<QSpinBox*>(control);
if (spinBox) param.value = spinBox->value();
// ... 其他类型同步 ...
}
}
}
}
核心功能 :遍历控件映射表,根据参数类型从对应控件中读取值并更新到 parameters 列表。
6.8. 数据同步:参数→控件 ConfigEditor::updateControlsFromParameters
js
void ConfigEditor::updateControlsFromParameters()
{
for (const auto& param : parameters) {
if (controlMap.contains(param.name)) {
QWidget *control = controlMap[param.name];
if (param.type == "bool") {
QCheckBox *checkBox = qobject_cast<QCheckBox*>(control);
if (checkBox) checkBox->setChecked(param.value.toInt() == 1);
// ... 其他类型同步 ...
}
}
}
}
核心功能 :将 parameters 列表中的值同步到对应UI控件,实现数据显示更新。
6.9. 打开按钮槽函数 ConfigEditor::on_btnOpen_clicked
js
void ConfigEditor::on_btnOpen_clicked()
{
QString filePath = QFileDialog::getOpenFileName(this, "打开INI文件", ".", "INI Files (*.ini);;All Files (*)");
if (!filePath.isEmpty()) {
currentFilePath = filePath;
if (loadIniFile(filePath)) {
setWindowTitle("INI配置工具 - " + filePath);
QMessageBox::information(this, "成功", "文件加载成功");
} else {
QMessageBox::warning(this, "失败", "无法加载文件");
}
}
}
核心功能 :
- 弹出文件选择对话框
- 调用 loadIniFile() 加载选中文件
- 更新窗口标题并显示操作结果提示
6.10. 保存按钮槽函数 ConfigEditor::on_btnSave_clicked
js
void ConfigEditor::on_btnSave_clicked()
{
QString filePath = currentFilePath;
if (filePath.isEmpty()) {
filePath = QFileDialog::getSaveFileName(this, "保存INI文件", ".", "INI Files (*.ini)");
}
if (!filePath.isEmpty()) {
static bool isSaving = false; // 防重复保存标志
if (isSaving) return;
isSaving = true;
bool success = saveIniFile(filePath);
if (success) {
currentFilePath = filePath;
setWindowTitle("INI配置工具 - " + filePath);
QMessageBox::information(this, "成功", "文件保存成功");
} else {
QMessageBox::warning(this, "失败", "无法保存文件");
}
isSaving = false;
}
}
核心功能 :
- 若未指定路径则弹出保存对话框
- 使用静态变量 isSaving 防止并发保存冲突
- 调用 saveIniFile() 保存数据
- 显示操作结果提示并更新窗口标题
完整代码
js
#include "configeditor.h"
#include "ui_configeditor.h"
#include <QFileDialog>
#include <QMessageBox>
#include <QFormLayout>
#include <QLineEdit>
#include <QCheckBox>
#include <QSpinBox>
#include <QDoubleSpinBox>
#include <QComboBox>
#include <QFile>
#include <QTextStream>
#include <QDebug>
ConfigEditor::ConfigEditor(QWidget *parent) :
QWidget(parent),
ui(new Ui::ConfigEditor)
{
ui->setupUi(this);
setWindowTitle("INI配置工具");
resize(900, 700); // 增大窗口默认尺寸
// 设置全局字体 - 将字体大小从调整为12
QFont globalFont("SimSun", 12);
this->setFont(globalFont);
// 初始化参数定义
initParameters();
// 创建控件
createControls();
// 确保信号槽只连接一次
// 先断开可能存在的连接
disconnect(ui->btnSave, SIGNAL(clicked()), this, SLOT(on_btnSave_clicked()));
// 再重新连接
connect(ui->btnSave, &QPushButton::clicked, this, &ConfigEditor::on_btnSave_clicked);
// 同样修复打开按钮的连接
disconnect(ui->btnOpen, SIGNAL(clicked()), this, SLOT(on_btnOpen_clicked()));
connect(ui->btnOpen, &QPushButton::clicked, this, &ConfigEditor::on_btnOpen_clicked);
}
ConfigEditor::~ConfigEditor()
{
delete ui;
}
void ConfigEditor::initParameters()
{
// 初始化所有参数定义(完整版本)
parameters = {
{"isDebug", "bool", 1, "调试模式开关", {}},
{"isMoreCheckThread", "bool", 1, "启用更多检查线程", {}},
{"isDebugRemote", "bool", 0, "远程调试开关", {}},
{"nProductSplitCount", "int", 1, "产品分割数量", {}},
{"nMaxGerberCount", "int", 1000000, "最大Gerber数量", {}},
{"isMoreBoard", "bool", 0, "多板支持", {}},
{"isUseKeyboard", "bool", 0, "启用键盘控制", {}},
{"isManualScanBarcode", "bool", 0, "手动扫描条形码", {}},
{"isRunDevExe", "bool", 0, "运行开发版程序", {}},
{"isUseMark", "bool", 1, "使用标记点", {}},
{"isUseHMRun", "bool", 1, "启用HM运行模式", {}},
{"isUseSREnhance", "bool", 1, "启用SR增强", {}},
{"isUseSREnhanceRun", "bool", 1, "运行时启用SR增强", {}},
{"isSaveOrgImage", "bool", 1, "保存原始图像", {}},
{"isSaveOrgImageTrain", "bool", 1, "保存训练用原始图像", {}},
{"isShowResultImage", "bool", 0, "显示结果图像", {}},
{"isExchangeGerberXY", "bool", 1, "交换Gerber的X和Y轴", {}},
{"isUseManualCheck", "bool", 1, "启用手动检查", {}},
{"isImportClearOld", "bool", 0, "导入时清空旧元件", {}},
{"isTestRun", "bool", 0, "测试运行模式", {}},
{"isInputPinNo", "bool", 0, "输入引脚编号", {}},
{"runSet", "int", 0, "运行设置", {}},
{"nGerberType", "int", 1, "Gerber类型", {}},
{"timeoutGetSlice", "int", 90, "获取切片超时时间(秒)", {}},
{"dscanOffsetUp", "double", 0.5, "扫描偏移量(上)", {}},
{"dscanOffsetDown", "double", 0.5, "扫描偏移量(下)", {}},
{"dWinExtend", "double", 5, "窗口扩展值", {}},
{"dNavRate", "double", 0.5, "导航速率", {}},
{"bgaAlgoVer", "int", 2, "BGA算法版本", {}},
{"sliceWidth", "int", 1536, "切片宽度", {}},
{"sliceHeight", "int", 1536, "切片高度", {}},
{"nRotate", "enum", -1, "旋转角度: -1不旋转, 0-90度, 1-180度, 2-270度",
{{"不旋转", -1}, {"90度", 0}, {"180度", 1}, {"270度", 2}}},
{"nFlip", "enum", 0, "镜像方式: -2不镜像, -1-xy轴, 0-x轴, 1-y轴",
{{"不镜像", -2}, {"xy轴镜像", -1}, {"x轴镜像", 0}, {"y轴镜像", 1}}},
{"nGerberCoordinateUnitDefault", "enum", 2, "坐标单位: 0-um, 1-mm, 2-mil, 3-in",
{{"微米(um)", 0}, {"毫米(mm)", 1}, {"密耳(mil)", 2}, {"英寸(in)", 3}}},
{"nGerberDiameterUnitDefault", "enum", 0, "直径单位: 0-um, 1-mm, 2-mil, 3-in",
{{"微米(um)", 0}, {"毫米(mm)", 1}, {"密耳(mil)", 2}, {"英寸(in)", 3}}},
{"nGerberZUnitDefault", "enum", 1, "Z轴单位: 0-um, 1-mm, 2-mil, 3-in",
{{"微米(um)", 0}, {"毫米(mm)", 1}, {"密耳(mil)", 2}, {"英寸(in)", 3}}},
{"dbUnitRateUM", "double", 0.001, "微米单位比率", {}},
{"dbUnitRateMil", "double", 0.00254, "密耳单位比率", {}},
{"dbUnitRateIn", "double", 25.4, "英寸单位比率", {}},
{"nMaxThreadCount", "int", 5, "最大检测线程数量", {}},
{"detectResultImageIndex", "int", 3, "检测结果图像索引", {}},
{"machineName", "string", "AXI9000-0002", "机器名称", {}},
{"deviceExeName", "string", "zmpublicServer.exe", "设备程序名称", {}},
{"deviceExePath", "string", "D:/zmAXI9100/zmpublicServer/zmpublicServer.exe", "设备程序路径", {}},
{"solderModelFile", "string", "D:/zmAXI9100/models/igbt_base_solder.engine", "焊接模型文件路径", {}},
{"voidModelFile", "string", "D:/zmAXI9100/models/igbt_base_void.engine", "空洞模型文件路径", {}},
{"barcodeFilePath", "string", "d:/temp/barcode", "条形码文件路径", {}},
{"SendDevicePort", "int", 5555, "发送设备端口", {}},
{"RevDevicePort", "int", 5556, "接收设备端口", {}},
{"RevDeviceIP", "string", "127.0.0.1", "接收设备IP", {}},
{"redisPort", "int", 6379, "Redis服务器端口", {}},
{"redisIP", "string", "192.168.60.80", "Redis服务器IP", {}},
{"dataServerPort", "int", 9901, "数据服务器端口", {}},
{"dataServerIP", "string", "192.168.60.80", "数据服务器IP", {}},
{"userServerIP", "string", "192.168.60.80", "用户服务器IP", {}},
{"userServerPort", "int", 9903, "用户服务器端口", {}},
{"appMode", "enum", 0, "运行模式: 0-机台软件, 1-检测服务器",
{{"机台软件", 0}, {"检测服务器", 1}}},
{"isUseServerRunAlgo", "bool", 0, "使用服务器运行算法", {}},
{"sendPort", "int", 6612, "发送端口", {}},
{"revPort", "int", 6611, "接收端口", {}},
{"revIP", "string", "192.168.60.80", "接收IP", {}},
{"isUseAoiResult", "bool", 1, "是否读取Aoi结果", {}},
{"aoiResultFilePath", "string", "d:/temp/aoi_result", "Aoi结果文件路径", {}},
{"isWaitReworkResult", "bool", 0, "是否等待复判结果", {}},
{"revReworkPort", "int", 9910, "复判接收端口", {}},
{"revReworkIP", "string", "192.168.60.80", "复判接收IP", {}}
};
}
void ConfigEditor::createControls()
{
QFormLayout *layout = new QFormLayout(ui->scrollAreaWidgetContents);
// 设置布局居中对齐
layout->setAlignment(Qt::AlignCenter);
// 设置控件间距
layout->setSpacing(12);
// 设置布局边距
layout->setContentsMargins(20, 20, 20, 20);
// 将字体大小调整为12
QFont labelFont("SimSun", 12);
QFont controlFont("SimSun", 12);
for (const auto& param : parameters) {
QLabel *label = new QLabel(param.name + "\n(" + param.comment + ")");
label->setFont(labelFont);
label->setWordWrap(true);
label->setMinimumWidth(220); // 增加标签最小宽度
label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
QWidget *control = nullptr;
if (param.type == "bool") {
QCheckBox *checkBox = new QCheckBox;
checkBox->setChecked(param.value.toInt() == 1);
checkBox->setFont(controlFont);
control = checkBox;
} else if (param.type == "int") {
QSpinBox *spinBox = new QSpinBox;
spinBox->setRange(-1000000, 100000000);
spinBox->setValue(param.value.toInt());
spinBox->setFont(controlFont);
spinBox->setMinimumWidth(150); // 增加控件最小宽度
control = spinBox;
} else if (param.type == "double") {
QDoubleSpinBox *doubleSpinBox = new QDoubleSpinBox;
doubleSpinBox->setRange(-10000, 10000);
doubleSpinBox->setDecimals(6);
doubleSpinBox->setValue(param.value.toDouble());
doubleSpinBox->setFont(controlFont);
doubleSpinBox->setMinimumWidth(150); // 增加控件最小宽度
control = doubleSpinBox;
} else if (param.type == "enum" && !param.options.isEmpty()) {
QComboBox *comboBox = new QComboBox;
comboBox->setFont(controlFont);
comboBox->setMinimumWidth(180); // 增加枚举控件宽度
for (auto it = param.options.begin(); it != param.options.end(); ++it) {
comboBox->addItem(it.key(), it.value());
if (it.value() == param.value) {
comboBox->setCurrentIndex(comboBox->count() - 1);
}
}
control = comboBox;
} else { // string
QLineEdit *lineEdit = new QLineEdit(param.value.toString());
lineEdit->setFont(controlFont);
lineEdit->setMinimumWidth(250); // 增加文本输入框宽度
control = lineEdit;
}
if (control) {
// 设置控件居中对齐
layout->addRow(label, control);
layout->setAlignment(control, Qt::AlignCenter);
controlMap[param.name] = control;
}
}
}
// 保存文件时设置编码
bool ConfigEditor::saveIniFile(const QString& filePath)
{
updateParametersFromControls();
QFile file(filePath);
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
return false;
}
QTextStream out(&file);
out.setCodec("UTF-8"); // 设置UTF-8编码
// 移除自动生成注释行
out << "[System]\n"; // 直接写入[System]节标题
// 循环写入参数,最后一个参数不添加换行符
for (int i = 0; i < parameters.size(); ++i) {
const auto& param = parameters[i];
out << param.name << "=" << param.value.toString();
if (!param.comment.isEmpty()) {
out << " ; " << param.comment;
}
// 仅在不是最后一个参数时添加换行
if (i != parameters.size() - 1) {
out << "\n";
}
}
file.close();
return true;
}
// 加载文件时设置编码
bool ConfigEditor::loadIniFile(const QString& filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return false;
}
QTextStream in(&file);
in.setCodec("UTF-8"); // 设置UTF-8编码
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line.isEmpty() || line.startsWith(';')) continue;
int eqIndex = line.indexOf('=');
if (eqIndex <= 0) continue;
QString key = line.left(eqIndex).trimmed();
QString value = line.mid(eqIndex + 1).trimmed();
// 移除注释部分
int commentIndex = value.indexOf(';');
if (commentIndex != -1) {
value = value.left(commentIndex).trimmed();
}
// 更新参数值
for (auto& param : parameters) {
if (param.name == key) {
if (param.type == "bool") {
param.value = value.toInt();
} else if (param.type == "int") {
param.value = value.toInt();
} else if (param.type == "double") {
param.value = value.toDouble();
} else if (param.type == "enum") {
param.value = value.toInt();
} else {
param.value = value;
}
break;
}
}
}
file.close();
updateControlsFromParameters();
return true;
}
void ConfigEditor::updateParametersFromControls()
{
for (auto& param : parameters) {
if (controlMap.contains(param.name)) {
QWidget *control = controlMap[param.name];
if (param.type == "bool") {
QCheckBox *checkBox = qobject_cast<QCheckBox*>(control);
if (checkBox) param.value = checkBox->isChecked() ? 1 : 0;
} else if (param.type == "int") {
QSpinBox *spinBox = qobject_cast<QSpinBox*>(control);
if (spinBox) param.value = spinBox->value();
} else if (param.type == "double") {
QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(control);
if (doubleSpinBox) param.value = doubleSpinBox->value();
} else if (param.type == "enum") {
QComboBox *comboBox = qobject_cast<QComboBox*>(control);
if (comboBox) param.value = comboBox->currentData();
} else {
QLineEdit *lineEdit = qobject_cast<QLineEdit*>(control);
if (lineEdit) param.value = lineEdit->text();
}
}
}
}
void ConfigEditor::updateControlsFromParameters()
{
for (const auto& param : parameters) {
if (controlMap.contains(param.name)) {
QWidget *control = controlMap[param.name];
if (param.type == "bool") {
QCheckBox *checkBox = qobject_cast<QCheckBox*>(control);
if (checkBox) checkBox->setChecked(param.value.toInt() == 1);
} else if (param.type == "int") {
QSpinBox *spinBox = qobject_cast<QSpinBox*>(control);
if (spinBox) spinBox->setValue(param.value.toInt());
} else if (param.type == "double") {
QDoubleSpinBox *doubleSpinBox = qobject_cast<QDoubleSpinBox*>(control);
if (doubleSpinBox) doubleSpinBox->setValue(param.value.toDouble());
} else if (param.type == "enum") {
QComboBox *comboBox = qobject_cast<QComboBox*>(control);
if (comboBox) {
for (int i = 0; i < comboBox->count(); ++i) {
if (comboBox->itemData(i) == param.value) {
comboBox->setCurrentIndex(i);
break;
}
}
}
} else {
QLineEdit *lineEdit = qobject_cast<QLineEdit*>(control);
if (lineEdit) lineEdit->setText(param.value.toString());
}
}
}
}
void ConfigEditor::on_btnOpen_clicked()
{
QString filePath = QFileDialog::getOpenFileName(this, "打开INI文件", ".", "INI Files (*.ini);;All Files (*)");
if (!filePath.isEmpty()) {
currentFilePath = filePath;
if (loadIniFile(filePath)) {
setWindowTitle("INI配置工具 - " + filePath);
QMessageBox::information(this, "成功", "文件加载成功");
} else {
QMessageBox::warning(this, "失败", "无法加载文件");
}
}
}
void ConfigEditor::on_btnSave_clicked()
{
QString filePath = currentFilePath;
if (filePath.isEmpty()) {
filePath = QFileDialog::getSaveFileName(this, "保存INI文件", ".", "INI Files (*.ini);;All Files (*)");
}
if (!filePath.isEmpty()) {
// 添加标志防止重复保存
static bool isSaving = false;
if (isSaving) return;
isSaving = true;
bool success = saveIniFile(filePath);
if (success) {
currentFilePath = filePath;
setWindowTitle("INI配置工具 - " + filePath);
QMessageBox::information(this, "成功", "文件保存成功");
} else {
QMessageBox::warning(this, "失败", "无法保存文件");
}
isSaving = false;
}
}
七、创建UI文件(configeditor.ui)
js
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConfigEditor</class>
<widget class="QWidget" name="ConfigEditor">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>900</width>
<height>700</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>880</width>
<height>600</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Open|QDialogButtonBox::Save</set>
</property>
<button class="QPushButton" name="btnOpen">
<property name="text">
<string>打开</string>
</property>
</button>
<button class="QPushButton" name="btnSave">
<property name="text">
<string>保存</string>
</property>
</button>
</widget>
</item>
</layout>
</widget>
<layoutdefault spacing="6" margin="11"/>
<resources/>
<connections/>
</ui>