马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder 事件机制全解:TriggerEvent / PostEvent、事件参数 Message 对象与事件返回值(PB10 基准 · PB12.5 实测)
阅读说明
① 适用版本:PowerBuilder 10 基准(PB 8 及以上语法同源,PB 11.5+ 可用但未另行验证);不涉及 PBIDEA 组件。
② 支持数据库:本文不涉及数据库,示例为纯 PowerScript 语言层代码。
③ 操作系统与环境:Windows 7 及以上;无需任何第三方运行库,导入对象即可运行。
④ 难度系数:★★★☆☆(概念层结论较多,但全部可最小化验证)。
⑤ 前置阅读:建议先读本目录《数组与结构全解》《面向对象全解》《共享对象与多线程实战》三篇;本文与其互补——那篇讲"共享对象里的 PostEvent 异步边界",这篇讲"事件本身的触发、传参、返回值与残留"。
一、事件和函数,到底差在哪
PB 里事件(Event)和函数(Function)长得几乎一样:都能带参数、都能有返回值、都能挂在对象上。但有三条本质区别,先用一张表说清:
| 维度 | 函数 | 事件 | | 谁来调 | 只能你的代码显式调用 | 运行时环境可以替你调(点击、计时、系统消息),代码也能调 | | 返回值 | 调用表达式直接拿到 | 脚本里的 return 一直在,但只有"直调"拿得到,经 TriggerEvent 触发拿不到 | | 覆盖行为 | 后代覆盖祖先,super:: 逐级上溯 | 后代写了同名脚本就是覆盖祖先,祖先脚本不再自动执行 |
第三条在《面向对象全解》那篇已经用 super:: 实测过,本文不再重复。本文集中解决前两条加"参数怎么传"。
先看声明。在事件列表里声明一个带返回值、带参数的自定义事件,导出源码长这样(这两行是 PB 导出格式的标准写法,注意 type long):
- // 导出源码视角:带返回值的事件声明用 "event type <返回类型> <事件名>(参数)"
- // 事件原型写在 global type 块内
- event type long ue_add ( long a, long b )
- // 脚本体单独成块
- event type long ue_add(long a, long b);/*类型化参数事件:直接调用并返回值*/
- return a + b
- end event
复制代码
这里有个实测过的编译红线:带 return 的事件必须声明成 event type <类型>。如果事件没写返回类型(subroutine 型事件),脚本里却写了 return 表达式,导入直接报 C0092 "Return expression in subroutine"——本轮实测踩过,第一版探针就栽在这里。没写返回类型的事件只能裸 return 或不写。
二、三种触发方式:返回值行为完全不同
同一个事件,三种触发方式,返回值语义各不相同——这是本文最重要的一组实测结论。
- // 演示:同一个事件 ue_touch(脚本内 return 99),三种触发方式对照
- // 输入值:无外部输入,直接演示
- long ll_direct
- long ll_trig
- boolean lb_post
- nvo_evt_probe lnv
- lnv = create nvo_evt_probe
- // 方式一:直调(event 调用)——同步,拿到事件脚本自己的 return
- ll_direct = lnv.event ue_touch() // 实测 = 99
- // 方式二:TriggerEvent——同步执行脚本,但返回值恒为 1(成功标志),不是 99
- ll_trig = lnv.TriggerEvent('ue_touch') // 实测 = 1
- // 方式三:PostEvent——只负责入队,返回 boolean 表示"排队成功与否"
- lb_post = lnv.PostEvent('ue_touch') // 实测 = true(事件名不存在时 = false)
- MessageBox('三种触发方式', '直调 return=' + String(ll_direct) + &
- '~nTriggerEvent 返回=' + String(ll_trig) + &
- '~nPostEvent 返回=' + String(lb_post))
- destroy lnv
复制代码
实测结论(PB10 与 PB12.5 双版一致,26 条硬断言两版全中):
- 直调:object.event ue_x(...) 同步执行,返回值就是脚本里的 return 值。要拿事件的处理结果,只能用这种方式。
- TriggerEvent:同样同步执行(脚本当场跑完),但返回值恒为 1(成功)或 -1(事件名不存在,实测 B3 = -1 且不抛异常)。事件脚本里的 return 值被 TriggerEvent 丢弃——实测事件 return 7,TriggerEvent 返回 1。
- PostEvent:异步入队,返回 boolean(true 入队成功 / false 事件不存在,实测)。它连"同步执行"都没有,返回值只反映排队结果。
顺带钉死一个易错点:直调事件的语法是 object.event 事件名(参数),event 这个关键字不能省——省了就按函数名解析,编译报 C0051 Unknown function name(本轮实测于测试包装器编译)。
三、事件参数:类型化参数是正道,Message 两槽是补充
事件传参有两条路,能力差别很大。
第一条路:类型化参数(推荐)。 事件像函数一样声明参数表,直调时按类型传:
- // 事件声明带类型化参数,直调时按位置传参,返回值、参数类型编译期全检查
- long ll_sum
- nvo_evt_probe lnv
- lnv = create nvo_evt_probe
- ll_sum = lnv.event ue_add(3, 4) // 实测 = 7
- MessageBox('类型化参数事件', 'ue_add(3, 4) = ' + String(ll_sum))
- destroy lnv
复制代码
第二条路:TriggerEvent / PostEvent 的通用参数槽。 这两个函数的参数表是 (事件名, word, long)——注意顺序,第二个参数进 WordParm,第三个参数进 LongParm。实测对照:
- // 演示:TriggerEvent 两个数字槽各落谁家
- // 事件 ue_readmsg 内部读取 message.wordparm / message.longparm 存入实例变量
- nvo_evt_probe lnv
- lnv = create nvo_evt_probe
- // 42 放 word 槽、0 放 long 槽
- lnv.TriggerEvent('ue_readmsg', 42, 0) // 实测:WordParm=42,LongParm=0
- // 0 放 word 槽、99 放 long 槽
- lnv.TriggerEvent('ue_readmsg', 0, 99) // 实测:WordParm=0,LongParm=99
- MessageBox('参数槽归属', '第一组 WordParm=' + String(lnv.il_word) + &
- ' LongParm=' + String(lnv.il_long) + &
- '~n注意:il_long/il_word 是 ue_readmsg 里存下来的值')
- destroy lnv
复制代码
这里有几个实测细节值得单独说:
- StringParm / DoubleParm / PowerObjectParm 与 TriggerEvent、PostEvent 无关。触发后实测 StringParm 长度仍为 0、DoubleParm 仍为 0。这三个属性是 OpenWithParm / CloseWithReturn 窗口家族的领地(窗口部分依赖真实窗口环境,本篇未实测,仅静态核对官方文档口径)。
- word 槽的边界:Message.WordParm 类型是 UInt(上限 65535)。本机两版 PBVM 实测传 65537 原样落在 WordParm(用 long 变量接读,未丢失);但跨版本稳妥起见,别往 word 槽塞超过 65535 的值,大数值一律走 long 槽(实测 65537 走 long 槽无损)。
- 双槽不够用怎么办:要么用类型化参数直调(编译期检查最完整);要么把多个值打包进结构或 NVO,经实例变量传递;窗口间传对象才轮到 OpenWithParm + Message.PowerObjectParm。
四、Message 残留与排队参数快照:两个反直觉实测
Message 是全局对象(整个应用只有一个实例),这带来两个行为,都是实测出来的。
行为一:不带参触发,Message 上一轮的值原样残留。
- // 演示:先触发 (word=0, long=99),再不带任何参数触发另一个事件
- // ue_touch 内部读取 message.longparm 存入 il_touch_long
- nvo_evt_probe lnv
- lnv = create nvo_evt_probe
- lnv.TriggerEvent('ue_readmsg', 0, 99) // LongParm=99
- lnv.TriggerEvent('ue_touch') // 不带参触发
- MessageBox('残留实测', 'ue_touch 里读到的 LongParm=' + &
- String(lnv.il_touch_long) + '(实测 = 99,上一轮的值原样残留)')
- destroy lnv
复制代码
所以写"无参事件"时,永远不要默认 Message 属性是干净的。要么每次都显式赋值,要么干脆不依赖 Message。
行为二(好消息):PostEvent 排队的参数是"入队时的快照",不会被后续触发污染。 经典恐惧是"Message 是单例,我 PostEvent 之后别人又 TriggerEvent 把参数改了,我的回调是不是就读错了"。实测否定了这个担心:
- // 演示:先 PostEvent 排队(long=11),紧接着 TriggerEvent(long=99),等派发后看读到几
- nvo_evt_probe lnv
- long li
- lnv = create nvo_evt_probe
- lnv.PostEvent('ue_readmsg', 0, 11) // 排队:参数 11 入队
- lnv.TriggerEvent('ue_readmsg', 0, 99) // 立即执行:il_long=99
- for li = 1 to 50
- Yield() // 泵消息循环,让排队事件有机会派发
- next
- // 实测:立即执行后 il_long=99;派发后 il_long=11(快照未被污染)
- MessageBox('排队参数快照', 'TriggerEvent 立即执行读 99;~n' + &
- 'Yield 后排队事件派发读 11(实测)——入队时参数已快照')
- destroy lnv
复制代码
连排两个 PostEvent 也实测了:先排 111 再排 222,泵完消息后读到 222,两个事件各带各的参数,互不覆盖。PostEvent 的参数安全性比传说中好——但前提是参数走 word/long 槽;字符串和对象槽不归它管,自然也谈不上快照。
五、PostEvent 的派发时机:消息循环说了算
PostEvent 只是入队。事件真正执行,要等消息循环被泵到那一刻:当前脚本执行完、Yield() 被调用、或系统空闲。本轮在无界面的 PBVM 运行环境实测:PostEvent 后连调 50 次 Yield(),排队事件稳定被派发(实例计数器 +1,参数原样到达)。
由此推出两条纪律:
- 长计算里不要指望 PostEvent 的回调及时执行。一个不泵消息的循环里,排队事件永远轮不上——这正是《共享对象与多线程实战》那篇"PostEvent 0 ms 返回"的另一面:返回快,是因为它什么都不等。
- PostEvent 之后立刻读 Message.LongParm 是无意义的——派发时机不确定,你读到的是"当下"不是"届时"。回调里读 Message 才是正确姿势(快照安全性见上一节)。
六、动态调用:event dynamic 与一条重要的实测差异
祖先类型的变量装着后代实例时,想调后代独有的事件,用 event dynamic:
- // 演示:后代独有事件 ue_dyn,用祖先变量 dynamic 调用
- // nvo_evt_child 继承自 nvo_evt_probe,独有事件 ue_dyn(把 il_dyn 置 1)
- nvo_evt_child lch
- nvo_evt_probe lbase
- lch = create nvo_evt_child
- lbase = lch // 祖先变量装后代实例
- lbase.event dynamic ue_dyn() // dynamic:编译期不检查事件是否存在
- MessageBox('dynamic 调用', '后代事件执行后 il_dyn=' + String(lch.il_dyn) + &
- '(实测 = 1)')
- destroy lch
复制代码
以及一条必须写清的实测差异:dynamic 调用不存在的事件,本机两版 PBVM 均不报错——不抛 RuntimeError、脚本静默不执行。《面向对象全解》实测过函数版 dynamic 调不存在的方法会抛运行时错误 65,事件版在这台环境上是"静默无效"。别指望运行时替你抓事件名拼错。
七、错误路径:两个返回值就是全部防御
事件名是字符串参数,编译期不检查,拼错只能靠这两个返回值兜底(均实测):
- TriggerEvent('不存在的事件') → 返回 -1,不抛异常;
- PostEvent('不存在的事件') → 返回 false。
- // 演示:错误路径的两个返回值
- nvo_evt_probe lnv
- long ll_rc
- boolean lb_ok
- lnv = create nvo_evt_probe
- ll_rc = lnv.TriggerEvent('ue_no_such') // 实测 = -1
- lb_ok = lnv.PostEvent('ue_no_such') // 实测 = false
- MessageBox('错误路径', 'TriggerEvent 不存在的事件=' + String(ll_rc) + &
- '~nPostEvent 不存在的事件=' + String(lb_ok) + &
- '~n都不抛异常,必须判返回值')
- destroy lnv
复制代码
八、完整示例:用事件做"可取消的保存流程"
把上面的结论拼成一个能跑的完整对象:of_save 保存前先问 ue_confirm(返回 0 放行、非 0 拦截),通过后触发 ue_log 记录。后代对象覆盖 ue_confirm 就能拦截特定动作——这正是 DataWindow ItemChanged 返回 1 拦截输入的同款模式。
- // nvo_evt_demo 核心脚本(完整源码见附件包)
- // 事件 ue_confirm:确认回调,默认放行
- event type long ue_confirm(string as_action);/*确认回调:默认放行(返回 0 继续执行)*/
- return 0
- end event
- // 事件 ue_log:记录日志,取 Message.LongParm 作为本次记录值
- event ue_log();/*记录日志:取 Message.LongParm 作为本次记录值*/
- il_log_count = il_log_count + 1
- il_last_log = message.longparm
- end event
- // 函数 of_save:先问 ue_confirm,通过后触发 ue_log
- public function string of_save(string as_name);/*保存流程:0 放行/非 0 拦截*/
- long ll_rc
- string ls_out
- ll_rc = this.event ue_confirm('save:' + as_name) // 直调拿返回值,不能走 TriggerEvent
- if ll_rc <> 0 then
- ls_out = 'canceled:rc=' + String(ll_rc)
- return ls_out
- end if
- this.TriggerEvent('ue_log', 0, Len(as_name)) // LongParm 传名称长度
- ls_out = 'saved:' + as_name
- return ls_out
- end function
复制代码- // 调用示例(子类 nvo_evt_demo_child 覆盖 ue_confirm:含 delete 字样一律返回 1)
- // 输入值:两个测试用的动作名
- string ls_name1
- string ls_name2
- nvo_evt_demo ld
- nvo_evt_demo_child ldc
- ls_name1 = 'abc' // 正常动作
- ls_name2 = 'delete row 5' // 会被子类拦截的动作
- ld = create nvo_evt_demo
- ldc = create nvo_evt_demo_child
- MessageBox('父类放行', ld.of_save(ls_name1) + '~n日志条数=' + &
- String(ld.of_log_count()) + '~n记录值=' + String(ld.of_last_log()))
- // 实测输出:saved:abc / 日志条数=1 / 记录值=3
- MessageBox('子类拦截', ldc.of_save(ls_name2) + '~n放行的:' + &
- ldc.of_save(ls_name1) + '~n日志条数=' + String(ldc.of_log_count()))
- // 实测输出:canceled:rc=1 / saved:abc / 日志条数=1
- destroy ld
- destroy ldc
复制代码
两版实测输出完全一致:R1_save=saved:abc、R2_count=1、R3_last=3、R4_del=canceled:rc=1、R5_ok=saved:abc、R6_count=1。注意第四项:拦截发生在 of_save 里、ue_confirm 返回非 0 时 ue_log 根本没触发,所以日志条数停在 1。
九、PB12.5 差异
无。 本文 26 条硬断言 + 完整示例 6 项输出,PB10 与 PB12.5 逐字节一致。
十、坑清单(14 条,全部实测背书)
- TriggerEvent 拿不到事件的 return(恒 1/-1);要返回值必须 event 直调。
- TriggerEvent 同步执行、PostEvent 异步入队;"同步/异步"的分界不是有没有返回值。
- TriggerEvent / PostEvent 参数槽是 (word, long):第二参进 WordParm、第三参进 LongParm。
- StringParm / DoubleParm / PowerObjectParm 与 TriggerEvent、PostEvent 无关(实测触发后原值不变)。
- Message 全局残留:不带参触发后 LongParm 是上一轮的值(实测 99 残留)。
- PostEvent 排队参数是入队快照,后续 TriggerEvent 改 Message 不会污染它(实测 11 存活)。
- 连排多个 PostEvent 各带各的参数(实测 111/222 → 派发读到 222)。
- PostEvent 派发时机=消息循环被泵(Yield / 空闲 / 当前脚本结束);长计算里排队事件不执行。
- PostEvent 返回 boolean(true/false),不是 long;用 long 接直接 C0008 编译错(本轮真实踩过)。
- TriggerEvent('不存在') 返 -1 不抛异常;PostEvent('不存在') 返 false——事件名拼错必须靠返回值兜底。
- event dynamic 调不存在的事件:本机两版 PBVM 静默无效(对照:函数版 dynamic 报运行时错误 65)。
- 带 return 的事件必须声明 event type <类型>;subroutine 型事件写 return 表达式 直接 C0092。
- word 槽理论是 UInt(≤65535);本机 65537 原样可达但别赌跨版本行为,大数值走 long 槽。
- 直调事件漏写 event 关键字 → 按函数解析报 C0051;object.event ue_x(...) 是完整语法。
十一、选型建议
| 场景 | 用法 | | 需要处理结果 / 编译期检查 | 类型化参数事件 + event 直调 | | 框架式回调(放行/拦截) | 事件返回值约定(0/非 0),直调读取 | | 通知型、不需要结果、允许稍后执行 | PostEvent(回调内读 Message) | | 需要立即执行但走统一入口 | TriggerEvent(别指望返回值) | | 传对象/结构 | 类型化参数直调,或实例变量通道;窗口间才用 OpenWithParm |
一句话收束:事件是"给别人留钩子"的机制,函数是"自己干活"的机制。留钩子就按返回值约定设计(像 ItemChanged 那样),自己干活就写函数——两种触发函数(TriggerEvent/PostEvent)只在"运行时才知道要调谁"的场景才真正不可替代。
本文全部代码经 PB10 与 PB12.5 双轨实机验证:导入 0 错误 ×2、全量重编译 0 错误 ×2、运行时断言 26/26 双版全中、完整示例 6 项输出两版一致、静态检查 0 错 0 提醒。窗口相关的 OpenWithParm/CloseWithReturn 段落依赖真实窗口环境,本篇未实测,仅静态核对。PB10 兼容版 PBL 附件见下方回帖。 |