马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_logger 与 uo_timer 做统一分级日志与周期定时任务
阅读说明
1. 适用版本:PBIDEA 1.x(uo_logger / uo_timer 是 PBIDEA 组件,底层走 PbIdea.dll;经典 PowerBuilder 无这两个对象)。
2. 支持数据库:无(本文为纯日志与定时器组件,不依赖任何数据库)。
3. 操作系统与环境要求:Windows 7+,已安装 PBIDEA 运行库(PbIdea.dll);工程需引用 websuite.pbl。
4. 难度系数:★★☆☆☆(入门偏易,会写事件处理即可上手)。
5. 其它阅读说明:前置知识——了解 PB 的「实例变量」与「事件」概念即可;示例准备——把 websuite.pbl 中的 uo_logger、uo_timer 加入应用库列表(或窗口里直接放这两个对象)。
调试一个 PB 程序时,新手最常做的三件事:弹 MessageBox、把过程写在注释里、出错就对着屏幕发呆。问题很明显——MessageBox 会卡住界面、临时信息关掉就没了、生产环境你根本不在现场。
PBIDEA 给了两个很轻量的组件来根治这个习惯:
- uo_logger:把运行过程写成分级日志文件,支持按天滚动、按大小回滚、UTF-8 中文,替代杂乱的 MessageBox 调试。
- uo_timer:基于线程的定时器,按毫秒周期触发,不占用 UI 线程、不和窗口自带的 Timer() 打架,用来做心跳、轮询、定时清理最合适。
这篇把这两个组件讲全:API 全貌、五种典型用法、跨线程红线、和老方案(MessageBox / 窗口 Timer / 自己写文件)的对比,看完就能直接用在项目里。
一、核心概念与原理
1.1 为什么不用 MessageBox 调试
MessageBox 本质是「模态阻塞」:弹出后整个线程停住等你点确定。它在你坐在电脑前排错时有用,但在三件事上彻底失效:
- 定时任务、后台线程跑到一半——你点不过来,也根本不该弹。
- 生产环境你不在现场——日志文件能留痕,MessageBox 一关就没。
- 需要统计「今天一共触发了多少次」「哪个环节最慢」——弹窗给不了数据。
日志的价值是把「一次性、看不见的过程」变成「可回看、可检索的记录」。uo_logger 直接落盘成文本文件,你随时能打开看,也能归档、能发给同事排查。
1.2 uo_logger 的底层
uo_logger 是 PBIDEA 在 PbIdea.dll 里封装的一套 C 风格文件日志(loggerOpen / loggerInfo / loggerClose 等),对外暴露成 PB 的 nonvisualobject。它有几点设计值得记住:
- 自动创建/销毁:uo_logger 被声明为「全局自动实例化」(global uo_logger uo_logger),你第一次引用它时构造函数自动 loggerCreate(),对象销毁时 loggerDestroy()。所以绝大多数场景你连 create 都不用写,直接 uo_logger.open(...) 即可。
- 分级:info / warn / error / trace / debug / critical 六个级别,对应不同严重度,方便在发布环境只留 warn/error。
- printf 风格格式化:写日志用 ('格式串', 参数...) 的变参写法,%s 填字符串、%d 填整数,和你写 C 的 printf 一样。
- 三种打开方式:普通文件、按天滚动、按大小回滚——下面逐一给可运行示例。
1.3 uo_timer 的底层
uo_timer 也是 nonvisualobject,但它带一个 timer 事件。启动后,PBIDEA 在独立线程里按毫秒周期触发这个事件——这是它和窗口 Timer() 函数最大的区别:窗口 Timer 跑在 UI 线程、一个窗口只能有一个、还容易和 DataWindow 的 Timer 风格控件冲突;uo_timer 跑在后台线程、可以建多个、互不干扰。
它的生命周期很简单:
- start(mesc) 启动,mesc 是毫秒(如 1000 = 1 秒)。
- 周期到了就触发该对象的 timer 事件,你在事件里写要重复做的事。
- stop() 停止;对象销毁时析构函数也会自动 stop()。
二、uo_logger 函数全貌
下面是所有对外方法(参数里的 readonly 是 PBIDEA 写法,照抄即可):
| 方法 | 返回值 | 说明 | | open(string logFile) | boolean | 打开一个普通日志文件 | | open(string logFile, boolean daily) | boolean | daily=true 时按天滚动,文件名自动带日期 | | open(string logFile, long max_file_size, int max_file_count) | boolean | 单文件超 max_file_size 字节就滚到下一文件,最多保留 max_file_count 个 | | close() | — | 关闭日志文件 | | info(string fmt, ...) | boolean | 写 info 级日志 | | warn(string fmt, ...) | boolean | 写 warn 级日志 | | error(string fmt, ...) | boolean | 写 error 级日志 | | trace(string fmt, ...) | boolean | 写 trace 级日志 | | debug(string fmt, ...) | boolean | 写 debug 级日志 | | critical(string fmt, ...) | boolean | 写 critical 级日志 | | Flush() | — | 把内存缓冲立即刷到磁盘 | | SetUtf8(boolean utf8) | — | true 时按 UTF-8 落盘(中文不乱码) | | OpenFile(...) / Write(...) / CloseFile() | — | 兼容老对象 uo_logfile 的写法,Write 等价于 info |
几个关键点:① 三个 open 都返回 boolean,失败(路径不存在 / 没权限 / 文件被占用)会返回 FALSE,必须判断;② 写日志后如果不 Flush(),内容可能还缓在内存里没落盘,Flush() 保证立刻可见;③ 默认按 ANSI 落盘,中文环境务必 SetUtf8(true),否则文件里中文是乱码。
三、uo_logger 典型用法
3.1 基本日志:打开 + 分级写入
前置:工程已引用 websuite.pbl,uo_logger 全局实例可直接用,无需 create。
步骤:1) 在按钮 clicked 事件里贴入下面代码;2) 运行后点按钮;3) 打开 C:/app/logs/app.log 即可看到分级日志。 - // 示例输入:日志文件完整路径(建议绝对路径,避免落在不确定的“当前目录”)
- string ls_logfile
- ls_logfile = 'C:/app/logs/app.log'
- // 示例输入:要记录的业务信息
- string ls_user
- ls_user = 'admin'
- long ll_order_id
- ll_order_id = 10086
- // 打开日志文件,失败返回 FALSE——一定要判断
- boolean lb_ok
- lb_ok = uo_logger.open(ls_logfile)
- if not lb_ok then
- MessageBox('日志打开失败', '检查路径是否存在、是否有写入权限:' + ls_logfile)
- return
- end if
- // 分级写入:printf 风格,%s 填字符串、%d 填整数
- uo_logger.info('用户 %s 登录成功', ls_user)
- uo_logger.warn('订单 %d 处理较慢,耗时偏长', ll_order_id)
- uo_logger.error('订单 %d 处理异常:数据库连接失败', ll_order_id)
- // 立即把缓冲刷到磁盘(否则可能还留在内存里)
- uo_logger.Flush()
- MessageBox('日志已写入', '请查看文件:' + ls_logfile)
复制代码
3.2 按天滚动日志
前置同上。适合「长期运行的服务」,每天一个文件,方便按日期归档排查。 - // 示例输入:基础文件名(最终会带日期后缀,如 app_2026-08-30.log)
- string ls_base
- ls_base = 'C:/app/logs/app.log'
- // 示例输入:是否按天滚动
- boolean lb_daily
- lb_daily = true
- boolean lb_ok
- lb_ok = uo_logger.open(ls_base, lb_daily)
- if not lb_ok then
- MessageBox('失败', 'open 返回 FALSE,检查路径权限')
- return
- end if
- uo_logger.info('程序启动,日期滚动日志已开启')
- uo_logger.Flush()
- MessageBox('已开启', '每日一个文件,文件名形如 app_2026-08-30.log')
复制代码
3.3 按大小回滚(防止单文件无限膨胀)
前置同上。适合「高频写日志」场景——单文件到上限自动切下一个,并只保留最近 N 个,不会把磁盘写满。 - // 示例输入:日志基础名
- string ls_base
- ls_base = 'C:/app/logs/app.log'
- // 示例输入:单文件上限(字节),这里 5MB
- long ll_max_size
- ll_max_size = 5 * 1024 * 1024
- // 示例输入:最多保留几个文件
- int li_max_count
- li_max_count = 5
- boolean lb_ok
- lb_ok = uo_logger.open(ls_base, ll_max_size, li_max_count)
- if not lb_ok then
- MessageBox('失败', 'open 返回 FALSE')
- return
- end if
- uo_logger.info('循环写入场景启动,超过 %d 字节自动滚到下一文件', ll_max_size)
- uo_logger.Flush()
- MessageBox('已开启', '单文件上限 ' + string(ll_max_size) + ' 字节,最多保留 ' + string(li_max_count) + ' 个')
复制代码
3.4 UTF-8 中文输出(务必开)
前置同上。默认 ANSI 落盘,中文环境几乎必乱码,记得 SetUtf8(true)。 - // 示例输入:日志路径
- string ls_logfile
- ls_logfile = 'C:/app/logs/cn.log'
- boolean lb_ok
- lb_ok = uo_logger.open(ls_logfile)
- if not lb_ok then
- MessageBox('失败', 'open 返回 FALSE')
- return
- end if
- // 默认按 ANSI 落盘,中文可能乱码;显式开启 UTF-8
- uo_logger.SetUtf8(true)
- uo_logger.info('中文日志:导出任务完成,共 %d 条', 200)
- uo_logger.Flush()
- MessageBox('已写入', '文件以 UTF-8 编码保存,中文不乱码')
复制代码
3.5 兼容老对象 uo_logfile
前置同上。如果你历史代码用的是老 uo_logfile(OpenFile/Write/CloseFile),无需改写,Write 等价于 info: - // 示例输入:老式日志路径
- string ls_logfile
- ls_logfile = 'C:/app/logs/legacy.log'
- boolean lb_ok
- lb_ok = uo_logger.OpenFile(ls_logfile)
- if not lb_ok then
- MessageBox('失败', 'OpenFile 返回 FALSE')
- return
- end if
- // Write 等价于 info,其余用法一致
- uo_logger.Write('老接口兼容日志:订单 %d 处理完成', 10086)
- uo_logger.CloseFile()
- MessageBox('已写入', '用 OpenFile/Write/CloseFile 兼容写法完成')
复制代码
四、uo_timer 函数全貌
| 方法 / 事件 | 说明 | | start(long mesc) | 启动定时器,每 mesc 毫秒触发本对象的 timer 事件 | | start(long mesc, readonly powerobject bindObject) | 同上,但把触发路由到 bindObject 的 timer 事件(让别的对象来响应) | | stop() | 停止定时器 | | event timer() | 周期到了自动触发的事件,重复逻辑写在这里 |
注意:uo_timer 是「线程异步」定时器。它在后台线程触发 timer 事件,所以事件里不要直接操作 UI 控件(dw_1.Retrieve、文本框赋值等)——跨线程操作界面是 PB 红线,应通过事件/Post 回到 UI 线程再做。只做「写日志、算数值、调无界面的后台对象」这类事是安全的。
五、uo_timer 典型用法
5.1 在窗口里放一个定时器并周期触发
前置:窗口 w_main 上已放入 uo_timer 对象,命名为 iuo_timer(或声明实例变量 uo_timer iuo_timer)。
步骤:1) 在 iuo_timer 的 timer 事件里贴入「事件代码」;2) 在窗口按钮 cb_start 的 clicked 里贴入「启动代码」;3) 在 cb_stop 里贴入「停止代码」;4) 运行窗口点开始,2 秒一次看到日志与弹窗。
启动代码(按钮 cb_start 的 clicked):
- // 示例输入:定时周期(毫秒),这里 2 秒一次
- long ll_interval
- ll_interval = 2000
- // 如果用的是实例变量而非窗体上的对象,先 create
- iuo_timer = create uo_timer
- // 启动:每 ll_interval 毫秒触发一次 iuo_timer 的 timer 事件
- iuo_timer.start(ll_interval)
- MessageBox('定时器已启动', '每 ' + string(ll_interval) + ' 毫秒触发一次')
复制代码
停止代码(按钮 cb_stop 的 clicked):
- // 示例输入:要停止的定时器实例(必须与启动时同一个)
- // iuo_timer 是窗口实例变量,已在启动时使用
- if isvalid(iuo_timer) then
- iuo_timer.stop()
- destroy iuo_timer
- end if
- MessageBox('定时器已停止', '周期任务结束')
复制代码
timer 事件代码(写在 iuo_timer 的 timer 事件里):
- // 实例变量 il_tick 计数触发次数(在窗口/对象里声明:long il_tick = 0)
- il_tick = il_tick + 1
- // 每次触发写一条日志(uo_timer 在后台线程,写文件日志是安全的)
- uo_logger.info('定时任务第 %d 次触发,执行心跳检查', il_tick)
- // 仅为演示可观察:正式环境请删除此行,改为你的业务逻辑
- MessageBox('定时触发', '第 ' + string(il_tick) + ' 次')
复制代码
5.2 把触发路由到别的对象(bindObject 重载)
前置同上。如果你不想单独建 uo_timer 事件,也可以让「窗口自己的 timer 事件」来响应:把窗口 this 作为 bindObject 传进去。 - // 示例输入:周期(毫秒)
- long ll_interval
- ll_interval = 1000
- // 把触发路由到当前窗口(this)的 timer 事件,而不是 iuo_timer 的
- iuo_timer = create uo_timer
- iuo_timer.start(ll_interval, this)
- MessageBox('已启动', '触发将走本窗口的 timer 事件')
复制代码
窗口 w_main 的 timer 事件里就可以写周期逻辑了(用法同上,写日志、算数、别碰 UI)。
六、边界情况与常见坑
- open 不判断返回值:路径不存在、目录没建、文件被别的进程占用,open 都会静默返回 FALSE,之后 info 写啥都不落盘。所有 open 都配 if not lb_ok then ... 判断。
- printf 占位符与参数要匹配:%s 配字符串、%d 配整数,数量也要一致。写成 uo_logger.info('订单 %d', ls_user)(%d 却传字符串)属于 C printf 的未定义行为,多半乱码甚至崩。一个坑:long 用 %d 没问题,但 double 要用 %f,小数别套错。
- 中文乱码:没 SetUtf8(true) 时按 ANSI 落盘,在 GBK 系统上中文可能变成问号。发布到不同语言环境前一定先确认编码。
- 不 Flush 看不到最新日志:日志有内存缓冲,Flush() 才立刻落盘。程序异常退出或你想实时 tail 文件时,关键节点后补 Flush()。
- 跨线程操作 UI 是红线:uo_timer 跑在后台线程,timer 事件里直接 dw_1.Retrieve()、sle_1.text = ... 会出问题。正确做法:在 timer 事件里只做无界面工作(写日志、算数、调后台对象),需要用界面的部分通过 this.event trigger 事件名() 或 Post 回 UI 线程执行(呼应 Day10 uo_thread 的跨线程红线)。
- 多个定时器互相独立:你可以建 iuo_timer_a、iuo_timer_b 各跑各的周期,互不影响;停的时候要分别 stop() 对应的实例,别漏掉。
- 析构会自动 stop,但业务清理要靠自己:uo_timer 销毁时析构函数自动 stop(),可它不会帮你保存状态、写「停止日志」。需要在停止时落盘记录什么,自己在 stop() 之前显式写。
- 全局 uo_logger 是单例:直接用全局 uo_logger 只会写一个文件。如果一个程序要同时写「业务日志」和「错误日志」两个文件,用 create uo_logger 建独立实例分别 open。
七、与相近方案对比
7.1 日志方案对比
| 方案 | 优点 | 缺点 | 适用 | | MessageBox 调试 | 零成本、立刻看到 | 阻塞界面、不留痕、生产环境用不了 | 临时排错 | | 自己写文件函数 | 完全可控 | 要自己处理打开/缓冲/编码/滚动 | 不想引入组件 | | 老 uo_logfile | 兼容旧代码 | 功能少、无分级/滚动 | 历史工程 | | uo_logger | 分级 + 按天/按大小滚动 + UTF-8 + 零依赖 | 需 PBIDEA 运行库 | 正式项目首选 |
7.2 定时方案对比
| 方案 | 线程 | 数量 | 与 UI 冲突 | 适用 | | 窗口 Timer() 函数 | UI 线程 | 每窗口 1 个 | 易和 DataWindow Timer 控件冲突 | 简单单一定时 | | Windows SetTimer API | UI 线程 | 需自己管 | 同上 | 不引组件时 | | uo_thread 周期任务 | 后台线程 | 灵活 | 不冲突 | 重活、要异步 | | uo_timer | 后台线程 | 多个 | 不冲突 | 轻量心跳/轮询 |
结论:轻量、要跑多个、不想占 UI 的定时,直接用 uo_timer;重活、要和数据/界面深度交互的,配合 uo_thread 丢后台。
八、进阶扩展
- 日志级别随环境升降:开发时 debug/trace 全开,发布时只留 warn/error,既保留排错信息又不刷屏。
- 定时 + 日志组合:在 uo_timer 的 timer 事件里写 uo_logger.info('心跳 %d', il_tick),就能得到一份「程序是否还活着」的运行轨迹,出事了直接翻日志。
- 日志归档:按大小/日期滚动产生的历史日志,可用 Day16 讲的 uo_zip 定期打包压缩,避免散落一堆小文件。
- 多文件日志:用 create uo_logger 建两个实例,一个写业务、一个写错误,故障排查时一眼分开。
把「弹 MessageBox 排错」换成「uo_logger 写日志 + uo_timer 做周期任务」,你的程序会从「人盯着才跑得明白」变成「自己留下运行档案」。这两个组件都很小,但养成习惯后,定位和复盘问题的成本会大幅下降。 |