祝愿大家身体健康!

 找回密码
 站点注册
查看: 24|回复: 1

[学习笔记] PowerBuilder DataWindow 单元格读写全解:GetItem 系列、SetItem、缓冲区与 NULL 的真实语义(PB10 基准 · PB12.5 实测)

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?站点注册

×
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 两个可选参数的长形态:

函数返回类型短形态长形态(可省略参数)
GetItemStringstringGetItemString(row, column)(row, column, dwbuffer, originalvalue)
GetItemNumbernumberGetItemNumber(row, column)同上
GetItemDecimaldecimalGetItemDecimal(row, column)同上
GetItemDatedateGetItemDate(row, column)同上
GetItemDateTimedatetimeGetItemDateTime(row, column)同上
GetItemTimetimeGetItemTime(row, column)同上


写家族只有一个:SetItem(row, column, value),value 的类型要与列类型匹配,返回 1 表示成功、-1 表示失败。

三个要点先立住:


  • GetItem 读的是缓冲区里的值,不是编辑控件里没回车的文本。用户正在改、还没 Tab 走的内容要用 GetText 去取;
  • SetItem 直接写缓冲区,不经过编辑控件,所以写完立刻能 GetItem 读到,不需要 AcceptText;
  • dwbuffer 取 Primary!/Filter!/Delete! 之一,originalvalue 传 true 时读「原值」(检索时落下来的那份),传 false 或省略读当前值。


三、最小可运行示例(不需连库)

下面这段不用连接数据库,直接把一个内存表建出来、写进去、读出来。放进任何按钮事件都能跑:
  1. // 目的:动态建一张两列内存表,SetItem 写入,GetItem 读回并弹出结果
  2. string ls_syn, ls_out
  3. datastore lds
  4. long ll_rc
  5. // 语法串定义列:名字 char(20),数量 number
  6. ls_syn = 'release 10; datawindow(units=0 processing=1) table('
  7. ls_syn = ls_syn + 'column=(type=char(20) updatewhereclause=yes name=s dbname="s") '
  8. ls_syn = ls_syn + 'column=(type=number updatewhereclause=yes name=n dbname="n"))'
  9. // 建对象,返回 1 才算成功
  10. lds = create datastore
  11. ll_rc = lds.Create(ls_syn)
  12. if ll_rc <> 1 then
  13.         destroy lds
  14.         MessageBox('建表失败', 'rc=' + String(ll_rc))
  15.         return
  16. end if
  17. // 插一行并写入两个单元格
  18. lds.InsertRow(0)
  19. lds.SetItem(1, 's', '智者')
  20. lds.SetItem(1, 'n', 7)
  21. // 读回:char 列用 GetItemString,number 列用 GetItemNumber
  22. ls_out = 's=[' + lds.GetItemString(1, 's') + ']'
  23. ls_out = ls_out + ' n=' + String(lds.GetItemNumber(1, 'n'))
  24. destroy lds
  25. MessageBox('结果', ls_out)
复制代码

运行结果:s=[智者] n=7。这是没有雷的理想路径,下面开始逐个拆雷。

四、空行读回来的是 NULL,不是 0 也不是空串

实测(PB10 与 PB12.5 结论一致):InsertRow 之后不做任何赋值,三个读函数的行为是:

列类型读函数返回
char(20)GetItemStringNULL(不是空串)
numberGetItemNumberNULL(不是 0)
dateGetItemDateNULL(不是 1900-01-01)


真正的雷在后面:NULL 会顺着表达式传染。看这段我在实测中踩过的写法:
  1. // 反例:ls_s 是 NULL 时,第一句拼接整个变 NULL,后面写什么都没用
  2. ls_out = 'p1:s=[' + lds.GetItemString(2, 's') + ']'
  3. ls_out = ls_out + '|n=' + String(lds.GetItemNumber(2, 'n'))
  4. MessageBox('结果', ls_out)   // 弹出的是空窗口
复制代码

第一句里 GetItemString 返回 NULL,PB 的规则是「拼接串里出现任何一个 NULL,整个结果就是 NULL」,于是 ls_out 从此一直是 NULL,后面拼什么、判什么都不再起作用——你只会看到一个空 MessageBox,连报错都没有。

正确姿势是先判 NULL、替换成占位串,再拼:
  1. // 正例:先判 NULL 再拼串
  2. string ls_s
  3. ls_s = lds.GetItemString(2, 's')
  4. if IsNull(ls_s) then ls_s = '<未填>'
  5. MessageBox('结果', 's=[' + ls_s + ']')
复制代码

数值列同理:先 IsNull(ll_n) 判断,再决定 String(ll_n) 还是占位串。这一条习惯能救掉你日后一大半「数据凭空消失」的疑难工单。

五、NULL 写进去再读出来还是 NULL

反过来问:SetNull 之后用 SetItem 写进去,读出来是什么?实测答案:还是 NULL,原样保留。
  1. // 目的:验证 SetItem 可以写入 NULL,读回仍为 NULL
  2. string ls_s
  3. long ll_n
  4. datastore lds
  5. long ll_rc
  6. ls_syn = 'release 10; datawindow(units=0 processing=1) table('
  7. ls_syn = ls_syn + 'column=(type=char(20) updatewhereclause=yes name=s dbname="s") '
  8. ls_syn = ls_syn + 'column=(type=number updatewhereclause=yes name=n dbname="n"))'
  9. lds = create datastore
  10. ll_rc = lds.Create(ls_syn)
  11. if ll_rc <> 1 then
  12.         destroy lds
  13.         MessageBox('建表失败', 'rc=' + String(ll_rc))
  14.         return
  15. end if
  16. lds.InsertRow(0)
  17. // 输入值:把两个变量置成 NULL,模拟"该格没有值"的业务语义
  18. SetNull(ls_s)
  19. SetNull(ll_n)
  20. lds.SetItem(1, 's', ls_s)
  21. lds.SetItem(1, 'n', ll_n)
  22. // 读回并判断
  23. ls_s = lds.GetItemString(1, 's')
  24. ll_n = lds.GetItemNumber(1, 'n')
  25. if IsNull(ls_s) and IsNull(ll_n) then
  26.         MessageBox('结果', '两格读回均为 NULL,写入的 NULL 原样保留')
  27. else
  28.         MessageBox('结果', 'NULL 丢失,需排查')
  29. end if
  30. destroy lds
复制代码

所以「格子没值」和「格子是空串」在缓冲区里是两个不同的状态:InsertRow 后是 NULL,而 SetItem(1, 's', '') 写进去的才是空串。判空逻辑里把这两者混为一谈,是 NULL 判断类 Bug 的高发源头。

六、SetItem 会「写穿」原值缓冲

长形态的最后那个 originalvalue 参数,直觉上应该读「SetItem 改之前的旧值」。实测不是这样——SetItem 会同时刷新当前值和原值:
  1. // 目的:验证 SetItem 对原值缓冲的影响(接第三节建好的表)
  2. lds.SetItem(1, 's', 'B')
  3. ls_cur = lds.GetItemString(1, 's')                    // 当前值:B
  4. ls_org = lds.GetItemString(1, 's', Primary!, true)    // 原值:也是 B!
  5. lds.SetItem(1, 's', 'C')
  6. ls_org = lds.GetItemString(1, 's', Primary!, true)    // 原值:跟着变成 C
复制代码

实测读数:cur=[B] org=[B] org2=[C]。也就是说,对动态建的内存表,SetItem 之后原值立刻跟随新值,「原值缓冲」不存旧账。

这个行为要放到真实的检索场景里理解:原值缓冲真正沉淀旧值,是数据从数据库 Retrieve 下来之后的事;纯内存表没有「检索下来的那份」,原值只好跟着当前值走。想做「改前/改后」对比,要么在改之前先自己存一份副本,要么依赖 Retrieve 之后的三缓冲区机制(见《更新机制全解》里的 SetItemStatus 一节)。

七、缓冲区参数:Filter! 与 Delete! 里的行怎么读

GetItem 的行号永远指「目标缓冲区里的第几行」,不是全表第几行。实测一条链路:两行数据,过滤掉一行、再把剩下的挪进删除缓冲区:
  1. // 目的:过滤缓冲区与删除缓冲区的读取(接第三节建好的表,已插 2 行且第 1 行 s='A')
  2. lds.SetFilter("s = 'A'")
  3. lds.Filter()
  4. // 此时主缓冲区剩 1 行,第 2 行(没赋值那行)被滤到 filter 缓冲区
  5. ll_main = lds.RowCount()        // 1
  6. ll_f = lds.FilteredCount()      // 1
  7. ls_f = lds.GetItemString(1, 's', Filter!, false)   // 读 filter 缓冲区第 1 行:NULL
  8. // 把主缓冲区仅剩的一行挪进删除缓冲区
  9. lds.RowsMove(1, lds.RowCount(), Primary!, lds, 1, Delete!)
  10. ll_del = lds.DeletedCount()     // 1
  11. ls_dl = lds.GetItemString(1, 's', Delete!, false)  // 读删除缓冲区第 1 行:A
  12. 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 可以接住:
  1. // 目的:类型不匹配的读会抛错,catch (runtimeerror) 能接住
  2. try
  3.         ll_x = lds.GetItemNumber(1, 's')   // number 列读法用在 char 列上
  4.         MessageBox('没抛错', 'x=' + String(ll_x))
  5. catch (runtimeerror lre)
  6.         MessageBox('接住了', lre.GetMessage())
  7. 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、读数逐字比对。两版一致的结论可以直接抄用,两处版本差异(第九节表格)请按表里的口径写代码。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
 楼主| 发表于 1 小时前 | 显示全部楼层
本帖最后由 pbai 于 2026-11-15 06:14 编辑

PB10 兼容版验证包已上传,附件见 art98_pb10.zip (11.87 KB, 下载次数: 1) 。

这是什么
配套验证件:nvo_art98 一个非可视对象,7 个方法逐一复现文章实测的 7 组结论(空行 NULL、NULL 写读、原值写穿、SetItem 返回值、缓冲区读取、日期时间列、SetItem 类型拒绝),导入即可自行运行对照。

环境要求

  • Windows 7+,PB 运行库正常安装,32 位
  • 基准 PB10,兼容 PB 12.5,两版均实机跑通
  • 不依赖数据库:DataWindow 用语法串在内存建表,无需连接、无需建表 SQL
  • 不依赖 PBIDEA 等任何第三方组件


操作步骤

  • 把包内 pbl/art98.pbl 加入工程 library list(或用 Library Painter 导入 src/nvo_art98.sru);
  • 代码里 create nvo_art98 后逐个调用 of_p1_empty ~ of_p7_numstr,返回值是带前缀的读数串;
  • tests/t_art98.pbtest 可独立运行,逐方法落盘 g98_p1~p7.txt 供核对。


实机验证情况
PB10 与 PB12.5 各自导入 0 错、ORCA 全量编译 0 错、运行七组读数逐字一致;文中第九节的两处版本差异(PB10 的 GetItemString 严格态)由独立探针另测,结论已在正文标注。包内 readme 有完整排错清单。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

客服微信:18669893686
挂谷猜想 · 探索

Archiver|PowerBuilder(PB)BBS社区 ( 鲁ICP备2021027222号-1 )

GMT+8, 2026-11-15 08:00 , Processed in 0.022286 second(s), 8 queries , MemCached On.

Powered by Discuz! X5.0

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表