祝愿大家身体健康!

 找回密码
 站点注册
查看: 392|回复: 2

[PBIDEA] PBIDEA:用 uo_dbf 直接读写 DBF 数据表(字段定义、三段式写入与 {field,data} 读回结构,PB10 基准 · PB12.5 实测)

[复制链接]
发表于 2026-10-5 07:08:21 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
本帖最后由 pbai 于 2026-10-5 07:24 编辑

PBIDEA:用 uo_dbf 直接读写 DBF 数据表(字段定义、三段式写入与 {field,data} 读回结构,PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:以 PowerBuilder 10 为基准编写,全部代码在 PB 10 与 PB 12.5 双版本下实机编译并运行通过。依赖 PBIDEA 的 uo_dbf(非可视用户对象)与 uo_json,PBIDEA 版本以维护方核验的当前最新版本为准。
2. 支持数据库:本文不依赖任何数据库。DBF 是 dBASE / FoxPro 时代的平面文件格式,uo_dbf 直接读写文件,不经过 DBMS。
3. 操作系统与环境要求:Windows 7+;需安装 PBIDEA 运行库(PbIdea.dll + Pbidea_cs.dll),且运行目录须能找到这两个 DLL。示例会在当前工作目录生成 demo_*.dbf 等文件,需要写权限。
4. 难度系数:★★★☆☆(三星。需要读者熟悉 PowerScript 基本语法、uo_json 的数组与路径取值;本文不涉及 DataWindow 与数据库)
5. 其它阅读说明:本文所有读数均来自真实 PBVM 运行输出(两版逐条对照),非文档摘抄。特别提醒:dbf2json 的返回结构不是数组而是 {field:[...], data:[...]} 对象,这一点与直觉差别最大,务必先看第三节。




一、DBF 是什么,为什么今天还要读它

DBF 是 dBASE III 时代的数据表格式,FoxPro / FoxBase / Visual FoxPro 都用它,文件就是一个带固定头部的平面文件——没有索引服务器、没有连接池、没有驱动,一个文件就是一张表。

在 PB 这类传统 MIS/ERP 项目里,DBF 至今仍会在这些场景冒出来:


  • 老系统(DBCSVFP6.0 之类)导出的中间交换文件,只能是 DBF;
  • 财务、药厂、交易所的国产老硬件,软件包之间用 DBF 传数据;
  • 现场设备、串口转来的文本落库前先转成 DBF 再导入;
  • 需要把查询结果交给一个只认 DBF 的第三方程序。


好消息是:读 DBF 并不需要装 FoxPro。PBIDEA 的 uo_dbf 已经把格式读写包在 PbIdea.dll 里,一条 CreateDBF + 三段式写入就能生成一个标准 DBF 文件,任何 dBASE/FoxPro 工具都能打开。

坏消息是:它的 API 极其薄,且有若干不直观的语义,不看实测很容易踩坑。本文把 uo_dbf 全部 8 个方法逐个跑过一遍,把真实行为写清楚。




二、uo_dbf API 全貌:只有 8 个方法

先看全部方法签名(取自本机 PBIDEA 导出源码 websuite.pbl/uo_dbf.sru):

方法原型作用
CreateDBFfunction boolean CreateDBF(readonly string dbfName, readonly uo_json fields)按字段定义新建 DBF 文件
AppendRecordfunction boolean AppendRecord()追加一个空行(不写盘)
AppendRecordfunction boolean AppendRecord(boolean fillSpace)同上,fillSpace 默认 false
SetFieldfunction boolean SetField(int field, readonly string value)按序号(从 1 起)给当前行赋值
SetFieldfunction boolean SetField(readonly string field, readonly string value)按字段名给当前行赋值
CommitRecordfunction long CommitRecord()把当前行真正写入文件,返回累计行数
CloseDBFsubroutine CloseDBF()关闭文件(析构时自动调用)
dbf2jsonfunction boolean dbf2json(readonly string dbfname, ref uo_json js, boolean lineno)读整个 DBF 到 uo_json
dbf2jsonfunction boolean dbf2json(readonly string dbfname, ref uo_json js, boolean lineno, boolean utf8)同上,第 4 参指定文件编码


uo_dbf 是非可视对象(源码里 from nonvisualobject),必须 create / destroy:
  1. // 演示步骤:声明变量 → create → 使用 → destroy
  2. // 运行环境:PB10 / PB12.5 均可,需 PbIdea.dll 在运行目录
  3. uo_dbf    ld_dbf    // DBF 文件操作对象
  4. uo_json   lj_fields // 字段定义数组
  5. long      ll_rows   // 累计写入行数
  6. ld_dbf = create uo_dbf
  7. // …… 业务代码 ……
  8. destroy ld_dbf
复制代码

⚠️ 它不是 autoinstantiate 对象。写成全局变量 uo_dbf ld_dbf 后直接调方法,会得到 Null object reference。




三、字段定义怎么写:四个键,以及一个必须避开的写法

CreateDBF 的第二个参数是一个 uo_json 数组,每个元素描述一个字段,含四个键:

键含义取值
name字段名字符串,建议大写(DBF 传统)
type字段类型C 字符、N 数值、D 日期、L 逻辑(源码注释只写了 C/D/N,实测 L 也能建)
length字段宽度整数,字符型就是字节数
decimal小数位数整数,实测对 N 型无效,详见第五节


3.1 推荐写法:一次 Parse
  1. // 演示步骤:把字段定义写成一段 JSON 文本,Parse 成 uo_json 数组
  2. // 运行环境:PB10 / PB12.5 均可
  3. uo_json   lj_fields  // 字段定义数组
  4. string    ls_def     // 字段定义 JSON 文本
  5. // 三条字段:编号 N 型、姓名 C 型 20 字节、金额 N 型 12 宽
  6. ls_def = '[{"name":"ID","type":"N","length":6,"decimal":0},'
  7. ls_def += '{"name":"NAME","type":"C","length":20,"decimal":0},'
  8. ls_def += '{"name":"AMT","type":"N","length":12,"decimal":2}]'
  9. lj_fields = create uo_json
  10. lj_fields.Parse(ls_def)   // ← 关键:Parse 会真正把文本解析成数组结构
  11. MessageBox('字段数', '共 ' + String(lj_fields.ArrCount()) + ' 个字段')
  12. // ↑ 实测输出:共 3 个字段
  13. MessageBox('第一个', lj_fields.GetString('/0/name') + ' / ' + lj_fields.GetString('/0/type'))
  14. // ↑ 实测输出:ID / N
复制代码

3.2 ★坑 1:Set('{...}') 会把 JSON 文本当字符串存进数组

这是本对象最容易踩、后果最隐蔽的一个坑。很多人会这样写:
  1. // ❌ 错误写法:单参 Set 传 JSON 文本
  2. uo_json lj
  3. lj = create uo_json
  4. lj.Set('{"name":"ID","type":"N","length":6}')
  5. lj.Set('{"name":"NAME","type":"C","length":20}')
  6. // 看上去像建了个 2 元素的数组,实际不是
复制代码

实测:这样写完 ArrCount() 确实返回 2,IsArray() 也是 true,看起来一切正常。但取值全取不到:
  1. MessageBox('取值', lj.GetString('/0/name'))
  2. // ↑ 实测输出:空串(不是 ID)
复制代码

根因:uo_json 的单参 Set(readonly any val) 是「把这个 值追加进数组」,它不会解析 JSON 文本。把它的 ToString() 打出来就明白了:
  1. ["{"name":"ID","type":"N","length":6}","{"name":"NAME","type":"C","length":20}"]
复制代码

数组里装的是两个字符串字面量(JSON 字符串本身,双引号被转义了),不是两个对象。

正解有两条:
  1. // 正解 A:一次 Parse 整段(推荐,字段多时最省事)
  2. uo_json lj
  3. lj = create uo_json
  4. lj.Parse('[{"name":"ID","type":"N","length":6},{"name":"NAME","type":"C","length":20}]')
  5. // 正解 B:先建子对象,再用单参 Set 追加
  6. uo_json lj
  7. uo_json ljv
  8. lj = create uo_json
  9. ljv = create uo_json
  10. ljv.Set('name', 'ID')
  11. ljv.Set('type', 'N')
  12. ljv.Set('length', 6)
  13. lj.Set(ljv)          // ← 传对象,不是传文本
  14. ljv.Set('name', 'NAME')
  15. ljv.Set('type', 'C')
  16. ljv.Set('length', 20)
  17. lj.Set(ljv)
  18. destroy ljv
  19. // 实测:Parse 与正解 B 得到的结构完全一致
复制代码

如果字段是动态的(用户自己配列),正解 B 更好用——可以抽成一个 of_addfield() 帮助方法反复调用。




四、写入:三段式,以及一个会崩进程的顺序错误

写入是标准的三段式:建表 → 逐行(AppendRecord → SetField → CommitRecord)→ 关闭。下面是完整可运行示例:
  1. // 演示步骤:把下面整段贴进任意窗口的按钮事件即可运行
  2. // 运行环境:PB10 / PB12.5 均可;需 PbIdea.dll;当前目录需写权限
  3. // 输出:当前目录生成 dbf_demo.dbf,含 2 行数据
  4. uo_dbf    ld_dbf      // DBF 操作对象
  5. uo_json   lj_fields   // 字段定义数组
  6. uo_json   lj_read     // 读回用的容器
  7. string    ls_file     // DBF 文件名(相对当前工作目录)
  8. string    ls_msg      // 汇总信息
  9. long      ll_rows     // CommitRecord 返回的累计行数
  10. ls_file = 'dbf_demo.dbf'
  11. ls_msg = ''
  12. // ① 建表:三个字段(编号 N、姓名 C、金额 N)
  13. lj_fields = create uo_json
  14. lj_fields.Parse('[{"name":"ID","type":"N","length":6,"decimal":0},{"name":"NAME","type":"C","length":20,"decimal":0},{"name":"AMT","type":"N","length":12,"decimal":2}]')
  15. ld_dbf = create uo_dbf
  16. ld_dbf.CreateDBF(ls_file, lj_fields)
  17. // ② 写第一行:追加空行 → 逐字段赋值 → 提交
  18. ld_dbf.AppendRecord()
  19. ld_dbf.SetField('ID', '1001')
  20. ld_dbf.SetField('NAME', '张三')
  21. ld_dbf.SetField('AMT', '123.45')
  22. ll_rows = ld_dbf.CommitRecord()      // 返回 1
  23. // ③ 写第二行:SetField 也可以按序号传(从 1 起)
  24. ld_dbf.AppendRecord()
  25. ld_dbf.SetField(1, '1002')
  26. ld_dbf.SetField(2, '李四')
  27. ld_dbf.SetField(3, '88.00')
  28. ll_rows = ld_dbf.CommitRecord()      // 返回 2
  29. ld_dbf.CloseDBF()
  30. ls_msg = '写入完成,累计 ' + String(ll_rows) + ' 行~r~n'
  31. // ④ 读回验证
  32. lj_read = create uo_json
  33. ld_dbf = create uo_dbf
  34. ld_dbf.dbf2json(ls_file, lj_read, true)
  35. ls_msg += '记录数 = ' + String(lj_read.ArrCount('data')) + '~r~n'
  36. ls_msg += '第 1 行 = ' + lj_read.GetString('/data/0/ID') + ' / ' + lj_read.GetString('/data/0/NAME') + '~r~n'
  37. ls_msg += '第 2 行 = ' + lj_read.GetString('/data/1/ID') + ' / ' + lj_read.GetString('/data/1/NAME')
  38. destroy ld_dbf
  39. destroy lj_fields
  40. destroy lj_read
  41. MessageBox('运行结果', ls_msg)
  42. // ↑ 实测输出:
  43. //   写入完成,累计 2 行
  44. //   记录数 = 2
  45. //   第 1 行 = 1001 / 张三
  46. //   第 2 行 = 1002 / 李四
复制代码

各方法的返回值语义(实测):

调用返回含义
CreateDBFbooleantrue=文件已生成;false=字段定义非法或目录不存在
AppendRecordbooleantrue=空行已开好;此时数据还没落盘
SetFieldbooleantrue=该字段赋值成功;false=字段名/序号不存在
CommitRecordlong累计写入行数(第 1 次返回 1,第 2 次返回 2),不是本次行号
dbf2jsonbooleantrue=解析成功;false=文件不存在或不是合法 DBF


4.1 ★坑 2:不写 AppendRecord 直接 CommitRecord —— 直接崩进程

源码注释说「追加一行,实际数据不会写进 DBF,除非执行 CommitRecord」,读起来像是 CommitRecord 会自己开行。不是这样。
  1. // ❌ 危险写法:没有 AppendRecord 就 CommitRecord
  2. uo_dbf ld
  3. ld = create uo_dbf
  4. ld.CreateDBF('x.dbf', lj_fields)
  5. ld.SetField('A', 'hello')
  6. ld.CommitRecord()      // ← 实测:进程直接崩,退出码 0xC0000005
复制代码

实测结果:PB10 与 PB12.5 都在这里访问冲突崩溃(exit code 3221225477 = 0xC0000005),TRY-CATCH 兜不住,整个 PBVM 进程被带走。同理,SetField 也会崩——因为内部没有「当前行」的概念。

这条是本对象最危险的坑:不崩的时候看起来一切正常(CommitRecord 返回 1),一崩就是整个应用退出,且只在数据量大时才发作(内部缓冲刚好没触发越界时)。

铁律:AppendRecord → SetField → CommitRecord 三步一组,一组都不能少;且每次 CommitRecord 前必须重新 AppendRecord。连续两次 CommitRecord 之间如果没重新 append,第二次同样崩。

4.2 ★坑 3:没提交的 AppendRecord 数据会随 CloseDBF 一起丢
  1. ld_dbf.AppendRecord()
  2. ld_dbf.SetField('A', 'lost')
  3. ld_dbf.CloseDBF()        // 没 CommitRecord
  4. // 实测:文件存在,字段结构正确,但 data 数组长度 = 0,这行数据彻底丢了
复制代码

和「事务」的直觉一致:AppendRecord 只是把当前行搬进内存缓冲区,CommitRecord 才写文件。批量写时如果中途异常,已经 commit 的行留在文件里,没 commit 的那行消失——这一点在「写一半失败要回滚」的场景下要特别注意,DBF 没有回滚接口,只能重建文件。

4.3 AppendRecord(fillSpace) 的 fillSpace 实测无效

源码给了 AppendRecord(boolean fillSpace) 重载,注释说 fillSpace 默认 false,直觉是「true = 用空格填满字段宽度」。实测两种参数返回结果完全一致:

写法未赋值的 B 字段读回
AppendRecord()""(空串)
AppendRecord(true)""(空串)


两种读回都是 {"recordno":1,"A":"aa","B":""}。结论:不要依赖这个参数,未赋值的 C 型字段读回一律是空串。




五、读回:返回的是 {field, data} 对象,不是数组

这是本对象第二个必须记住的点,也是我第一次实测时读错的地方。

dbf2json 的第二个参数是 ref uo_json,调用后这个对象被填成这样的结构:
  1. {
  2.   "field": [
  3.     {"name":"ID","type":"N","length":6,"decimal":0},
  4.     {"name":"NAME","type":"C","length":20}
  5.   ],
  6.   "data": [
  7.     {"recordno":1,"ID":1001,"NAME":"张三","QTY":12},
  8.     {"recordno":2,"ID":1002,"NAME":"LiSi","QTY":99}
  9.   ]
  10. }
复制代码

也就是说:


  • 顶层是对象不是数组 —— ArrCount() 返回 0,IsArray() 返回 false;
  • 记录在 data 键下 —— 要用 ArrCount('data') 取长度;
  • 字段元信息在 field 键下 —— 可以据此还原列类型,不必自己记;
  • 行号在 recordno,从 1 起。


正确读法:
  1. // 演示步骤:先取 data 数组长度,再按 /data/<行号>/<字段名> 取值
  2. // 运行环境:PB10 / PB12.5 均可
  3. uo_json   lj_read  // 读回容器
  4. long      ll_n     // 记录条数
  5. long      ll_i     // 循环下标
  6. lj_read = create uo_json
  7. ld_dbf.dbf2json('dbf_demo.dbf', lj_read, true)
  8. ll_n = lj_read.ArrCount('data')      // ← 不是 lj_read.ArrCount()
  9. ls_msg = '共 ' + String(ll_n) + ' 行~r~n'
  10. for ll_i = 1 to ll_n
  11.         ls_msg += lj_read.GetString('/data/' + String(ll_i - 1) + '/NAME') + '~r~n'
  12. next
  13. destroy lj_read
复制代码

路径下标从 0 起(/data/0/ 是第 1 行),这与 uo_json 的通用规则一致;但 recordno 字段的值从 1 起,两者别混。

5.1 lineno 与 utf8 两个开关


  • lineno:true 时每行多一个 recordno 键;false 时没有。实测 dbf2json(path, js, false) 返回 true、记录数正常,只是 GetString('/data/0/recordno', 'NORECNO') 取默认值 NORECNO。
  • utf8:第 4 参,声明文件编码,默认 false(即按 GBK/ANSI 读)。这个开关实测与直觉相反:


调用读出的中文
dbf2json(path, js, true, false)张三丰(正确)
dbf2json(path, js, true, true)??????(六个问号)


uo_dbf 写出来的文件是 GBK/ANSI,所以读的时候 utf8 传 false。传 true 会把 GBK 字节当 UTF-8 解,中文全部变成问号,且返回值仍然是 true——不报错,静默出错。

5.2 ★坑 4:decimal 定义对 N 型无效,小数会被吃掉

字段定义里 decimal 是白写的。实测:
  1. lj_fields.Parse('[{"name":"AMT","type":"N","length":12,"decimal":2}]')
  2. ld_dbf.CreateDBF('x.dbf', lj_fields)
  3. ld_dbf.AppendRecord()
  4. ld_dbf.SetField('AMT', '123.45')
  5. ld_dbf.CommitRecord()
  6. // 读回:AMT = 123   ← 小数部分没了
复制代码

读回的 field 元信息里 "decimal":0,值也确实是 123(GetDouble 同样得 123)。length 是生效的(12 就是 12),只有 decimal 丢失。

这不是 bug,是 DBF 格式本身如此:dBASE 的 N 型字段在文件里是定长右对齐的 ASCII 文本("        123.45"),小数点只是这段文本里的一个字符。uo_dbf 显然按整数解析这段文本再转成 JSON 数字,于是小数点后被丢弃。

如果你的业务真的有金额小数,两个办法:


  • 把 N 型当 C 型用:{"name":"AMT","type":"C","length":12,"decimal":0},然后自己在 PB 里 Double() 转换。读回是字符串 "123.45",精度不丢。
  • 导出时用字符串拼接:C 型存 "123.45",目标系统(FoxPro 侧)自己再转。


顺带一个坑:N 型读回的是 JSON 数字,不是字符串。GetString('/data/0/AMT') 能拿到 "123",但如果值本身是 0,或者你用 GetLong 去接一个本该是小数的值,类型转换会掩盖问题——读 N 型统一用 GetDouble,C 型统一用 GetString。

5.3 ★坑 5:日期型读回被转成 yyyy/mm/dd,且空值是空串
  1. lj_fields.Parse('[{"name":"CDATE","type":"D","length":8,"decimal":0}]')
  2. // ... 写入 '20261005' ...
  3. // 读回:{"CDATE":"2026/10/05"}    ← 不是 "20261005"
复制代码

DBF 的 D 型是 8 字节 YYYYMMDD,但 uo_dbf 读回时会格式化成 yyyy/mm/dd。如果你要拿这个值回写给别的系统,必须先 Replace(日期串, '/', '') 去掉斜杠。

更糟的是往 D 型塞非法日期:
  1. ld_dbf.SetField('D1', 'notadate')   // 返回 true,不报错
  2. // 读回:{"D1":"nota/da/te"}          ← 8 个字符被硬塞进去,还插了斜杠
复制代码

SetField 返回 true,因为从它的角度「8 个字符,放得下」。所以 D 型必须自己保证格式:String(Date(...,'yyyymmdd')) 之类,写之前校验长度和数字性。

D 型空值倒是正常:写 '' 读回 ""。




六、边界与失败行为实测表

下表全部是双版本实跑结果,PB10 与 PB12.5 行为完全一致:

场景实测行为
CreateDBF 传空数组 []返回 false,不生成文件
字段数组缺 name 键返回 false,不生成文件(其它键齐也不行)
字段 type 传非法值 "X"返回 true 并生成文件,field 里原样保留 "type":"X",不校验
目标目录不存在返回 false,不生成文件
SetField 字段名不存在返回 false,当次赋值被忽略,其余字段正常写
SetField 序号越界(如第 9 个字段,表中只有 1 个)返回 false,同上
C 型写入超长值(字段 length=4,写 ABCDEFGH)返回 false,该字段留空,不是截断
重复字段名(两个都叫 A)CreateDBF 返回 true 能建;读回 JSON 里出现两个 "A" 键,后者覆盖前者(A":"v1" 与 A":"" 并存,解析时只认一个)
L 逻辑型({"type":"L","length":1})能建能写;读回 field 里 type 变成 "C",值保留写入的 "T"
N 型写负数 -456正常,读回 -456
N 型写空串 ''返回 true,读回 0(不是空串,也不是空)
对已存在的文件再 CreateDBF返回 true,但新表头覆盖旧表头、数据全清空(实测旧文件 1 行 → 重开后 data 长度为 0)
dbf2json 读不存在的文件返回 false,目标 uo_json 保持空对象 {}
未 CreateDBF 就 AppendRecord / SetField全部返回 false,CommitRecord 返回 0,不崩(与「有表但不 Append」不同,那种会崩)
dbf2json 取不存在的字段返回传入的默认值(GetString(path, 'DEF') → DEF)


三条最需要记住的:


  • CreateDBF 覆盖同名文件不报错。它返回 true,看起来像「建好了」,但你之前写的数据全没了。要做「追加导出」必须先判断 FileExists(),走另一条路(读出旧数据 → 重建 → 一起写回)。
  • C 型超长赋值返回 false 而不是截断。这其实比截断好——至少你知道数据丢了。但要注意:只看 CreateDBF 的返回值不够,每行至少要检查 CommitRecord 的返回值是不是递增。
  • 类型不校验。type 传 "X" 也能建文件,FoxPro 那边打开可能报错。字段定义是给外部工具看的,合法性要自己保证。





七、完整边界对照演示

把上面那些坑逐条跑一遍,方便你对照自己的代码:
  1. // 演示步骤:逐条验证边界行为,观察每个返回值
  2. // 运行环境:PB10 / PB12.5 均可;当前目录需写权限
  3. // 输出:弹窗列出每条边界的真实返回值
  4. uo_dbf    ld
  5. uo_json   lj
  6. uo_json   ljr
  7. string    ls_r
  8. boolean   lb_b
  9. long      ll_n
  10. ls_r = ''
  11. // ① 空字段数组 → 建不出文件
  12. lj = create uo_json
  13. lj.Parse('[]')
  14. ld = create uo_dbf
  15. lb_b = ld.CreateDBF('b1.dbf', lj)
  16. ls_r += '空数组建表 = ' + String(lb_b) + '(文件存在=' + String(FileExists('b1.dbf')) + ')~r~n'
  17. ld.CloseDBF()
  18. destroy ld
  19. destroy lj
  20. // ② 非法 type → 居然能建
  21. lj = create uo_json
  22. lj.Parse('[{"name":"A","type":"X","length":4,"decimal":0}]')
  23. ld = create uo_dbf
  24. lb_b = ld.CreateDBF('b2.dbf', lj)
  25. ls_r += '非法type建表 = ' + String(lb_b) + '(文件存在=' + String(FileExists('b2.dbf')) + ')~r~n'
  26. ld.CloseDBF()
  27. destroy ld
  28. destroy lj
  29. // ③ 未建表直接写 → 全部 false,不崩
  30. ld = create uo_dbf
  31. lb_b = ld.AppendRecord()
  32. ls_r += '未建表追加 = ' + String(lb_b) + '~r~n'
  33. lb_b = ld.SetField('A', 'x')
  34. ls_r += '未建表赋值 = ' + String(lb_b) + '~r~n'
  35. ld.CloseDBF()
  36. destroy ld
  37. // ④ C 型超长 → false,字段留空
  38. lj = create uo_json
  39. lj.Parse('[{"name":"S","type":"C","length":4,"decimal":0}]')
  40. ld = create uo_dbf
  41. ld.CreateDBF('b4.dbf', lj)
  42. ld.AppendRecord()
  43. lb_b = ld.SetField('S', 'ABCDEFGH')
  44. ls_r += '超长赋值 = ' + String(lb_b) + '~r~n'
  45. ld.CommitRecord()
  46. ld.CloseDBF()
  47. ljr = create uo_json
  48. ld = create uo_dbf
  49. ld.dbf2json('b4.dbf', ljr, true)
  50. ls_r += '读回 S = [' + ljr.GetString('/data/0/S') + '](长度' + String(Len(ljr.GetString('/data/0/S'))) + ')~r~n'
  51. destroy ljr
  52. destroy ld
  53. destroy lj
  54. // ⑤ 覆盖已有文件 → 数据被清空
  55. lj = create uo_json
  56. lj.Parse('[{"name":"A","type":"C","length":4,"decimal":0}]')
  57. ld = create uo_dbf
  58. ld.CreateDBF('b5.dbf', lj)
  59. ld.AppendRecord()
  60. ld.SetField('A', 'old')
  61. ld.CommitRecord()
  62. ld.CloseDBF()
  63. lj = create uo_json
  64. lj.Parse('[{"name":"B","type":"C","length":4,"decimal":0}]')
  65. ld = create uo_dbf
  66. lb_b = ld.CreateDBF('b5.dbf', lj)
  67. ls_r += '覆盖建表 = ' + String(lb_b) + '~r~n'
  68. ld.CloseDBF()
  69. ljr = create uo_json
  70. ld = create uo_dbf
  71. ld.dbf2json('b5.dbf', ljr, true)
  72. ll_n = ljr.ArrCount('data')
  73. ls_r += '覆盖后记录数 = ' + String(ll_n) + '~r~n'
  74. destroy ljr
  75. destroy ld
  76. destroy lj
  77. // ⑥ 非法日期照收不误
  78. lj = create uo_json
  79. lj.Parse('[{"name":"D1","type":"D","length":8,"decimal":0}]')
  80. ld = create uo_dbf
  81. ld.CreateDBF('b6.dbf', lj)
  82. ld.AppendRecord()
  83. lb_b = ld.SetField('D1', 'notadate')
  84. ls_r += '非法日期赋值 = ' + String(lb_b) + '~r~n'
  85. ld.CommitRecord()
  86. ld.CloseDBF()
  87. ljr = create uo_json
  88. ld = create uo_dbf
  89. ld.dbf2json('b6.dbf', ljr, true)
  90. ls_r += '读回 D1 = ' + ljr.GetString('/data/0/D1') + '~r~n'
  91. destroy ljr
  92. destroy ld
  93. destroy lj
  94. // ⑦ 读不存在的文件
  95. ljr = create uo_json
  96. ld = create uo_dbf
  97. lb_b = ld.dbf2json('not_exist.dbf', ljr, true)
  98. ls_r += '读不存在 = ' + String(lb_b) + ',容器 = ' + ljr.ToString() + '~r~n'
  99. destroy ljr
  100. destroy ld
  101. MessageBox('边界实测', ls_r)
  102. // ↑ 实测输出:
  103. //   空数组建表 = false(文件存在=false)
  104. //   非法type建表 = true(文件存在=true)
  105. //   未建表追加 = false
  106. //   未建表赋值 = false
  107. //   超长赋值 = false
  108. //   读回 S = [](长度0)
  109. //   覆盖建表 = true
  110. //   覆盖后记录数 = 0
  111. //   非法日期赋值 = true
  112. //   读回 D1 = nota/da/te
  113. //   读不存在 = false,容器 = {}
复制代码




八、与其它方案的选型

方案适用优点缺点
uo_dbf(PBIDEA)读写 DBF 交换文件不装 FoxPro;8 个方法够用;PB 直接对接只支持平面 DBF,无索引/关系/备注文件;decimal 无效;中文只走 GBK
FoxPro / Visual FoxPro ODBC需要完整 DBF 特性支持索引、备注、关系要装 FoxPro 或 ODBC 驱动,部署重;中文代码页易配错
文本 CSV(uo_csv 或 dw.SaveAs)只为交换数据,不在乎格式双方都能读;无二进制结构逗号/引号/编码坑;丢类型(N 型变字符串)
uo_kv / uo_config自己的中间格式完全可控外部工具读不了,通用性差


选型建议:如果对方明确说「要 DBF」,就用 uo_dbf,并主动确认三件事——① 小数字段能否改成 C 型自己转;② 中文是不是 GBK;③ 要不要行号。确认完再动手,能省掉后面 90% 的扯皮。

如果只是「导出个文件给同事导进 Excel」,优先 CSV——DBF 的中文和类型转换坑不值得趟。




九、PB12.5 差异

本文全部 API 调用与边界行为在 PB10 与 PB12.5 双版本下逐条对跑,读数逐字节一致。未发现版本差异:


  • 8 个方法的签名、返回值语义、崩溃行为完全相同;
  • {field, data} 返回结构相同,lineno / utf8 两个开关行为相同;
  • 5.2 的 decimal 丢失、5.3 的日期格式化与非法日期硬塞、6. 的全部边界行为两版一致;
  • 4.1 的「不 AppendRecord 直接 CommitRecord 崩溃」两版都是 0xC0000005。


唯一的环境差异是依赖库:uo_dbf 与 uo_json 都通过 SYSTEM library "PbIdea.dll" 调用外部函数,所以运行时必须让 PbIdea.dll 和 Pbidea_cs.dll 都在可执行文件的搜索路径里(PB 的 Path 环境变量或 exe 同目录)。缺任何一个都会在 create uo_json 时报「Error opening DLL library PbIdea.dll」。这与 PB 版本无关,PB10 和 PB12.5 一样。




十、坑清单速查


  • ★不 AppendRecord 直接 CommitRecord / SetField 会崩进程(0xC0000005),TRY-CATCH 兜不住。三段式一组都不能少,每次 commit 前都要重新 append。
  • ★dbf2json 返回 {field:[...], data:[...]} 对象,不是数组。ArrCount() 得 0,要用 ArrCount('data'),路径是 /data/<行号>/<字段名>,下标从 0 起。
  • ★decimal 对 N 型无效,"123.45" 写进去读出来是 123。有小数用 C 型自己转。
  • ★utf8 第 4 参与直觉相反:uo_dbf 写的文件是 GBK,读的时候要传 false;传 true 中文全变问号,且返回值仍是 true(静默出错)。
  • ★Set('{...}') 不会解析 JSON 文本,会把整段当字符串存进数组。要么 Parse 整段,要么先建 uo_json 子对象再 Set(对象)。
  • ★CreateDBF 覆盖同名文件返回 true 且清空数据。导出前必须 FileExists() 判断。
  • ★D 型读回是 yyyy/mm/dd,不是 YYYYMMDD,回写前要 Replace(日期串,'/','')。
  • D 型不校验日期格式:SetField('D1','notadate') 返回 true,读回 nota/da/te。
  • CommitRecord 返回累计行数,不是本次行号;可以不检查,但要检查它是否在递增(不递增说明上一行没写成功)。
  • AppendRecord(fillSpace) 参数无效,true / false 行为一致,未赋值字段读回一律空串。
  • C 型超长赋值返回 false 而不是截断,该字段留空。
  • N 型空串写入读回 0;负数正常。
  • type 不校验,传 "X" 也能建文件,出错时由 FoxPro 侧报。
  • 重复字段名能建能写,但读回的 JSON 里有重复键,解析时只认一个。
  • L 型读回 type 变成 "C",值保留。
  • uo_dbf 非 autoinstantiate,必须 create/destroy。
  • 必须 CloseDBF,未 commit 的数据会随关闭丢失;析构里也会自动调一次,但显式关闭更保险。
  • PbIdea.dll 与 Pbidea_cs.dll 都要在运行路径,缺则在 create uo_json 时报错。





十一、实机验证情况

验证项PB 10PB 12.5
源码导入(pypower pbl import)0 错0 错
全量编译(rebuild --type full)0 错0 错
PBVM 运行(.pbtest)通过通过
断言数主用例 60 项 + 边界 14 例同左
两版诊断输出比对逐字节一致同左


验证方式说明:


  • 写入链路:建表 → 追加 2 行 → 提交,文件从 32 字节(仅表头)增长到 231 字节,磁盘文件真实落盘;
  • 读回链路:dbf2json 读出 recordno 1/2、ID 1001/1002、NAME 张三/LiSi、中文 MEMO 完整;
  • 边界矩阵:14 个独立用例(decimal、utf8 开关、fillSpace、跳过 AppendRecord、覆盖文件、负数/空值、逻辑型、重复字段名、非法 type、缺 name、非法目录、非法日期、D 型空值、读不存在文件),每例单独跑一次(崩溃隔离),lineno 开关另测;
  • 崩溃点定位:4.1 的崩溃用「一例一次独立运行」复现,两版退出码均为 3221225477(0xC0000005);
  • 未实测部分:本对象无未实测段落。所有涉及文件系统的行为都在 Windows 本机真实文件上跑过;DBF 头结构的字节级校验(对照 FoxPro 打开验证)本机没有 FoxPro 环境,未做——文中关于「DBF 格式本身不含小数类型定义」的解释来自格式规范与读回元信息(field[].decimal 恒为 0)两条证据推断,而非 FoxPro 侧交叉验证。


本文所有「实测」标记的读数都可复现。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 2026-10-5 07:22:19 | 显示全部楼层
dbf57demo_pb10.zip (13.21 KB, 下载次数: 0)

PBIDEA:uo_dbf DBF 读写示例(PB10 兼容版 PBL)

按惯例附上本文示例的 PB10 兼容版工程包,双轨实机验证通过。

一、适用版本与环境
适用版本:PowerBuilder 10(基准),同时兼容 PB 12.5 及更高版本。
环境要求:
1. Windows 7 及以上;
2. PBIDEA 运行库:PbIdea.dll 与 Pbidea_cs.dll 两个文件都要在 exe 同目录(或系统 PATH)——缺任何一个都会在 create uo_json 时报 "Error opening DLL library PbIdea.dll";
3. 你的工程需要能解析 PBIDEA 的 websuite.pbl(提供 uo_dbf / uo_json),且格式要与你的 PB 大版本匹配(PB10 库不能在 PB12.5 下直接用);
4. 示例会在当前工作目录生成 dbf_demo.dbf,该目录需可写。

二、是否依赖数据库
不依赖。本示例纯文件读写,不连任何 DBMS,不需要装 FoxPro,也不需要 ODBC 驱动。

三、包内文件清单(9 个条目)

dbf57demo.pbt      PB 工程文件
dbf57demo.pbl      已编译的 PB10 库(内含 nvo_dbf_demo + app_dbf_demo)
使用说明.txt       完整说明(GBK)
src/nvo_dbf_demo.sru    示例对象源码
extra/tests/dbf_demo_main.pbtest  实机验证脚本源码
extra/evidence/诊断输出_PB10.txt   PB10 运行输出
extra/evidence/诊断输出_PB12.5.txt  PB12.5 运行输出
extra/evidence/交付对象输出_PB10.txt
extra/evidence/交付对象输出_PB12.5.txt


四、部署步骤

第 1 步 确认 PbIdea.dll 与 Pbidea_cs.dll 已在 exe 目录
第 2 步 导入 nvo_dbf_demo.sru(PB IDE 用 File → Import → Import from File;
     或命令行 pypower pbl import 你的.pbl nvo_dbf_demo.sru --pb-version 100 --encoding gbk --pbt 你的.pbt)
第 3 步 检查 PBT 的 liblist 能解析到 websuite.pbl(单行、分号分隔)
第 4 步 全量编译,0 错误
第 5 步 调用(见下)


五、调用方法
nvo_dbf_demo 暴露一个入口:of_run(boolean lb_write),lb_write=true 走写入+读回演示。
  1. nvo_dbf_demo lnvo
  2. string ls_out
  3. lnvo = create nvo_dbf_demo
  4. ls_out = lnvo.of_run(true)
  5. destroy lnvo
  6. MessageBox('DBF 演示', ls_out)
复制代码

实测输出:
  1. 写入完成:dbf_demo.dbf
  2. 文件长度:300 字节
  3. 记录条数:2
  4. 字段个数:4
  5. 第1行:1001 | 张三 | 12 | 第一行
  6. 第2行:1002 | LiSi | 99 | row2
复制代码

六、三个最要紧的坑(详见包内使用说明的 18 条清单)

1. 不写 AppendRecord 直接 CommitRecord —— 崩进程,退出码 0xC0000005,TRY-CATCH 兜不住。
   铁律:AppendRecord → SetField → CommitRecord 三步一组,每次 commit 前都要重新 append。
2. dbf2json 返回的是 {field:[...], data:[...]} 对象不是数组:
   ArrCount() 恒返回 0,要用 ArrCount('data'),路径 /data/<行号>/<字段名>,下标从 0 起。
3. dbf2json 第 4 参 utf8 与直觉相反:uo_dbf 写的是 GBK,读时要传 false;
   传 true 中文全变问号,而且返回值仍然是 true(静默出错)。


七、实机验证情况(如实标注)

本包 PBL:--pb-version 100 全量编译 0 错误;pbl list 校验对象齐全。

双轨运行:PB 10 与 PB 12.5 均「编译 0 错 + PBVM 运行 PASS」,
两版诊断输出逐字节一致(主用例各 944 字节 BYTE-IDENTICAL,
交付对象各 138 字节 BYTE-IDENTICAL)。

边界矩阵 14 个独立用例(decimal / utf8 开关 / fillSpace / 跳过 AppendRecord /
覆盖文件 / 负数与空值 / 逻辑型 / 重复字段名 / 非法 type / 缺 name /
非法目录 / 非法日期 / D 型空值 / 读不存在文件),每例单独跑一次做崩溃隔离。

★未实测部分:
· DBF 文件头与记录区的字节级格式校验未做 —— 本机没有 FoxPro 环境,
  无法交叉验证生成的 DBF 能否被第三方工具正常打开。上生产前建议用真实
  FoxPro 工具试开一次。
· DBF 的索引(.cdx)/ 备注(.dbt)/ 关系表 uo_dbf 根本不支持(源码只有平面表读写)。
· 超过 100 万行的大文件、并发读写同一 DBF、网络路径上的 DBF 均未实测。

共享共进共赢
Sharing And Win-win Results
发表于 2026-10-12 02:49:41 | 显示全部楼层
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-11-14 12:28 , Processed in 0.022940 second(s), 9 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表