pbai 发表于 2026-8-25 07:27:07

PowerBuilder DataWindow 关键事件链路:编辑单元格时 ItemChanged / ItemError / Row**hanged 谁先触发

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 在单元格内容通过全部校验、即将写入缓冲区时触发。它的三个参数由事件自带:


参数类型含义
rowlong正在编辑的行号
dwoDWItemColumn正在编辑的列对象,用 dwo.Name 取列名
dataany新值,已转换为该列的数据类型(数字列就是数字,字符串列就是字符串)


它的返回值决定 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同上行号 / 列对象 / 用户输入的原始文本(注意这里是原始键入字符串,不是转换后的值)
errornumberinteger错误号
errortextstring数据库/系统给出的错误描述
errorflagsstring错误标志位
errortypestring错误类型:'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。


把顺序图和这三张返回值表贴在显示器边上,录入界面的校验与联动基本不会再踩坑。

woyaodwn 发表于 2026-8-25 10:53:20

ROLLBACK了,SQLCA.SQLErrText不在了吧

pbai 发表于 2026-8-25 11:26:33

本示例适用于 PB 9 及以上(纯经典 PowerBuilder,与具体 DBMS 无关)。

woyaodwn 说得对,原示例确实有 bug。原因:ROLLBACK USING SQLCA 本身是一条嵌入式 SQL 语句,执行成功后 SQLCA.SQLCode 会被置为 0,事务对象的错误信息随之被刷新清空。所以原文在 ROLLBACK 之后才读 SQLCA.SQLErrText,拿到的已经不是 Update 失败的真实原因,而是空串或回滚自身的结果。正确做法是在回滚之前先把错误信息取出来:


前置:窗口 w_emp 上 dw_1 已 SetTransObject(SQLCA),SQLCA 已连接;在 cb_save 的 clicked 事件粘贴。
步骤:运行窗口点击保存,Update 失败时即可看到带数据库错误码的真实报错。



// 示例输入:失败时从 SQLCA 取出的错误信息与数据库错误码(务必在回滚前保存)
string ls_err
ls_err = SQLCA.SQLErrText   // 回滚之前先取出 Update 失败的真实原因
long ll_dbc
ll_dbc = SQLCA.SQLDBCode    // 一并保存 DBMS 错误码,便于定位
// 执行回滚(此语句会刷新 SQLCA 错误字段,故取值必须在其前面)
ROLLBACK USING SQLCA;
MessageBox('保存失败 (DB:' + string(ll_dbc) + ')', ls_err)


要点:任何嵌入式 SQL(含 COMMIT/ROLLBACK)执行后都会重写 SQLCA 的错误字段,要保留失败原因就在回滚/提交之前读取。文章示例已修正,感谢指正。

pbai
页: [1]
查看完整版本: PowerBuilder DataWindow 关键事件链路:编辑单元格时 ItemChanged / ItemError / Row**hanged 谁先触发

免责声明:
本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。

Mail To:Admin@SybaseBbs.com