资讯动态

ATL、duilib、Win32与Qt下的INI配置文件读写实战

发布时间:2026/10/22 11:56:10 来源:尧图企业网站定制
做 Windows 客户端这些年代码里来来去去换过好几套 UI 框架ATL 时代写 COM 组件和宿主窗口后来转战 duilib 做自绘界面再往后因为跨平台需求又捡起了 Qt但一直有个老伙计没换过——INI 配置文件。身边不少人一听 INI 就摆手觉得上个世纪的东西研究它浪费时间有这工夫直接上 XML、JSON 不行吗我每次都是笑笑然后指出一个事实直到今天大量成熟产品的用户偏好、窗口状态、上次会话记录用的依然是 INI 文件。这个格式轻量到可以说没有学习成本任何编辑器都能打开修改排错时一眼就能看穿线程安全压力也比数据库/注册表小得多。这篇我把在 ATL、duilib、Win32 和 Qt 四套环境下读写 INI 的完整做法串起来包含这套格式的底层规则、Windows 原生读写 API 的取舍、以及我在真实工程里反复踩过的那些坑尽量做到看完就能直接抄去用。1. 先拆掉 INI 本身格式规则、API 族和绕不开的缓存陷阱1.1 最小的文件格式约定INI 文件的结构简单到可以用三句话概括用[方括号]分节每节下面是键值的键值对分号或行首井号开头的是注释。[window] width1024 height768 maximized0 ; 上面是主窗口相关配置 [user] nameAdmin last_login_time2024-05-12 18:30:00这个格式有几个隐藏约定很多人写了几年都未必注意节名和键名不区分大小写但实际开发中建议统一用大写或统一小写否则同事之间对配置的时候容易混乱。值本质上全是字符串。1024也好、0也好读取后用_ttoi()或atoi()转换即可不需要 API 替你解析类型。注释只能占整行行尾注释不算。有些写配置的人喜欢在值后面加备注比如width1024 ; 宽度这个分号会直接变成值的一部分。文件编码在 Unix 时代几乎没人关心但在 Windows 上ANSI 和 UTF-8 这个坑能埋掉一整个版本后面我会单独展开。1.2 Windows 原生读写 API 的完整盘点Win32 提供了一组专用于 INI 的 API很多人以为只有GetPrivateProfileString这一个函数实际整个家族可以覆盖所有场景。函数名作用关键注意点GetPrivateProfileString读取指定节下的字符串键值返回值是拷贝的字符数不包含结尾空字符GetPrivateProfileInt读取整数键值失败返回默认值内部仍按字符串读再转到 32 位整数WritePrivateProfileString写入字符串键值传 NULL 键名可删除整个节GetPrivateProfileSection读取一个节的全部键值对返回的是多段 C 字符串内存布局恶心WritePrivateProfileSection整节批量写入会覆盖节内原有内容操作要小心GetPrivateProfileSectionNames枚举文件里所有节名同样是多段 C 字符串不要用GetProfileString、WriteProfileString这套不带 Private 的函数它们读写的不是你的配置文件而是系统内部的win.ini是对全局环境的污染生产环境里用它们是给自己挖坑。1.3 缓存机制为什么你改了文件却没读到新值这一小节是我最想让做后台服务和长时间运行的程序的人记住的。Windows 在实现这套 API 时内部会缓存最近访问过的几个 INI 文件的信息。正常情况下调用WritePrivateProfileString写入后系统会自动使相关缓存失效下次读取必然拿到新值。但我在实战里遇到过两回诡异情况另一个线程不通过 API直接用fopen/std::ofstream覆盖写同一个 INI 文件此时系统懒于检查文件时间戳读取端可能连续读到旧值。一个循环里反复读同一个键外部程序用记事本改文件后立刻保存偶尔出现改了但读不出来的延迟。规避办法很简单所有写入操作统一走 Win32 API不要混合使用文件流直写和系统 API 写同一个文件如果怀疑缓存没刷新调用WritePrivateProfileString(nullptr, nullptr, nullptr, path)这种清缓存的技巧或者在关键节点用文件不存在则重建的方式绕开。提示把这套 API 当成操作系统提供的一个小数据库来用——入口统一、写入统一、缓存由系统管别自己另起炉灶去读文本。2. ATL 工程CString 加上原生 API 就能搭起的配置层2.1 为什么 ATL 没有专门的 INI 封装类ATL 的核心定位是轻量 COM 模板库它的设计哲学就是只给你需要的那块砖所以它确实没有像 MFC 的CWinApp::WriteProfileString那样现成的配置类。早期我用 ATL 写 ActiveX 控件和窗口宿主时压根没想过去引入 MFC 或第三方配置库直接用CString配合原生 API 自己封装了一个不到两百行的CSimpleIni一用就是好几年。这里给一个最精简的示范你可以直接抄#pragma once #include atlbase.h #include atlstr.h class CSimpleIni { public: static CString GetString(LPCTSTR section, LPCTSTR key, LPCTSTR defaultValue, LPCTSTR filePath) { CString buffer; ::GetPrivateProfileString(section, key, defaultValue, buffer.GetBuffer(1024), 1024, filePath); buffer.ReleaseBuffer(); return buffer; } static BOOL SetString(LPCTSTR section, LPCTSTR key, LPCTSTR value, LPCTSTR filePath) { return ::WritePrivateProfileString(section, key, value, filePath); } static int GetInt(LPCTSTR section, LPCTSTR key, int defaultValue, LPCTSTR filePath) { return ::GetPrivateProfileInt(section, key, defaultValue, filePath); } };注意GetBuffer之后一定要ReleaseBuffer否则 CString 内部长度信息错乱后续拼接、比较全都会出幺蛾子。2.2 ATL 工程里最容易踩的两个坑路径与字符集第一个坑是文件路径。GetPrivateProfileString的最后一个参数如果传入相对路径解析基准是进程的当前工作目录。而 ATL 工程往往作为 IE 插件、Shell 扩展被外部 EXE 加载当前目录完全不可控写 INI 时经常出现明明代码执行了文件却出现在莫名其妙的位置的问题。正确做法是进程启动早期就用GetModuleFileName拿 EXE 完整路径抠出目录再拼上 config.ini 之类的名字CString GetExeDir() { TCHAR buffer[MAX_PATH] {0}; ::GetModuleFileName(nullptr, buffer, MAX_PATH); CString path(buffer); int pos path.ReverseFind(_T(\\)); if (pos 0) path path.Left(pos 1); return path; } CString GetConfigPath() { return GetExeDir() _T(config.ini); }第二个坑是字符集。老一批 ATL 工程默认是 ANSI 编译LPCTSTR就是LPCSTR。如果你的工程后来切换到了 Unicode而磁盘上存量 INI 文件还是 ANSI 保存的用GetPrivateProfileStringW读取时系统会按当前系统 ANSI 代码页转码中文通常没事但如果文件被某些编辑器保存成带 BOM 的 UTF-8第一个节名就可能被 BOM 字符污染导致整个节读不到。我的建议是老工程统一维持 ANSI新工程统一用 UTF-8 无 BOM并且写文件时自己声明编码约定别把决定权交给用户。3. duilib 工程实战INI 与 XML 各管一摊的配置分工3.1 搞清楚 UI 布局和业务配置的边界很多 duilib 新手最容易犯的错是把控件属性、布局参数、皮肤切换逻辑全塞进 INI 文件里。我接手过的一个播放器项目就是这么干的界面上按钮位置微调一下配置文件里就得改七八个键。这种耦合带来的维护成本高到离谱。正确分工是duilib 的 XML 文件负责界面描述控件坐标、样式、嵌套关系、图片资源路径这些跟着版本走属于程序的一部分。INI 文件负责用户状态和运行时偏好上次勾选的选项、记住的窗口大小、默认皮肤编号、登录用户名。这两者边界清晰之后界面改版只需要换 XML运行时配置写 INI互不干扰排错时也一眼能看出来问题出在代码、布局文件还是用户配置。3.2 一个完整的加载链路INI 读到控件状态假设现在的需求是程序启动时根据 INI 里[main]节的show_float_ball1决定主界面上悬浮球是否显示。duilib 工程里的常见写法是这样的// 主窗口初始化完成之后 void CMainFrame::InitWindow() { __super::InitWindow(); CString configPath GetConfigPath(); int showBall ::GetPrivateProfileInt(_T(main), _T(show_float_ball), 0, configPath); CLabelUI* pLabel static_castCLabelUI*(m_PaintManager.FindControl(_T(floatBallLabel))); if (pLabel) { pLabel-SetVisible(showBall 1); } }注意这里我没有用 duilib 自己的任何配置类因为 duilib 压根没有 INI 读取能力自带的是 XML 序列化。直接调用 Win32 API 反而最干净。3.3 duilib 工程里的中文编码老方案的遗产duilib 本身对 Unicode 的支持一直有点微妙早期版本部分控件只认 ANSI 字符串所以很多老项目的 XML、INI、资源文件全是 ANSI(GBK) 编码。后来切到 Unicode 编译后最容易出现的问题是INI 文件里[窗口]这类中文节名/键名读取不到XML 文件里的中文属性正常但读取 INI 得到的却是乱码。处理方案我比较推荐两步走第一步把所有存量 INI 统一规整为 ANSI 编码用 Windows API 的 A 版本函数读写保证兼容第二步新工程从创建起就用 UTF-8 无 BOM同时避免使用中文节名和中文键名——配置文件是给人读的但不代表要让非 ASCII 字符掺和进键名这是减少跨语言协作摩擦的最小成本方案。提示你在 duilib 工程里若遇到配置无效节读取为空之类的问题第一反感应是查编码而不是查代码逻辑这个习惯能帮你省下大量时间。4. Qt 的 QSettings同一个 INI 文件另一套读取思路4.1 QSettings 与 INI 格式的映射逻辑Qt 的QSettings是跨平台配置封装构造时指定QSettings::IniFormat就能直接读写 INI 格式文件。它和 Win32 API 最核心的差异是键名里用/表示层级直接对应 INI 文件的节。#include QSettings #include QVariant void SaveConfig(const QString configPath) { QSettings settings(configPath, QSettings::IniFormat); settings.setValue(window/width, 1024); settings.setValue(window/height, 768); settings.setValue(window/maximized, false); settings.setValue(user/name, QString::fromUtf8(Admin)); settings.sync(); } void LoadConfig(const QString configPath) { QSettings settings(configPath, QSettings::IniFormat); int width settings.value(window/width, 800).toInt(); int height settings.value(window/height, 600).toInt(); bool maximized settings.value(window/maximized, false).toBool(); }上面写出来的磁盘文件内容就是[window] width1024 height768 maximizedfalse [user] nameAdmin注意区别Qt 的布尔值默认写false/true而 Win32 API 那套通常用0/1。如果你要在同一个文件里混合两种读取方式就要统一约定别让 Qt 写出来的文件给老代码读的时候判断出错。4.2 跨代际项目里最头痛的编码差异QSettings在 Qt 5 之后的版本里处理 ini 文件默认按 UTF-8 解码这个行为和 Windows 原生的 ANSI 惯例正好相反。我接过一个混合工程UI 层已经迁到 Qt但历史配置还是 GBK 编码结果QSettings读出来的中文用户名全是乱码排查了大半天才定位到编码。如果你遇到同样的问题三个实用解决方案存量文件统一转码写一个临时的小工具遍历旧配置用QTextCodec将 GBK 读成 Unicode再按 UTF-8 重写为新的 INI。一次性解决后续全用QSettings。涉及中文的键值不用 Qt 读写在 Qt 工程里直接用GetPrivateProfileStringW读这一部分再用QString::fromWCharArray转到 Qt 字符串。跨框架混着用没问题反而最匹配现实。彻底放下历史包袱配置文件损坏就坏了吧程序检测到解析异常或者乱码特征时直接备份旧文件然后重建默认配置。4.3 QSettings 的缓存同步别等用户骂了才 refreshQSettings有自己的一套缓存机制。同一个实例里多次value()不会有问题但如果一个进程里多个实例同时操作或者进程外部修改了 INIvalue()读到的可能还是旧缓存。这时候要调用QSettings::sync()或者重新构造一个新的 QSettings 对象前者把内存改动落盘后者强制重新读取。注意Qt 里几乎每个QSettings实例构造时都会尝试打开并读取整个文件所以不要在一个高频率循环里反复构造它性能上得不偿失。正确做法是程序启动时读一次配置变更时写一次。5. 多框架共存的完整工程路径策略、接口设计和窗口位置记忆真实案例5.1 配置文件到底该放哪绿色版与安装版的路径策略INI 文件的一大优势就是支持绿色运行但也因此在路径决策上容易混乱。我的工程经验是分场景处理便携/绿色版软件配置放在 EXE 同目录下用户拷走整个目录配置跟着走体验最好。ATL 和 duilib 老工程基本都这么干。安装版软件如果装在 Program Files 下坚决不要把配置文件写在安装目录里UAC 会让你写不进去。正确姿势是放到%APPDATA%\你的产品名\config.ini获取目录可以用SHGetFolderPath或QStandardPaths::AppDataLocation。服务或后台进程优先传完整路径别依赖当前目录服务的工作目录经常是C:\Windows\System32稍微粗心就把配置写进了系统目录。5.2 一套接口两个后端Win32 原生和 QSettings 的共存设计如果产品线很长UI 框架还处在duilib 老功能维护中、Qt 新模块并行开发的过渡期不建议各写各的配置代码。我这个阶段的做法是抽象一层ConfigManager接口统一用宏或者运行时环境变量选择后端实现。对比项Win32 API 后端QSettings 后端路径处理相对路径按进程工作目录解析坑多相对路径一般按进程启动目录但换工作目录也会变编码适配对 ANSI/GBK 天然友好默认按 UTF-8 解码缓存行为系统维护写入 API 自动刷新需要 sync 重新构造才保险跨平台能力只适用于 Windows全平台原生支持 macOS/Linux线程安全同一进程内并发写同一文件有风险官方建议实例间不共享加锁更稳这个设计的核心价值在于迁移期间新旧代码读写同一份 INI而业务层只面对一套 Get/Set 接口。等到 Qt 完全接管后替换掉旧后端即可界面上读配置的代码一行都不用改。5.3 真实案例记住窗口位置和上次状态这个场景几乎每个桌面软件都有。我的实现逻辑是主窗口收到WM_CLOSE时记录WINDOWPLACEMENT启动时读取并应用。写出来就是// 保存 WINDOWPLACEMENT wp; wp.length sizeof(wp); ::GetWindowPlacement(m_hWnd, wp); CString path GetConfigPath(); ::WritePrivateProfileString(_T(window), _T(left), itoa(wp.rcNormalPosition.left), path); ::WritePrivateProfileString(_T(window), _T(top), itoa(wp.rcNormalPosition.top), path); ::WritePrivateProfileString(_T(window), _T(width), itoa(wp.rcNormalPosition.right - wp.rcNormalPosition.left), path); ::WritePrivateProfileString(_T(window), _T(height), itoa(wp.rcNormalPosition.bottom - wp.rcNormalPosition.top), path); ::WritePrivateProfileString(_T(window), _T(maximized), wp.showCmd SW_MAXIMIZE ? _T(1) : _T(0), path);读取的时候有两处细节必须处理保存到 INI 之前要做一次屏幕边界校验防止用户之前把窗口拖到了副屏幕上副屏拔掉后主屏上根本找不到窗口。我一般保存前先比对工作区大小超出合理范围就重置为居中。读取的left/top/width/height全要用临时变量接住确认完整后再一次性SetWindowPos避免窗口在启动过程中来回跳动导致闪烁和初始化逻辑被重复触发。5.4 实践体会与收尾把这四套方案都实际做过一遍之后我最想强调的一点其实不是技术细节而是选择惯性。在 ATL 和 duilib 的工程里用 Win32 API在 Qt 工程里用 QSettings这是最省力的不需要强行统一成一个库。但如果你的产品像我经历过的那样新旧框架并行三五年那就值得为配置文件单独建一层轻量封装把路径、编码、默认值策略一次性定规矩。INI 文件虽然简单它承载的却是用户对软件的那一点点记得我上次的选择的期待把这一层做扎实比在界面特效上多花一周功夫更让用户感到贴心。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑