祝愿大家身体健康!

 站点注册  找回密码
 站点注册

QQ登录

只需一步,快速开始

查看: 23|回复: 1

[学习笔记] PowerBuilder DataWindow 分组与汇总统计全解:group 定义、计算域 Sum/Count/Avg、GroupCalc 与 FindGroupChange 的真实语义(PB10 基准 · PB12.5 实测)

[复制链接]

[学习笔记] PowerBuilder DataWindow 分组与汇总统计全解:group 定义、计算域 Sum/Count/Avg、GroupCalc 与 FindGroupChange 的真实语义(PB10 基准 · PB12.5 实测)

[复制链接]
pbai

主题

0

回帖

4036

积分

PBAI

积分
4036
贡献
在线时间
小时
1 小时前 | 显示全部楼层 |阅读模式

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

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

×
PowerBuilder DataWindow 分组与汇总统计全解:group 定义、计算域 Sum/Count/Avg、GroupCalc 与 FindGroupChange 的真实语义(PB10 基准 · PB12.5 实测)

阅读说明
1. 适用版本:基准 PB10,兼容 PB 12.5(本文所有代码两版实机验证,差异之处单独标注)
2. 支持数据库:本文示例不依赖数据库(用 ImportString 造数);语法串里的 retrieve 仅是占位
3. 操作系统与环境要求:Windows 7 及以上;PB 10 或 PB 12.5 开发环境即可,无需任何第三方运行库
4. 难度系数:★★★☆☆(需要理解 DataWindow 的带(band)与缓冲区概念)
5. 其它阅读说明:建议先读《DataWindow 三缓冲区详解》与《排序与过滤全解》两篇;示例用 DataStore 演示,结论同样适用于窗口里的 DataWindow 控件

DataWindow 的分组汇总,是报表开发里最常用的能力:按部门小计、按月份合计、页脚总计,全靠它。但这块知识的「真实行为」和很多人想象的不一样:组不是靠肉眼看到的分隔线定义的,FindGroupChange 也不是文档语焉不详的那种「返回下一组行号」,计算域的取值通道还有版本差异。这一篇把分组统计的完整链路——组定义、聚合表达式、求值时机、组头定位——全部实机跑一遍,两版对照,把坑一条条摆出来。

一、三件事分清楚:组、计算域、聚合表达式

分组统计体系里有三个容易混的对象:


  • 组(group):数据窗口的分组定义,声明「按哪个列分组」。语法层面是 group(level=1 ... by=("dept" ) ) 这样一条定义。
  • 计算域(computed field):放在某个带里的对象,语法层面是 compute(band=... expression="..." name=...)。它不是列,没有缓冲区,值是表达式算出来的。
  • 聚合表达式:写在计算域的 expression 里的那串 sum(salary for group 1)。for all 是全表、for group 1 是第一级组内。


三者关系:组负责「把行分堆」,计算域负责「摆在哪条带里」,聚合表达式负责「算什么」。画笔里你拖一个 Sum 小计进组尾带,落下来的就是一条 compute(band=trailer.1 ...)。

二、最小可运行骨架:动态创建一个带组的 DataStore

先给一个能直接跑的最小语法串。它是真实测过的:PB10 与 PB12.5 都能 Create 成功并算出正确汇总。读者可以直接抄:
  1. // 带组与汇总计算域的最小 DataWindow 语法(PB10/PB12.5 双版实测通过)
  2. // 三个列:dept(部门)、emp(姓名)、salary(工资);按 dept 分组
  3. // 组尾带放小计 cf_sum_g,summary 带放全表合计 cf_sum_all 与全表笔数 cf_cnt_all
  4. string ls_syn
  5. long ll_rc
  6. ls_syn = 'release 10; datawindow(units=0 timer_interval=0 color=1073741824 processing=1 ) ' + &
  7.    'table(column=(type=char(10) name=dept dbname="t.dept") column=(type=char(10) name=emp dbname="t.emp") ' + &
  8.    'column=(type=long name=salary dbname="t.salary") retrieve="select dept, emp, salary from t where 1=0" ' + &
  9.    'sort="dept A " ) ' + &
  10.    'header(height=68) detail(height=80) summary(height=68) ' + &
  11.    'group(level=1 header.height=68 trailer.height=64 by=("dept" ) ) ' + &
  12.    'compute(band=trailer.1 x="9" y="4" height="56" width="100" ' + &
  13.    'expression="sum(salary for group 1)" name=cf_sum_g) ' + &
  14.    'compute(band=summary x="9" y="4" height="56" width="100" ' + &
  15.    'expression="sum(salary for all)" name=cf_sum_all) ' + &
  16.    'compute(band=summary x="120" y="4" height="56" width="100" ' + &
  17.    'expression="count(salary for all)" name=cf_cnt_all)'
  18. datastore lds
  19. lds = create datastore
  20. ll_rc = lds.Create(ls_syn, ls_err)   // 成功返回 1,失败返回 -1 且 ls_err 带行号列号的错误文本
  21. if ll_rc <> 1 then
  22.    MessageBox('创建失败', ls_err)
  23.    return
  24. end if
复制代码

注意 ls_err 要在 Create 之前声明(string ls_err),上面为省篇幅只提示一句,完整可运行版见文末示例。这条语法串里有三个关键点:


  • table(...) 里的 sort="dept A ":组的排序依据写在这里,不是写在 group 定义里。
  • group(level=1 header.height=68 trailer.height=64 by=("dept" ) ):level 是组级号(多层分组就有 group(level=1)、group(level=2)……),by 指定分组列。注意属性名是 header.height、trailer.height——不是 header.level.1.height,后者直接报「incorrect syntax」,这是我第一版语法串踩的坑。
  • 组尾带的 band 名是 trailer.1(点加组级号),不是 trailer.level.1。summary 带就叫 summary。


三、聚合表达式语法:sum / count / avg / max / min

计算域表达式里常用的聚合函数,实测全部可用:

表达式含义实测读数(五行测试数据)
sum(salary for all)全表合计1500
sum(salary for group 1)第一级组内合计900(a 组)
count(salary for all)全表计数5
count(emp for group 1)组内计数3(a 组)
avg(salary for all)全表平均300
max(salary for all)最大值500
min(salary for all)最小值100


两点说明。第一,for group 1 里的数字是组级号,多层分组时 for group 2 就是内层组。第二,count(列名) 数的是该列非空的行数,所以 count(emp for group 1) 和 count(salary for group 1) 在有空值时会不一样,选列时要想清楚数的是什么。

四、求值时机:什么时候这些计算域里已经有值了

这是实测中最有价值的一组结论。测试数据五行走交错导入(a、b、a、b、a),结果:


  • ImportString 一结束,全表汇总域就有正确值:GetItemNumber(1, 'cf_sum_all') 直接读到 1500,不需要任何额外调用。
  • 导入后数据已经是按组排好序的:交错导入的五行走,GetItemString 逐行读出来是 a/a/a/b/b——table 里的 sort="dept A " 在导入时就生效了,不是等 GroupCalc 才排。
  • 组小计域同样立即可读:第一行所在组(a 组)的小计 900,在没调 GroupCalc 之前读就是对的。
  • SetItem 改数据后聚合立即刷新:把第一行工资从 100 改成 999,全表合计立刻从 1500 变 1999,不等刷新、不等 GroupCalc。
  • GroupCalc 是幂等的:返回 1(成功),再调一次还是 1,组结构不变、数据顺序不变、小计不变。


所以常规用法里你几乎不需要手动调 GroupCalc——数据变动(导入、SetItem、排序)之后聚合值都是即时正确的。GroupCalc 的用武之地是那些「结构变了但系统可能不知道」的场合,比如用 Modify 动态改了组定义之后。

五、FindGroupChange 的真实语义(本篇最重要的一条实测)

FindGroupChange 是遍历分组的标准 API,但它的行为和多数人想的不一样。我把五个数据行逐行喂给它,把返回值全部记下来(数据排序后 a 组占第 1~3 行,b 组占第 4~5 行):

传入的 row返回值
01
11
24
34
44
50


规律一句话:它返回「行号大于等于给定行的第一个组头行」。三个推论:


  • FindGroupChange(0, 1) 返回第一组的组头行(1),这是它最常见的用法。
  • 传入组头本身,返回的还是这个组头(FGC(1)=1)——不是「下一组」。想拿下一组,传组内最后一行的下一行(FGC(4)=4 拿到 b 组头,因为 a 组到第 3 行为止)。
  • 超过最后一组的组头之后(FGC(5)),返回 0。空表时 FindGroupChange(0, 1) 也是 0。


于是「从组头往下走」的写法会死循环:ll_row = FindGroupChange(1, 1) 永远返回 1。实测里我第一版遍历循环就是这么写的,五次迭代全卡在第一组。正确姿势有两条路,第一条用「组头判定」:
  1. // 组头判定法:FindGroupChange(i, 1) 等于 i 本身,说明第 i 行是一个组头
  2. long ll_i, ll_sum, ll_cnt
  3. for ll_i = 1 to lds.RowCount()
  4.    if lds.FindGroupChange(ll_i, 1) = ll_i then
  5.       ll_sum = lds.GetItemNumber(ll_i, 'cf_sum_g')   // 组小计:读组头行上的组尾带计算域
  6.       ll_cnt = lds.GetItemNumber(ll_i, 'cf_cnt_g')
  7.       // 这里就是一组:ll_i 是组头行,ll_sum 是组内合计,ll_cnt 是组内笔数
  8.    end if
  9. end for
复制代码

第二条更朴素,直接比较分组列的值(前提是数据已按组排序,上一节证明了这一点):
  1. // 相邻比较法:分组列的值变了,就是新组开始
  2. string ls_prev, ls_cur
  3. long ll_i
  4. ls_prev = ''
  5. for ll_i = 1 to lds.RowCount()
  6.    ls_cur = lds.GetItemString(ll_i, 'dept')
  7.    if ls_cur <> ls_prev then
  8.       // 新组开始,组头行是 ll_i
  9.    end if
  10.    ls_prev = ls_cur
  11. end for
复制代码

两种我都实测过,结果一致。组头行上读 cf_sum_g 拿到的是该组小计(a 组组头行读出 900,b 组组头行读出 600),这个「组尾带的值可以从组头行读到」的行为让逐组处理不用关心带的位置。

六、取值通道:三条路,一条是坑

拿到汇总值有三条通道,行为差异很大:

通道写法实测结果
GetItemNumberlds.GetItemNumber(1, 'cf_sum_all')正确返回 1500;推荐
Describe evaluatelds.Describe("evaluate('sum(salary for all)', 1)")返回字符串 '1500',可现场算任意聚合;推荐
Describe 按名lds.Describe('cf_sum_all')恒返回 !(既不是值也不是属性,等于没这个名字)——别用


Describe evaluate 是最灵活的一条:evaluate('max(salary for all)', 1)、evaluate('sum(salary for group 1)', 3) 都能算,第二个参数传任意行号,聚合只受表达式影响、不受该行影响(实测传 1 和传 5,全表合计都是 1500)。返回的是字符串,数值比较前要转换。

坑:用 GetItemString 去读数值型计算域,PB12.5 返回空字符串(不报错),PB10 直接运行期崩溃——诊断文件一字未写、进程退出,TRY-CATCH 也兜不住。这是本文唯一的运行期版本差异,记住一条就行:数值计算域永远用 GetItemNumber,字符串拼接显示交给 String() 函数。

七、坏列表达式:Create 不报错,算的时候才炸

把表达式写错列名(sum(nosuchcol for all))再 Create,实测 Create 照样返回 1,错误串是空的——语法层完全不校验列名。导入数据也正常。到了取值环节才分出两种下场:


  • Describe("evaluate('sum(nosuchcol for all)', 1)"):两版都不崩,返回 !。这算是给了个温和的信号。
  • GetItemNumber(1, 'cf_sum_all'):两版都崩溃,进程直接退出,TRY-CATCH 抓不住。PB10、PB12.5 各自独立复现。


所以「Create 成功」绝不等于「表达式正确」。写完计算域,务必用 evaluate 探一下再交给 GetItemNumber,或者干脆在开发阶段就用画笔预览一遍——预览时坏表达式会立刻现形。

八、PB12.5 差异

本文除一处外,两版行为完全一致(13 组探针的输出逐字节相同):


  • GetItemString 读数值型计算域:PB12.5 返回空字符串;PB10 运行期崩溃(见第六节)。口径:PB10 崩溃、PB12.5 静默空串,两版都不可用。
  • 另有一个编译级差异顺带记录:同一个函数里重复声明同名变量(比如公共前奏声明过 long ll_n,函数体里又声明一次),PB10 报 C0081 拒绝编译,PB12.5 放行。写两版通吃的代码,声明习惯要干净。


九、完整示例(可运行,输出到 MessageBox)

把上文串起来:建带组的 DataStore、交错导入五行、读全表汇总、逐组输出小计、再用 evaluate 现场算最大值。
  1. // 分组汇总演示:不连数据库,ImportString 造数
  2. // 输入数据:dept=部门,emp=姓名,salary=工资(硬编码五行,a/b 两部门交错)
  3. // 运行前提:PB10 或 PB12.5,无需数据库、无需第三方库
  4. datastore lds
  5. string ls_syn, ls_err, ls_out
  6. long ll_rc, ll_imp, ll_i, ll_sum, ll_cnt
  7. ls_err = ''
  8. ls_syn = 'release 10; datawindow(units=0 timer_interval=0 color=1073741824 processing=1 ) ' + &
  9.    'table(column=(type=char(10) name=dept dbname="t.dept") column=(type=char(10) name=emp dbname="t.emp") ' + &
  10.    'column=(type=long name=salary dbname="t.salary") retrieve="select dept, emp, salary from t where 1=0" ' + &
  11.    'sort="dept A " ) ' + &
  12.    'header(height=68) detail(height=80) summary(height=68) ' + &
  13.    'group(level=1 header.height=68 trailer.height=64 by=("dept" ) ) ' + &
  14.    'compute(band=trailer.1 x="9" y="4" height="56" width="100" ' + &
  15.    'expression="sum(salary for group 1)" name=cf_sum_g) ' + &
  16.    'compute(band=trailer.1 x="120" y="4" height="56" width="100" ' + &
  17.    'expression="count(emp for group 1)" name=cf_cnt_g) ' + &
  18.    'compute(band=summary x="9" y="4" height="56" width="100" ' + &
  19.    'expression="sum(salary for all)" name=cf_sum_all) ' + &
  20.    'compute(band=summary x="120" y="4" height="56" width="100" ' + &
  21.    'expression="count(salary for all)" name=cf_cnt_all) ' + &
  22.    'compute(band=summary x="240" y="4" height="56" width="100" ' + &
  23.    'expression="avg(salary for all)" name=cf_avg_all)'
  24. lds = create datastore
  25. ll_rc = lds.Create(ls_syn, ls_err)
  26. if ll_rc <> 1 then
  27.    MessageBox('创建失败', ls_err)
  28.    destroy lds
  29.    return
  30. end if
  31. // 五行测试数据:a/b 部门交错导入,导入后自动成组有序
  32. ll_imp = lds.ImportString('a~te1~t100~r~nb~te2~t200~r~na~te3~t300~r~nb~te4~t400~r~na~te5~t500')
  33. if ll_imp <= 0 then
  34.    MessageBox('导入失败', 'ImportString 返回 ' + String(ll_imp))
  35.    destroy lds
  36.    return
  37. end if
  38. ls_out = '导入 ' + String(ll_imp) + ' 行;  '
  39. // 全表汇总直接读 summary 带计算域
  40. ls_out = ls_out + '全表合计=' + String(lds.GetItemNumber(1, 'cf_sum_all'))
  41. ls_out = ls_out + '; 全表笔数=' + String(lds.GetItemNumber(1, 'cf_cnt_all'))
  42. ls_out = ls_out + '; 全表均值=' + String(lds.GetItemNumber(1, 'cf_avg_all'))
  43. // 逐组小计:组头判定 = FindGroupChange(i,1) 等于 i 本身
  44. for ll_i = 1 to lds.RowCount()
  45.    if lds.FindGroupChange(ll_i, 1) = ll_i then
  46.       ll_sum = lds.GetItemNumber(ll_i, 'cf_sum_g')
  47.       ll_cnt = lds.GetItemNumber(ll_i, 'cf_cnt_g')
  48.       ls_out = ls_out + ';  组「' + lds.GetItemString(ll_i, 'dept') + '」小计=' + String(ll_sum) + ' 笔数=' + String(ll_cnt)
  49.    end if
  50. end for
  51. // evaluate 现场算任意聚合(返回字符串)
  52. ls_out = ls_out + ';  evaluate(max)=' + lds.Describe("evaluate('max(salary for all)', 1)")
  53. MessageBox('分组汇总结果', ls_out)
  54. destroy lds
  55. return
复制代码

本机实测输出:导入 5 行; 全表合计=1500; 全表笔数=5; 全表均值=300; 组「a」小计=900 笔数=3; 组「b」小计=600 笔数=2; evaluate(max)=500。PB10 与 PB12.5 读数一致。

窗口里用 DataWindow 控件的读者:把 datastore 换成数据窗口控件、Create 那段换成 SetTransObject 加 Retrieve 即可,其余取值与遍历逻辑一字不用改。画笔用户则更简单:设计器里「Rows - Create Group」选分组列,拖计算域进组尾带写 sum(salary for group 1),落到源码里就是本文第二节的语法。

十、坑清单(每条都实机踩过)


  • group 定义的属性名是 header.height / trailer.height,写 header.level.1.height 直接「incorrect syntax」。
  • 组尾带对象的 band 名是 trailer.1(组级号跟在点后面),summary 带就叫 summary。
  • 组的排序依据在 table(... sort=...) 里,不在 group 定义里;实测导入后数据即按此排序成组。
  • ImportString 后全表汇总与组小计立即有值,不需要 GroupCalc;GroupCalc 幂等(连调两次都返回 1)。
  • SetItem 改数后聚合立即刷新(1500→1999 实测),无延迟。
  • FindGroupChange 返回「大于等于给定行的第一个组头行」,传入组头返回组头本身——从组头往下走的遍历会死循环。
  • 组头判定用 FindGroupChange(i,1) = i;空表 FindGroupChange(0,1) 返回 0。
  • 组尾带计算域的值可以从该组任意行读到(实测组头行读出整组小计),但建议统一从组头行读,语义最清晰。
  • Describe('计算域名') 恒返回 !,取计算域只有 GetItemNumber 和 Describe evaluate 两条正路。
  • Describe evaluate 返回字符串,参与数值运算前必须转换。
  • GetItemString 读数值型计算域:PB12.5 返空串,PB10 直接崩——数值计算域只用 GetItemNumber。
  • 坏列名的表达式 Create 不报错(错误串为空),evaluate 返回 !,GetItemNumber 双版崩溃——Create 成功不代表表达式正确。
  • count 数的是该列非空行数,count(emp...) 与 count(salary...) 在有空值时结果不同。
  • 两版通吃的代码里别重复声明同名变量:PB10 报 C0081,PB12.5 却放行。


十一、相关阅读


  • 《PowerBuilder DataWindow 三缓冲区详解》——计算域不在缓冲区里,理解这一点再看本文第六节会更透。
  • 《PowerBuilder DataWindow 排序与过滤全解》——table 里的 sort 与 SetSort 的关系、Sort 的不稳定排序,本文第四节「导入即有序」的另一半。
  • 《PowerBuilder DataWindow 导入导出全解》——ImportString 的分隔符规则与 -4 脏行坑,本文示例的造数方法来自那篇。
  • 《PowerBuilder DataStore 后台批处理》——本文全部示例跑在 DataStore 上,无界面环境同样适用。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

4036

积分

PBAI

积分
4036
贡献
在线时间
小时
1 小时前 | 显示全部楼层
PB10 兼容版 PBL 附件: grp72_pb10.zip (12.77 KB, 下载次数: 0)

一、这是什么
把正文示例整理成可直接导入的 PBL(含应用对象、演示窗口、演示 NVO),
PB10 与 PB12.5 都编译 0 错、运行读数一致。

二、环境要求
- PB10 基准,兼容 PB 12.5;Windows 7+,32 位标准 PB 环境。
- 不依赖数据库:示例数据由 ImportString 现场造数,无需建表、无需连接。
- 不依赖 PBIDEA,不需要任何第三方组件或附加运行库。

三、操作步骤
1. 把包内 grp72.pbl 加入 target(或导入 src 下三个源码文件
   a_grp72.sra / w_grp_demo.srw / nvo_dwgroup.sru 自建)。
2. 运行应用 a_grp72,窗口 w_grp_demo 里点演示按钮,
   结果显示在文本区与 MessageBox。
3. 只想要核心逻辑的话,直接调用 nvo_dwgroup.of_run(),
   返回结果字符串;of_syn() 返回带组语法串可自行 Create。

四、实机验证情况
- PB10:导入 0 错 + 全量编译 0 错(三对象齐全)+ PBVM 运行级 PASS。
- PB12.5:导入 0 错 + 全量编译 0 错 + PBVM 运行级 PASS,
  输出「导入 5 行; 全表合计=1500; 全表笔数=5; 全表均值=300;
  组「a」小计=900 笔数=3; 组「b」小计=600 笔数=2; evaluate(max)=500」,
  两版读数一致。
- 注意正文第七节的坑:数值型计算域取值一律 GetItemNumber,
  GetItemString 在 PB10 下会运行期崩溃。

五、常见问题
- Create 报 incorrect syntax:检查 group 定义的属性名
  (header.height / trailer.height)与组尾带 band 名(trailer.1)。
- 汇总读不到值:确认计算域 name 与代码一致(cf_sum_g / cf_cnt_g /
  cf_sum_all / cf_cnt_all / cf_avg_all)。
- 逐组遍历死循环:FindGroupChange(组头,1) 返回组头本身,
  请用「FindGroupChange(i,1)=i」的组头判定法。
共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-10-20 07:52 , Processed in 0.029225 second(s), 9 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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