ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

PB报表自定义系统:基于DataWindow的动态配置化实践

PB报表自定义系统:基于DataWindow的动态配置化实践 简介这是一份基于PowerBuilderPB开发的报表自定义系统完整源码包面向PB应用开发人员与需要定制企业报表的技术人员。系统核心依托PB的数据窗口DataWindow能力支持通过调整布局、添加计算列、设置过滤条件等方式实现报表灵活自定义并涵盖数据连接、权限控制、任务调度等企业级模块设计思路。包内共41个文件以bmp图标资源、pbl库文件、pbw工作区及pbt目标文件等PB工程关键文件为主同时包含txt/htm说明文档、db数据库及ini配置文件整体仅839KB结构精简便于直接打开工程学习。目前已有209人学习下载。源码开放透明便于研读报表生成逻辑、进行二次开发和功能扩展也可作为PB报表系统从设计到落地的参考范例。1. PB报表自定义系统是怎么把报表做成“活”的如果一套老ERP跑了十年业务方每个月还在提“把这列宽度调一下”“加个合计行”“日期格式换成中文”而你还在用PB代码硬写每个DataWindow那这个PB报表自定义系统就是要把你从这种重复劳动里捞出来。它不是报表展示引擎的替代品而是围绕PowerBuilder的DataWindow做了一层配置化和动态化封装报表对象、数据源、列布局、计算字段和过滤参数都可以按业务需求调整部分场景甚至做到不重新编译就能发布。适合正在维护PB项目、准备把报表模块从硬编码改成可配置结构的开发者和系统运维。2. DataWindow与数据源绑定自定义报表的地基PB的自定义报表表面上是在改报表格式本质上是在管理DataWindow对象的加载、绑定和展示。理解这一层后面所有“自定义”都是在同一个骨架上做变体。2.1 为什么自定义报表必须围绕DataWindowPowerBuilder的DataWindow对象同时承担了数据绑定和界面渲染一个DataWindow对象里既保存了SQL、列定义也保存了控件位置、颜色、字体、编辑风格。传统的做法是把这些报表对象写死在PBL库里报表需求一变就改对象源码、重新编译整个应用。而自定义报表系统的思路是把报表对象当成“可切换资源”运行时通过ReportName加载不同的DataWindow对象把用户调整过的列宽、顺序、过滤条件保存到配置表或文件里下次打开时自动套用。这套思路在PB里能走通靠的是DataWindow的两种能力第一是DataObject属性可以随时切换第二是Describe/Modify这组元数据接口能够读取和修改DataWindow内部几乎所有的属性。其他PowerBuilder控件很难做到这种深度。2.2 动态绑定数据源从DataObject到Retrieve在报表模块中我一般会在基类窗口放一个名为dw_report的DataWindow控件所有业务报表子窗口继承它。打开窗口时按传入的报表编号加载对应的DataWindow对象然后绑定事务对象并检索数据。代码如下// 窗口的Open事件或自定义函数中 string ls_report_name ls_report_name d_rpt_sales_month // 切换DataWindow对象名称来自PBL或者配置表 dw_report.DataObject ls_report_name // 绑定全局事务对象SQLCA也可以用自己创建的事务对象 dw_report.SetTransObject(SQLCA) // 执行检索as_ym是数据源SQL中定义的检索参数 dw_report.Retrieve(as_ym)这段代码的逻辑是先通过字符串指定报表对象再调用SetTransObject让DataWindow和数据库会话关联最后通过Retrieve把数据灌入控件。需要注意三点一是DataObject赋错名字不会立即报错只有Retrieve时才可能报“DataWindow does not have a retrieve argument”之类的错误二是Retrieve参数必须与报表对象里定义的数据源检索参数数量和类型严格一致三是如果应用同时使用多个数据库连接不要全局只用一个SQLCA应该用各自的事务对象。实际项目中我还会把ls_report_name从配置表读取而不是从代码里传。配置表可以包含报表编码、报表对象名、所属分类、是否启用等字段。这样新加一张报表只需要新增数据窗口对象和一条配置记录不需要改业务代码。2.3 不同报表场景下的DataWindow显示样式DataWindow能做成表格、分组、单据、交叉表等样式选错样式会让自定义逻辑多绕很多弯。下面是我常用的选型表。样式类型适合场景自定义报表时注意点Grid明细列表、多列查询结果列宽可拖拽适合用户直接调整但列头固定不适合单据Tabular分页明细、多记录列表行列布局灵活适合做分组统计的底表Group分类汇总报表分组层级写死在对象里动态改分组条件比较麻烦Freeform单据、表单、单记录每个字段独立摆放适合做卡片式自定义CrossTab行列交叉统计、透视表行列轴通过表达式配置数据量大时性能要单独优化当你不确定业务方要什么样式时默认用Grid最安全。等到用户明确需要分组汇总或者单据打印再专门创建Group或Freeform对象。CrossTab适合数据源聚合后返回少量行列的场景不适合直接把明细表丢上去做交叉表否则Retrieve回来数据量大运行期统计计算会明显卡顿。3. 布局、计算列、过滤参数把报表“自定义”落到实处自定义报表系统能不能落地取决于三个操作是否可配置用户改布局后能不能存住、计算列能不能动态改、检索数据能不能按条件精确拉取。下面分三种情况处理。3.1 列布局自定义别用SRD文件用配置表很多PB资料提到可以把DataWindow导出成SRD文件用户改完再导入。我实际项目中试过SRD在PBL版本不同、列名大小写不一致时经常出现导出成功导入空白的“幽灵问题”尤其是给非程序员操作时一次失败就会让他们放弃整个系统。更稳的做法是只保存用户的布局偏好而不是整份对象源码。建一张user_report_layout表字段至少包含user_id、report_id、col_name、seq、width、visible、format。用户调整列宽或顺序后用循环把每个列的信息读出来// 将当前列布局保存到配置表 long ll_count, i string ls_col, ls_width, ls_vis // 读取列数量 ll_count Long(dw_report.Describe(DataWindow.Column.Count)) for i 1 to ll_count // 获取第i列的列名 ls_col dw_report.Describe(DataWindow.Column. String(i) .Name) if ls_col !Err then continue // 读取列宽和可见性 ls_width dw_report.Describe(ls_col .Width) ls_vis dw_report.Describe(ls_col .Visible) // 写回user_report_layout表这里省略INSERT语句 INSERT INTO user_report_layout(user_id, report_id, col_name, seq, width, visible) VALUES (:ls_user, :ls_rpt, :ls_col, :i, :ls_width, :ls_vis); // 用UPDATE更合适避免每次插入重复数据 next这段代码的核心是Describe方法返回以字符串表示的列属性。Column.Count返回列数而不是DataWindow控件对象名遍历时通过Column.N.Name拿列名再用列名加后缀Width、Visible读取对应属性。要注意Describe返回“!Err”表示取不到属性常见原因是列名写错或当前DataWindow是Graph对象没有通用列属性。由于PowerScript里Describe结果很多是字符串读取Width这种数值属性时也要转成Long再写入数据库避免把默认值存成空字符串。恢复布局时反过来查询配置表对每列执行Modify设置Width和Visible再调用dw_report.SetRedraw(False)和SetRedraw(True)包住避免用户看到列陆续变化。3.2 动态计算列Modify写表达式而不是加对象用户说“给我加个金额列等于数量乘单价还要千分位格式”这是自定义报表里最高频的需求。如果在painter里加列再发版那就不叫自定义了。用Modify动态创建计算列更合适string ls_create // 创建一个位于detail band的计算列 ls_create Create Column( banddetail x1050 y0 height80 width600 alignment1 taborder0 namecalc_amt tag visible1 expressionqty * price format#,##0.00 border0 color33554432 edit.styleEdit) // Modify返回空串表示成功非空串是错误原因 if dw_report.Modify(ls_create) then MessageBox(报表错误, 计算列创建失败) return -1 end ifCreate Column语法里最重要的两个参数是name和expression。expression里写的是DataWindow列名不是数据库表字段名如果列名包含空格或者特殊字符要用引号包住。format直接沿用DataWindow的显示格式串可以控制千分位、小数位。这段话要注意创建的列会直接进布局但不会自动加进SQL结果集所以它只能用已有字段参与运算不能引用SELECT中不存在的字段。空值计算是另一个坑qty或price任一为null时结果就是null显示为空而不是0。建议把表达式写成if(null, 0, qty) * if(null, 0, price)避免后续排序或合计使用时出现空结果。如果你需要动态修改一个已经存在的计算列表达式可以用// 动态修改计算列表达式 dw_report.Modify(calc_amt.Expressionif(isnull(qty),0,qty) * if(isnull(price),0,price))这种修改只影响当前运行期不会改变PBL里的对象。如果想下次打开还保留得把表达式存到配置表在Retrieve之后再执行一次。3.3 检索参数和过滤条件什么时候用Retrieve什么时候用SetFilter自定义报表系统里最容易写乱的是“过滤”。有人把所有过滤都放客户端有人把所有过滤都塞进SQL。我的经验是两级分流需要走数据库索引、减少网络传输的用Retrieve参数在SQL里过滤已经加载到客户端且数据量不大的用SetFilter表达式。// 第一级SQL检索参数数据源中定义为 where order_date :begin_dt dw_report.Retrieve(dt_begin, dt_end) // 第二级客户端过滤只显示业务员姓“王”的记录 dw_report.SetFilter(emp_name like 王%) dw_report.Filter()Retrieve是重新发起SQL每次调用都会重新查询数据库适合参数变化后必须返回新数据集的情况。SetFilter和Filter不重新查库只在已经存在的结果集里筛选适合在Retrieve之后继续做临时条件。你可以在一次Retrieve后多次修改Filter不会产生数据库回话开销。但要记住SetFilter的表达式解析器跟SQL不完全相同字符串要用单引号列名必须存在于DataWindow中否则Filter()执行时会弹“Bad filter expression”错误。在实战里我会把用户的自定义过滤条件拼成表达式后先调用dw_report.SetFilter(ls_expr)如果返回-1就提示“过滤条件格式不正确”避免Filter()执行到一半才报错。两种方式的对比如下。方式执行时机使用场景典型性能特征Retrieve参数每次Retrieve触发SQL日期范围、订单状态等必须由数据库过滤减少传输行数但每次都查询SetFilter只过滤已加载数据业务员筛选、临时条件无数据库开销适合小结果集列隐藏/禁用不影响数据过滤权限控制、布局简化零成本4. 源码二次开发User Object、权限控制与高频报错拿到一套PB报表自定义系统源码直接改业务代码是最慢的维护方式。我会先用一两天时间把源码里的公共能力抽出来做成可复用的用户对象再顺着权限和报错两条路径把系统边界摸清。4.1 把导出、打印、预览封装成用户对象报表源码里最容易被复制粘贴的是导出Excel那段代码。每个窗口写一遍后面想加个“导出后自动关闭”都没法全局改。标准做法是新建一个非可视用户对象uo_report_base里面放一个DataStore类型实例变量ids_report然后实现导出函数// uo_report_base中定义对象函数 f_export_excel public function integer f_export_excel(string as_filepath) long ll_ret // SaveAs的第三个参数表示是否导出列标题 ll_ret this.ids_report.SaveAs(as_filepath, Excel!, true) if ll_ret 1 then return 1 else // 常见失败原因是路径无权限或文件被Excel占用 return -1 end if end function所有业务报表窗口继承这个用户对象或者把dw_report控件作为对象内部变量这样调用时只有一个入口。SaveAs里的Excel!是枚举在PowerBuilder 12/R2时代能导出兼容xls的文件如果客户需要xlsx直接SaveAs不支持我一般会导出CSV或制表符文件再调用Office COM转换。你在集成这个源码时要先确认客户端的PowerBuilder版本和Office版本很多“导出点没反应”都是因为Excel版本不兼容。除了导出我还会在这个用户对象里加一个f_preview函数统一设置打印预览。注意被很多人当成“printpreview()函数”的东西在PB里并不是DataWindow方法而是打印属性。// 开启打印预览 dw_report.Modify(DataWindow.Print.PreviewYes) // 关闭打印预览 dw_report.Modify(DataWindow.Print.PreviewNo)DataWindow本身没有printpreview()这种函数因此网上搜到的“pb的数据窗口有printpreview()函数吗”答案就是没有独立函数通过Modify修改Preview属性。这个知识点放在用户对象里可以保证所有报表统一行为比如预览时强制显示打印边距可以继续加一句Modify(DataWindow.Print.Preview.RulersYes)。4.2 报表权限从列禁用到行级数据范围企业里的报表系统基本都有权限需求。高权限角色看全部数据普通角色只能看本部门或本月数据。实现时我建议分成三层做功能级权限控制报表菜单是否显示列级权限控制敏感列可见性行级权限控制Retrieve或Filter条件。列级权限的代码不复杂但要注意每次Retrieve后重新执行因为DataWindow刷新后属性可能被重置。// 在Retrieve后调用 if not uf_check_right(as_user, cost_amount) then dw_report.Modify(cost_amount.Visible0) dw_report.Modify(cost_amount.Height0) else dw_report.Modify(cost_amount.Visible1) dw_report.Modify(cost_amount.Height100) end if行级权限的常见实现是拼接过滤条件。风险在于过滤条件来自外部输入时容易拼出非法表达式我的做法是让权限配置表里只存白名单部门编号SQL里用In子句配合参数化检索实现// 根据权限组织构造检索参数不直接拼用户输入 if uf_is_admin(as_user) then dw_report.Retrieve() // 管理员查全部 else dw_report.Retrieve(as_user) // SQL中 where dept_head :ls_user end if如果你们源码里的过滤是写在DataWindow对象里的记得搜索一下retrieve argument和WHERE条件不要在多个地方重复过滤否则会出现“权限配置了但报表还是看到全部数据”的假放权问题。4.3 高频报错表null、日期转换、打印预览我在处理类似源码时会被问到三类问题报表保存报错null、字符串转日期失败、打印预览无反应。整理成表。现象直接原因排查路径报表保存/检索报错nullDataWindow列值与数据源不匹配或Retrieve参数传入空值检查Retrieve参数是否isnull在调用前给默认值pb 字符串转日期失败日期字符串格式与当前系统默认格式不一致先统一成yyyy-mm-dd再转换或用格式参数解析打印预览无反应把预览当成方法调用没有修改Preview属性改用Modify修改Print.Preview属性并确保打印机驱动存在PB里日期转换是个高频坑很多人写死转换格式换一台机器就变。比如// 不推荐依赖系统默认日期格式 date ld_date ld_date Date(2024/08/15) // 推荐先统一成 yyyy-mm-dd 再交给Date() string ls_date ls_date 2024/08/15 ls_date Replace(ls_date, /, -) ld_date Date(ls_date)如果日期字符串还带时间比如“2024-08-15 14:30:00”我一般先取前10位再转Date。源码里如果大量使用String(date, yyyy-mm-dd)做输出我建议全部收口进公共函数后面调整日期格式只改一处。5. 运行期动态微调与批量验证把自定义报表做成可持续维护的模块5.1 微调时先关重绘再批量Modify用户在预览界面拖列宽、调颜色、换格式代码逻辑都是一连串Modify。如果每改一个属性就重绘一次界面会闪到没法用。我把这种场景统一包在关闭重绘里dw_report.SetRedraw(false) // 批量执行Modify dw_report.Modify(emp_name.Font.Height280) dw_report.Modify(emp_name.Color33554432) dw_report.Modify(calc_amt.Format#,##0) // 全部改完再统一刷新 dw_report.SetRedraw(true)SetRedraw(false)不是取消更新而是挂起重绘改完后的SetRedraw(true)触发一次整体刷新。这个技巧对上百个报表对象尤其明显能减少大部分“DataWindow操作很慢”的误判。5.2 用脚本批量验证报表对象可用性自定义系统里报表数量多了以后你没法靠人工逐个打开验证。我通常写一个检查用DataStore遍历PBL中所有d_开头的对象执行Create并尝试Retrieve。伪代码类似// 遍历当前PBL库中的对象名 string ls_liblist, ls_obj integer li_count long li_fd ls_liblist report.pbl li_fd FileOpen(validate_result.txt, LineMode!, Write!) // 用LibraryDirectory枚举对象名这里简化为循环读取 // 对每个d_开头的对象 // ids_check CREATE DataStore // ids_check.DataObject ls_obj // ids_check.SetTransObject(SQLCA) // if ids_check.Retrieve() 0 then FileWrite(li_fd, ls_obj failed) end if // DESTROY ids_check FileClose(li_fd)这种验证只能跑通数据源是否有效没法覆盖打印格式是否正确。再负责一点的做法是用DataWindow.Describe(DataWindow.Table.Select)把SQL拿出来放到数据库的explain计划里看扫描行数重点找出全表扫描的报表。5.3 慢报表先看查询计划再改DataWindow自定义报表越多性能问题越集中。用户说“打开报表卡十秒”第一个动作不是去调DataWindow属性而是看它关联的SQL有没有走到索引。可以用一行代码取出DataWindow当前执行语句string ls_sql ls_sql dw_report.Describe(DataWindow.Table.Select) // 放到数据库工具里查看执行计划如果这条SQL在大表上做了全表扫描再去改过滤条件或索引。改完SQL后记得让数据库重新收集统计信息否则执行计划还是按老数据分布估算Retrieve照样慢。本文还有配套的精品资源点击获取
返回列表