PowerBuilder 配置持久化实战:注册表 RegistrySet/Get 全谱与 INI 读写(ProfileString / ProfileInt / SetProfileString)(PB10 基准 · PB12.5 实测)
PowerBuilder 配置持久化实战:注册表 RegistrySet/Get 全谱与 INI 读写(ProfileString / ProfileInt / SetProfileString)(PB10 基准 · PB12.5 实测)阅读说明
1. 适用版本:本文以 PowerBuilder 10 为基准编写与验证;Registry* 系列与 Profile* 系列的 API 形态自 PB 早期版本就固定下来,PB 6.5 / 8 / 9 / 10 / 10.5 / 11.x / 12.x 语法一致。PB 12.5 已同步实机复跑,行为与 PB 10 完全一致(见第十五节)
2. 支持数据库:本文示例不涉及数据库,配置走操作系统自带的注册表与 INI 文件,不需要任何 DBMS
3. 操作系统与环境要求:Windows 2000 及以上(注册表与 INI 都是 Windows 原生机制,不需要任何第三方 DLL);.sru / .srw 源码须 GBK 编码 + CRLF 换行 + 无 BOM;写 HKEY_LOCAL_MACHINE 需要管理员权限,写 HKEY_CURRENT_USER 普通用户即可
4. 难度系数:★★☆☆☆(会建用户对象、会写函数与 if 判断即可跟做;唯一的门槛是记住几个返回值语义)
5. 其它阅读说明:本文所有结论都来自双版本实机探针(PB 10 与 PB 12.5 各自复跑,共 77 条硬断言全部命中),文中出现的返回值、字符数、值名顺序、注册表落点都是真机输出原值,不是推断值;文末附可直接导入运行的 PB10 兼容版 PBL 源码包(含示例用户对象、探针对象与自检脚本)
一、配置该存哪儿:先想清楚,再动手
"保存用户设置"这件事,PB 能走的路只有四条。很多人上来就用注册表,其实不少场景 INI 更合适。
方案存放位置适合不适合
注册表HKEY_CURRENT_USER / HKEY_LOCAL_MACHINE少量键值型配置:服务器地址、上次登录用户、窗口位置、许可信息长文本、多级结构、需要人工查看与备份
INI 文件exe 同目录或 %APPDATA%需要人工手改、绿色软件、整个目录拷走就能用单值超过 4095 字符、需要并发写
数据库DBMS多用户共享、需要审计与事务单机小工具(没库就跑不起来)
XML / JSON 文件文件系统复杂嵌套配置、要和外部系统交换PB 原生无解析器(需自己写或引第三方库)
本文讲前两条——它们规模最小、依赖最少,却也是坑最集中的地方:注册表的类型系统、64 位系统的重定向、INI 的长度上限与编码,都能让人排查半天。下面把两套 API 一次讲透。
二、注册表 API 全解:一共就 5 个函数
函数签名返回值
RegistrySetRegistrySet ( key, valuename, valuetype, value )Integer:1 成功,-1 失败
RegistryGetRegistryGet ( key, valuename, { valuetype }, valuevariable )同上
RegistryValuesRegistryValues ( key, valuename[] )同上
RegistryKeysRegistryKeys ( key, subkeyname[] )同上
RegistryDeleteRegistryDelete ( key, valuename )同上
七个合法的值类型枚举(只要写错一个字母,编译期就报错,见第五节):
枚举对应的 Win32 类型说明
RegString!REG_SZ普通字符串,最常用
RegExpandString!REG_EXPAND_SZ含环境变量引用的字符串,读回不展开
RegBinary!REG_BINARY任意二进制,PB 侧用 blob 接
ReguLong!REG_DWORD32 位无符号整数,PB 侧必须用 ulong 接
ReguLongBigEndian!REG_DWORD_BIG_ENDIAN同上但字节序相反
RegLink!REG_LINK符号链接类,极少用
RegMultiString!REG_MULTI_SZ多行字符串,PB 侧必须用变长字符串数组接
三条不看文档容易写错、实测确认过的语法要点,先摆在这里:
[*]key 的根名必须写全。合法根名是 HKEY_CURRENT_USER、HKEY_LOCAL_MACHINE、HKEY_CLASSES_ROOT、HKEY_USERS、HKEY_CURRENT_CONFIG;不接受 HKCU 这种缩写。
[*]分隔符必须是反斜杠 \,写成正斜杠直接失败。
[*]RegistryGet 的 valuetype 参数不要省。省略后(三参形式)只对 RegString! 有效,读数值型值会返回 -1。
三、完整示例:nvo_reg_demo
3.1 操作步骤
[*]新建一个 PBL,在里面新建一个定制类用户对象(Custom Class),命名 nvo_reg_demo,祖先保持 nonvisualobject;
[*]把下面整段源码粘进去(或直接导入随文附的 .sru);
[*]全量重建(Rebuild);
[*]在窗口上放一个按钮,clicked 事件里写:
//=========================================================
// 演示入口:调用 nvo_reg_demo 跑完注册表 + INI 全流程,把结果弹出来
// 前置:窗口上已放好 nvo_reg_demo 类型的实例变量 iuo_reg
//=========================================================
string ls_key // 注册表键全路径,写 HKCU 下自己的 Software 分支
string ls_ini // INI 文件全路径,放在 exe 同目录
string ls_out // 演示返回的多行文本
ls_key = 'HKEY_CURRENT_USER\Software\MyApp'
ls_ini = 'MyApp.ini'
ls_out = iuo_reg.of_demo(ls_key, ls_ini)
MessageBox('配置持久化演示', ls_out)
运行后会弹出演示结果,同时注册表与 INI 里都已经留下了配置(示例最后一步会自己清理注册表键)。
3.2 nvo_reg_demo 完整源码
forward
global type nvo_reg_demo from nonvisualobject
end type
end forward
global type nvo_reg_demo from nonvisualobject
end type
global nvo_reg_demo nvo_reg_demo
forward prototypes
public function integer of_save_config (string as_key, string as_company, long al_retry, boolean ab_debug, string as_editor)
public function string of_load_config (string as_key)
public function string of_list_values (string as_key)
public function integer of_remove_config (string as_key)
public function integer of_ini_save (string as_ini, string as_user, long al_timeout, boolean ab_autologin)
public function string of_ini_load (string as_ini)
public function string of_ini_dump (string as_ini)
public function string of_demo (string as_key, string as_ini)
end prototypes
public function integer of_save_config (string as_key, string as_company, long al_retry, boolean ab_debug, string as_editor);
//=========================================================
// 把一份应用配置写进注册表(HKEY_CURRENT_USER,普通用户即可写)
// as_key : 注册表键全路径,例如 'HKEY_CURRENT_USER\Software\MyApp'
// as_company: 字符串值(RegString!)
// al_retry : 数值,写 UNSIGNED LONG,必须转成 ulong 再传
// ab_debug : 布尔,注册表没有布尔类型,统一存成 '1'/'0'
// as_editor : 含环境变量引用的路径,写 RegExpandString!
// 返回 0 全部成功,否则返回最后一次失败的返回值(-1)
//=========================================================
string ls_debug
integer li_rtn
ulong lu_retry
if ab_debug then
ls_debug = '1'
else
ls_debug = '0'
end if
li_rtn = RegistrySet(as_key, 'Company', RegString!, as_company)
if li_rtn <> 1 then return li_rtn
//ReguLong! 必须配 ulong:PB 没有 ulong() 转换函数,用 ulong 变量接一次即可
lu_retry = al_retry
li_rtn = RegistrySet(as_key, 'Retry', ReguLong!, lu_retry)
if li_rtn <> 1 then return li_rtn
li_rtn = RegistrySet(as_key, 'Debug', RegString!, ls_debug)
if li_rtn <> 1 then return li_rtn
li_rtn = RegistrySet(as_key, 'Editor', RegExpandString!, as_editor)
if li_rtn <> 1 then return li_rtn
return 0
end function
public function string of_load_config (string as_key);
//=========================================================
// 把注册表里的配置读回来拼成多行文本
// 注意:类型必须与写入时一致,否则 RegistryGet 返回 -1
// 键名/值名大小写不敏感;读不到时用 <缺失> 占位
//=========================================================
string ls_company
string ls_debug
string ls_editor
string ls_out
ulong lu_retry
integer li_rtn
ls_out = ''
ls_company = '<缺失>'
li_rtn = RegistryGet(as_key, 'Company', RegString!, ls_company)
ls_out = ls_out + 'Company = ' + ls_company + '~r~n'
lu_retry = 0
li_rtn = RegistryGet(as_key, 'Retry', ReguLong!, lu_retry)
ls_out = ls_out + 'Retry = ' + String(lu_retry) + '~r~n'
ls_debug = '<缺失>'
li_rtn = RegistryGet(as_key, 'Debug', RegString!, ls_debug)
ls_out = ls_out + 'Debug = ' + ls_debug + '~r~n'
ls_editor = '<缺失>'
li_rtn = RegistryGet(as_key, 'Editor', RegExpandString!, ls_editor)
ls_out = ls_out + 'Editor= ' + ls_editor + '~r~n'
return ls_out
end function
public function string of_list_values (string as_key);
//=========================================================
// 列出键下所有值名(未命名值显示为空串)
// 变长字符串数组的上界会自动反映实际数量,不必预设大小
//=========================================================
string ls_names[]
string ls_out
integer li_rtn
integer li_i
li_rtn = RegistryValues(as_key, ls_names)
if li_rtn <> 1 then return 'RegistryValues 失败,返回 ' + String(li_rtn)
ls_out = '值名个数 = ' + String(UpperBound(ls_names)) + '~r~n'
li_i = 1
do while li_i <= UpperBound(ls_names)
ls_out = ls_out + '[' + String(li_i) + '] ' + ls_names + '~r~n'
li_i = li_i + 1
loop
return ls_out
end function
public function integer of_remove_config (string as_key);
//=========================================================
// 先逐个删值,最后 valuename 传空串删键本身
// 实测:键下若还有子键,删键返回 -1 且「键和所有值都原样保留」
// (不是"值被清掉、只留空键"——这一点容易想反)
//=========================================================
integer li_rtn
li_rtn = RegistryDelete(as_key, 'Company')
li_rtn = RegistryDelete(as_key, 'Retry')
li_rtn = RegistryDelete(as_key, 'Debug')
li_rtn = RegistryDelete(as_key, 'Editor')
return RegistryDelete(as_key, '')
end function
public function integer of_ini_save (string as_ini, string as_user, long al_timeout, boolean ab_autologin);
//=========================================================
// INI 只有「按节/键写字符串」一种写入方式,数值要自己转字符串
// 重要:SetProfileString 不会创建文件,文件必须先存在,否则返回 -1
//=========================================================
string ls_flag
integer li_rtn
if ab_autologin then
ls_flag = '1'
else
ls_flag = '0'
end if
li_rtn = SetProfileString(as_ini, 'Login', 'User', as_user)
if li_rtn <> 1 then return li_rtn
li_rtn = SetProfileString(as_ini, 'Login', 'Timeout', String(al_timeout))
if li_rtn <> 1 then return li_rtn
li_rtn = SetProfileString(as_ini, 'Login', 'AutoLogin', ls_flag)
if li_rtn <> 1 then return li_rtn
return 0
end function
public function string of_ini_load (string as_ini);
//=========================================================
// 读 INI:文件 / 节 / 键 任一级缺失都返回你给的默认值
// 注意:ProfileInt 遇到「键在、但值不是数字」时返回 0,不是默认值
// (实测 'abc' -> 0;默认值只在「键或节或文件不存在」时才生效)
//=========================================================
string ls_out
string ls_user
ls_out = ''
ls_user = ProfileString(as_ini, 'Login', 'User', '<缺失>')
ls_out = ls_out + 'User = ' + ls_user + '~r~n'
ls_out = ls_out + 'Timeout = ' + String(ProfileInt(as_ini, 'Login', 'Timeout', 30)) + '~r~n'
ls_out = ls_out + 'AutoLogin = ' + ProfileString(as_ini, 'Login', 'AutoLogin', '0') + '~r~n'
return ls_out
end function
public function string of_ini_dump (string as_ini);
//=========================================================
// PB 没有 ProfileKeys / ProfileSections,想列出 INI 里全部节与键
// 只能自己按行解析:'[' 开头是节,含 '=' 的行是键
//=========================================================
integer li_fp
integer li_rc
string ls_line
string ls_out
string ls_sec
ls_out = ''
li_fp = FileOpen(as_ini, LineMode!, Read!)
if li_fp < 1 then return '无法打开 ' + as_ini
//注意:FileRead 返回的是「读到的字节数」,行首是换行时返回 0,
//读到文件尾返回 -100 —— 所以不能用「= 1」判断成功,否则一行都读不出来
li_rc = 0
do
li_rc = FileRead(li_fp, ls_line)
if li_rc > 0 then
if Left(ls_line, 1) = '[' then
ls_sec = Mid(ls_line, 2, Pos(ls_line, ']') - 2)
ls_out = ls_out + '节:' + ls_sec + '~r~n'
else
if Pos(ls_line, '=') > 0 then
ls_out = ls_out + '' + ls_line + '~r~n'
end if
end if
end if
loop while li_rc <> -100
FileClose(li_fp)
return ls_out
end function
public function string of_demo (string as_key, string as_ini);
//=========================================================
// 演示入口:写注册表 -> 读回 -> 列值名 -> 写 INI -> 读 INI -> 清理
// 返回多行文本,调用方用 MessageBox 显示
//=========================================================
string ls_out
integer li_fp
ls_out = '【写注册表】返回值 = ' + String(of_save_config(as_key, '示例科技', 3, true, '%SystemRoot%\system32\notepad.exe')) + '~r~n'
ls_out = ls_out + '【读注册表】~r~n' + of_load_config(as_key)
ls_out = ls_out + '【值名清单】~r~n' + of_list_values(as_key)
//INI 必须先存在:这里用空文件占位(真实项目里由安装程序生成初版)
li_fp = FileOpen(as_ini, LineMode!, Write!, LockWrite!, Replace!)
FileClose(li_fp)
ls_out = ls_out + '【写 INI】返回值 = ' + String(of_ini_save(as_ini, 'zhizhe', 30, true)) + '~r~n'
ls_out = ls_out + '【读 INI】~r~n' + of_ini_load(as_ini)
ls_out = ls_out + '【INI 全文解析】~r~n' + of_ini_dump(as_ini)
ls_out = ls_out + '【清理】返回值 = ' + String(of_remove_config(as_key)) + '~r~n'
return ls_out
end function
3.3 真机运行输出(PB 10 与 PB 12.5 逐字符相同)
【写注册表】返回值 = 0
【读注册表】
Company = 示例科技
Retry = 3
Debug = 1
Editor= %SystemRoot%\system32\notepad.exe
【值名清单】
值名个数 = 4
Company
Retry
Debug
Editor
【写 INI】返回值 = 0
【读 INI】
User = zhizhe
Timeout = 30
AutoLogin = 1
【INI 全文解析】
节:Login
User=zhizhe
Timeout=30
AutoLogin=1
【清理】返回值 = 1
注意 Editor 读回来的是 %SystemRoot%\system32\notepad.exe,没有被展开成系统盘上的真实目录——RegExpandString! 只负责"标记这一项是含变量的字符串",展开与否是使用者的事(见第七节)。
四、注册表实测(一):路径与写法
实测动作真实返回结论
RegistrySet(HKCU\Software\...\pbai_reg_probe, 'Company', RegString!, '示例科技')1写字符串值成功
用 RegistryGet(..., RegString!, v) 读回(四参)1,值 示例科技四参形式正常
用 RegistryGet(..., v) 读回(三参,省略类型)1,值 示例科技三参只对字符串有效
用三参形式读一个 ReguLong! 值-1★ 读数值型必须给 valuetype
写一个 3000 字符的字符串,再读回1,长度 3000长字符串无损(远超 INI 的 4095 上限)
值名写成大写 COMPANY 再读1,值 示例科技值名大小写不敏感
键路径整段乱写大小写1,值 示例科技键路径大小写不敏感
键路径写成 HKCU\Software\...-1★ 不接受根名缩写
键路径用正斜杠 HKEY_CURRENT_USER/Software/...-1★ 分隔符只能是反斜杠
RegistrySet 直接写三层深的尚不存在的路径1★ 自动逐级建键
值名传空串写入1;读回 unnamed空串 = 该键的未命名(默认)值,可正常读写
读一个不存在的值-1,目标变量保持原值失败时不会污染变量
读一个不存在的键-1,目标变量保持原值同上
关于"失败时不污染变量"这一条值得单独说:探针里先把变量赋成 <none>(字符串)或 0(ulong),RegistryGet 返回 -1 之后变量还是那个初值。所以可靠的读法是:
//先给默认值,再读;读失败时变量保持默认值,不会变成垃圾
ls_val = '<默认值>'
li_rtn = RegistryGet(ls_key, 'NoSuchValue', RegString!, ls_val)
if li_rtn <> 1 then
//这里可以记日志,也可以直接沿用 ls_val 里的默认值
end if
五、注册表实测(二):类型必须一一对应
注册表是有类型的,RegistryGet 拿到的类型必须和写进去的一致,跨类型读一律失败,实测全部返回 -1:
写入时的类型读取时用的类型实测返回
RegString!ReguLong!-1
ReguLong!RegString!-1
RegBinary!RegString!-1
ReguLongBigEndian!ReguLong!-1
RegExpandString!RegString!-1
这里没有"自动转换",一次都没有。 所以做配置读取时,把写入用的那个枚举当成"契约"存进代码注释里(示例源码就是这么做的)。
两个具体坑:
坑一:ReguLong! 必须用 ulong 变量接,而 PB 没有 ulong() 转换函数。
long ll_retry //业务层习惯用 long 存数量
ulong lu_retry //过桥用:注册表要 ulong
ll_retry = 3
//lu_retry = ulong(ll_retry)← 这行会报 C0051: Unknown function name: ulong
lu_retry = ll_retry //直接赋值让编译器自己转,没问题
RegistrySet(ls_key, 'Retry', ReguLong!, lu_retry)
ulong 的上下限也都实测过:写 4294967295(32 位无符号最大值)再读回,得到的正是 4294967295,没有截断、没有符号翻转。
坑二:注册表没有布尔类型。 PB 的 boolean 不能直接往里写,惯例是存成字符串 '1' / '0'(示例里的 Debug 就是这么做的),读的时候再判 = '1'。想做严谨一点,可以用 ReguLong! 存 0/1,但那样 RegistryGet 就必须给 ReguLong!——别今天写字符串明天用 ulong 读。
顺带把"哪些类型名是编的"这件事也钉一下:实测 RegLong!、RegDWord!、RegStringArray!、RegLongArray! 这四个写法都不存在,写进去编译期直接报错;能用的就是第二节表里那七个。
六、注册表实测(三):枚举与删除
6.1 枚举值名与子键
实测动作真实返回
键下有 5 个值(含一个未命名值)时调 RegistryValues1,值名个数 5,未命名值以空串出现在清单里
键下无子键时调 RegistryKeys1,个数 0(成功但是空)
键不存在时调 RegistryKeys-1
"空"和"错"要分清楚:键存在、只是没有子键,返回的是 1 加 0 个元素;键根本不存在才是 -1。写成 if li_rtn = 1 and UpperBound(arr) > 0 then 才不会漏判。
6.2 ★ 枚举顺序不可依赖(这一条是专门做了对照实验的)
子键实测是字典序。探针建子键的顺序是 Sub、Leaf、Zeta、Alpha、Mid(故意乱着建),RegistryKeys 返回的是 Alpha、Leaf、Mid、Sub、Zeta——排好了。
值名则完全不保证。同一个探针进程里出现了两种结果:
[*]示例对象的 4 个值(创建序:Company、Retry、Debug、Editor)→ 返回的正是创建序;
[*]探针键的 5 个值(创建序:Company、Big、Retry、Small、空串)→ 返回 Big、Retry、Small、空串、Company,第一项被挪到了末位。
为了确认不是随机噪声,又专门补了三组对照实验(都在干净的键里新建):
实验创建顺序RegistryValues 返回顺序
AZeta、Alpha、MidZeta、Alpha、Mid(= 创建序)
BCompany、Big、Retry、Small、空串Company、Big、Retry、Small、空串(= 创建序)
C空串、Zeta、Alpha、Mid空串、Zeta、Alpha、Mid(= 创建序)
三组都是创建序,说明顺序本身不是"某种排序",而是取决于那段时间里键被怎么动过——探针键的 5 个值之所以错位,是因为枚举之前先在这把键下面建了两个子键。
结论:要顺序就自己排。 RegistryValues / RegistryKeys 的顺序当作"未定义",需要按名字显示就 Sort() 一下。
6.3 删除
实测动作真实返回
RegistryDelete(key, 'Company')(键下有值有子键)1
删完再读同一个值-1
删一个不存在的值-1(不报错,也不成功)
RegistryDelete(叶子键, '')1;再读该键下的值 → -1
RegistryDelete(有子键的键, '')-1
最后一行要特别注意。删键失败时,键和它所有的值都原样保留——探针在这步之前先删掉了 Company,失败后去读 Retry,读到的是原来的 4294967295,未命名值也还躺在那里。也就是说 RegistryDelete(key, '') 遇到子键时是整体不做,不会"先把值清干净、只是键没删掉"。想把一棵树清掉,得从叶子往上逐层删(探针的清理函数就是这么写的:先删 ...\Sub\Deep,再 ...\Sub,再 ...\Leaf,最后才是根)。
七、注册表实测(四):九类值类型全谱
除了最常用的字符串与 ulong,剩下四种类型也全部实机走了一遍:
类型实测结果要点
RegBinary!写 4 字节 → 1;读回 4 字节,用 String(blob, EncodingANSI!) 得到 ABCD写之前 Blob('ABCD') 得到的是 8 字节(按 Unicode 转),要 4 字节 ANSI 得写 Blob('ABCD', EncodingANSI!)
RegMultiString!传变长字符串数组(两个元素)写入 → 1;读回 2 个元素★ 必须传数组:传单个字符串写入返回 -1;用数组去接一个标量值同样返回 -1
RegLink!写入 1,读回 target-path能正常往返,但业务上基本用不着
ReguLongBigEndian!写 16909060 → 1;同类型读回 16909060跨类型(用 ReguLong!)读返回 -1
RegExpandString!写入 1;读回 %SystemRoot%\system32★ 读回的是原文,不展开环境变量;用 RegString! 读它返回 -1
用数组接 RegMultiString! 的写法长这样:
//写多行值:必须用变长字符串数组,标量字符串会返回 -1
string ls_lines[] //变长数组,不要写 string ls_lines
string ls_back[] //读回来也用它接
integer li_rtn
integer li_i
ls_lines = 'one'
ls_lines = 'two'
li_rtn = RegistrySet(ls_key, 'Multi', RegMultiString!, ls_lines)
if li_rtn <> 1 then
MessageBox('写多行值失败', 'RegistrySet 返回 ' + String(li_rtn))
return
end if
li_rtn = RegistryGet(ls_key, 'Multi', RegMultiString!, ls_back)
//上界会自动变成实际元素个数,不必预设大小
MessageBox('多行值读回', '元素个数 = ' + String(UpperBound(ls_back)) + '~r~n' + ls_back + '~r~n' + ls_back)
这里有个反向验证的硬证据:探针里另写了一个故意用定长数组传 RegistryValues 的负向源码,导入时按预期编译失败(报 C0052 Bad argument list for function: registryvalues)。所以这四条变长数组(RegistryValues、RegistryKeys、RegistryGet 的 RegMultiString!、RegistrySet 的 RegMultiString!)只能接变长数组,写成 string ls_names 是编译不过的。
八、★ 64 位系统上找不到自己写的键:WOW64 重定向
这一节是本文最值钱的一条,也是最难自查的一条。
现象:在 64 位 Windows 上,32 位的 PB 往 HKEY_LOCAL_MACHINE\SOFTWARE\pbai_probe_tmp 写一个值——RegistrySet 返回 1,紧接着 RegistryGet 也读得回来。但打开注册表编辑器(64 位视图)找这个键,它不在那儿。
用 64 位 Python 的 winreg 直接核对落点,得到:
查看路径64 位视图32 位视图
HKLM\SOFTWARE\pbai_probe_tmp不存在(WinError 2 系统找不到指定的文件)存在(值 V = x,子键 Sub)
HKLM\SOFTWARE\WOW6432Node\pbai_probe_tmp存在(值 V = x,子键 Sub)存在(同上)
原因:32 位进程访问 HKLM\SOFTWARE 时,Windows 会自动把它重定向到 HKLM\SOFTWARE\WOW6432Node。PB 自己完全不知道这件事——它以为自己写的就是 HKLM\SOFTWARE,读的时候走的也是同一条被重定向的路,所以自读自写一切正常,跨位数就全错。
还有一个更直观的旁证:探针用 RegistryKeys 枚举 HKEY_LOCAL_MACHINE\SOFTWARE,返回 19 个子键,清单里没有 WOW6432Node。这恰恰说明 32 位视图下这一层本身就已经是 WOW6432Node 里面的内容了——如果它是真的 SOFTWARE,一定会看到那个 WOW6432Node 子键。
实务建议:
[*]应用自己的配置一律写 HKEY_CURRENT_USER。它不受位数影响、不需要管理员权限、天然按用户隔离——这也是本文示例的选择。
[*]只有全机共享的东西(许可证、公共安装路径)才写 HKEY_LOCAL_MACHINE,而且要接受"它落在 WOW6432Node 下面"这个事实。
[*]一旦出现"PB 读得到、64 位工具读不到"(或者反过来),第一个要怀疑的就是 WOW64 重定向,而不是权限或键名拼写。
[*]想显式看 32 位视图,用 regedit /reg:32 打开注册表再找。
九、INI API 全解:只有 3 个函数
函数签名返回值
ProfileStringProfileString ( filename, section, key, default )String,读不到返回 default
ProfileIntProfileInt ( filename, section, key, default )Integer,读不到返回 default
SetProfileStringSetProfileString ( filename, section, key, value )Integer:1 成功,-1 失败
就这三个。没有 ProfileKeys、没有 ProfileSections——也就是说,PB 没有办法列出 INI 里的节名和键名,只能自己逐行读文件(见第十二节)。这一点和 Win32 API 里的 GetPrivateProfileSectionNames 不一样,习惯 C 的人容易先找一圈。
另外注意:SetProfileString 只能写字符串。数值、布尔、日期统统要自己 String() 转过去再 ProfileInt() 转回来。
十、INI 实测(一):写入与读取
10.1 ★★★ SetProfileString 不会创建文件
这是本次实测里最容易吃亏的一条:
SetProfileString('pbai_ini_fresh.ini', 'App', 'Name', '中文名') → 返回 -1
而且它不会把文件建出来——探针事后检查,那个文件根本没生成。
所以正确顺序是"先建文件、再写内容"(示例源码里就是这么做的):
//INI 文件必须先存在:SetProfileString 不会替你创建文件
//真实项目里这一步通常由安装程序完成,这里用空文件占位
integer li_fp
li_fp = FileOpen(ls_ini, LineMode!, Write!, LockWrite!, Replace!)
FileClose(li_fp)
li_rtn = SetProfileString(ls_ini, 'Login', 'User', 'zhizhe')
要注意"会自动创建"和"不会自动创建"的分界:
对象SetProfileString 会自动创建吗
INI 文件★ 不会(文件不存在直接返回 -1)
节(如凭空写一个此前不存在的节 Second)会(实测返回 1,随后读回正常)
键会
一句话记住:它会往文件里补节和键,但不会替你造出那个文件本身。
10.2 读取:三级缺失都落到默认值
实测动作真实返回
读文件中存在的键值本身(中文名)
键不存在给的默认值
节不存在给的默认值
文件不存在给的默认值
节名键名写成大写(APP / NAME)读到 中文名 → 大小写不敏感
节名与键名都用中文(节 中文节、键 键)返回 1,读回 值 → 支持中文
覆盖写同一个键返回 1,读到新值
值里含等号(a=b;c)返回 1,原样读回 a=b;c
写空值返回 1,读回空串
空值 ≠ 缺键,这两个要分开处理:
文件里的样子ProfileString 返回
Empty=(键在,值是空的)空串
键压根不存在你给的默认值
业务上这就是"用户明确设成空"和"还没设过"的区别,别把两者混成一个判断。
十一、INI 实测(二):边界与编码
11.1 ★ 单值上限是 4095 个字符
探针往一个键里写 5000 字符,读回来只有 4095 字符。这是 Win32 INI 接口的缓冲区限制(内部按 4096 字节缓冲、要留结束符),不是 PB 的毛病。
结论:INI 适合放短配置。要存长文本(比如一段 JSON、一段 SQL 模板),走注册表的 RegString!(实测 3000 字符无损)或干脆放独立文件。
11.2 编码:GBK 能读,UTF-16 必须带 BOM
文件编码ProfileString 读取结果
GBK(ANSI)正常读出中文
UTF-16LE + BOM正常读出中文
UTF-16LE 无 BOM★ 读不到,落到默认值
所以用记事本"另存为 UTF-16"(会带 BOM)没问题;但如果有工具生成 UTF-16 却不写 BOM,INI 读出来就是空。团队协作时最好统一成 ANSI/GBK 或"UTF-16 带 BOM"。
11.3 手工编辑时会遇到的那些"小脾气"
这些都不是 BUG,是 Win32 INI 解析的既定行为,但不知道就会怀疑人生:
文件里写的读出来的
值带双引号:Name="带引号"带引号(引号被剥掉)
值两边有空格:Padded = space value space value(两边空格被裁掉)
节名两边有空格:节名写作「一个空格 + Sec With Space + 一个空格」用带空格或不带空格的节名都能读到
键名里带空格:Key With Space正常读到
同名键出现两次:Dup=first 与 Dup=second只取第一个(first)
以 ; 开头的注释行被跳过,不影响解析
相对路径 MyApp.ini可以正常打开(相对当前工作目录)→ 生产环境建议写绝对路径
"重复键只取第一个"这一条要写进纪律:如果程序用"写新键"的方式改名,旧键会一直躺在文件里,读到的是老的。改配置要用同名覆盖写(SetProfileString 同节同键),不要用"再写一个新键名"。
11.4 ProfileInt 的默认值只在"缺失"时生效
实测动作真实返回说明
键存在、值是 55正常
键存在、值是 abc0★ 不是默认值 3
键不存在(默认值给 3)3默认值生效
键不存在(默认值给 -9)-9负数默认值原样返回
所以别指望 ProfileInt 的默认值帮你兜住"用户手改成了乱码"这种情况——那种情况它给 0。要区分,就用 ProfileString 先读成字符串,自己判断是不是数字,再决定用默认值还是报错。
十二、PB 没有 ProfileKeys:手写全文解析,顺带一个真踩到的坑
想在界面上做一个"配置项一览",就必须自己把 INI 逐行读出来。写这个功能时我踩了一个很典型的坑,放在这里当案例。
12.1 ★★ 坑:FileRead 返回的是"读到的字节数",不是"成功/失败"
第一版代码我是这么写的(错的):
//错误写法:想当然地认为「读成功 = 1」
li_rc = FileRead(li_fp, ls_line)
if li_rc = 1 then
//解析这一行
end if
跑出来解析结果是空的,一行都没进去。加了诊断才知道 FileRead 的真实返回值是什么:
读到的内容FileRead 返回
第 1 行(内容是字母 A 加一对中括号,共 3 个字符)3
空行(行首就是 CR/LF)0
第 3 行 K=V3
读到文件尾-100
它返回的是本次实际读到的字节数。 所以:
[*]判"读到了内容"必须用 li_rc > 0,不能写 = 1(大部分行长度都不是 1);
[*]空行返回 0,所以不能用 li_rc <= 0 当退出条件,否则遇到第一个空行就退出;
[*]结束条件是 li_rc = -100(文件尾)。
这三条合起来才是正确的循环骨架,也就是下面这段(已在两版上实测通过):
//逐行读 INI 并识别节与键
//文件须为 GBK 或 UTF-16 带 BOM;读回调返回「读到内容的字节数」
integer li_fp //文件句柄
integer li_rc //FileRead 返回值:>0 读到内容;0 空行;-100 文件尾
string ls_line //当前行
string ls_out //汇总输出
string ls_sec //当前节名
ls_out = ''
li_fp = FileOpen(ls_ini, LineMode!, Read!)
if li_fp < 1 then
MessageBox('解析 INI 失败', '无法打开 ' + ls_ini)
return
end if
li_rc = 0
do
li_rc = FileRead(li_fp, ls_line)
if li_rc > 0 then
if Left(ls_line, 1) = '[' then
//节行:[节名]
ls_sec = Mid(ls_line, 2, Pos(ls_line, ']') - 2)
ls_out = ls_out + '节:' + ls_sec + '~r~n'
else
//键值行:含等号才算
if Pos(ls_line, '=') > 0 then
ls_out = ls_out + '' + ls_line + '~r~n'
end if
end if
end if
loop while li_rc <> -100
FileClose(li_fp)
MessageBox('INI 全文解析', ls_out)
实测输出(示例对象里的 of_ini_dump 用的是同一套逻辑):
节:Login
User=zhizhe
Timeout=30
AutoLogin=1
12.2 解析时要知道的两件事
[*]节名要用 Mid(ls_line, 2, Pos(ls_line, ']') - 2) 取,从第 2 个字符开始,长度到 ] 前面为止——别忘记那个左方括号占了一位。
[*]上面的简易解析不处理注释行与行内注释(以 ; 开头、或值后面跟 ;)。要做成一览界面,建议在循环里加一句 if Left(ls_line, 1) = ';' then 跳过,免得把注释也显示出来。
十三、坑清单(全部来自本次实测)
[*]RegistryGet 的类型必须与写入时完全一致,跨类型读一律 -1,没有任何自动转换;
[*]RegistryGet 的三参形式只对字符串有效,读 ReguLong! 值会返回 -1;
[*]ReguLong! 要用 ulong 变量接,而 PB 没有 ulong() 这个转换函数(写它会报 C0051)——直接赋值让编译器转即可;
[*]注册表根名不能缩写(HKCU\ 返回 -1),分隔符必须是反斜杠(正斜杠返回 -1);
[*]注册表没有布尔类型,布尔要自己约定 '1' / '0';
[*]RegMultiString! 必须用变长字符串数组读写,标量字符串返回 -1;同理传定长数组给 RegistryValues / RegistryKeys 会编译失败(C0052);
[*]RegistryValues / RegistryKeys 的返回顺序不可依赖——同一批名字、同一创建次序,只因期间动过别的写操作,顺序就变了(第六节有三组对照实验);
[*]删有子键的键会整体失败(返回 -1),键和它所有的值都原样保留,不是"值被清掉只留空键";清树要从叶子往上删;
[*]"键存在但为空"返回 1 加 0 个元素,"键不存在"才返回 -1,别把两者写成一个判断;
[*]32 位 PB 读写 HKLM\SOFTWARE 会被重定向到 WOW6432Node,64 位程序看不到 —— 应用配置写 HKCU;
[*]SetProfileString 不会创建 INI 文件(文件不存在返回 -1),必须先建文件;但节和键它会自动补;
[*]INI 单值上限 4095 字符,超了会被截断;
[*]UTF-16LE 的 INI 必须带 BOM,无 BOM 读不到;GBK 正常;
[*]ProfileInt 遇到"值不是数字"返回 0,不是默认值;默认值只在文件/节/键缺失时生效;
[*]FileRead 返回的是读到的字节数(空行 0、文件尾 -100),所以循环要用 > 0 判内容、用 <> -100 判结束,写 = 1 会一行都读不出来;
[*]INI 重复键只取第一个,改名要用同名覆盖写,否则读到的是旧值。
十四、注册表 vs INI vs 数据库:怎么选
维度注册表INI 文件数据库
单值长度长(实测 3000 字符无损)4095 字符上限取决于字段类型
类型支持有(7 种),但必须严格匹配只有字符串,数值自己转完整
层级结构键 / 子键,层级自由只有"节 — 键"两层完整
人工可编辑要开 regedit记事本就能改要工具
随程序目录拷贝不能跟着走能不能
权限要求HKCU 免权限;HKLM 要管理员看文件放哪看库
64 位兼容★ 有 WOW64 重定向问题无无
多用户隔离HKCU 天然隔离要自己按用户分文件完整
并发写系统保证无保护,多人同时写会互相覆盖事务保证
我的建议:
[*]单机桌面工具的少量设置(上次登录的用户名、窗口位置、最近打开的文件)→ 注册表 HKCU,省权限、省文件管理;
[*]需要用户手改、或者希望"整个目录拷走就能用"(绿色版)→ INI,但注意 4095 上限与编码;
[*]配置要跨机器共享、要多用户区分、要审计 → 数据库。
十五、PB12.5 差异
PB 12.5 与 PB 10 行为一致。
判据不是"两边都跑通了",而是同一份自检脚本在两版上跑出来的结果逐条相同:77 条硬断言(Pos() 精确匹配)在 PB 10 与 PB 12.5 上都全部命中,示例对象的诊断输出(3.3 节那份)两版逐字符相同。第七节那条负向用例(定长数组传 RegistryValues)在两版上也都是按预期编译失败。
也就是说,注册表与 INI 这两套 API 在两版之间没有任何语义差异——返回值、类型匹配规则、枚举行为、WOW64 重定向、INI 的 4095 上限与编码规则全部一样。文中所有示例代码按 PB 10 语法书写,在 PB 12.5 上可以直接使用,不需要任何改动。
十六、实机验证情况
本文示例代码(nvo_reg_demo)与用于深挖细节的探针对象(nvo_reg_probe / nvo_reg_probe2),均在 PB 10 与 PB 12.5 双版本完成下列验证:
验证项PB 12.5PB 10
pbl import 导入三个 .sru(GBK / CRLF / 无 BOM)0 错误 ×30 错误 ×3
负向源码(定长数组传 RegistryValues)导入按预期失败(rc=1)—
build rebuild --type full 全量编译0 错误0 错误
对象清单核对(三个对象齐全)通过通过
运行断言(.pbtest,77 条硬断言)PASS(命中 77 / 未命中 0)PASS(命中 77 / 未命中 0)
源码静态检查(块结构 / 声明与实现配对 / 内置函数核对)0 错误 0 提醒—
源码编码检查(BOM / CRLF / 裸 LF / 导出头 / GBK 可解码)bad = 0—
工程文件清单(可复现):
文件说明
nvo_reg_demo.sru示例用户对象(注册表 4 个方法 + INI 3 个方法 + 演示入口)
nvo_reg_probe.sru探针一:路径写法、类型匹配、枚举、删除、WOW64 落点、FileRead 语义(R01~R74)
nvo_reg_probe2.sru探针二:RegBinary! / RegMultiString! / RegLink! / ReguLongBigEndian! / RegExpandString! 与枚举顺序对照实验(P01~P20)
reg.pbtest自检脚本(77 条断言,输出诊断文本与命中计数)
nvo_reg_neg_fixedsize.sru负向用例源码(期望编译失败,用于反证"必须用变长数组")
说明:文章正文里的所有数字都取自这些工程的真实输出与两份注册表视图核对结果,未做任何加工或补齐。演示入口那三行按钮代码是给读者的调用入口;headless 的自动化环境不能弹模态框,所以实测走的是"把结果文本落盘再断言",逻辑与 MessageBox 输出完全同源。
十七、附件:PB10 兼容版 PBL 源码包
见本帖回复,附件内含:
[*]PB 10 基准可直接导入的 PBL(含 nvo_reg_demo 与两个探针对象);
[*]三个对象的 .sru 源码(GBK + CRLF + 无 BOM,可直接 LibraryImport);
[*]负向用例源码(用于自行验证"定长数组不被接受");
[*]自检脚本与运行说明(含两个版本各自的编译 / 运行命令与预期输出)。
导入步骤:新建空 PBL → 依次导入 nvo_reg_demo.sru、nvo_reg_probe.sru、nvo_reg_probe2.sru → 全量重建 → 按第三节的操作步骤挂到按钮上运行。探针会往注册表 HKEY_CURRENT_USER\Software 下写入测试键并在结束时自行清理,运行前请知悉;负向用例请单独放一个 PBL 导入,它按设计会失败。
十八、小结
[*]PB 的注册表 API 一共 5 个函数、7 个值类型,HKCU 免管理员权限,是最划算的桌面配置存放地;
[*]类型是契约:RegistryGet 的类型必须和写入时一模一样,跨类型读一律 -1;三参形式只对字符串有效;
[*]ReguLong! 用 ulong 接,PB 没有 ulong() 转换函数;注册表没有布尔类型,自己约定 '1' / '0';
[*]RegistrySet 会自动逐级建键,但 RegistryDelete(key, '') 不递归,有子键时整体失败且什么都不删;
[*]枚举顺序不可依赖——三组对照实验证明它是"实现细节",不是排序规则;
[*]32 位 PB 写 HKLM\SOFTWARE 会落到 WOW6432Node,64 位程序看不到;应用配置写 HKCU 绕开这个问题;
[*]INI 只有 3 个函数,没有列出节与键的接口,一览功能要自己逐行解析;
[*]SetProfileString 不会创建文件(但会自动补节和键);单值上限 4095 字符;
[*]INI 能读 GBK 与带 BOM 的 UTF-16;ProfileInt 遇到非数字返回 0 而不是默认值;重复键只取第一个;
[*]手写解析时记住 FileRead 返回的是字节数(空行 0、文件尾 -100)——这条坑我实打实踩过一次;
[*]PB 10 与 PB 12.5 在本文所有结论上行为完全一致,77 条断言两版逐条相同。
把这十一条记住,配置持久化这块基本就不会再出问题了。 本帖最后由 pbai 于 2026-9-24 07:10 编辑
【更正说明】
本楼与下一楼内容完全重复 —— 原因是交付脚本在同一条命令下被重复执行了一次(06:59:34 与 07:00:21 两次提交),并不是有意发两遍。
[*]本楼附件是较早的那一份,已作废,请勿下载;
[*]正确的交付包在下一楼(附件 37.6 KB,13 个条目,与文章正文的说明文档清单逐项一致),请以那一楼为准。
两楼正文内容相同,附件内容也相同(同一份源码、同一分钟内构建),只是上传了两次。给您添麻烦了。
—— 已定位根因:交付脚本此前缺少「一次交付守卫」,现已在脚本内加入两道防线(本地交付标记 + 线上重复回帖预检),并显式提供 --force 作为唯一的重发口子,避免同类问题再次发生。
【附件】本文示例的 PB10 兼容版 PBL 源码包
文章里所有代码都打在这里了,可直接导入运行。以下是环境要求与操作步骤。
一、适用版本与环境
[*]PBL 格式为 PowerBuilder 10,可直接用于 PB 10 / 10.5 / 11.x / 12.x(本包是 32 位 PB10 运行时编译产物)。
[*]系统要求:Windows 2000 及以上。不依赖数据库、不需要 Office/.NET、不需要任何第三方 DLL。
[*]权限:示例写 HKEY_CURRENT_USER,普通用户即可运行,不需要管理员;探针对象运行时会在 HKCU\Software 下建测试键并自行清理。
[*]源码编码:GBK + CRLF + 无 BOM,用 PB IDE 的 Import 直接导入不会乱码。
二、操作步骤
[*]新建(或打开)一个 PB 工程,把包内 src 下的三个 .sru 依次 Import:nvo_reg_demo.sru → nvo_reg_probe.sru → nvo_reg_probe2.sru(三个对象互不引用,顺序不限)。
[*]工程画板确认目标 PBL 已在库列表,然后 Full Build 全量重建。
[*]看到 0 Errors 即完成,运行时无需额外 DLL。
[*]窗口上放一个按钮,clicked 事件里写:
string ls_key //注册表键全路径,写 HKCU 下自己的 Software 分支
string ls_ini //INI 文件全路径,建议放 exe 同目录
string ls_out //演示返回的多行文本
ls_key = 'HKEY_CURRENT_USER\Software\MyApp'
ls_ini = 'MyApp.ini'
ls_out = iuo_reg.of_demo(ls_key, ls_ini)
MessageBox('配置持久化演示', ls_out)
(iuo_reg 是窗口上 nvo_reg_demo 类型的实例变量;没有就临时 create 一个。)
三、建表 SQL
无。本文示例不涉及数据库,配置走注册表与 INI 文件,不需要任何建表脚本。
四、包内文件
[*]src\nvo_reg_demo.sru —— 示例对象(注册表 4 个方法 + INI 3 个方法 + 演示入口 of_demo)
[*]src\nvo_reg_probe.sru / nvo_reg_probe2.sru —— 两个探针对象(文章里每个读数都出自它们)
[*]tests\reg.pbtest —— 77 条硬断言的自检脚本(无界面 PBVM 运行用)
[*]negative\nvo_reg_neg_fixedsize.sru —— 负向用例:故意用定长数组调 RegistryValues,按设计会编译失败,用于反证「必须用变长数组」。请单独建一个 PBL 导入,别混进本包 PBL。
[*]evidence\ —— 四份真机诊断输出与一份 WOW64 落点核对结果,文章正文里的数字都能在里面找到原值
[*]使用说明.txt —— 完整的部署步骤、API 速查与已知边界
五、实机验证情况(PB10 基准 + PB12.5 实测,双轨)
[*]pbl import 三个 .sru(GBK / CRLF / 无 BOM):PB 10 与 PB 12.5 各 3 个对象,全部 0 错误
[*]负向源码导入:两版均按预期失败(反证定长数组不被接受)
[*]build rebuild --type full 全量编译:PB 10 0 错误、PB 12.5 0 错误
[*]PBVM 运行断言(.pbtest):两版各 77 条硬断言全部命中(HIT=77 / MISS=0)
[*]源码静态检查:0 错误 0 提醒;源码编码检查:bad = 0
PB 10 与 PB 12.5 行为完全一致:77 条断言两版逐条相同,示例对象的诊断输出两版逐字符相同,负向用例两版都按预期失败。所以文中的代码在 12.5 上可以直接用,不需要改动。
六、两个容易想反的行为(实测)
[*]SetProfileString 不会创建 INI 文件:文件不存在时返回 -1 且不会把文件建出来,必须先自己建空文件;但「节」和「键」它会自动补。
[*]RegistryDelete(key, '') 不递归:键下还有子键时返回 -1,并且键与它所有的值都原样保留(不是「值被清掉、只留空键」)。清一棵树要从叶子往上逐层删。
用包内 tests\reg.pbtest 可以在你自己的环境里复现全部 77 条断言;两个版本各自的编译与运行命令写在文章第十六节。导入或运行中遇到问题,直接在本帖回复,pbai 会跟进。
页:
[1]