马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder DataWindow 关键事件链路:编辑单元格时 ItemChanged / ItemError / Row**hanged 谁先触发
阅读说明
1. 适用版本:PB 9 及以上(本文按 PB 12.5 验证;纯经典 PowerBuilder 的 DataWindow 事件,不依赖 PBIDEA 运行库)
2. 支持数据库:与具体 DBMS 无关(本文讲的是 DataWindow 控件自身的编辑事件,SQL 仅在"保存前 AcceptText"示例里出现,用 ANSI 通用写法)
3. 操作系统与环境要求:Windows 7+,已安装 PowerBuilder 开发环境;演示需在窗口上放置 DataWindow 控件并关联一个 DataObject
4. 难度系数:★★☆☆☆
5. 其它阅读说明:前置知识为 PowerScript 基本语法与 DataWindow 取数(可先读 Day 1《DataWindow 三缓冲区》与 Day 11《嵌入式 SQL》);示例需在数据库中建好带 emp_name / salary / dept / remark / emp_age 等列的测试表
一、先说结论:一次编辑会依次触发什么
做录入界面时,最常问的就是:「我在格子里改了个数,到底哪个事件先跑?校验写在哪?改完跨行时又触发什么?」DataWindow 的编辑事件链路其实非常固定,记住下面这条主线就够了:
用户在单元格里改完内容 → 离开这个格子(Tab / 回车 / 点别的格 / 代码里 AcceptText)→ DataWindow 先做"校验" → 校验不过触发 ItemError、校验通过触发 ItemChanged → 如果焦点跨行了再触发 Row**hanged。
一句话版:ItemError 和 ItemChanged 是"二选一"(同一轮编辑只跑其中一个),Row**hanged 是"跨行才跑"。下面把这三个事件拆开讲透,再给一张完整的顺序图。
二、ItemChanged:值被接受前最后一道关口
ItemChanged 在单元格内容通过全部校验、即将写入缓冲区时触发。它的三个参数由事件自带:
| 参数 | 类型 | 含义 | | row | long | 正在编辑的行号 | | dwo | DWItemColumn | 正在编辑的列对象,用 dwo.Name 取列名 | | data | any | 新值,已转换为该列的数据类型(数字列就是数字,字符串列就是字符串) |
它的返回值决定 DataWindow 怎么处理这个新值,这是全文最该记牢的点:
- return 0:接受该值(默认行为),值写进缓冲区,行状态标记为 Modified!。
- return 1:拒绝该值、保持焦点在当前格子——用户必须改对才能离开(常用于强校验)。
- return 2:拒绝该值、但允许焦点移走,格子内容回滚到修改前的值。
下面这个例子做两件事:salary 为负时拒绝并卡焦点;dept 改变时顺手清空 remark(用 SetItem 直接写缓冲区,不会再次触发 ItemChanged,避免死循环)。
前置:窗口 w_emp 上放 DataWindow 控件 dw_1,DataObject=d_employee(含 salary、dept、remark 列,并已 Retrieve 出数据)。
步骤:1) 在 dw_1 的 ItemChanged 事件粘贴下方代码;2) 运行窗口;3) 把某行 salary 改成负数,会弹窗且焦点卡在该格,必须改回正数才能离开。 - // row / dwo / data 是 ItemChanged 事件自带的三个参数(行号 / 当前列对象 / 已转换的新值)
- // 前置条件:dw_1 关联 d_employee,含 salary、dept、remark 列;窗口已运行并 Retrieve 出数据
- // 示例输入:工资下限,负数视为非法
- decimal ldec_min_salary
- ldec_min_salary = 0
- // 跨列校验:salary 不允许为负
- if dwo.Name = 'salary' then
- if dec(data) < ldec_min_salary then
- MessageBox('校验失败', '工资不能为负数,请重新输入')
- return 1 // 1=拒绝该值并保持焦点,等待用户改正
- end if
- MessageBox('已接受', 'salary 新值:' + string(dec(data))) // 演示:看到接受后的值
- end if
- // 联动:dept 改变时清空 remark(SetItem 直接写缓冲区,不会再次触发 ItemChanged,避免递归)
- if dwo.Name = 'dept' then
- dw_1.SetItem(row, 'remark', '')
- end if
- return 0 // 0=接受新值(默认行为)
复制代码
三、ItemError:录不进类型时就找它
ItemError 在用户输入的内容过不了校验时触发——典型场景是往数字列里敲了字母、违反了列的编辑掩码、或没通过校验规则(validation expression)。它和 ItemChanged 互斥:同一轮编辑,要么走 ItemError,要么走 ItemChanged,不会两个都跑。
它的参数比 ItemChanged 多几个,用来告诉你"到底哪一步挂了":
| 参数 | 类型 | 含义 | | row / dwo / data | 同上 | 行号 / 列对象 / 用户输入的原始文本(注意这里是原始键入字符串,不是转换后的值) | | errornumber | integer | 错误号 | | errortext | string | 数据库/系统给出的错误描述 | | errorflags | string | 错误标志位 | | errortype | string | 错误类型:'datatype'(类型不对)/ 'mask'(掩码不符)/ 'validation'(校验规则不过)等 |
返回值决定"要不要弹 PB 默认错误框、焦点何去何从":
- return 0:弹 PB 默认错误框,保持焦点(默认行为)。
- return 1:不弹默认框,保持焦点(静默,用户必须改)。
- return 2:不弹默认框,允许焦点移走(值回滚)。
- return 3:弹默认框,允许焦点移走(值回滚)。
下面这个例子把"数字列输成字母"这种最常见的报错换成中文提示,并静默卡住焦点:
前置:dw_1 的 emp_age 列是 number 类型;用户可能误输字母。
步骤:在 dw_1 的 ItemError 事件粘贴下方代码;运行后在 emp_age 输入字母,会走自定义中文提示而非 PB 英文默认弹窗。 - // row / dwo / data / errornumber / errortext / errorflags / errortype 均为 ItemError 事件自带参数
- // 前置条件:dw_1 含 emp_age(number 类型) 列;窗口已运行
- // 示例输入:自定义的友好提示文案
- string ls_tip
- ls_tip = '请输入数字,例如 30'
- // 类型不匹配时给出中文提示,并静默保持焦点(不弹 PB 默认框)
- if errortype = 'datatype' then
- MessageBox('输入有误', ls_tip)
- return 1 // 1=不弹默认框,保持焦点,等待用户改
- end if
- return 0 // 0=其余错误类型沿用 PB 默认错误框并保持焦点
复制代码
四、Row**hanged:切行时才跑
Row**hanged 在焦点移动到另一行时触发,参数是 currentRow(获得焦点的新行号)。两个要点:
- 同一行内换列不触发(从 A 列 Tab 到本行 B 列,只有 ItemChanged,没有 Row**hanged)。
- 参数可能等于 0——数据清空、没有有效行时 currentRow=0,代码里要兜底。
它最适合做"行级联动":切到哪行就把那行的摘要回显到状态栏、或加载该行的子表。注意 GetItemXxx 读缓冲区不会触发任何编辑事件,可以放心用。
前置:dw_1 已 Retrieve 数据;窗口上另有一个 sle_status 单行编辑框显示当前行信息。
步骤:在 dw_1 的 Row**hanged 事件粘贴下方代码;运行后用鼠标点不同行,sle_status 实时刷新。 - // currentRow(long) 是 Row**hanged 事件自带的参数:当前获得焦点的行号
- // 前置条件:dw_1 已 Retrieve 出数据;窗口上有 sle_status 单行编辑框
- // 示例输入:无外部输入,直接使用事件参数 currentRow
- long ll_row
- ll_row = currentRow
- // 兜底:没有有效行时(如数据被清空)currentRow=0,避免读空行报错
- if ll_row <= 0 then
- sle_status.Text = '(无数据)'
- return
- end if
- // 读取当前行某列并回显到状态栏(GetItemString 不会触发任何编辑事件)
- string ls_name
- ls_name = dw_1.GetItemString(ll_row, 'emp_name')
- sle_status.Text = '当前行 ' + string(ll_row) + ':' + ls_name
- MessageBox('行切换', '已切到行 ' + string(ll_row) + ',姓名 ' + ls_name)
复制代码
五、完整触发顺序与"卡焦点"陷阱
把三个事件串起来,一次"改值并跨行"的完整链路是这样的(用文本画一遍,照着排错不会乱):
- 用户编辑单元格 → 离开该格(Tab / 点别处 / AcceptText)
- │
- ├─ 类型转换失败? ─► ItemError(errortype='datatype') ─► 按返回值处理(默认弹框+卡焦点)
- ├─ 编辑掩码失败? ─► ItemError(errortype='mask')
- ├─ 校验规则失败? ─► ItemError(errortype='validation')
- │
- └─ 全部通过 ─────► ItemChanged(row, dwo, data)
- ├─ return 0:接受,值入缓冲区
- ├─ return 1:拒绝 + 卡焦点(Row**hanged 不触发)
- └─ return 2:拒绝 + 放行(值回滚)
- 若焦点跨行移动 ───► Row**hanged(currentRow)
复制代码
最容易踩的坑就藏在最后这段:一旦 ItemChanged 返回 1(卡焦点),焦点没离开当前行,Row**hanged 就不会触发。所以"切行时加载子表"的逻辑如果写在 Row**hanged 里,而用户上一格一直没校验过,就会卡住不动——这不是 Row**hanged 没写对,是前面的 ItemChanged 把焦点拦了。排错时先确认前面有没有 return 1。
六、边界情况与常见坑(逐条排掉)
- SetItem 不触发 ItemChanged / ItemError。代码里 dw_1.SetItem(row, col, value) 是直接写缓冲区,绕过了编辑控件,所以既不会校验也不会触发这两个事件;它只会把行状态标成 Modified!。需要"静默改值又不想被事件拦"时就用它(联动清空 remark 那句就是这么干的)。
- AcceptText() 会强制触发校验链路。在保存、切行、切列之前调用 dw_1.AcceptText(),会把当前编辑框的内容按"类型→掩码→规则→ItemChanged"流程跑一遍:失败返回 -1 并触发 ItemError,成功返回 1 并触发 ItemChanged。保存前一定要先 AcceptText,否则用户最后敲的那格可能还没进缓冲区就直接 Update 了。
- 校验顺序固定为:数据类型 → 编辑掩码 → 校验规则 → ItemChanged。能靠"校验规则(validation expression)"挡住的,就别写进 ItemChanged 里硬拦,前者在编辑期就报、体验更顺。
- ItemChanged 返回 1 会一直卡焦点。如果校验逻辑有漏洞(比如永远不满足的条件),用户会被锁死在格子里出不去——强校验务必保证"合理输入能通过"。
- Row**hanged 在同行列间切换不触发。想做"列级联动"别放这里,放 ItemChanged;只有"行级"才用 Row**hanged。
- data 在 ItemChanged 与 ItemError 里含义不同:ItemChanged 的 data 是已转换的新值(数字列就是数字,用 dec(data)/string(data) 取);ItemError 的 data 是用户原始键入文本(字符串),要拿错误信息看 errortype / errortext。
- 保存前先 AcceptText 再 Update。下面这个按钮脚本是标准收尾写法:
前置:窗口 w_emp 上 dw_1 已编辑若干单元格;按钮 cb_save 用于提交。
步骤:在 cb_save 的 clicked 事件粘贴下方代码;点击保存时先把未提交的编辑刷入缓冲区,再做 Update。 - // 前置条件:dw_1 已编辑;按钮 cb_save 的 clicked 事件;SQLCA 已连接
- // 示例输入:保存时是否强制先结束当前编辑(通常固定为 true)
- boolean lb_flush
- lb_flush = true
- if lb_flush then
- // AcceptText 强制把编辑框内容按校验流程提交:
- // 校验失败返回 -1 并触发 ItemError;成功返回 1 并触发 ItemChanged
- if dw_1.AcceptText() = -1 then
- MessageBox('保存中止', '当前单元格校验未通过,请先修正')
- return
- end if
- end if
- // 真正写库(dw_1 已设置过 SetTransObject(SQLCA))
- if dw_1.Update() = 1 then
- COMMIT USING SQLCA;
- MessageBox('保存成功', '数据已提交')
- else
- ROLLBACK USING SQLCA;
- MessageBox('保存失败', SQLCA.SQLErrText)
- end if
复制代码
七、ItemChanged 校验 vs 校验规则 vs 事件脚本(怎么选)
同一个"salary 不能为负"的约束,至少有三种落地方式,别乱用:
| 方式 | 写在哪 | 何时生效 | 适用 | | 校验规则(validation expression) | DataWindow 画板里列的 Validation 表达式,如 salary >= 0 | 编辑离开该格时 | 简单的数据合法性(推荐首选,零代码) | | ItemError 事件 | 事件脚本 | 校验失败时 | 想定制错误提示文案 / 静默处理时用 | | ItemChanged 事件 return 1 | 事件脚本 | 值已通过类型校验、即将入缓冲时 | 需要跨列 / 跨行业务逻辑(如负值联动其它列、调接口校验唯一性)时用 |
经验法则:纯数据合法性优先用"校验规则",它在编辑期就拦、用户体验最好;只有需要自定义提示或跨列联动时才进 ItemError / ItemChanged。三者在同一次编辑里是"校验规则先拦 → 不过则 ItemError → 过了才 ItemChanged"的递进关系,不要重复写三遍同样的判断。
八、扩展:和缓冲区、状态、实时校验的关系
- 与 GetItemStatus / SetItemStatus:ItemChanged 返回 0 接受后,该行会被标成 Modified!(新增行是 NewModified!)。SetItem 同样会标 Modified!。SetItemStatus(row, 0, Primary!, NotModified!) 可强制标记为"未改"从而跳过 Update——常用于"逻辑改了但不想落库"的场景。
- EditChanged 事件(进阶):如果你要的是"用户每敲一个键就校验"(而不是离开格子才校验),用 EditChanged 事件——它每次按键都触发,参数是 row / dwo / data(当前编辑框文本)。典型用途是输入法联想、实时字数统计、即时格式修正。注意它触发极频繁,里面别写重逻辑。
- Update 前务必 AcceptText:前面第六、七节的保存脚本已经示范——Update() 只认缓冲区里的值,编辑控件里还没提交的最后那格,不 AcceptText 就丢了。
九、小结
本文讲清楚一件事:DataWindow 编辑单元格的事件链路是"ItemError 与 ItemChanged 二选一,Row**hanged 跨行才跑"。
- ItemChanged(row, dwo, data):data 是已转换新值;return 0 接受、1 卡焦点、2 回滚放行——负责跨列/跨行业务校验。
- ItemError(row, dwo, data, errornumber, errortext, errorflags, errortype):校验失败才触发,data 是原始键入文本,看 errortype 判断挂在哪步;return 0~3 控制是否弹默认框与焦点去向——负责定制错误提示。
- Row**hanged(currentRow):仅在焦点跨行时触发,做行级联动;注意 currentRow 可能为 0 要兜底。
- 牢记三条红线:SetItem 不触发编辑事件、AcceptText 强制触发校验链路、保存前先 AcceptText 再 Update。
把顺序图和这三张返回值表贴在显示器边上,录入界面的校验与联动基本不会再踩坑。 |