简介:这份面向 PowerBuilder 开发者的 DataWindow 实战技巧文档,围绕数据窗口只能追加不能修改、列自动折行、动画效果、选中行外观、动态属性修改、条件格式化、隐藏列、排序与过滤、计算字段、嵌套数据窗口、分组汇总、打印导出、行状态判断、缓存优化等常见开发需求,梳理出38条可直接落地的做法,适合希望提升数据界面交互与开发效率的初中级开发者查阅。压缩包共1个doc文件,约49KB,篇幅紧凑,便于按技巧检索和快速套用。目前已有405人学习下载,说明其在PB数据窗口实践中具有一定参考价值。文档不仅给出属性设置与PowerScript示例,还涉及单行/多行选中配色、Retrieve时保留原数据、日期字段初始化、调整列顺序、美化Grid等细节,可帮助读者理解属性表达式与事件配合的用法,并转化为可复用的界面优化与数据处理思路。
1. 一份 38 条的 DataWindow 技巧清单,能救多少个加班的夜晚
接手一套跑了六七年的 PB 客户端系统,最怕听到的一句话是「这张单据的界面改一下」。柜台上千个 DataWindow,每一个都可能是前任手工堆出来的,属性框里塞着谁也看不懂的表达式。真正让我少加了几十个夜班的,是一份 PB DataWindow 38 个技巧的清单:只追加不改记录、列自动折行、斑马纹、选中行高亮、快速批量删除、下拉数据窗口动态过滤、打印不换页,38 条几乎覆盖了日常改界面的全部套路。
这份 PB DataWindow 技巧清单的价值不在于「知道有这回事」,而在于每一条都能落到具体属性名、表达式字符串和事件代码上。它不是一本 DataWindow 的教科书,更像是一张前人踩过坑之后留下的便签墙,你照着抄就能跑起来,抄完再琢磨为什么要这么写。
它适合两类人:一类是刚接手老 PB 项目、打开 Painter 看到满屏属性页发懵的新人,照着改就行;另一类是准备给现有系统做 UI 统一和交互优化的老手,能从这里挑出可封装的模式。下面我按自己的理解,把这 38 条拆成能直接抄的结构、代码和坑。
2. 骨架拆解:38 条技巧在 DataWindow 四层结构里的位置
38 条技巧如果按顺序一条条背,记住第三条就忘了第一条。我习惯先把它们按「改动发生在哪一层」分成几堆,分完之后你会发现很多技巧其实是同一个机制的两种写法,比如第 1 条的 Protect 和第 24 条其实是重复的,第 16 条和第 22 条也是同一件事。
2.1 四层归类:配置层、表达式层、缓冲区层、运行时 API 层
DataWindow 这东西表面上看是一个控件,实际上叠了四层东西:对象定义(Painter 里画出来的样子)、属性表达式(运行时动态求值的公式)、缓冲区(数据在内存里的几份副本)、以及 PowerScript 调用的那套 Modify/Describe 字符串协议。38 条技巧基本都能塞进这四层里。
| 层 | 载体 | 清单里的代表条目 | 改动方式 |
|---|---|---|---|
| 配置层 | Painter 的属性页、右键菜单 | 2 折行、5 日期初始值、6 列顺序、9 Line 建 Grid、20 只读、25 每页行数 | 勾选、填初始值、拖列标题 |
| 表达式层 | 各属性的 Expressions 页、计算列 | 1/24 Protect、4 选中行背景、8 斑马纹、10 CumulativeSum、16/22 分组行号、17 修改列变色 | 写伪 SQL 表达式 |
| 缓冲区层 | Primary / Filter / Delete | 7 Retrieve 不清除、14 快速删除、15 排序+过滤去重、18 RowsDiscard、21 检查重复行、28 取数过滤排序 | 调 RowsMove、SetFilter、Filter |
| 运行时 API 层 | Modify / Describe / GetChild / 指针函数 | 3 定时器动画、12 Datetime 查找、13 布尔属性、23 冻结列、26 closeQuery、27 GetChild、30~37 取值与指针 | 拼字符串、事件里写脚本 |
分完堆之后有个明显的好处:出问题的时候能快速定位该看哪儿。界面长得不对,先去配置层和表达式层;数据不对,先去缓冲区层;代码报错或者属性改了没反应,基本都在运行时 API 层的字符串协议上,尤其是那个~t转义。
表达式层是这份清单里密度最高的一层,也是最容易让人翻车的一层。它用的是一门长得像 SQL 但又不是 SQL 的小语言,函数有IsRowNew()、GetRow()、CurrentRow()、First(... FOR Page)、First(... FOR Group 1)、Last(...)、CumulativeSum(... for all),还有字段名.Original这种取原值的写法。这些都是跟着 DataWindow 对象在运行时求值的,写错一个函数名不会编译报错,只会在界面上安静地什么都不发生。
2.2 表达式层的两个硬规则:默认值 + ~t + 条件式
表达式属性最反直觉的地方在于它不是「一个值」,而是「默认值 + 条件表达式」两段,中间用一个 tab 隔开。在 Painter 的属性框里你看不出来,因为它是分两栏显示的;一旦落到 Modify 的字符串里,就必须用 PB 的转义符~t显式写出来。
// Painter 里在列的 Protect 属性表达式框填: // If(IsRowNew(), 0, 1) 0 = 可编辑,1 = 只读 // 等价的 Modify 写法,注意 '1~tIf(IsRowNew(),0,1)' 这一段 // 1 是默认值,~t 之后才是条件表达式 string ls_rc ls_rc = dw_1.Modify("emp_name.Protect = '1~tIf(IsRowNew(),0,1)'") // Modify 返回空串才代表成功,返回其他内容是错误描述 if ls_rc <> "" then MessageBox("属性设置失败", ls_rc) end if这里有几个参数值得单独说清楚。emp_name是 DataWindow 里的列对象名,不是数据库字段名,两者经常不一致,用dw_1.Describe("emp_name.dbname")能查到实际字段。Protect的值0和1分别代表可编辑和只读,写成False/True、'No'/'Yes'也认,第 13 条讲的就是这件事——PB 内部一律按字符串存属性,所以你写哪种形式效果都一样。~t是 tab 的转义写法,写成\t或者真的敲一个 tab 进去,在多数情况下都不认。
还有一个更隐蔽的规则:Modify 的完整语法是对象名.属性 = '值',等号两边有空格、值必须用单引号包住,如果值里本身含有单引号(比如字符串比较),要用~"或双引号处理。这套协议对格式极其敏感,多一个空格少一个引号就是一句英文错误信息糊你脸上。
2.3 缓冲区层:三份数据副本决定了删除和过滤的行为
很多 PB 新手觉得 DataWindow 的行为「玄学」,根源是没意识到它内部有三份数据:Primary(当前显示的主缓冲区)、Filter(被 SetFilter 过滤掉的行)、Delete(被 DeleteRow 标记删除的行)。你看到的行数、你删掉的行、你Update()时下发到数据库的行,分别来自不同的缓冲区。
// 过滤:把 dept_id 不等于 '100' 的行从 Primary 移到 Filter dw_1.SetFilter("dept_id = '100'") dw_1.Filter() // 此时 RowCount() 只统计 Primary,FilteredCount() 统计 Filter long ll_show = dw_1.RowCount() long ll_hidden = dw_1.FilteredCount() // 删除:DeleteRow 只是打标记移到 Delete 缓冲区 // 要真正落库,还得调 dw_1.Update() 才会生成 DELETE 语句 dw_1.DeleteRow(3) long ll_deleted = dw_1.DeletedCount()参数上要留意SetFilter接收的是一个布尔表达式字符串,语法和 WHERE 子句接近但不完全一样,它支持字段名[-1]这种「上一行」引用(第 15 条去重就是靠city <> city[-1] or GetRow() = 1实现的)。Filter()执行完之后,被过滤掉的行并没有消失,SetFilter("")再Filter()一次它们就全回来了。
理解了这三个缓冲区,第 14 条的RowsMove就不难懂了:与其循环DeleteRow一行一行地挪,不如一次性把整段行从 Primary 搬到 Delete,省掉每删一行就重排一次行号的开销。
// 清空式删除:把 Primary 里 1 到 RowCount 行整体搬到 Delete 缓冲区 // 参数顺序:源dw, 起始行, 行数, 源缓冲区, 目标dw, 目标行, 目标缓冲区 dw_1.RowsMove(dw_1, 1, dw_1.RowCount(), Primary!, dw_1, 1, Delete!)2.4 一条技巧的完整落地路径
拿第 1 条「使列只能追加不能修改」走一遍,就能看出四层是怎么串起来的。先在 Painter 里打开 DataWindow,逐列双击进属性页,在 Expressions 页的 Protect 栏填If(IsRowNew(), 0, 1),保存——这是配置层加表达式层。然后 Preview 一下,插一行新记录试试能不能编辑,翻到老记录看是不是点不动——这是最朴素的验证。如果要在运行时按角色切换,再写一段 Modify——这是运行时 API 层。最后发现用户改完不保存直接关窗口,新增的行还在缓冲区里,就得在 closeQuery 里加AcceptText()和ModifiedCount() + DeletedCount()的判断,这是清单第 26 条。
同一条需求,四层各出一份力,缺一层就出问题。后面几章我按这个思路,把清单里最常用的几组技巧拆开讲。
3. 行级控制:用 Protect 表达式做出「只能追加不能修改」的表
「只能追加不能修改」是业务系统里出现频率最高的诉求之一,比如流水账、操作日志、审批痕迹表。很多人的第一反应是把整张表设成只读,结果新增也做不了了。正确的做法是保护到列这一级,让 Protect 属性跟着「这一行是不是新插入的」动态求值。
3.1 Painter 里的配置:逐列挂上 IsRowNew 判断
打开要改的 DataWindow,双击任意一列进入属性页,切到 Expressions 标签页,找到 Protect 这一栏,填进去:
If(IsRowNew(), 0, 1)含义是:如果当前行是新增行(IsRowNew()返回 true),Protect 取 0,可编辑;否则取 1,只读。要对所有需要保护的列都做一遍,通常就是除主键、序号之外的全部数据列。
这里有个容易忽略的点:IsRowNew()判断的是「这一行是否由 InsertRow 插入且尚未 Update 成功」,不是「这一行是不是刚被用户点过」。所以已经保存过的新记录,下次检索出来就不再是 New 状态,自动变成只读,这正好符合流水表的语义。
配完之后记得把 DataWindow 保存,然后 Preview 验证。Preview 里可以直接用菜单插行,插出来的行能编辑,翻到检索出来的行点不动,就说明配对了。
3.2 Tab Order 和 Protect 的区别:为什么设成 0 不够用
清单第 20 条和第 24 条都提到了「只读」,但手段不一样,经常有人混用。
Tab Order 设成 0 的效果是这列不参与 Tab 键遍历,鼠标点上去也不会获得焦点,看起来确实不能编辑了。但它的语义是「跳过这一列」,不是「禁止修改」——代码里调SetItem照样能改,界面上如果用户用鼠标拖选一片区域再输入,某些情况下也能绕过去。更要命的是 Tab Order 是静态的,设成 0 就是 0,没法按行判断。
Protect 属性才是真正意义上的「这一列在这一行能不能改」,而且可以写表达式,可以按行、按角色、按字段状态动态变化。清单第 17 条用column_name <> column_name.Original判断字段是否被改过并标红,用的也是同一套原值比较机制。
至于第 20 条里提到的readonly属性,那是整张 DataWindow 级别的开关,一行代码dw_1.Object.DataWindow.ReadOnly = "Yes"全表锁死,适合纯展示场景。三种手段的适用边界很清楚:整表只读用 ReadOnly,整列只读用 Tab Order 0 或 Protect=1,按行控制只读必须用 Protect 表达式。
3.3 运行时切换:Modify 的字符串拼接与回滚
实际项目里更常见的是「平时可编辑,点了审核按钮之后变只读」这种动态需求。这时候就得用 Modify 在运行时下发表达式。
// 把指定列的 Protect 切成「仅新增行可编辑」 // as_cols 用逗号分隔,传空串表示处理 DataWindow 里的全部列 // 返回 0 成功,-1 失败(失败原因放在 ref 参数里) public function integer uf_set_appendonly (datawindow adw, string as_cols, ref string as_err); long ll_cnt, ll_i string ls_col, ls_rc string ls_cols[] // 取 DataWindow 里的列对象总数 ll_cnt = Long(adw.Describe("DataWindow.Column.Count")) // 没指定列名就遍历全部列 if Trim(as_cols) = "" then for ll_i = 1 to ll_cnt ls_col = adw.Describe("#" + String(ll_i) + ".name") ls_rc = adw.Modify(ls_col + ".Protect = '1~tIf(IsRowNew(),0,1)'") if ls_rc <> "" then as_err = ls_col + ": " + ls_rc return -1 end if next else // 指定了列名就只处理这几列 ll_cnt = uf_split(as_cols, ",", ls_cols) for ll_i = 1 to ll_cnt ls_col = Trim(ls_cols[ll_i]) ls_rc = adw.Modify(ls_col + ".Protect = '1~tIf(IsRowNew(),0,1)'") if ls_rc <> "" then as_err = ls_col + ": " + ls_rc return -1 end if next end if return 0 end function这段代码里最关键的不是逻辑,而是两个细节。第一个是Describe("#" + String(ll_i) + ".name"),用序号反查列对象名,比手敲列名可靠得多,尤其是接手别人做的 DataWindow,列名可能是emp_name_t也可能是col_1。第二个是每次 Modify 都把返回值接住,一旦非空立刻带上列名返回,否则你只会看到「改了一半生效一半没生效」这种没法查的现象。
回滚也很简单,把 Protect 恢复成默认值就行:
// 恢复全部列可编辑,Protect 默认值 0,没有条件表达式 string ls_col long ll_i for ll_i = 1 to Long(dw_1.Describe("DataWindow.Column.Count")) ls_col = dw_1.Describe("#" + String(ll_i) + ".name") dw_1.Modify(ls_col + ".Protect = '0'") next注意恢复的时候不要写成'0~t',尾巴上多一个 tab 有些版本会解析失败,老老实实写'0'。
3.4 验证链路:AcceptText、ItemChanged、ItemError 的顺序
Protect 只是不让用户输入,但它管不住「已经输入但还没提交到缓冲区」的数据。DataWindow 的编辑值要经过四步校验才会真正进缓冲区,清单第 11 条把这四步列得很清楚:数据类型是否正确、是否符合有效性规则、数据是否被改动、是否通过 ItemChanged 事件。任何一步不通过,都会触发 ItemError。
所以真正的只读控制要配合AcceptText():
// 保存前先把编辑框里还没提交的值推进缓冲区 // 返回 1 表示成功,-1 表示有数据没通过校验 if dw_1.AcceptText() = -1 then MessageBox("提示", "有数据未通过校验,请检查后再保存") return end if // 再判断到底有没有改动,避免用户什么都没改也弹保存框 if dw_1.ModifiedCount() + dw_1.DeletedCount() = 0 then MessageBox("提示", "数据没有变化,无需保存") return end ifModifiedCount()数的是 Primary 和 Filter 缓冲区里被改过的行,DeletedCount()数的是 Delete 缓冲区。两个都是 0 才代表用户真的没动过东西。这个判断放在 closeQuery 事件里就是清单第 26 条的效果:用户点关闭时先 AcceptText,再问要不要保存,选取消就return 1拦住关闭。
4. 展示层改造:折行、斑马纹、选中行高亮和 Grid 线条
功能对了之后,接下来就是让界面别那么「上世纪」。这一组技巧几乎不涉及数据逻辑,全是属性表达式和 Painter 配置,但改完之后客户的直观感受差别巨大,属于投入产出比最高的部分。
4.1 自动折行:三步配置加一个汉字不断行的坑
列里的内容太长显示不全,最常见的做法是让用户横向滚动,体验很差。清单第 2 条给的做法是让这一列自动折行,具体三步:双击列进属性页,Position 标签勾上 Autosize Height,Edit 标签取消 Auto Horz Scroll;然后右键点 Detail 带(就是那条写有 Detail 的灰色横带),Properties 里也勾上 Autosize Height;保存。
只勾列不勾 Detail 带是最常见的失败原因,因为列的高度会被带区高度截断,表现就是「折行了但只看到一行」。反过来的情况也有,Detail 带勾了、列没勾,行高变大了但内容还是不折。
这里有个 PB 的老毛病要提前知道:连续的中文汉字中间没有标点或空格,系统会当成一个单词,不会在中间断开。所以一段两百字的备注如果没有任何标点,折行之后会横向溢出。常见的绕法是在数据入库前按固定字数插入换行,或者在 DataWindow 里加一个计算列做截断拼接,别指望它自己断。
4.2 斑马纹和选中行高亮:Detail 带的 Color 表达式
单双行不同色(斑马纹)是清单第 8 条,做法是在 Detail 带上右键 → Properties → Expressions → Color 里填:
IF(MOD(GETROW(),2)=0, RGB(192,192,192), RGB(255,255,255))GETROW()返回当前行的行号,MOD(...,2)=0判断偶数行,偶数行灰、奇数行白,预览一下就能看到效果。注意这个表达式必须写在 Detail 带的 Color 上,不是写在某一列的 Color 上——写在列上会变成「这一列斑马纹、那一列不斑马」,整行看起来就花了。
选中行高亮稍微复杂一点,因为它要区分单行选择和多行选择。单行选择最简单,直接在每个字段的背景色表达式里写:
if(GetRow()=CurrentRow(), RGB(255,126,0), RGB(0,0,0))CurrentRow()是当前光标所在行,GetRow()是正在绘制的这一行,两者相等就说明这行被选中了。第一个 RGB 是选中色,第二个是未选中色。取当前选中行的时候用GetRow(),不要用GetSelectedRow(),后者是给多选模式用的,在单选场景下经常返回 0。
4.3 多行选择的 flag 计算列方案
多行选择光靠属性表达式做不出来,因为 DataWindow 自己不知道「哪些行被用户点过」。清单第 4 条给的思路很巧妙:往 SQL 里塞一个常量列当标记位。
-- 在 DataWindow 的 SQL 里加一个常量列,返回结果集时自带 flag 字段 select emp_id, emp_name, dept_id, 1 as flag from employee where status = 'A'回到 Painter 会看到多出来一个 flag 列,把它设成隐藏,然后所有列的背景色表达式改成if(flag=1, RGB(255,126,0), RGB(255,255,255))。最后在 DataWindow 控件的 clicked 事件里翻转这个标记:
// dw_1 的 clicked 事件,row 是事件自带的参数,表示点击的行号 long ll_row ll_row = row if ll_row < 1 then return // 翻转标记位:1 变 0,0 变 1 if this.Object.flag[ll_row] = 1 then this.Object.flag[ll_row] = 0 else this.Object.flag[ll_row] = 1 end ifthis.Object.flag[ll_row]是直接按行号读写某个字段的值,比SetItem快,也不用关心字段类型。行数很多的时候(几千行以上)连续点击会有轻微闪烁,可以在事件开头SetRedraw(false)、结尾SetRedraw(true),但单次点击其实可以不开,开了反而容易和其他刷新逻辑打架。
拿选中的行就是遍历 flag:for i = 1 to RowCount()里判断Object.flag[i] = 1,收集行号。这套方案的好处是标记位跟着行数据走,排序、滚动都不会错位。
4.4 用 Line 画 Grid:让线条跟着行高走
PB 自带的 Grid 风格 DataWindow 边框粗细是固定的,想做粗外框、细内线的打印表格就无能为力。清单第 9 条的做法是用 Tabular 风格加手工画的 Line 来模拟,问题是一改行高,所有竖线的起点终点全得重调,纯体力活。
解法是把线条的坐标交给表达式算。选中竖线,在 Expressions 页把 y1 填0、y2 填rowheight() - 1;横线的 y1 和 y2 都填rowheight() - 5。rowheight()是运行时函数,返回当前行的实际高度,这样无论行高怎么变,线条都自动贴边。
剩下要手工设的只有 x1、x2(横向位置)和横线的长度。改完预览一下,你会发现行高拉大之后线也跟着拉长,再也不用一行行去对坐标了。
4.5 定时器动画:一个容易被忽略的内置能力
清单第 3 条讲的是 DataWindow 自带定时器。双击 DataWindow 打开属性对话框,把 Timer Interval 设成大于 0 的毫秒数,这个 DataWindow 就会按这个间隔触发 Timer 事件。有了它,做闪烁提醒、颜色渐变都不需要额外控件。
// 列的 Background.Color 表达式,用当前时间的末位数字算一个渐变值 // Right(String(Now(), 'hhmmssf'), 1) 取毫秒的末位,0~9 循环 RGB(Integer(Right(String(Now(),'hhmmssf'),1)) * 256 / 10, & Integer(Right(String(Now(),'hhmmssf'),1)) * 256 / 10, 0)Now()返回当前时间,格式串'hhmmssf'里的f是毫秒。定时器每次触发都会重新求值这个表达式,背景色就在暗黄到亮黄之间循环。这个技巧实际项目里用得不多,但做报警行闪烁、进度提示的时候很省事,比另起一个 Timer 控件再手动刷Modify干净。
5. 避坑与排查:DataWindow 五个高频翻车现场
这一章是我这些年攒下来的血泪经验,每一条都真实踩过,按「现象 → 原因 → 解决」写,遇到类似症状可以直接对号入座。
5.1 现象:点了新增插了几行,一切查询条件再 Retrieve,新行全没了
原因:Retrieve()默认行为是先清空 Primary 缓冲区再取数,你手工InsertRow进去还没Update的行属于「脏数据」,一并被清掉。
解决:在 DataWindow 的 RetrieveStart 事件里写return 2。
// dw_1 的 RetrieveStart 事件 // 0 或 1 = 正常清空后检索 // 2 = 不清空缓冲区,把新检索的数据追加到后面 return 2要注意return 2之后重复数据的风险:如果新检索的结果和缓冲区里已有的行主键相同,就会真的出现两条一样的记录。稳妥的做法是配合主键判断,或者在追加完之后用 5.1 之外的去重手段处理(清单第 21 条那套排序加过滤查重就能用上)。另外,如果这个 DataWindow 是用在查询界面上的,本来就应该清空重来,别不加区分地全加上return 2。
5.2 现象:Modify 执行完了,界面一点变化没有,或者弹出一段英文
原因:Modify 的字符串协议对格式极度敏感,常见的有四种错法——把~t写成了\t或者真的 tab;列对象名写成了数据库字段名;值没用单引号包住;等号两边少了空格。
解决:三步排查。先接住返回值,空串才是成功;再用 Describe 回读确认属性真的变了;最后实在拿不准,就在 Painter 里手工配一遍,然后把 DataWindow 导出成语法文件,看它自己是怎么写这个属性的。
// 第一步:接返回值 string ls_rc ls_rc = dw_1.Modify("emp_name.Protect = '1~tIf(IsRowNew(),0,1)'") if ls_rc <> "" then MessageBox("Modify 失败", ls_rc) // 错误信息里通常会带出是哪个对象出的问题 return end if // 第二步:Describe 回读,确认表达式确实生效 string ls_now ls_now = dw_1.Describe("emp_name.Protect") // 正常应该返回类似 "1~tIf(IsRowNew(),0,1)" 的内容回读这一步特别重要,因为 Modify 有时候会返回空串但实际没生效(比如对象名对但属性名拼错成Protects),只有 Describe 能给你确凿证据。清单第 13 条提到「导出一个 DataWindow 看它的源码」,说的就是这个思路——PB 自己生成的语法永远是对的,照着它的格式抄不会错。
5.3 现象:循环 DeleteRow 删多行,结果只删掉一半,或者删错了行
原因:每删一行,后面的行号整体前移一位,而循环变量还在往前走。for i = 1 to RowCount()里删掉第 1 行之后,原来的第 2 行变成了第 1 行,循环却走到 i=2,正好跳过。
解决:倒序删,或者一次性搬走。
// 写法一:从后往前删,行号前移不影响还没处理的行 long ll_i for ll_i = dw_1.RowCount() to 1 step -1 dw_1.DeleteRow(ll_i) next // 写法二:整段搬到 Delete 缓冲区,适合全部清空的场景 dw_1.RowsMove(dw_1, 1, dw_1.RowCount(), Primary!, dw_1, 1, Delete!)还有个更隐蔽的坑:如果 DataWindow 上设了过滤条件,被过滤掉的行在 Filter 缓冲区里,RowCount()根本不统计它们,RowsMove(..., Primary!, ...)也搬不到它们。要连过滤行一起处理,得先SetFilter("")再Filter()把它们放回 Primary,或者分别对两个缓冲区操作。
5.4 现象:下拉数据窗口(DDDW)动态过滤写了,下拉列表还是全部数据
原因:GetChild返回 -1 说明这个列根本不是下拉列,后面的代码全是空转;或者子数据窗口没设事务对象;或者拼 SQL 的时候原来的 where 没截干净,拼出两个 where 直接语法错误。
解决:先判断返回值,再规范地截断和拼接,拼完回读确认。
// dw_1 的 ItemFocusChanged 事件 integer li_rc string ls_sql, ls_dept DataWindowChild ldwc li_rc = dw_1.GetChild("dept_id", ldwc) if li_rc = -1 then return // 不是下拉数据窗口,直接退出 ldwc.SetTransObject(SQLCA) // 子数据窗口要单独设事务对象,这步最容易漏 // 取原始 SQL,统一转小写后找 where 的位置,避免大小写不一致漏判 ls_sql = ldwc.GetSQLSelect() if Pos(Lower(ls_sql), " where ") > 0 then ls_sql = Left(ls_sql, Pos(Lower(ls_sql), " where ") - 1) end if // 用当前行的 dept_id 作为过滤条件重新拼 ls_dept = dw_1.GetItemString(dw_1.GetRow(), "dept_id") ls_sql = ls_sql + " where dept_id = '" + Trim(ls_dept) + "'" ldwc.SetSQLSelect(ls_sql) ldwc.Retrieve()这里Lower()那一步是我后来补上的:原 SQL 里经常写成大写的WHERE,直接Pos(ls_sql, " where ")找不到,截断就失效了,拼出来的语句变成select ... where ... where ...,SetSQLSelect直接失败但不报错,表现就是「下拉还是老数据」。另外这个事件在用户每次切换焦点时都会触发,条件没变就别重复 Retrieve,加个缓存变量能省不少数据库往返。
5.5 现象:Describe 取汇总带上的计算列,拿回来一个空串或者 "!"
原因:计算列对象名不对,或者这个计算列不在正确的带区,或者 DataWindow 还没 Retrieve 过、表达式求值为空。Describe遇到求值失败会返回"!",遇到对象不存在返回空串,这两种情况经常被当成「值为空」处理,然后一转数字就报类型错误。
解决:先列出所有对象名确认计算列确实存在,再取表达式本身,最后才取值。
string ls_val, ls_expr // 确认 compute_1 这个对象存在,并且看看它的表达式写的是什么 ls_expr = dw_1.Describe("compute_1.Expression") if ls_expr = "" or ls_expr = "!" then MessageBox("提示", "compute_1 不存在,先用 DataWindow.Objects 列出所有对象名") return end if // 取值,第二个参数 1 表示在当前页范围内求值 ls_val = dw_1.Describe("Evaluate('compute_1',1)") if ls_val = "!" or ls_val = "" or IsNull(ls_val) then MessageBox("提示", "汇总列求值失败,检查带区和 Retrieve 状态") return end if decimal ld_total ld_total = Dec(ls_val) // 数值型必须转换,Describe 返回的永远是字符串Describe("DataWindow.Objects")返回的是所有对象名的 tab 分隔列表,排查对象名问题时这是第一手证据。第 34 条那个Evaluate用法的关键在于第二个参数:传 1 是当前页,传 0 是全部数据,做全表汇总和分页汇总要区分开。
6. 二次开发:把 38 条技巧收进一个可复用工具对象
38 条技巧散落在各个窗口的事件里,时间一长就是灾难:同一个 Protect 字符串有五种写法,改一个规则要翻十几个窗口。我现在接手 PB 项目的第一件事,就是建一个不可视对象nvo_dw_util,把重复率最高的那几组操作封进去。
封装的第一层是表达式常量。把所有用到的属性表达式集中定义成实例变量,别让字符串散落在代码里:
// nvo_dw_util 的实例变量区 // 统一维护所有 DataWindow 表达式,改规则只改这里 constant string CS_PROTECT_APPENDONLY = "1~tIf(IsRowNew(),0,1)" constant string CS_COLOR_STRIPE = "IF(MOD(GETROW(),2)=0, RGB(192,192,192), RGB(255,255,255))" constant string CS_COLOR_SELECTED = "if(flag=1, RGB(255,126,0), RGB(255,255,255))" constant string CS_COLOR_MODIFIED = "IF(emp_name <> emp_name.Original, RGB(255,0,0), RGB(0,0,0))"第二层是函数,按「设置类」和「查询类」分成两组。设置类的签名统一成(datawindow adw, string as_cols, ref string as_err),成功返回 0、失败返回 -1 并带出原因;查询类统一返回字符串或行号数组,内部把 Describe 的"!"和空串都当成失败处理,不让调用方去猜。上面第 3 章那个uf_set_appendonly就是按这个规范写的。
第三层是命名约定。列对象名一律用字段名加下划线加类型后缀,标题加_t,计算列加_c。清单第 31 条和第 35 条那种靠GetObjectAtPointer()反推对象名的技巧,前提就是命名有规律——如果列名是col_1到col_20,反推出来的东西没有任何意义。第 33 条遍历所有列标题取_t.text的写法,也是建立在同一套命名规范上的。
验证方法我一般做一个小自测窗口,左边放一个 DataWindow 控件,右边一组按钮,逐个触发封装的函数,每个函数执行完立刻Describe回读并显示在文本框里。这套东西不追求自动化,只求改完工具对象之后能五分钟内把七八个常用技巧全过一遍,别等到业务窗口里才发现某个 Modify 悄悄失败了。
最后说个习惯上的教训。早年我图快,改完表达式属性直接保存提交,从来不 Preview,结果客户现场一打开就是一片红——column_name.Original里column_name被我写成了实际字段名,但 DataWindow 里那个列对象早就被改过名,表达式求值失败,整列颜色变成了默认的黑色。从那以后我每次动 DataWindow 的属性表达式,都强制先Describe回读一遍确认字符串没写歪,再 Preview 翻两页看真实渲染效果,两个都过了才提交。这套笨办法救过我不少次,希望帮到你。
本文还有配套的精品资源,点击获取