马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_file 做文件读写全解(文本与 blob 直读直写、句柄式读写、文件加密与占用查询,PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:一律以文末【版本快照】为准;本文写作与实测基准为 PB10,PB 12.5 双轨读数逐字一致(快照与两版对照见第十二节)
2. 支持数据库:本文全程不连数据库 —— 纯本地文件操作,所有示例在你机器上就能直接跑
3. 操作系统与环境要求:Windows 7 及以上;需安装 PBIDEA 运行库(uo_file 位于 websuite 库,依赖 PbIdea.dll),PB10 与 PB12.5 各需配套版本
4. 难度系数:★★☆☆☆(二星。方法都是「一个函数一件事」的直路子,难点集中在字节定位、编码与句柄语义这几处坑)
5. 其它阅读说明:目录版块里已写过 uo_dir(目录枚举)、uo_stream(内存二进制流)、uo_blob(二进制数据处理)三篇,本文不重复它们的活儿,分工见第一节;示例代码可放进任意 NVO 方法或窗口按钮事件里直接运行
一、uo_file 是什么,跟近邻们怎么分工
PB 原生的文件三件套 FileOpen/FileRead/FileWrite 有个老毛病:文本和二进制是两条路(LineMode!/TextMode!/Binary! 模式选错全盘错),blob 读写还得自己管长度。uo_file 把「文件」这件事拆成了两层:
- 直读直写层:一个函数搞定整个文件 —— BlobFromFile/BlobToFile(二进制)、StringFromTextFile/StringToTextFile(文本,自动转码)。不用管打开关闭,读完即走。
- 句柄层:OpenFile → ReadFile/WriteFile/Seek/ReadLine → CloseFile,适合大文件分段处理、定点改写、加锁独占。
uo_file 一共 49 条有效声明(对照随版导出源码),按用途分五类:
| 类别 | 代表方法 | 本文是否实测 | | 文本直读直写 | StringToTextFile / StringFromTextFile | 是(全边界) | | blob 直读直写 | BlobFromFile / BlobToFile | 是(全边界) | | 句柄式读写 | OpenFile / ReadFile / WriteFile / Seek / ReadLine / CloseFile | 是(全边界) | | 加密与拆分 | EncryptoFile / DecryptoFile / EncryptoFileRead / SplitTextFile | 是 | | 查找与占用 | FindFile / QueryFileLocker / fileGetFullPath | 是(FindFile 限 3 参形态) | | 对话框与杂项 | GetFileOpenName / GetFileSaveName / SetUserOption / Printf / 剪贴板文件列表 | 否(对话框需人交互;剪贴板文件列表已由 uo_clipboard 那篇覆盖,Printf 属控制台输出,测试 harness 里取不到) |
跟近邻的分工边界,一句话说清:
- uo_dir 管「目录」:列文件、建删目录、文件时间与大小,它不开文件内容;
- uo_stream 管「内存」:那是把一块内存当流来读写,不落盘;
- uo_blob 管「字节本身」:hex/base64、按类型读写、校验值,输入输出都是内存里的 blob;
- uo_file 管「磁盘上的文件」:从文件到字符串/blob 的搬运、句柄、锁、加密落盘。
四者经常串着用:uo_file 读进来,uo_blob 处理,uo_file 写回去。
二、文本直读直写:一个函数管编码
StringToTextFile(字符串, 文件名, utf8)、StringFromTextFile(文件名) 是我最推荐的两个入门方法,因为编码这件事它们替你做了一大半。先看能直接跑的例子:
- // 目的:把一段含中文的文本写进文件再读回来(ANSI/GBK 编码)
- uo_file lf
- boolean lb_w
- string ls_cn, ls_back
- lf = create uo_file
- // 待写入内容:前段定义,直接赋值
- ls_cn = '总量=你好 明细 123'
- // 写文件:第三参 utf8=false 走 ANSI(中文 Windows 上就是 GBK)
- lb_w = lf.StringToTextFile(ls_cn, 'ex1.txt', false)
- // 读回来:StringFromTextFile 自动识别编码并转换
- ls_back = lf.StringFromTextFile('ex1.txt')
- destroy lf
- MessageBox('文本往返', '写入=' + String(lb_w) + ' 读回=[' + ls_back + ']')
复制代码
实测读数:写入=true 读回=[总量=你好 明细 123],两版一致。
第三参 utf8 是本文第一个值得背下来的开关,实测字节账目如下(内容固定为「你好」两个汉字):
| utf8 参数 | 落盘字节数 | 字节内容 | 读回 | | false | 4 | GBK 双字节编码 | 2 字符,原样 | | true | 9 | 3 字节 BOM(EF BB BF)+ 6 字节 UTF-8 | 2 字符,原样 |
三条实测结论:
- utf8=true 会写 BOM。不想要 BOM 的场合(比如给某些只认纯 UTF-8 的老系统供数),得意识到文件头多了 3 个字节。
- StringFromTextFile 的「自动编码转换」是真的:BOM 头、GBK、UTF-8 三种形态我各写了一版,读回全是原字符串,没让我碰一个转码函数。没有 BOM 的 UTF-8 文件读回也正常。
- 空字符串写不进去:StringToTextFile('', 文件, false) 返回 false,文件根本不创建。想造一个 0 字节占位文件,用 BlobToFile 配空 blob?也不行,照样 false。这一点跟「PB 原生 FileOpen 直接创建空文件」的习惯不同,注意。
三、blob 直读直写:二进制落盘两条函数
- // 目的:任意二进制内容落盘与回读(不要求是文本)
- uo_file lf
- blob lb_src, lb_back
- boolean lb_w
- long ll_len
- lf = create uo_file
- // 待写入的二进制:由字符串转 blob。PB10 起每字符 2 字节(UTF-16LE)
- lb_src = Blob('PK-HEADER-91-BINARY')
- // 写文件:目标已存在则整文件截断重写
- lb_w = lf.BlobToFile(lb_src, 'ex2.bin')
- // 读回
- lb_back = lf.BlobFromFile('ex2.bin')
- ll_len = Len(lb_back)
- destroy lf
- MessageBox('blob 往返', '写入=' + String(lb_w) + ' 字节数=' + String(ll_len) + ' 内容=[' + String(lb_back) + ']')
复制代码
实测读数:写入=true 字节数=38 内容=[PK-HEADER-91-BINARY](19 个字符 × 2 字节 = 38,两版一致)。
这一节的坑比文本层多:
- Blob(字符串) 是 UTF-16LE,每字符 2 字节。Blob('ABC') 的 Len 是 6 不是 3。想让字节数等于字符数,走 StringToTextFile(它会转 ANSI/GBK)或者用 uo_blob 的编码转换。
- 覆盖即截断:对已有文件 BlobToFile 写入更短内容,文件长度就是新内容的长度,不会残留旧尾巴。实测先写 12 字节再写 4 字节,文件从 12 变 4。StringToTextFile 同样截断(长内容 17 字节改写短内容 5 字节,落盘 5 字节)。
- 读不存在的文件不报错也不返回 NULL:BlobFromFile('不存在.bin') 返回一个 Len=0、IsNull=false 的空 blob。判断「文件在不在」要靠 uo_dir.exist 或 PB 原生 FileExists,别拿空 blob 当信号。
- 空 blob / 空串写文件一律 false(见第二节第 3 条),这是防误清空的好设计,但也意味着「用这组函数造 0 字节文件」这条路不存在。
四、句柄式读写:OpenFile 一家的真实语义
直读直写够用就别碰句柄;要分段读、定点改、加锁,才用这一层。先给一个按行读文本的完整例子:
- // 目的:句柄式按行读文本 —— 先造 GBK 文本文件,再打开逐行读
- uo_file lf
- boolean lb_o, lb_r1, lb_r2, lb_r3, lb_r4, lb_c
- string ls_l1, ls_l2, ls_l3, ls_l4, ls_out
- lf = create uo_file
- // 先造一个 GBK 编码的三行文本文件(CRLF 分行)
- lf.StringToTextFile('L1' + Char(13) + Char(10) + 'L2' + Char(13) + Char(10) + 'L3', 'ex3.txt', false)
- // 打开已存在的文件(1 参形态:可读可写,但文件必须已存在)
- lb_o = lf.OpenFile('ex3.txt')
- // 逐行读;到文件尾(EOF)返回 false
- lb_r1 = lf.ReadLine(ls_l1)
- lb_r2 = lf.ReadLine(ls_l2)
- lb_r3 = lf.ReadLine(ls_l3)
- lb_r4 = lf.ReadLine(ls_l4)
- lb_c = lf.CloseFile()
- destroy lf
- ls_out = '打开=' + String(lb_o) + ' 行1=[' + ls_l1 + '] 行2=[' + ls_l2 + '] 行3=[' + ls_l3 + '] EOF=' + String(lb_r4)
- MessageBox('按行读', ls_out)
复制代码
实测读数:打开=true 行1=[L1] 行2=[L2] 行3=[L3] EOF=false,两版一致。注意第 4 次 ReadLine 到了 EOF 返回 false,且 ref 参数被清空。
OpenFile 有四个重载,实测语义如下:
| 形态 | 文件已存在 | 文件不存在 | 实测结论 | | OpenFile(文件名) | 能开,可读也可写 | 失败(false) | 1 参不创建文件,别拿它当「新建」用 | | OpenFile(文件名, read!) | 能开 | 失败(false) | 编译通过;read! 是合法枚举值 | | OpenFile(文件名, write!) | 能开(整写截断,见下) | 自动创建 | 新建文件靠它 | | OpenFile(文件名, read!/write!, lockFile) | lockFile=true 独占 | 同上 | 锁语义见第五节 | | OpenFile(文件名, …, lockFile, isTempFile) | 同上 | 同上 | isTempFile=true 关闭即删,实测 FileExists=false |
三个最容易翻车的语义,全部实测过:
- Seek 的单位是字节,不是字符。对 UTF-16 内容(WriteFile 写出来的东西)做定位时最容易懵。实测:12 字节文件('ABCDEF' 的 UTF-16LE),Seek(2) 返回 2(字节位置),紧接 WriteFile(Blob('XY')) 覆盖第 2~5 字节,结果内容是 AXYDEF —— 第二个字符'B'起被换掉。再确认一次三种 method:
- // 目的:演示 Seek 三种定位方式(0=从文件头,1=从当前位置,2=从文件尾)
- uo_file lf
- blob lb
- long ll1, ll2, ll3
- lf = create uo_file
- // write! 打开(文件不存在会自动创建),写入 12 字节
- lf.OpenFile('ex5.bin', write!)
- lf.WriteFile(Blob('ABCDEF'))
- // 从头偏移 2 字节
- ll1 = lf.Seek(2, 0)
- // 从当前位置再前进 2 字节
- ll2 = lf.Seek(2, 1)
- // 从文件尾回退 4 字节
- ll3 = lf.Seek(-4, 2)
- lf.CloseFile()
- lf.OpenFile('ex5.bin', write!)
- lf.Seek(8)
- lf.WriteFile(Blob('XY'))
- lf.CloseFile()
- lb = lf.BlobFromFile('ex5.bin')
- destroy lf
- MessageBox('Seek 语义', '三种返回=' + String(ll1) + '/' + String(ll2) + '/' + String(ll3) + ' 覆写后=[' + String(lb) + ']')
复制代码
实测读数:三种返回=2/4/8 覆写后=[ABCDXY]。返回值是移动后的字节位置;第 8 字节处覆写 4 字节,正好换掉第 5、6 个字符('EF'→'XY'),文件保持 12 字节。
- WriteFile 是「从指针处覆盖」,不截断尾部。只有「write! 打开后直接从头整写」才等效截断(实测 12 字节文件整写 4 字节 → 4 字节);一旦 Seek 过再写,尾部原样保留。想要「定点改一个字段、后面不动」,这正是你要的;想要「截断重写」,就别 Seek,从头整写。
- ReadFile 带 readSize 参数时按字节读、指针跟着走;不带参数读整个文件。实测连续两次 ReadFile(lb, 3) 拿到的是相邻的 3 字节(第二次的 String 显示出半个 UTF-16 字符的乱码,这正是按字节走的铁证);Seek(0) 归位后重读正常。全量读之后指针仍在文件头(Seek(0,1) 查询为 0),可以接着部分读。
句柄层还有三条边界,实测结论直接给:
- 未打开句柄调 ReadFile/WriteFile/Seek 不崩:返回 0 / 0 / -1,温和失败。CloseFile 未开过句柄返回 false,关两次也是 false。
- read! 打开的句柄 WriteFile 照样写进去!这是全篇最意外的一条:read! 只是「打不开不存在的文件」的语义声明,运行时不拦写。我拿 read! 句柄对 8 字节 GBK 文件 WriteFile 了 10 字节 UTF-16,返回 wn=10,文件被写坏。别指望 read! 保护文件,要保护用 lockFile 或权限。
- append! 编译不过:OpenFile(文件, append!) 报 C0052。fileaccess 这个枚举实测只认 read! 和 write!。追加写入的替代方案:write! 打开 → Seek(0, 2) 到尾部 → WriteFile。
还有一条配套发现:WriteFile(Blob(字符串)) 写的是 UTF-16LE,而 ReadLine 按字节扫 CRLF。两者混用必错位:我用 WriteFile 写了 12 字节的 'L1\r\nL2',ReadLine 只读出半个字符。文本按行走,就用 StringToTextFile 造文件(GBK 单字节)再 ReadLine;二进制按块走,就 WriteFile + ReadFile,别交叉。
五、锁与占用:lockFile 与 QueryFileLocker
文件被别的进程占用是运维排障的常客,uo_file 给了两件工具。
- // 目的:独占锁 + 查询文件被哪个进程占用
- uo_file lf1, lf2
- boolean lb1, lb2, lb3, lb4
- long ll_pid
- string ls_exe, ls_out
- lf1 = create uo_file
- lf2 = create uo_file
- // 造文件
- lf1.StringToTextFile('LOCK91', 'lock.txt', false)
- // 独占打开(第三参 lockFile=true)
- lb1 = lf1.OpenFile('lock.txt', write!, true)
- // 第二个对象再开:读、写都进不去
- lb2 = lf2.OpenFile('lock.txt', read!)
- lb3 = lf2.OpenFile('lock.txt', write!)
- // 释放后第二家立刻能开
- lf1.CloseFile()
- lb4 = lf2.OpenFile('lock.txt', write!)
- // 顺手演示占用查询:pid + 进程完整路径(ref 参数带回)
- lf2.CloseFile()
- lf2.OpenFile('lock.txt', write!)
- ll_pid = lf2.QueryFileLocker('lock.txt', ls_exe)
- lf2.CloseFile()
- destroy lf1
- destroy lf2
- ls_out = '独占=' + String(lb1) + ' 别家读=' + String(lb2) + ' 别家写=' + String(lb3) + ' 释放后=' + String(lb4)
- MessageBox('锁与占用', ls_out + ' 占用进程=' + String(ll_pid) + ' ' + ls_exe)
复制代码
实测读数(本机):独占=true 别家读=false 别家写=false 释放后=true;QueryFileLocker 拿到 pid=7644,exe 带回调用者自己的完整进程路径——因为测试进程自己握着句柄。文件一关,pid 回 0、exe 回空串。
这条在排障时的用法很直接:客户说「文件删不掉/写不进」,把文件名丢给 QueryFileLocker,谁占用一目了然。
六、文件加密落盘:EncryptoFile 三件套
- // 目的:敏感文件加密落盘、解密还原(密钥字符串自定义)
- uo_file lf
- boolean lb_e, lb_d
- string ls_back
- lf = create uo_file
- // 明文先落一份
- lf.StringToTextFile('PAY-2026-91', 'pay.txt', false)
- // 加密:源文件 → 密文文件
- lb_e = lf.EncryptoFile('pay.txt', 'pay.enc', 'k91')
- // 解密:密文 → 明文;密钥错则返回 false 且不生成目标文件
- lb_d = lf.DecryptoFile('pay.enc', 'pay2.txt', 'k91')
- ls_back = lf.StringFromTextFile('pay2.txt')
- destroy lf
- MessageBox('文件加密', '加密=' + String(lb_e) + ' 解密=' + String(lb_d) + ' 还原=[' + ls_back + ']')
复制代码
实测读数:加密=true 解密=true 还原=[PAY-2026-91],两版一致。四个实测结论:
- 密钥错了不给你解出一堆乱码:DecryptoFile 返回 false,目标文件根本不生成。防御干净,别拿解密失败猜密钥。
- EncryptoFile 第四参 compress=true 是「先压后密」:往返解密正常(实测还原原样)。但注意小文件压缩反而更大——14 字节明文,不压密文 48 字节、压缩后密文 56 字节。压缩只在重复度高的较大文件上有收益,别无脑开。
- EncryptoFileRead(密文文件, 密钥) 直接返回明文 blob,少落一次盘。但它返回的是原始字节的 blob:直接 String(lb) 显示乱码(blob 默认按 UTF-16 解释),要 String(lb, EncodingANSI!) 才是明文。这一步没做的话,你会以为解密坏了——我第一眼就是这么误判的。
- 密文文件大小与明文无固定比例(14 字节明文 → 48 字节密文),别用文件长度当校验,解密后比对内容或走 uo_blob 的摘要。
七、FindFile:4 参是幌子,3 参才是真身
这是本篇最重要的「防坑」发现。随版导出源码里 FindFile 有两个重载:
| 形态 | 源码里有吗 | 实测编译 | 实测运行 | | FindFile(目录, 扩展名, 是否含子目录) | 有 | 通过 | 返回命中个数 | | FindFile(目录, 扩展名, 是否含子目录, ref string files[]) | 有 | C0084: Bad number of arguments | 不可用 |
4 参那个 ref 数组形态编译不过——类型库里没有注册它,签名探测当场砍掉。所以拿文件列表只有一条路:3 参形态 + 事件回调(子类继承 uo_file、覆写 ue_filefind 事件)。实测返回值本身可信(递归查 *.txt 返回 43 个命中),但要注意两点:
- ue_filefind 事件实测一次都没触发:我造了覆写事件的子类,把命中计数和文件名都记进实例变量,跑完事件计数仍是 0、文件名仍是空。返回个数是对的,事件回调链路在这个运行形态下没接通。要把「遍历过程」拿到手,现阶段更稳的是 uo_dir 的 files/dirs 枚举;ue_filefind 的行为我标注为未实测通过,等后续版本再验。
- 扩展名参数要带通配:'*.txt' 有命中,裸写 'txt' 返回 0。别按「扩展名不带点」的直觉写。
八、SplitTextFile:大文本拆分
SplitTextFile(文件名, 每份大小M) 返回拆出的份数。实测 26 字节小文件按 1M 拆,返回 1(不足一份也算一份)。参数单位是 M,所以它的用武之地是日志、导出文本这类大文件按块切割;文件名会自动带序号后缀。小文件拆它纯属浪费,1M 以下的直接整读。
九、实测坑清单(照着避,一条是一条)
- Blob(字符串) 是 UTF-16LE,每字符 2 字节;要 ANSI 字节数走 StringToTextFile。
- StringToTextFile utf8=true 带 3 字节 BOM;对端不认 BOM 时要注意。
- 空字符串/空 blob 一律写不进(返回 false),0 字节文件造不出来。
- BlobFromFile 读不存在的文件返回空 blob(非 NULL、不报错),文件存在性判断交给 FileExists/uo_dir.exist。
- 1 参 OpenFile 不创建文件;新建文件必须 write!。
- 1 参与 read! 形态对已存在文件都能写——read! 不拦 WriteFile,别拿它当只读保护。
- append! 编译不过(C0052);追加 = write! + Seek(0, 2)。
- Seek 单位是字节;UTF-16 内容下「字节 × 2 = 字符」。
- WriteFile 从指针处覆盖、不截尾;整写(不 Seek)才等效截断。
- ReadFile(n) 按字节读、指针跟走;ReadLine 按 ANSI 字节扫行,与 UTF-16 内容混用必错位。
- QueryFileLocker 的 ref 参数带回进程完整路径,返回值是 PID;文件未被占用时 PID=0。
- EncryptoFileRead 返回原始字节 blob,显示要 String(lb, EncodingANSI!)。
- FindFile 4 参形态编译不过(C0084),3 参 + 事件回调是唯一路;扩展名要带 *.。
- CloseFile/析构都会释放句柄(uo_file 的 destructor 里就调了 CloseFile),但长循环里仍建议显式关,Windows 句柄配额不是无限的。
十、PB12.5 差异与版本快照
10.1 版本快照(2026-11-08 核验)
| 项目 | 实测值 | | 版本源目录 | 维护方 PBIDEA 最新构建(10 = PB10 轨,125 = PB12.5 轨) | | 运行库字节数与 md5 | 18,291,200 字节 / 740f9f474d841e3fa4d06d532ecd8994 | | PE 导出函数数 | 2608(解析 PE 导出表得出,不看目录名、不看体积) | | 源码导出目录 | export_20261010(GBK、无 BOM、575 文件) | | 核验日期 | 2026-11-08 |
PBIDEA 的版本会持续更新,运行库字节数与 md5 每次补丁都可能变,而接口行为以当时的构建为准。本文全部签名均与该快照导出的 uo_file 源码逐条对照;如果你手上的 PbIdea.dll 不是这个字节数,建议先按本文第二节的最小示例跑一遍再上生产。
10.2 两版行为对照
本轮全部用例(含负探针)PB10 与 PB12.5 两版编译读数与运行读数逐条一致,未发现版本差异。唯一提醒:Blob() 的 UTF-16LE 行为是 PB10 起的 Unicode 内码所致,PB9 及更早版本(ANSI 内码)下 Blob('ABC') 是 3 字节——跨老版本移植时这一处要重验。
十一、与相近方案对比
| 需求 | 推荐 | 理由 | | 整文件读进内存改一改写回去 | uo_file 直读直写 | 两行搞定,自动转码 | | 大文件分段处理 / 定点改写 | uo_file 句柄层 | Seek 按字节定位,WriteFile 覆盖不截尾 | | 列目录、建删目录、文件时间 | uo_dir | uo_file 的 FindFile 事件链路未实测通过 | | 内存流式组装数据(不落盘) | uo_stream | uo_file 一切操作都过磁盘 | | 字节级加工(hex/base64/摘要) | uo_blob | 拿到 blob 之后的事归它 | | 独占写文件 / 查占用进程 | uo_file lockFile + QueryFileLocker | 原生 FileOpen 的 LockReadWrite! 跨进程行为不稳 |
十二、实机验证情况
- 探针规模:38 个探测 NVO、4 个文章示例方法,全部走真实编译与运行。PB10 基线(--pb-version 100 全量编译 0 错误、对象齐全)与 PB12.5 实跑(PBVM 运行 .pbtest 逐例 PASS)双轨完成,两版读数逐字比对一致。
- 负向探测:append! 形态(C0052)、FindFile 4 参形态(C0084)两条「源码有但用不了」的路已实测排除;GetFileOpenName/GetFileSaveName 对话框族、SetUserOption/GetUserOption、Printf、AddAppPathToEnv 未实测(需人工交互或改变进程环境变量),文中未将其作为示例。
- 未实测标注:ue_filefind 事件回调(返回个数可信、事件未触发)已如实标注;SplitTextFile 仅验证小文件行为,大文件切分边界(多 M 文件、序号命名)未展开。
- 文中全部示例代码均来自实跑通过的用例,读数如实引用,无一行推演代码。
|