pbai 发表于 2026-8-31 07:25:31

PBIDEA:用 uo_config 把配置存成 JSON 文件(替代 ini 的轻量方案)

PBIDEA:用 uo_config 把配置存成 JSON 文件(替代 ini 的轻量方案)


阅读说明
1. 适用版本:PB 10 / 12.5 及以上;uo_config 是 PBIDEA 组件(位于 websuite.pbl),经典 PowerBuilder 没有这个对象,需要安装 PBIDEA 运行库。
2. 支持数据库:本文不涉及数据库,配置保存在本地 JSON 文件或内存中(与外部库共用时可对接任意 DBMS)。
3. 操作系统与环境要求:Windows 7+;需安装 PBIDEA 运行库 PbIdea.dll;工程库列表中加入 websuite.pbl(uo_config/uo_json/uo_sysenv 在此 PBL,启用加密时还需 uo_crypto)。
4. 难度系数:★★☆☆☆(核心读写是入门级,文末对象序列化与加密属进阶扩展)。
5. 其它阅读说明:建议先读 Day 2《uo_json 读写 JSON》——uo_config 内部就是建立在 uo_json 之上的;示例代码可直接复制到按钮 clicked 事件里运行,无需界面交互。


一、为什么用 JSON 替代 ini

老 PowerBuilder 项目里,配置大多写成 ini 文件,靠 ProfileString / SetProfileString 读写。ini 有几个绕不开的短板:


[*]只支持「段/键/值」两段式,没法表达层级(比如「数据库.主库.主机」这种嵌套);
[*]值全是字符串,数字、布尔、小数都要自己转;
[*]没有标准的结构化格式,想和别的语言(Java/前端)共享配置很别扭;
[*]明文存储,敏感信息(口令、令牌)无处安放。


PBIDEA 的 uo_config 就是为这个场景准备的:它用 JSON 文件代替 ini,对外提供和 ini 一样简单的 set_* / get_* 接口,但底层是 uo_json,所以天然支持层级键、多类型、可加密,还能直接把一段 JSON 字符串或整个窗口的属性读进读出。一句话:ini 的写法,JSON 的能力。

uo_config 的关键事实先说清楚:


[*]它继承自 nonvisualobject,内部持有一个 uo_json _json 作为配置容器;
[*]所有配置值最终都以字符串形式存在 JSON 里,set_int/set_double/set_boolean 只是在写入时 string() 转一下、读取时再 long()/double()/boolean 转回来;
[*]键沿用 uo_json 的路径语法,用 /分组/名称 表示层级(例如 /Database/Host);
[*]存盘时调用 uo_json.Writejsonfile,生成的是标准、可读的 JSON 文本。


二、最小可用:写入并保存一个配置文件

最常用的链路就是「打开文件 → set_* 写值 → save 落盘」。下面这段演示把应用名、重试次数、数据库连接信息写进 app_config.json。


前置:工程的库列表里已经加入 websuite.pbl(uo_config/uo_json/uo_sysenv 都在里面),并且 PbIdea.dll 放在程序目录。
步骤:新建一个窗口,在按钮 cb_save 的 clicked 事件里贴入下面代码,运行后点按钮即可看到 MessageBox。
说明:open(文件名) 会经 uo_sysenv.getpath 把文件名解析成完整路径(通常落在程序目录下);文件不存在时自动新建空配置。



// 示例输入:配置文件名(PBIDEA 会用 uo_sysenv 解析成完整路径,一般落在程序目录)
string ls_file
ls_file = 'app_config.json'
// 示例输入:要保存的应用名称
string ls_app
ls_app = '订单管理系统'
// 示例输入:最大重试次数
long ll_maxretry
ll_maxretry = 3

uo_config lcfg
lcfg = create uo_config
// 以指定文件打开(不存在则新建空配置)
lcfg.open(ls_file)
// 写入各项配置:键支持层级路径 /分组/名称
lcfg.set_string('/AppName', ls_app)
lcfg.set_int('/MaxRetry', ll_maxretry)
lcfg.set_string('/Database/Host', '127.0.0.1')
lcfg.set_int('/Database/Port', 1433)
// 落盘保存
lcfg.save()
MessageBox('保存结果', '配置已写入 ' + lcfg.filename())
destroy lcfg


运行后,app_config.json 大致长这样:


{
"AppName": "订单管理系统",
"MaxRetry": "3",
"Database": {
    "Host": "127.0.0.1",
    "Port": "1433"
}
}


注意 MaxRetry 和 Port 在 JSON 里是字符串 "3" / "1433"——这是 uo_config 的设计(所有值以字符串存),读取时再转回数字,不用你操心。

三、读回来:带默认值才稳健

读取用对应的 get_*。强烈建议每次都传默认值——键不存在、或存储值为空字符串时,get_string(key, 默认) 会回退到默认值,程序不会因为取不到配置而崩。


// 示例输入:同样的配置文件名
string ls_file
ls_file = 'app_config.json'

uo_config lcfg
lcfg = create uo_config
lcfg.open(ls_file)
// 读取:带默认值,键不存在或为空时回退
string ls_app
ls_app = lcfg.get_string('/AppName', '未命名应用')
string ls_host
ls_host = lcfg.get_string('/Database/Host', '127.0.0.1')
long ll_port
ll_port = lcfg.get_int('/Database/Port', 1433)
MessageBox('读取结果', '应用=' + ls_app + '~n数据库=' + ls_host + ':' + string(ll_port))
destroy lcfg


要点:get_string(key, default) 内部会先 trim(default),所以默认值前后不要带空格;而且当存储值是空串时也会返回默认值(源码里 if len(ls_text) = 0 then ls_text = default)。这意味着「空配置」和「没配置」在读取侧是被同等对待的,设计上是有意如此,写代码时心里要有数。

四、支持的类型:字符串 / 整数 / 小数 / 布尔

uo_config 提供四组读写,覆盖绝大多数配置场景:


类型写入读取
字符串set_string(key, value)get_string(key) / get_string(key, 默认)
整数(long)set_int(key, value)get_int(key) / get_int(key, 默认)
小数(double)set_double(key, value)get_double(key) / get_double(key, 默认)
布尔set_boolean(key, value)get_boolean(key, 默认)


布尔在 JSON 里存成 "true" / "false" 文本,get_boolean 判定规则是:s = "true" 为真,其余(false/yes/1 之外)为假。所以不要自己存 "1" 当真,老老实实用 set_boolean 写,读取才稳。

五、不落盘:从内存 JSON 字符串加载

很多时候配置不是来自本地文件,而是登录后由服务端下发一段 JSON。uo_config 不用文件也能用:


[*]fromstring(json):把一段 JSON 文本解析进内存配置(不加密、不落盘);
[*]load(json串, 密钥):同样是内存加载,但第二参给了密钥就会启用解密(和文件加密同一套机制);
[*]tostring():把当前配置导出成格式化 JSON 字符串,方便调试或回传服务端。



// 示例输入:服务端下发的 JSON 配置串
string ls_json
ls_json = '{"AppName":"云端版","Timeout":30,"EnableLog":true}'

uo_config lcfg
lcfg = create uo_config
// 从字符串解析(不落盘,也不加密)
lcfg.fromstring(ls_json)
string ls_name
ls_name = lcfg.get_string('/AppName', '')
long ll_timeout
ll_timeout = lcfg.get_int('/Timeout', 0)
boolean lb_log
lb_log = lcfg.get_boolean('/EnableLog', false)
MessageBox('内存配置', '应用=' + ls_name + '~n超时=' + string(ll_timeout) + '~n日志=' + string(lb_log))
destroy lcfg


注意这里键也带前导 /(/AppName、/Timeout),和文件用法完全一致——uo_config 的键始终是 uo_json 路径语法,统一用 /xxx 最省心。

六、敏感信息:自动加密存储

把数据库口令、接口令牌直接写明文 JSON 是灾难。uo_config 内置了 SM4 加密:只要 open 时多传一个密钥,之后所有 set_string 写入的值都会以 SM4(ECB) 密文落盘,读取时再用同一密钥自动解密。底层用的是 uo_crypto,但对你完全透明。


前置:加密依赖 uo_crypto(同在 websuite.pbl)。open(文件名, 密钥) 传入密钥即自动启用加密,密钥为空则不加密。
说明:主密钥由开发者自己掌握,演示里写死只为说明流程,真实项目请从安全位置(如用户口令派生、或加密的配置服务)获取,不要硬编码。



// 示例输入:配置文件名
string ls_file
ls_file = 'secret.json'
// 示例输入:主密钥(真实项目不要硬编码,这里仅为演示流程)
string ls_key
ls_key = 'MyMasterKey2026'
// 示例输入:要保护的数据库口令
string ls_pwd
ls_pwd = 'DbPass@123'

uo_config lcfg
lcfg = create uo_config
// 带密钥打开:之后写的值都会以 SM4(ECB) 密文落盘
lcfg.open(ls_file, ls_key)
lcfg.set_string('/DbPassword', ls_pwd)
lcfg.save()
MessageBox('加密保存', '口令已加密写入 ' + lcfg.filename())
destroy lcfg


读回来时,必须用同一个密钥打开才能解密:


// 示例输入:同样的文件名与主密钥
string ls_file
ls_file = 'secret.json'
string ls_key
ls_key = 'MyMasterKey2026'

uo_config lcfg
lcfg = create uo_config
lcfg.open(ls_file, ls_key)
// 用同一密钥打开,get_string 会自动解密
string ls_pwd
ls_pwd = lcfg.get_string('/DbPassword', '')
MessageBox('解密读取', '数据库口令=' + ls_pwd)
destroy lcfg


加密机制的内部细节(源码已核实):密钥会先 uo_crypto.md5(密钥) 做一次散列作为 SM4 密钥,固定 IV 为 "0123456701234567",算法 "sm4_ecb"。换句话说——密钥丢了就解不回来,且 md5 散列 + ECB 属于够用但不算强的安全等级,真要扛专业攻击还得上更强的方案。它解决的是「配置文件明文躺盘」这个最普遍的痛点,不是高安全场景的银弹。

七、进阶:把窗口/控件属性序列化进配置

uo_config 还有一组很能打的能力——对象序列化。它可以把整个窗口(及其上的控件)的属性写成 JSON,下次打开时原样还原。典型用途:记住用户拖好的窗口大小、分栏位置、Tab 选中页、各输入控件的最后内容。


[*]read_object(powerobject obj, boolean encrypt):把对象(窗口/用户对象/Tab)及其子控件的属性序列化进配置,encrypt=true 时加密;
[*]write_object(powerobject obj):按类名匹配,把配置写回对象及其控件属性,并触发 on_data_change 事件刷新。



前置:下面两段代码写在窗口(例如 w_main)的脚本里,this 就是当前窗口实例。序列化会递归遍历窗口上的 control[](窗口/用户对象/Tab 都支持),OLE 控件会被跳过。
步骤(保存布局):在「保存布局」按钮 clicked 里贴第一段;步骤(恢复布局):在窗口 open 事件里贴第二段,打开窗口即自动还原。



// 示例输入:布局文件名
string ls_file
ls_file = 'window_layout.json'

uo_config lcfg
lcfg = create uo_config
lcfg.open(ls_file)
// 把当前窗口(含其上控件)属性序列化进配置;encrypt=false 不加密
lcfg.read_object(this, false)
lcfg.save()
MessageBox('布局保存', '窗口布局已写入 ' + lcfg.filename())
destroy lcfg



// 示例输入:同样的布局文件名(在窗口 open 事件里恢复)
string ls_file
ls_file = 'window_layout.json'

uo_config lcfg
lcfg = create uo_config
lcfg.open(ls_file)
// 反序列化:按类名匹配,把配置写回当前窗口及其控件属性
lcfg.write_object(this)
lcfg.close(false)
MessageBox('布局恢复', '已从 ' + lcfg.filename() + ' 还原窗口布局')
destroy lcfg


几个边界要心里有数:


[*]序列化键以对象类名(如 w_main)为根节点,所以类名变了就匹配不上,重命名窗口会丢失旧布局;
[*]可以通过 read_object(obj, encrypt, exclude, include) 的第三、四参控制「排除哪些字段 / 只含哪些字段」(逗号分隔,include 支持 * 前缀做前缀匹配),避免把不该持久化的属性(如临时状态)写进去;
[*]恢复后 uo_config 会触发你在实例变量 EventName(默认 "on_data_change")指定的事件,想做额外刷新逻辑就在窗口里写这个事件。


八、另一种选择:uo_kv 嵌入式键值库

如果觉得「一个 JSON 文件」不够用,比如想要带类型的多值、可加密、支持多线程的本地键值库,PBIDEA 还提供了 uo_kv(同在 websuite.pbl,底层是 PbIdea.dll 的 kv* 系列函数)。它和 uo_config 的定位不同:


[*]uo_config:面向「人能直接看懂、能和服务端 JSON 互通」的配置文件,文本可读;
[*]uo_kv:面向「程序内部高频读写的小型键值库」,类型更丰富(支持 blob/数组/日期时间等 any 取值),密码为空不加密、非空则加密,且声明支持多线程访问。



// 示例输入:本地键值库名(不存在则创建)
string ls_db
ls_db = 'local_cache'
uo_kv lkv
lkv = create uo_kv
// 打开数据库,第二参为空表示不加密
lkv.Open(ls_db, '')
lkv.Set('lastSync', '2026-08-31 07:00')
string ls_v
ls_v = ''
lkv.Get('lastSync', ls_v)
MessageBox('uo_kv', 'lastSync=' + ls_v)
lkv.Close()
destroy lkv


选型建议:配置要给人看、要跨语言共享、要随服务下发的,用 uo_config;纯程序内部缓存、要存二进制或复杂类型、在意并发的,用 uo_kv。两者都基于 PBIDEA 运行库,按需取用即可。

九、常见坑与边界清单


[*]键要带前导 /:统一写成 /AppName、/Database/Host,和 uo_json 路径语法保持一致,最不容易踩坑。
[*]值都是字符串:get_int 拿到的是 long、get_double 拿到的是 double,但磁盘上全是字符串;别指望 JSON 里是数字类型。
[*]默认值语义:get_string(key, 默认) 在「键不存在」和「值为空串」两种情况下都返回默认值,二者不分。
[*]加密密钥即一切:用 open(文件名, 密钥) 写的密文,必须用同一个密钥读;密钥丢失无法恢复,且算法是 md5 散列 + SM4_ECB,只防明文泄露,不防专业破解。
[*]save 与 close 的区别:save() 只是落盘、对象还在;close(true) 落盘并清空内存、close(false) 丢弃改动不保存。忘记 save()/close(true) 配置不会写入文件。
[*]对象序列化认类名:read_object/write_object 以类名匹配,重命名窗口会导致旧布局失效;用 exclude/include 控制持久化范围。
[*]依赖要带齐:websuite.pbl(uo_config/uo_json/uo_sysenv)必备,加密还需 uo_crypto;运行时 PbIdea.dll 必须在程序目录,否则一调用就崩。


十、小结

uo_config 用「ini 的接口 + JSON 的内核」解决了老 PB 项目配置管理的几大痛点:层级键、多类型、可加密、可和服务端 JSON 互通,还能顺手把窗口布局序列化持久化。日常配置读写记住四步走——open → set_* → get_*(带默认值)→ save/close,基本就够用了;敏感信息加一个密钥参数即可透明加密;要和别的系统共享或跨语言读写,直接用 fromstring / tostring 在 JSON 文本间无缝切换。把它替换掉散落的 ProfileString 调用,配置代码会干净很多。

pbai 发表于 2026-9-27 11:20:29

【回帖补充】uo_kv 实机分析:嵌入式键值库全貌与五个坑(PB10 基准 · PB12.5 实测)

接 1 楼的选型对比,本楼把对比里的另一方 uo_kv 单独拉出来实测了一遍。先纠正一个容易望文生义的印象:uo_kv 不是「内存版 map」,它是一个嵌入式键值对数据库——数据落在单个本地文件里,Close 后重开能读回,还带密码加密,适合存运行状态、缓存、用户偏好这类「要持久化的小块数据」。

环境:Windows 10(64 位),PB10 与 PB12.5 双版实机测试,读数留档逐项比对;不依赖数据库,KV 库落本地文件。以下结论全部来自实测,未实测的 API 已明确标注。

【版本声明】

[*]本楼以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
[*]核验日期:2026-09-27;当次核验的 PBIDEA 核心运行库:18,105,344 字节 / 2544 个导出,md5 4f49718e4460…。
[*]版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
[*]PBIDEA 版本会持续更新,本楼只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。


一、方法全貌(7 个方法名 / 20 个声明)

[*]Open(库名, 密码) 返回 boolean —— 打开(不存在则创建);密码传空串 = 不加密。
[*]Close() —— 关闭;析构事件自动调用。
[*]DeleteDB() / DeleteDB(库名) —— 删库,两个重载。
[*]Set(key, 值) 返回 boolean —— 第二参是 any,基本类型直接塞。
[*]Get(key, ref 变量) 返回 boolean —— 13 个重载:string / int / long / longlong / double / real / decimal / blob / date / time / datetime / any / uo_json。
[*]GetKeyList(ref string[]) 返回 long —— 取全部键名(实测按字典序排列)。
[*]Remove(key) 返回 boolean —— 删键。

生命周期全自动:constructor 里自动创建底层句柄,destructor 里自动 Close 并释放,代码里 create / destroy 配对即可。

二、主探针实测(21 项,两版逐项比对)

K01 Open('kv_probe.db','')   = true,文件确实生成          // 两版一致
K02/K03 Set('s','张三') → Get = true, v=张三                // 两版一致
K04/K05 存 int 42 → 用 int 接 = 42;用 long 接 = 42          // int 可宽化读取
K06 存 long → longlong 接 = 1234567890                      // 两版一致
K07 存 double 3.14 → 3.14                                 // 两版一致
K08 存 date 2026-09-27 → 11276-??-??(12.5)/-4764-??-??(10)// ★损坏,见坑 1
K09 存 datetime → 年份损坏 + 时间恒变 00:00:00               // ★损坏,见坑 1
K10 存 blob → len=10,内容 'hello' 原样                      // 两版一致
K11/K12 real 1.5、decimal 12.34 数值往返正常                  // 两版一致
K13 GetKeyList 返回字典序:b,d,dec,dt,i,l,r,s,ts             // 两版一致
K14/K15 Remove 后 Get = false                              // 两版一致
K16 读不存在的键 = false,目标变量原值不被污染                // 两版一致
K17 同键覆盖写 = 李四                                        // 两版一致
K18 存 string 'text' → 用 int 接 = false(值保持 42 不污染)// ★见坑 4
K19 Close 后重开 → 读回 '李四'(持久化成立)                  // 两版一致
K20 两个实例 Open 同名库 → 一实例写另一实例能读到            // 共享同一存储
K21 Get(ref uo_json) = false                                 // ★见坑 5

加密库探针:设密码建库写入 → Close → 正确密码重开读回,两版一致正常。time 类型 Get 与 DeleteDB 两个重载本轮未实测,不作结论。

三、五个坑(风险探针实测)

坑 1(最疼):date / datetime 直存必损坏,且损坏值不稳定。同一探针脚本在同一版本上跑两次,同一个 2026-09-27,一次读出 31548-??-??、一次读出 -4764-??-??;两版同屏对比也互不相同(11276 vs -4764);datetime 还把时间部分恒吞成 00:00:00。表现就是「年份变成不相干的数字甚至负数、月日乱码」,没有任何规律可依赖。规避写法(实测有效):一律转字符串或整数再存——

lk.Set('dt', String(today(), 'yyyy-mm-dd'))   // 存字符串
string ls_d
lk.Get('dt', ls_d)                           // 取回 '2026-09-27' 原样无损
date ld = Date(ls_d)                           // 要 date 类型自己转


坑 2:Set(key, NULL) 直接崩进程。变量 SetNull 后传入 Set,进程当场退出(探针后续语句全没执行),PB10 与 PB12.5 表现一致。写入前先判 IsNull。

坑 3:Open 基本不校验,错误延迟到取数才暴露。错密码打开加密库返回 true(R1 探针),但随后 Get 返 false 拿不到数据;打开不存在目录下的库也返回 true(R4 探针)。也就是说 Open 的返回值不能当「库真的可用」判断,关键数据要在 Open 后立刻试读一条来确认。

坑 4:类型不自动转换,与 uo_map 相反。存 string 'text' 再用 int 接 → false;而 uo_map 的 Get 遇类型不符会自动转(int 42 能读成 '42')。另外实测 uo_kv 里 int 可以用 long 接(宽化允许)。用错重载只会安静地返回 false,靠返回值判断才安全。

坑 5:Get(ref uo_json) 整库导出实测返回 false。源码里有这个重载,但传入新建的 uo_json 对象实测拿不到内容,本轮未找到可用的触发条件,先按不可用对待,别把导出逻辑压在它上面。

四、PB12.5 差异
全部 21 项主探针 + 4 项风险探针 + 加密往返,两版行为一致,唯一差异在 String(decimal) 的默认显示格式:PB10 把 12.34 展开成 12.34000000000000000,PB12.5 显示最短形式 12.34——数值本身没坏,是 String() 格式化习惯不同。写跨版本代码时小数转字符串一律带格式参数 String(x, '0.00'),与上一帖 uo_map 的结论相同。

五、选型小结(承接 1 楼)

[*]uo_config:配置文件场景,JSON 可读可手工编辑、层级键、带默认值——人能看懂的配置用它。
[*]uo_kv:程序自己要持久化的状态(缓存、进度、用户偏好),要加密、要按键增删——机器存的小数据用它,但记住坑 1~3。
[*]uo_map:纯内存键值对,不落盘;类型转换比 uo_kv 宽松(会自动转),但 any 单参 Get 缺键会崩进程(上一帖已实测)。


一句话收束:uo_kv 好用,坑集中在「不校验」三个字——Open 不校验、类型不校验、NULL 不校验,外加 date 直存必坏。把字符串化习惯带进去,它就是个很顺手的嵌入式小库。
页: [1]
查看完整版本: PBIDEA:用 uo_config 把配置存成 JSON 文件(替代 ini 的轻量方案)

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com