马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
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[1]) // 实测输出: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[i] = '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[1] = '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) 一行 | | 判断目录存在 | 无原生;常误用文件 API | dirs() 枚举父目录判断 | | 取/改文件时间 | 无原生脚本级 API | GetTime / 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 双版本实机验证,两版行为一致。
|