马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder DataWindow 单元格读写全解:GetItem 系列、SetItem、缓冲区与 NULL 的真实语义(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5,两版均实机验证;文中两处版本差异已在第九节单列成表
2. 支持数据库:本文示例不依赖数据库(用动态 DataWindow 在内存里建表),连接库与不连库都照常适用
3. 操作系统与环境要求:Windows 7+,PB 运行库(PBVM)正常安装即可,无需第三方组件
4. 难度系数:★★☆☆☆(DataWindow 基础之上一点,重点在语义细节而非技巧)
5. 其它阅读说明:建议先读《DataWindow 三缓冲区详解》,本文多处用到 Primary!/Filter!/Delete! 的概念;示例代码可直接放进按钮事件运行
一、这一篇讲什么
DataWindow 的数据读写,入口就是两个函数家族:GetItem 系列读,SetItem 写。听起来是入门级话题,但真正写业务代码时,这里埋的雷比任何一个高级特性都多:
- 新插一行没赋值,读出来是空串还是 0?都不是,是 NULL——而 PB 的 NULL 会顺着字符串拼接传染,让你整段输出凭空消失;
- SetItem 往 number 列写字符串「123」,返回值是 1 还是 -1?
- 行号越界、列名写错,哪些静默、哪些抛错、抛了错 try-catch 接不接得住?
这一篇把这些问题全部实机测清。所有结论都在 PB10 与 PB12.5 双轨跑过,两版行为一致的地方放心用,不一致的地方单独成表。
二、API 全貌:6 个 Get + 1 个 Set
读家族按返回类型分六个,每个都有两种形态——不带缓冲区参数的短形态,和带 dwbuffer、originalvalue 两个可选参数的长形态:
| 函数 | 返回类型 | 短形态 | 长形态(可省略参数) | | GetItemString | string | GetItemString(row, column) | (row, column, dwbuffer, originalvalue) | | GetItemNumber | number | GetItemNumber(row, column) | 同上 | | GetItemDecimal | decimal | GetItemDecimal(row, column) | 同上 | | GetItemDate | date | GetItemDate(row, column) | 同上 | | GetItemDateTime | datetime | GetItemDateTime(row, column) | 同上 | | GetItemTime | time | GetItemTime(row, column) | 同上 |
写家族只有一个:SetItem(row, column, value),value 的类型要与列类型匹配,返回 1 表示成功、-1 表示失败。
三个要点先立住:
- GetItem 读的是缓冲区里的值,不是编辑控件里没回车的文本。用户正在改、还没 Tab 走的内容要用 GetText 去取;
- SetItem 直接写缓冲区,不经过编辑控件,所以写完立刻能 GetItem 读到,不需要 AcceptText;
- dwbuffer 取 Primary!/Filter!/Delete! 之一,originalvalue 传 true 时读「原值」(检索时落下来的那份),传 false 或省略读当前值。
三、最小可运行示例(不需连库)
下面这段不用连接数据库,直接把一个内存表建出来、写进去、读出来。放进任何按钮事件都能跑:
- // 目的:动态建一张两列内存表,SetItem 写入,GetItem 读回并弹出结果
- string ls_syn, ls_out
- datastore lds
- long ll_rc
- // 语法串定义列:名字 char(20),数量 number
- ls_syn = 'release 10; datawindow(units=0 processing=1) table('
- ls_syn = ls_syn + 'column=(type=char(20) updatewhereclause=yes name=s dbname="s") '
- ls_syn = ls_syn + 'column=(type=number updatewhereclause=yes name=n dbname="n"))'
- // 建对象,返回 1 才算成功
- lds = create datastore
- ll_rc = lds.Create(ls_syn)
- if ll_rc <> 1 then
- destroy lds
- MessageBox('建表失败', 'rc=' + String(ll_rc))
- return
- end if
- // 插一行并写入两个单元格
- lds.InsertRow(0)
- lds.SetItem(1, 's', '智者')
- lds.SetItem(1, 'n', 7)
- // 读回:char 列用 GetItemString,number 列用 GetItemNumber
- ls_out = 's=[' + lds.GetItemString(1, 's') + ']'
- ls_out = ls_out + ' n=' + String(lds.GetItemNumber(1, 'n'))
- destroy lds
- MessageBox('结果', ls_out)
复制代码
运行结果:s=[智者] n=7。这是没有雷的理想路径,下面开始逐个拆雷。
四、空行读回来的是 NULL,不是 0 也不是空串
实测(PB10 与 PB12.5 结论一致):InsertRow 之后不做任何赋值,三个读函数的行为是:
| 列类型 | 读函数 | 返回 | | char(20) | GetItemString | NULL(不是空串) | | number | GetItemNumber | NULL(不是 0) | | date | GetItemDate | NULL(不是 1900-01-01) |
真正的雷在后面:NULL 会顺着表达式传染。看这段我在实测中踩过的写法:
- // 反例:ls_s 是 NULL 时,第一句拼接整个变 NULL,后面写什么都没用
- ls_out = 'p1:s=[' + lds.GetItemString(2, 's') + ']'
- ls_out = ls_out + '|n=' + String(lds.GetItemNumber(2, 'n'))
- MessageBox('结果', ls_out) // 弹出的是空窗口
复制代码
第一句里 GetItemString 返回 NULL,PB 的规则是「拼接串里出现任何一个 NULL,整个结果就是 NULL」,于是 ls_out 从此一直是 NULL,后面拼什么、判什么都不再起作用——你只会看到一个空 MessageBox,连报错都没有。
正确姿势是先判 NULL、替换成占位串,再拼:
- // 正例:先判 NULL 再拼串
- string ls_s
- ls_s = lds.GetItemString(2, 's')
- if IsNull(ls_s) then ls_s = '<未填>'
- MessageBox('结果', 's=[' + ls_s + ']')
复制代码
数值列同理:先 IsNull(ll_n) 判断,再决定 String(ll_n) 还是占位串。这一条习惯能救掉你日后一大半「数据凭空消失」的疑难工单。
五、NULL 写进去再读出来还是 NULL
反过来问:SetNull 之后用 SetItem 写进去,读出来是什么?实测答案:还是 NULL,原样保留。
- // 目的:验证 SetItem 可以写入 NULL,读回仍为 NULL
- string ls_s
- long ll_n
- datastore lds
- long ll_rc
- ls_syn = 'release 10; datawindow(units=0 processing=1) table('
- ls_syn = ls_syn + 'column=(type=char(20) updatewhereclause=yes name=s dbname="s") '
- ls_syn = ls_syn + 'column=(type=number updatewhereclause=yes name=n dbname="n"))'
- lds = create datastore
- ll_rc = lds.Create(ls_syn)
- if ll_rc <> 1 then
- destroy lds
- MessageBox('建表失败', 'rc=' + String(ll_rc))
- return
- end if
- lds.InsertRow(0)
- // 输入值:把两个变量置成 NULL,模拟"该格没有值"的业务语义
- SetNull(ls_s)
- SetNull(ll_n)
- lds.SetItem(1, 's', ls_s)
- lds.SetItem(1, 'n', ll_n)
- // 读回并判断
- ls_s = lds.GetItemString(1, 's')
- ll_n = lds.GetItemNumber(1, 'n')
- if IsNull(ls_s) and IsNull(ll_n) then
- MessageBox('结果', '两格读回均为 NULL,写入的 NULL 原样保留')
- else
- MessageBox('结果', 'NULL 丢失,需排查')
- end if
- destroy lds
复制代码
所以「格子没值」和「格子是空串」在缓冲区里是两个不同的状态:InsertRow 后是 NULL,而 SetItem(1, 's', '') 写进去的才是空串。判空逻辑里把这两者混为一谈,是 NULL 判断类 Bug 的高发源头。
六、SetItem 会「写穿」原值缓冲
长形态的最后那个 originalvalue 参数,直觉上应该读「SetItem 改之前的旧值」。实测不是这样——SetItem 会同时刷新当前值和原值:
- // 目的:验证 SetItem 对原值缓冲的影响(接第三节建好的表)
- lds.SetItem(1, 's', 'B')
- ls_cur = lds.GetItemString(1, 's') // 当前值:B
- ls_org = lds.GetItemString(1, 's', Primary!, true) // 原值:也是 B!
- lds.SetItem(1, 's', 'C')
- ls_org = lds.GetItemString(1, 's', Primary!, true) // 原值:跟着变成 C
复制代码
实测读数:cur=[B] org=[B] org2=[C]。也就是说,对动态建的内存表,SetItem 之后原值立刻跟随新值,「原值缓冲」不存旧账。
这个行为要放到真实的检索场景里理解:原值缓冲真正沉淀旧值,是数据从数据库 Retrieve 下来之后的事;纯内存表没有「检索下来的那份」,原值只好跟着当前值走。想做「改前/改后」对比,要么在改之前先自己存一份副本,要么依赖 Retrieve 之后的三缓冲区机制(见《更新机制全解》里的 SetItemStatus 一节)。
七、缓冲区参数:Filter! 与 Delete! 里的行怎么读
GetItem 的行号永远指「目标缓冲区里的第几行」,不是全表第几行。实测一条链路:两行数据,过滤掉一行、再把剩下的挪进删除缓冲区:
- // 目的:过滤缓冲区与删除缓冲区的读取(接第三节建好的表,已插 2 行且第 1 行 s='A')
- lds.SetFilter("s = 'A'")
- lds.Filter()
- // 此时主缓冲区剩 1 行,第 2 行(没赋值那行)被滤到 filter 缓冲区
- ll_main = lds.RowCount() // 1
- ll_f = lds.FilteredCount() // 1
- ls_f = lds.GetItemString(1, 's', Filter!, false) // 读 filter 缓冲区第 1 行:NULL
- // 把主缓冲区仅剩的一行挪进删除缓冲区
- lds.RowsMove(1, lds.RowCount(), Primary!, lds, 1, Delete!)
- ll_del = lds.DeletedCount() // 1
- ls_dl = lds.GetItemString(1, 's', Delete!, false) // 读删除缓冲区第 1 行:A
- MessageBox('缓冲区', '主=' + String(ll_main) + ' 滤=' + String(ll_f) + ' 删=' + String(ll_del) + ' 删区值=' + ls_dl)
复制代码
实测读数:主=1 滤=1 删=1 删区值=A。注意 ls_f 那次读回的是 NULL(被滤走的是没赋值的行)——再次提醒第四节:拼串前先判 NULL。
八、类型规则:读错类型抛错、写错类型返 -1
读写两侧的容错策略完全不对称,这是本主题最重要的实测发现:
读侧——GetItemNumber 读 char 列,抛运行时错误,错误文本是「DataWindow column type does not match GetItem type」,try-catch 可以接住:
- // 目的:类型不匹配的读会抛错,catch (runtimeerror) 能接住
- try
- ll_x = lds.GetItemNumber(1, 's') // number 列读法用在 char 列上
- MessageBox('没抛错', 'x=' + String(ll_x))
- catch (runtimeerror lre)
- MessageBox('接住了', lre.GetMessage())
- end try
复制代码
写侧——SetItem 对「类型不符」反而静默:往 number 列写字符串(连「123」这种可转换的都不行,一律不转换),返回 -1,不抛错、不写进:
| 操作 | 结果(两版一致) | | SetItem(1, 'n', 7) | 返回 1,立即生效 | | SetItem(1, 'n', '123') | 返回 -1,静默拒绝,不做自动转换 | | SetItem(1, 'n', 'abc') | 返回 -1,静默拒绝 | | GetItemNumber 读 char 列 | 抛运行时错误(可 catch) | | GetItemNumber 读越界行 | 抛运行时错误(可 catch) |
所以防御写法要分两侧:读之前信不过列类型就用 try-catch 兜;写之后必须检查返回值是不是 1,静默的 -1 不会主动告诉你写失败了。
九、越界与坏列名:静默与抛错的分界线(含版本差异)
同样是「参数给错」,四种情形的待遇不同,其中两种还有版本差异:
| 情形 | PB12.5 实测 | PB10 实测 | | SetItem 列名不存在('nosuch') | 抛运行时错误,可 catch | 抛运行时错误,可 catch | | SetItem 行号越界(99) | 静默返回 -1 | 静默返回 -1 | | GetItemString 行号越界 | 返回空串,不抛错 | 抛运行时错误,可 catch | | GetItemNumber 行号越界 | 抛运行时错误,可 catch | 抛运行时错误,可 catch | | GetItemString 读 number 列 | 返回空串,不抛错 | 抛运行时错误,可 catch |
规律看出来了:SetItem 对「参数越界」宽容(返 -1)、对「列名不存在」严格(抛错);GetItemString 在 PB12.5 里被放宽成静默返空串,PB10 则保持严格。GetItemNumber 两版都严格。
PB10 基准 · PB12.5 实测的口径在这张表里落地:想让代码两版行为一致,就不要依赖 GetItemString 的「宽松态」——越界行先用 RowCount() 判断,跨类型取值一律不用 GetItemString 硬读,这样写出来的代码两版跑起来一个样。
十、坑清单
- NULL 传染:空行 GetItem 读回 NULL,拼进字符串整条变 NULL,输出凭空消失且无报错——先 IsNull 后拼接;
- NULL 与空串是两个状态:InsertRow 后是 NULL,SetItem 写 '' 才是空串,判空逻辑要分开处理;
- SetItem 写穿原值:original=true 读到的是最新值,别指望它存旧账;
- SetItem 不做类型转换:'123' 写 number 列也是 -1,写完必须查返回值;
- 读侧类型必须严格匹配:GetItemNumber 读 char 列抛错,两版一致;
- 越界行为有版本差异:GetItemString 的越界/跨类型读,PB12.5 返空串、PB10 抛错,跨版本代码不要踩这条线;
- GetItem 不看编辑控件:用户正在输入未 Tab 走的文本用 GetText,进了缓冲区才轮到 GetItem;
- 行号是缓冲区内的行号:读 Filter!/Delete! 时,第 1 行指该缓冲区第 1 行,不是主缓冲区行号。
十一、和 GetText / Describe 怎么分工
最后把三个「取值」的入口排个队,避免混用:
- GetItem 系列:读缓冲区里已落定的值,类型化返回,本篇主角。适合算逻辑、写业务规则;
- GetText / SetText:面向当前编辑控件,处理「用户正在敲、还没进缓冲区」的文本,ItemChanged 事件里的常客;
- Describe 带 Evaluate:一次性表达式求值,能聚合(Sum 之类)、能跨行列,适合报表性质的取数,但拿回的是字符串。
一句话分工:控件态用 GetText,缓冲态用 GetItem,跨行算账用 Evaluate。三者写混了,症状通常是「值时对时不对」——按这个排队先分清楚数据在哪一层,再看函数用错没有。
本文全部结论来自双轨实机验证:PB10 与 PB12.5 各自 import 全部探针对象、ORCA 全量编译 0 错误、PBVM 运行 PASS、读数逐字比对。两版一致的结论可以直接抄用,两处版本差异(第九节表格)请按表里的口径写代码。 |