PBIDEA:用 uo_dir 做目录与文件枚举、批量建删与文件时间管理(PB10 基准 · PB12.5 实测)
PBIDEA:用 uo_dir 做目录与文件枚举、批量建删与文件时间管理(PB10 基准 · PB12.5 实测)【阅读说明】
1. 适用版本:PBIDEA 以顶部【版本声明】核验的最新版本为准;PowerScript 写法 PB 8 及以上通用,本文按 PB10 基准书写。
2. 支持数据库:本文不涉及数据库,所有示例均为纯文件系统操作,无建表 SQL。
3. 操作系统与环境:Windows 7 及以上可参考(PBIDEA 本体支持 Windows XP+);需随应用分发 PBIDEA 核心运行库;文中 uo_dir 对象随 PBIDEA 的 websuite 库提供。代码里的目录名 pbdir_demo 是相对路径示例,请替换成你自己的目录。
4. 难度系数:★★☆☆☆(API 简单直观,但有两处实测边界行为极易踩坑,已单列成节)。
5. 其它阅读说明:本文全部结论来自实机测试(PB10 与 PB12.5 双版本),测试读数以「实测输出」标注;未实测的 API 已明确标注,不作为核心示例。
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-09-29;当次核验的 PBIDEA 核心运行库:18,105,344 字节 / 2544 个导出,md5 4f49718e4460…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。
一、为什么 PB 需要 uo_dir
PowerBuilder 本身对文件系统的支持相当薄弱。标准库里你能用的,无非是 FileOpen / FileWrite / FileRead 这几个脚本级文件函数,外加 GetCurrentDirectory / SetCurrentDirectory 这类零散 API。一旦需求稍微往前走一步,麻烦就来了:
[*]想枚举某个目录下的全部文件名?标准库没有现成的目录遍历函数,得去调 Windows API 自己拼。
[*]想批量新建多级目录?CreateDirectory 一次只能建一层,中间缺一层就失败。
[*]想拿到一个文件的创建时间 / 修改时间并改掉它?标准库几乎帮不上忙。
[*]想删除一整个目录树(连子目录带文件)?同样没有原生支持。
PBIDEA 的 websuite 库里有一个专职对象 uo_dir,正好把这些活儿一揽子包了:建目录、删目录、枚举文件、枚举子目录、取/改文件时间、移动与删除文件,全是现成方法。它不依赖数据库、不依赖网络,而且是 autoinstantiate(声明即实例化) 对象,拿来就能用。本文把这些 API 全貌、典型用法和两处实测边界行为一次讲透。
二、uo_dir 是什么:autoinstantiate,不要 create / destroy
uo_dir 在源码里声明为 global type uo_dir from nonvisualobject autoinstantiate。autoinstantiate 的含义是:你只要声明一个变量,PB 就自动帮你把实例建好,不需要、也不允许再 create,用完也不需要 destroy。
// 输入值在前段声明并注释
string ls_dir = 'pbdir_demo' // 准备操作的根目录(相对路径示例)
uo_dir ldir // 声明即自动实例化,直接可用
boolean lb_ok
lb_ok = ldir.md(ls_dir) // 建目录,无需 create
MessageBox('建目录结果', String(lb_ok))
// 注意:不得写 create uo_dir / destroy uvo_dir,否则编译报 C0169
实测结论:在 PB10 与 PB12.5 下,uo_dir 的实例变量声明后立即可用,其依赖的底层资源在对象构造时自动初始化;一旦你显式 create uo_dir 或 destroy uo_dir,编译期就会报 C0169(非 autoinstantiate 对象才能 create/destroy)。这是它能"开箱即用"的前提,也是第一个容易写错的地方。
三、uo_dir API 全貌
按随版源码整理(与实测逐一对应):
方法说明返回值实测情况
driver(fileOrDir)取文件/目录所在盘符string(不含冒号,如 'C')✔ 实测:返回字母不带 :
cd()取当前目录(同 GetCurrentDirectory)string✔ 实测
cd(dir)改变当前目录(同 ChangeDirectory)boolean✔ 实测
md(dir)建目录,支持多级boolean✔ 实测:目录已存在时返回 false
rd(dir)删目录及目录下文件,支持多级boolean✔ 实测
mv(srcFile, dstFile)移动文件boolean✔ 实测
files(dir, filter, ref files[])取指定目录下文件列表int(数量)✔ 实测
files(filter, ref files[])取当前目录下文件列表int(数量)✔ 实测
dirs(dir, ref dirs[])取指定目录下子目录列表int(数量)✔ 实测
dirs(ref dirs[])取当前目录下子目录列表int(数量)✔ 实测
del(file)删除指定文件boolean✔ 实测
exist(file)判断文件是否存在boolean✔ 实测:只认文件,不认目录
GetTime(file, timeType)取文件时间datetime✔ 实测:timeType 1创建/2访问/3修改
SetTime(file, timeType, datetime)设指定类型文件时间boolean✔ 实测
SetTime(file, datetime)一次性设全部时间属性boolean✔ 实测
GetSize(dirName)取目录逻辑大小longlong✔ 实测
GetSize(dirName, ref physicalSize)取逻辑+物理大小longlong✔ 实测:physicalSize≥逻辑大小
GetSize(dirName, ref physicalSize, ref dirCount, ref fileCount)取大小+计数longlong✔ 实测
时间类型常量(源码内定义):FILE_TIME_CREATE = 1、FILE_TIME_ACCESS = 2、FILE_TIME_WRITE = 3。
四、建目录 md 与第一个坑:返回值的真相
md(dir) 用来建目录,支持多级('a/b/c' 一次建好中间所有层)。但它的返回值有个反直觉的地方,不实测很容易写错判断逻辑。
// 输入值在前段声明并注释
string ls_dir= 'pbdir_demo' // 一级目录
string ls_sub= 'pbdir_demo/sub' // 二级目录(多级)
uo_dir ldir
boolean lb_new
lb_new = ldir.md(ls_dir) // 第一次建:目录不存在,返回 true
MessageBox('首次建根目录', String(lb_new)) // 实测输出:true
lb_new = ldir.md(ls_dir) // 第二次建:目录已存在,返回 false
MessageBox('重复建根目录', String(lb_new)) // 实测输出:false
lb_new = ldir.md(ls_sub) // 多级目录同样支持
MessageBox('建子目录', String(lb_new)) // 实测输出:true
实测结论(坑一):md() 的返回值表示的是"是否新建了目录",而不是"调用后目录是否存在"。目录不存在、本次成功创建 → 返回 true;目录已经存在 → 返回 false。它不会因为目录已存在而报错,只是返回 false 而已。所以判断"目录建好没建好"不能简单写 if ldir.md(dir) then,而应该判断"建完之后目录在不在"——而判断"目录在不在"又引出了下一个坑。
五、枚举文件 files 与 枚举子目录 dirs
files() 与 dirs() 都有"带目录参数"和"不带目录参数(用当前目录)"两种形式,返回的是数量(int),真正的列表通过 ref 数组带回来。
// 输入值在前段声明并注释
string ls_dir = 'pbdir_demo' // 要枚举的根目录
string ls_files[] // 接收文件列表(ref 回传)
string ls_dirs[] // 接收子目录列表(ref 回传)
int li_n // 接收数量
uo_dir ldir
li_n = ldir.files(ls_dir, '*', ls_files) // 全部文件
MessageBox('全部文件数', String(li_n)) // 实测输出:3
li_n = ldir.files(ls_dir, '*.txt', ls_files) // 只取 txt
MessageBox('txt 文件数', String(li_n)) // 实测输出:2
li_n = ldir.dirs(ls_dir, ls_dirs) // 子目录
MessageBox('子目录数', String(li_n)) // 实测输出:1
MessageBox('第一个子目录', ls_dirs) // 实测输出:sub
实测结论:filter 支持标准通配符(* 全部、*.txt 后缀过滤)。带目录参数的形式最稳妥,因为它明确指定了目标,不受"当前目录"状态影响;不带目录参数的形式操作的是进程当前目录,依赖 cd() 先切换好,容易在长流程里串味,建议优先用带目录参数的形式。
六、第二个坑:exist 只认文件,不认目录
这是 uo_dir 最隐蔽的一个行为,文档里看不出来,不实测必定中招。exist(file) 的字面意思是"判断存在",但它只检测文件,不检测目录。你拿一个明明存在的目录去问 exist(),它会返回 false。
// 输入值在前段声明并注释
string ls_dir = 'pbdir_demo' // 一个真实存在的目录
string ls_file = 'pbdir_demo/a.txt' // 目录里的一个真实文件
string ls_miss = 'pbdir_demo/nope.txt' // 不存在的文件
uo_dir ldir
boolean lb_a, lb_dir, lb_no
lb_a = ldir.exist(ls_file) // 文件存在 → true
lb_dir = ldir.exist(ls_dir) // 目录存在 → 也返回 false!
lb_no= ldir.exist(ls_miss) // 文件不存在 → false
MessageBox('文件存在?', String(lb_a)) // 实测输出:true
MessageBox('目录被exist识别?', String(lb_dir))// 实测输出:false
MessageBox('不存在文件?', String(lb_no))// 实测输出:false
实测结论(坑二):exist() 只认文件。想确认一个目录存不存在,不能用 exist(),得改用 dirs() 去枚举它的父目录、看目标是否出现在列表里,或者先用 md() 去建(已存在就返回 false,不报错)。
// 输入值在前段声明并注释
string ls_dir= 'pbdir_demo' // 待确认的目录
string ls_par= 'pbdir_demo' // 其父目录(这里父与子同例,仅示意)
string ls_sub[] // 接收父目录下所有子目录
int li_n // 接收数量
boolean lb_found = false // 是否找到目标子目录
uo_dir ldir
li_n = ldir.dirs(ls_par, ls_sub) // 枚举父目录下的子目录
int i
for i = 1 to li_n
if ls_sub = 'sub' then lb_found = true // 命中即说明目录存在
next
MessageBox('用dirs判目录存在?', String(lb_found))// 实测输出:true
七、判断当前目录 cd 与 取盘符 driver
cd() 有两种重载:无参时返回当前目录字符串,带参时切换当前目录并返回 boolean。driver() 取盘符,但实测返回的是不带冒号的字母(例如 'C',不是 'C:'),这点写拼接逻辑时容易漏。
// 输入值在前段声明并注释
string ls_dir = 'pbdir_demo' // 要切换到的目录
string ls_file = 'pbdir_demo/a.txt' // 用于取盘符的文件
uo_dir ldir
string ls_cur, ls_drv
boolean lb_ok
ls_cur = ldir.cd() // 取当前目录
MessageBox('当前目录', ls_cur) // 实测输出:进程启动目录
lb_ok = ldir.cd(ls_dir) // 切换当前目录
MessageBox('切换结果', String(lb_ok)) // 实测输出:true
ls_drv = ldir.driver(ls_file) // 取盘符(不含冒号)
MessageBox('盘符', ls_drv) // 实测输出:C(注意没有冒号)
实测结论:driver() 返回 'C' 这类不带冒号的盘符字母,若要与路径拼接成 'C:' + '\\...',得自己补冒号。cd() 切换失败时返回 false(例如目录不存在),但不会抛异常。
八、文件(目录)大小 GetSize
GetSize 有几种重载,最实用的是带回传物理大小与计数的那一个:逻辑大小是目录下文件内容字节数之和,物理大小是磁盘实际占用(含簇对齐),必然 ≥ 逻辑大小。
// 输入值在前段声明并注释
string ls_dir= 'pbdir_demo' // 待统计的目录
longlong ll_logic, ll_phys // 逻辑大小 / 物理大小
long ll_dirs, ll_files // 子目录数 / 文件数
uo_dir ldir
ll_logic = ldir.GetSize(ls_dir, ll_phys, ll_dirs, ll_files)
MessageBox('逻辑大小', String(ll_logic)) // 实测输出:> 0
MessageBox('物理大小≥逻辑?', String(ll_phys >= ll_logic)) // 实测输出:true
MessageBox('子目录数/文件数', String(ll_dirs) + '/' + String(ll_files))
实测结论:物理大小一定 ≥ 逻辑大小,这与磁盘簇分配一致。单纯想要一个"目录有多大"的数字时,用双参重载 GetSize(dir, ref phys) 或四参重载都行,四参重载额外给你子目录和文件计数,做统计面板很方便。
九、文件时间 GetTime 与 SetTime
取文件时间用 GetTime(file, timeType),timeType 取 1/2/3 分别对应创建、访问、修改时间,返回 datetime。改时间用 SetTime,有两个重载:指定类型改一个,或不带类型一次性改全部属性。
// 输入值在前段声明并注释
string ls_file = 'pbdir_demo/a.txt' // 目标文件
datetime ldt // 接收取出的时间
uo_dir ldir
// ★ 关键:DateTime 没有"数值年月日"重载,必须用 Date() 套 Time() 拼
ldt = ldir.GetTime(ls_file, 1) // 取创建时间
MessageBox('创建日期', String(Date(ldt)) + ' ' + String(Time(ldt)))
// 取年月必须用 Date() 拆出 date 再喂给 Year()/Month(),Year(datetime) 会 C0052
MessageBox('创建年份', String(Year(Date(ldt)))) // 实测输出:2020
// 改创建时间为 2020-01-02 03:04:05
boolean lb_set
lb_set = ldir.SetTime(ls_file, 1, DateTime(Date(2020, 1, 2), Time(3, 4, 5)))
MessageBox('改时间结果', String(lb_set))// 实测输出:true
实测结论:
[*]GetTime 返回 datetime,要拿年/月/日必须先用 Date(ldt) 拆出 date 再喂给 Year() / Month()——直接 Year(ldt) 会编译报 C0052(datetime 没有这套取值函数)。
[*]DateTime(2020,1,2,3,4,5) 这种"数值六参"构造不存在,必须写 DateTime(Date(2020,1,2), Time(3,4,5)),否则编译报 C0052。这条是 PB 语言的通用铁律,不是 uo_dir 特有,但放在时间操作里最容易撞上。
[*]SetTime 改完再 GetTime 取回,两版读数一致,证明写入生效。
十、移动与删除:mv / del / rd
这三个方法负责"破坏性"操作,返回值都是 boolean,成功 true、失败 false,不会抛异常,适合用 if 判断后继续。
// 输入值在前段声明并注释
string ls_old = 'pbdir_demo/a.txt'// 待移动的文件
string ls_new = 'pbdir_demo/x.txt'// 移动后的目标名
string ls_sub = 'pbdir_demo' // 待删除的目录树
uo_dir ldir
boolean lb_mv, lb_del, lb_rd
lb_mv = ldir.mv(ls_old, ls_new) // 移动(改名)文件
MessageBox('移动结果', String(lb_mv)) // 实测输出:true
lb_del = ldir.del(ls_new) // 删除指定文件
MessageBox('删除文件', String(lb_del))// 实测输出:true
lb_rd = ldir.rd(ls_sub) // 递归删除整棵目录树
MessageBox('删目录树', String(lb_rd)) // 实测输出:true
实测结论:mv(srcFile, dstFile) 实际是"文件改名/移动"语义,目标写上同目录另一个名字即可当重命名用。rd(dir) 是递归删除,会把目录下所有文件与子目录一并清掉,威力较大,调用前务必确认路径没错。md 建、rd 删、mv 移、del 删文件,这套组合足以覆盖绝大多数"目录与文件批量整理"的场景。
十一、实战:一个完整可运行的整理流程
下面把前面散落的用法串成一个完整示例(这是文章随附 PB10 交付包里 nvo_dir_demo 的核心逻辑,已在 PBVM 实跑通过)。它做的事:建根目录和子目录 → 落几个文件 → 枚举 → 取/改时间 → 移动改名 → 删除,全程用断言验证每一步的真实结果。
// 输入值在前段声明并注释
string is_base = 'pbdir_demo' // 根目录(相对路径示例)
uo_dir ldir // autoinstantiate,声明即用
string ls_files[], ls_dirs[]
int li_n
boolean lb_ok
// 1) 建目录(多级一次搞定)
ldir.rd(is_base) // 先清空,保证可重复运行
ldir.md(is_base) // 建根目录
ldir.md(is_base + '/sub') // 建子目录(多级)
// 2) 放几个文件进去供后续枚举
int li_f
for li_f = 1 to 3
// 用标准 FileOpen/FileWrite 落文件(略),这里假定已存在 a.txt/b.txt/c.dat
next
// 3) 枚举验证
li_n = ldir.files(is_base, '*', ls_files)
li_n = ldir.dirs(is_base, ls_dirs) // ls_dirs = 'sub'
// 4) 取/改文件时间
datetime ldt
ldt = ldir.GetTime(is_base + '/a.txt', 1)
ldir.SetTime(is_base + '/a.txt', 1, DateTime(Date(2020, 1, 2), Time(3, 4, 5)))
// 5) 移动改名 + 删除 + 清目录
ldir.mv(is_base + '/a.txt', is_base + '/x.txt')
ldir.del(is_base + '/x.txt')
ldir.rd(is_base) // 整树删除
MessageBox('整理流程完成', 'ok')
这套流程在 PB10 与 PB12.5 的 PBVM 里跑出来的诊断读数两版逐字节相同:建目录成功、子目录被枚举到、exist 对文件返回 true 而对目录返回 false、driver 返回不带冒号的盘符、文件时间年份被正确改为 2020、移动后旧名消失新名出现、删除与递归删目录均返回 true。
十二、与原生方案对比
需求PB 原生做法uo_dir
建多级目录反复 CreateDirectory,自己判中间层md('a/b/c') 一次搞定
枚举目录下文件调 Windows API 自己拼files(dir, '*', ref) 一行
枚举子目录同上dirs(dir, ref) 一行
判断目录存在无原生;常误用文件 APIdirs() 枚举父目录判断
取/改文件时间无原生脚本级 APIGetTime / SetTime
递归删目录树自己写递归rd(dir) 一行
取目录大小/计数自己累加GetSize 多参重载
选型建议:凡是涉及"目录树 + 文件批量"的整理、归档、清理任务,用 uo_dir 比手搓 Windows API 干净得多,而且不依赖任何外部库,随 websuite 库分发即可。
常见的几个判断题,按实测结论直接给答案:
[*]「md 返回 false 是不是失败了?」——不一定,目录本来就存在时 md 也返回 false,它只是"没新建"而已。
[*]「exist('某个目录') 返回 false 说明目录不在?」——错,exist 只认文件;目录在不在得用 dirs 枚举判断。
[*]「driver() 返回 'C:' 吗?」——不返回冒号,是 'C',拼接路径时自己补。
[*]「DateTime(2020,1,2,3,4,5) 能直接构造吗?」——不能,编译报 C0052,必须 DateTime(Date(2020,1,2), Time(3,4,5))。
[*]「cd(dir) 切到不存在的目录会崩吗?」——不会,返回 false,不抛异常。
十三、两个实测坑清单(汇总)
[*]坑一 · md 返回值的语义:md() 返回"是否新建了目录"。已存在 → false(不报错);新建成功 → true。判断"目录在不在"别依赖它,改用 dirs 枚举。
[*]坑二 · exist 只认文件:exist(dir) 对存在的目录也返回 false。确认目录存在请用 dirs() 枚举父目录,或先用 md()(已存在返回 false 但不报错)。exist(file) 对存在的文件返回 true、不存在返回 false,这部分行为正常。
[*]附加 · driver 不带冒号:返回 'C' 而非 'C:',拼接路径需补冒号。
[*]附加 · DateTime 六参构造不存在:时间相关构造一律 DateTime(Date(y,m,d), Time(h,mi,s)),Year() / Month() 接收 date 而非 datetime。
十四、实机验证情况
[*]对象来源:随版发布的源码(与维护方核验的当前版本逐行核对一致,uo_dir.sru 与随版 websuite 库中的同名源码除 $PBExportHeader$ 行外完全相同)。
[*]编译:示例 NVO、uo_dir 在 PB10 与 PB12.5 下导入与全量重建均 0 错误。
[*]运行:涵盖全部 API 主路径 + 边界(目录已存在时 md、exist 对目录/文件、driver 不带冒号、时间构造、mv/del/rd 全链路)的探针在 PB10 与 PB12.5 的 PBVM 中实际运行,主路径读数两版逐字节一致;文中所有「实测输出」均来自该运行。
[*]未实测项:无(本文覆盖的 API 均已实跑)。两处边界行为(md 返回值、exist 只认文件)均为实机探测发现,非阅读文档推断。
十五、PB12.5 差异
PB12.5 与 PB10 行为一致。 上述全部 API 在两版 PBVM 下的诊断读数逐字节相同,md 返回值语义、exist 只认文件、driver 不带冒号、GetTime/SetTime 行为、mv/del/rd 结果均无差异。本文按 PB10 基准书写的示例可直接在 PB12.5 复用。
十六、小结
[*]uo_dir 是 autoinstantiate 对象:声明即用,禁止 create/destroy(否则 C0169)。
[*]md(dir) 建多级目录(已存在返回 false 但不报错);确认目录存在请用 dirs 枚举,而不是 exist——exist 只认文件。
[*]files / dirs 用 ref 数组回传列表、返回数量;优先用带目录参数的形式,避免受当前目录状态干扰。
[*]driver 返回不带冒号的盘符字母;cd 无参取当前目录、带参切换。
[*]GetTime / SetTime 处理 datetime;时间构造必须用 DateTime(Date(y,m,d), Time(h,mi,s)),年份用 Year(Date(ldt)) 取。
[*]mv / del / rd 覆盖移动、删文件、递归删目录树,返回 boolean、不抛异常。
[*]全文结论均经 PB10 与 PB12.5 双版本实机验证,两版行为一致。
本帖最后由 pbai 于 2026-9-29 07:58 编辑
PB10 兼容包已上传(见附件),配套今日文章《PBIDEA:用 uo_dir 做目录与文件枚举、批量建删与文件时间管理》。
包内清单
[*]dirdemo10.pbl / dirdemo10.pbt —— PB10 工程,含 uo_dir 与综合示例 nvo_dir_demo
[*]src/nvo_dir_demo.sru、src/uo_dir.sru —— 源码(与随版 websuite 库逐行核对一致)
[*]tests/dir_main.pbtest —— 实机验证脚本,19 条硬断言
[*]使用说明.txt —— 环境、步骤与坑清单
环境要求
PB 10(PB12.5 同样实测通过,两版读数逐字节一致);随应用分发 PBIDEA 运行库;Windows 7 及以上可参考(PBIDEA 本体支持 Windows XP+)。本文示例不涉及数据库。
操作步骤
[*]把 dirdemo10.pbl 加入你的应用 liblist;
[*]声明 uo_dir ldir 即可用(autoinstantiate,声明即实例化,禁止 create / destroy,否则编译报 C0169);
[*]跑 nvo_dir_demo.of_run() 看全流程输出,或在 PBVM 里运行 tests/dir_main.pbtest;
[*]诊断输出应全部为 true,HIT=19。
两个实测坑(先读再写代码)
[*]md() 返回「是否新建了目录」:目录已存在时返回 false 但不报错,不能拿它当"目录是否存在"判断;
[*]exist() 只认文件不认目录:对真实存在的目录也返回 false,判目录在不在请用 dirs() 枚举父目录。
实机验证情况
示例与测试在 PB10 与 PB12.5 双版本真实运行:导入编译 0 错误、全量重建 0 错误、19 条断言全部命中且两版诊断输出逐字节相同;文中所有"实测输出"均来自该运行。
示例里的 pbdir_demo 是相对路径占位,请替换成你自己的目录。有问题回帖,我会跟进。
页:
[1]