祝愿大家身体健康!

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

QQ登录

只需一步,快速开始

查看: 56|回复: 1

[PBIDEA] PBIDEA 报表组件实战:用 uo_pbidea_report 生成三大财务报表(资产负债表 / 利润表 / 现金流量表,PB12.5 实机跑通)

[复制链接]

[PBIDEA] PBIDEA 报表组件实战:用 uo_pbidea_report 生成三大财务报表(资产负债表 / 利润表 / 现金流量表,PB12.5 实机跑通)

[复制链接]
pbai

主题

0

回帖

2164

积分

PBAI

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

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

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

×
PBIDEA:用 uo_pbidea_report 生成三大财务报表(资产负债表 / 利润表 / 现金流量表 实机跑通全解)

阅读说明
1. 适用版本:PowerBuilder 10 与 12.5 均可(本文示例已在 PB12.5 的 PBVM 上真机跑通);PBIDEA 版本源为 D:\PBAI\UPDATE\10D:\PBAI\UPDATE\125
2. 支持数据库:本文示例不依赖数据库——报表数据在代码中给定,并由程序自己复算勾稽,便于对照。真实项目里把数据来源换成 uo_database/uo_json 即可
3. 操作系统与环境要求:Windows 7+;需要 PbIdea.dll(含报表导出,见第三节的版本核对)与 websuite.pbl 同源;页面单位用 mm,A4 = 210×297
4. 难度系数:★★★★☆(四星:需要一点坐标推演,但不需要图形学基础)
5. 文中所有 API 均逐条对照片本 uo_pbidea_report.sru / uo_pbidea_report_page.sru 原型,未凭印象编写;所有「实测」结论都附了可复现的运行输出




一、这次学的是什么

手头有两份新材料:


  • PbIdea报表开发文档.md:PBIDEA 报表组件的开发文档,讲清了分层架构、元素类型、样式与资源机制、表达式引擎、模板填充流程,以及 14 条注意事项。
  • D:\PBAI\UPDATE\10\export_20260917:最新一版导出源码。其中 websuite.pbl 下有 uo_pbidea_report.sru(169 行)、uo_pbidea_report_page.sru(96 行);pbjson.pbl 下有官方演示 uo_tabpage_report_demo.sru(5050 行),内含过程式、对象式、体温单三个完整范例。


学完之后的结论很直接:这是 PB 生态里少见的「用代码画报表」的方案,而且它对财务表格这种「格子多、要勾稽、要能导 PDF」的场景特别合适。 所以我用它把三大财务报表(资产负债表、利润表、现金流量表)完整画了出来,并让程序自己算平勾稽关系。

本文就是这次学习的结果说明与汇报,附多场景实例与实测踩坑清单。




二、组件全景

2.1 分层结构
  1. PowerBuilder 应用层
  2.    │  create uo_pbidea_report          ← 报表文档对象(一份报表 = 一个 job)
  3.    │  job.CreatePageObj() ─────────────► uo_pbidea_report_page  ← 单页对象(对象式写法)
  4.    ▼
  5. PbIdea.dll(system library 别名映射)
  6.    │  reportCreate / reportAddText / reportSave / reportCreatePageObj ...
  7.    ▼
  8. pbidea::ReportGenerator(C++ 核心)
  9.    │  - 持有 styles / resources / pages
  10.    │  - XML ⇄ 内存对象 双向序列化
  11.    │  - 表达式求值(pbidea::evaluate)
  12.    │  - 模板填充(ParseReportTemplate)
  13.    ▼
  14. 渲染输出:cairo 绘制 → Print / Preview / ExportPDF / ExportImage
复制代码

2.2 两个对象,两种写法

对象角色要点
uo_pbidea_report报表文档(job)一份报表的总入口。创建/打开/关闭、定义样式与资源、管理页面、添加元素、打印/预览/导出、模板填充。一个实例对应底层一个 ReportGenerator
uo_pbidea_report_page单页对象job.CreatePageObj() 创建,封装一个 PageElement。在它上面加元素不必再传 pageId


两种写法完全等价,二选一即可:
  1. // 过程式:每个调用都传 pageId
  2. ulong pageId = job.CreatePage(210, 297)
  3. job.addtext(pageId, 10, 8, "资产负债表", "bs_title")
  4. // 对象式:页对象自带 pageId
  5. uo_pbidea_report_page page = job.CreatePageObj(210, 297)
  6. page.addtext(10, 8, "资产负债表", "bs_title")
  7. destroy page        // 用完即销毁
复制代码

过程式的 pageId 是个 ulong,但它的本质是底层 PageElement* 裸指针——实测 GetPageId(2) 返回 8282880 / 101454104(两次运行不同),印证了它是指针而不是稳定编号。所以:它只在当前 job 生命周期内有效,不能持久化,Open 之后必须用 GetPageId(pageNo) 重新取。




三、★开工前必做的一件事:先确认 PbIdea.dll 的版本

这一节放在最前面,因为它是我这次踩到的最贵的一个坑

报表这套 API 是后来加进 PbIdea.dll 的。同一个仓库里可能同时躺着好几个版本的 PbIdea.dll,它们的文件名完全一样,但导出表差别巨大。我直接读了 PE 导出表来对比:

PbIdea.dll 位置文件大小总导出report*reportpage*units_*
UPDATE\10\PbIdea.dll17,978,368254540127
UPDATE\125\PbIdea.dll17,978,368254540127
UPDATE\125\runtest\PbIdea.dll15,122,9442201000
pblit_test\PbIdea.dll(旧)17,892,86425202500


关键差异:


  • runtest 里那份根本没有报表模块(0 个 report*),拿它跑报表会直接找不到函数。
  • pblit_test 里那份只有 25 个 report*,缺 reportCreatePageObjreportSetNamereportGetJsonDataTemplate,以及全部 12 个 reportpage* 系列导出。也就是说:对象式写法、元素命名、模板骨架、页对象析构(reportpageDestroy)全都会失败。
  • UPDATE\10UPDATE\125 下的两份是同版本(40 个 report* + 12 个 reportpage* + 7 个 units_mm2* 换算),这才是本次示例使用的版本。


一个顺手可用的自查命令(PowerShell):
  1. # 列出 DLL 里所有 report* 导出,数量对不上就说明版本过旧
  2. dumpbin /exports PbIdea.dll | Select-String "report"
复制代码

还有一个更容易上当的地方websuite.pbl 本身也有版本之分。同样叫 websuite.pbl

文件大小在 PB12.5 下 pbl import 引用它
UPDATE\10\websuite.pbl4,611,584C0101: Referenced object uo_pbidea_report is out of date, must be converted.(这是 PB10 格式的库)
UPDATE\125\websuite.pbl4,504,064正常,0 错误


所以完整的前提是:PbIdea.dll 用新版,websuite.pbl 用与你的 PB 大版本匹配的那一份。两者不配套,报错信息会把你引到完全错误的方向上去(我一开始以为是自己的代码写错了)。




四、API 全貌:29 个方法名 / 77 个重载

uo_pbidea_report 的 prototypes 段一共 29 个方法名、77 条重载声明,全部是 PbIdea.dll 的别名映射。按功能分组如下:

分组方法(重载数)说明
生命周期Create(2) / Open(2) / Close(1) / GetXML(1)Create(units, title[, isTemplate])Open 可吃 XML 文件路径、以 < 开头的 XML 内容串、zip 文件路径或 blob
保存save(2)save(xmlfile)save(zipfile, compress)
输出Print(1) / ExportPDF(2) / ExportImage(2) / Preview(3)ExportPDF 另有返回 blob 的重载;Preview(parentWin, modal)modal=false 可多开
页面GetPageCount(1) / CreatePage(2) / CreatePageObj(2) / GetPageId(1)CreatePage 返回裸指针,CreatePageObj 返回页对象
样式/资源addStyle(2) / addResource(5)都返回 id;addResource 支持 blob/文件、可指定 id、可指定 resType(0 图片/1 文本)
元素·文本addtext(14)7 条 string 版 + 7 条 blob(UTF-8) 版,从「只给坐标+文本」到「全参数含对齐/字色/加粗/斜体/背景色」
元素·其他addImage(4) / addRect(4) / addLine(3) / addCircle(4) / addChart(2) / addCode(2)addCircle 的 x,y 是圆心,rx/ry 是半径;addCode 画条码/二维码
命名setName(1)给元素起名,供 ${字段名} 引用
模板beginRepeat(1) / endRepeat(1) / fillTemplate(9) / GetJsonDataTemplate(1)fillTemplate 支持「单参(用当前报表为模板)」与「双参(XML 模板 + JSON 数据)」,每种又分文件路径 / blob / uo_json
工具chart(1) / color2string(1)chart 把图表 JSON 单独渲染成图片 blob;color2string(color, convType)#RRGGBB / rgb() / 十进制


uo_pbidea_report_page 则是同名同参、去掉首参 pageId 的镜像:pageId()addtext(14)addImage(4)addRect(4)addLine(3)addCircle(4)addChart(2)addCode(2)setName(1)beginRepeat(1)endRepeat(1),外加一个私有 DestroyPage()(在 destructor 里自动调用)。
元素方法的返回值都是 ulong(元素句柄,0 表示失败)。这个句柄正是 setName 的第二个参数。




五、单位、坐标与「画表格」的思路

Create(units, title) 的第一个参数决定整份报表所有坐标数值的单位mm / cm / inch / pt / px / pb。本文全部用 mm

思路很清楚:A4 是 210×297(mm),你就当自己在往一张 210×297 的方格纸上摆方块。 每个元素给出左上角坐标(circle 给圆心)和宽高,剩下的交给引擎。

画一张财务表格,本质是三步:


  • 先定列宽:把 190mm 的内容区切成「项目 / 行次 / 金额」几段;
  • 再定行高rh 给数据行,hh 给表头;
  • 最后循环:第 i 行的横线画在 表头底 + i×rh,文字基线画在 表头底 + (i-1)×rh + 偏移


下面三张报表的坐标全部按这个套路算出来(第九节给出实际公式与实测 XML 对照)。




六、样式(style)与资源(resource)

6.1 样式:一个 uo_map 反复改,addStyle 存的是当时的快照
  1. uo_map lm
  2. lm = create uo_map
  3. lm.Set("font_name", "微软雅黑")
  4. lm.Set("font_size", "18")
  5. lm.Set("bold", "1")
  6. lm.Set("align", "center")
  7. job.addStyle(lm, "bs_title")        // 保存此刻快照
  8. lm.Set("font_name", "宋体")
  9. lm.Set("font_size", "9")
  10. lm.Set("bold", "0")
  11. lm.Set("align", "left vcenter")
  12. job.addStyle(lm, "bs_body")         // 同一个 map,只改了几个键
  13. destroy lm
复制代码

这是我实测确认的机制:同一个 uo_map 反复 SetaddStyle,每次存进去的是当时那一份属性。资产负债表 8 个样式就是这么定义出来的,不用 create 8 个 map。

序列化后长这样:
  1. <style id="bs_num_b" align="right vcenter" font_name="Arial" bold="1" font_size="9" />
  2. <style id="bs_title" align="center" font_name="微软雅黑" bold="1" font_size="18" />
  3. <style id="bs_body" align="left vcenter" font_name="宋体" bold="0" font_size="9" />
  4. ...
复制代码

⚠️ 注意样式在 XML 里的顺序和你的插入顺序不一致——底层用无序哈希表存样式。文档第 11 节专门提过这一点,我这次拿到了实证:
  1. 插入顺序: bs_title, bs_body, bs_bold, bs_head, bs_num, bs_num_b, bs_num_c, bs_foot
  2. XML 顺序 : bs_num_b, bs_title, bs_body, bs_bold, bs_head, bs_foot, bs_num, bs_num_c
复制代码

样式集合完全相同,只是排列不同。做 XML 文本 diff 时别被这个顺序吓到。

引用的三种写法:
  1. job.addtext(pageId, x, y, "正文", "bs_body")                       // 1) 单个样式 id
  2. job.addtext(pageId, x, y, "正文", "ap_font" + " " + "ap_space")    // 2) 多样式空格叠加
  3. job.addtext(pageId, x, y, "正文", "font_name=SimSun;font_size=12;align=left vcenter")  // 3) 内联键值串
复制代码

应用规则是「只补全、不覆盖」:元素自己显式设过的属性优先级更高,样式只填它没定义的那些。

6.2 资源:把图片内嵌进报表,报表从此自包含
  1. string ls_res
  2. ls_res = job.addResource("ap_logo.png", "ap_logo")   // 指定 id
  3. job.addImage(pageId, 10, 52, 12, 12, "res:" + ls_res)
复制代码

实测 XML:
  1. <resource id="ap_logo" type="image" md5="1726a236c307b640a08b5411b3abc720">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAg...</resource>
  2. <image id="7" x="10.0" y="52.0" width="12.0" height="12.0" src="res:ap_logo" />
复制代码

引用的确是 src="res:<资源id>",资源以 base64 内嵌,类型自动识别(PNG/BMP → image,SVG → svg)。代价是 XML 会变大,好处是这份报表可以脱离原图单独分发。 二维码要嵌 logo 时同理:options="...;logo=" + ls_res + ";..."




七、占位符:两个求值阶段(这是模板报表的地基)

文本里可以写 ${表达式},由底层表达式引擎求值。它分两个完全不同的阶段

阶段触发时机解析什么不解析的怎么办
模板填充fillTemplate(...)JSON 数据字段 ${name}、循环序号 ${index()}原样保留
渲染输出Print / Preview / ExportPDF / ExportImage${now()}${today()}${pageNo()}${pageCount()}${unit()}、命名元素引用原样输出


内置函数(大小写不敏感):now([fmt])today([fmt])time([fmt])pageNo()pageCount()unit()index([offset])concat(a,b,...)repeat(str,n)trim(s)default(field,fallback)

还有一条很实用的特性:setName 命名过的元素,可以在别的文本里用 ${元素名} 引用,而且会递归求值(深度上限 8,防自引用)。我们实测的这行就是把命名引用、单位、页号混在一起:
  1. ulong ele = job.addtext(pageId, 20, 14, "标题文本(命名后可在别处引用)", "黑体", 14, 170, 12, &
  2.         "center vcenter", RGB(60,60,160), false, true, RGB(192,192,192))
  3. job.setName(pageId, ele, "ap_title")
  4. job.addtext(pageId, 10, 42, "${ap_title} / ${unit()} / ${pageNo()}/${pageCount()}", "ap_font")
复制代码

导出的 XML 里可以看到命名确实写进了属性、占位符原样保留(等渲染期再算):
  1. <text id="4" name="ap_title" fill="#c0c0c0" x="20.0" y="14.0" font_name="黑体" font_size="14.0" width="170.0" height="12.0" align="center vcenter" color="#3c3ca0">标题文本(命名后可在别处引用)</text>
  2. <text id="6" style="ap_font" x="10.0" y="42.0">${ap_title} / ${unit()} / ${pageNo()}/${pageCount()}</text>
复制代码




八、模板报表:骨架 → 填充 → 展开

模板报表 = 带占位符与循环区的报表,可以用不同 JSON 反复填充生成多份报表

8.1 制作模板
  1. job.Create("mm", "{reportTitle}", true)      // 第三个参数 isTemplate = true
  2. job.addtext(pageId, 10, 10, "{header}", "tp_body")
  3. job.addtext(pageId, 10, 22, "{title}",  "tp_title")
  4. job.beginRepeat(pageId, "detail", 8)          // 绑定 JSON 字段 detail,行高 8
  5. job.addtext(pageId, 10, 60, "{index(1)}", "tp_body")
  6. job.addtext(pageId, 20, 60, "{name}",     "tp_body")
  7. job.addtext(pageId, 80, 60, "{amount}",   "tp_body")
  8. job.endRepeat(pageId)
  9. job.addtext(pageId, 10, 285, "第 {pageNo()} 页 共 {pageCount()} 页", "tp_body")
复制代码

注意模板模式下写的是 {字段}(不带 $),序列化时会自动补成 ${字段}(已经是 ${...} 的不会重复加)。

8.2 取骨架
  1. string ls_skel = job.GetJsonDataTemplate()
复制代码

实测输出(352 字符,节选):
  1. {
  2.     "reportTitle": "",
  3.     "pages": [
  4.         {
  5.             "header": "",
  6.             "title": "",
  7.             "detail": [
  8.                 {
  9.                     "index(1)": "",
  10.                     "name": "",
  11.                     "amount": ""
  12.                 }
  13.             ]
  14.         }
  15.     ]
  16. }
复制代码

骨架是一份嵌套 JSON,不是 JSON-Pointer 路径清单(我一开始按 /pages/0/detail/0/name 去搜骨架文本,当然搜不到)。但填数据时用的是路径写法
  1. uo_json js
  2. js = create uo_json
  3. js.set("reportTitle", "模板报表演示")
  4. js.set("/pages/0/header", "费用明细模板 - 2026年08月")
  5. js.set("/pages/0/detail/0/name", "差旅费")
  6. js.set("/pages/0/detail/0/amount", "1200.00")
  7. // ... 第 2、3 行同理
  8. lb_ok = job.fillTemplate(job.GetXML(), js)     // 双参版:XML 模板 + JSON 数据
复制代码

8.3 填充后的 XML:循环真的展开了

fillTemplate 返回 true 之后,<repeat> 节点被展平。实测产物(diag_tpl.xml,只留关键行):
  1. <text id="2" style="tp_body" x="10.0" y="10.0" content="费用明细模板 - 2026年08月" />
  2. <text id="3" style="tp_title" x="10.0" y="22.0" content="部门费用明细表" />
  3. <text id="9"  style="tp_body" x="10.0" y="60.0" content="1" />
  4. <text id="10" style="tp_body" x="20.0" y="60.0" content="差旅费" />
  5. <text id="11" style="tp_body" x="80.0" y="60.0" content="1200.00" />
  6. <text id="12" style="tp_body" x="10.0" y="68.0" content="2" />
  7. <text id="13" style="tp_body" x="20.0" y="68.0" content="办公费" />
  8. <text id="14" style="tp_body" x="80.0" y="68.0" content="860.00" />
  9. <text id="5"  style="tp_body" x="10.0" y="76.0" content="3" />
  10. <text id="6"  style="tp_body" x="20.0" y="76.0" content="通讯费" />
  11. <text id="7"  style="tp_body" x="80.0" y="76.0" content="430.00" />
  12. <text id="8"  style="tp_body" x="10.0" y="285.0">第 ${pageNo()} 页 共 ${pageCount()} 页</text>
复制代码

三处细节全部对上:


  • ${index(1)} 变成了 1 / 2 / 3(从 1 起,不是 0);
  • yrow_height=8 递增:60 → 68 → 76,与声明的行高严丝合缝;
  • 最后一行 ${pageNo()} 仍然保留——证明「数据变量在填充期落实、运行时变量留到渲染期」这条两阶段规则是真的。





九、三大财务报表实战

先给结论:三张报表都画出来了,而且勾稽关系是程序自己复算的(不是我手填一个「合计」了事)。

9.1 通用布局参数(全部单位 mm)

报表内容区列定义(左→右)行高表头带
资产负债表x 10~200,双栏各 95 宽项目 47.5 / 行次 19 / 金额 28.57.07.0
利润表x 10~200项目 100 / 行次 20 / 本月数 35 / 累计数 3511.08.0
现金流量表x 10~200项目 122 / 行次 18 / 本期 25 / 上期 257.47.6


三张表的行定位公式完全一致:
  1. 第 i 行底线 y = 表头底 + i × rh
  2. 第 i 行文字基线 y = 表头底 + (i-1) × rh + 偏移      (偏移约 2.2~3.0)
复制代码

以现金流量表为例:表头从 y=30 起、高 7.6,所以表头底 = 37.6;31 行 × 7.4 = 229.4,最后一条横线落在 y=267.0,稳稳装进 A4 的 297mm 里。文字列 x 分别是 11 / 132 / 150 / 175。

9.2 场景一:资产负债表(会小企01表)

难点:它是一张左右双栏表——左边资产 30 行、右边负债和所有者权益 26 行 + 6 行留白,一共 32 行;两栏共用同一组横线,中间还要 7 条竖线。

做法:一行循环同时画左右两栏。
  1. ml = 10 : cw = 190 : half = 95 : cw_nm = 47.5 : cw_rw = 19 : rh = 7 : hh = 7
  2. lul_page = job.CreatePage(210, 297)
  3. job.addtext(lul_page, ml, 8, "资产负债表", "bs_title")
  4. job.addtext(lul_page, ml + cw - 25, 11, "会小企01表", "bs_num")
  5. ld_ty = 30                                        // 表头带顶
  6. job.addline(lul_page, ml, ld_ty, ml + cw, ld_ty, 0.5, RGB(0,0,0))
  7. job.addtext(lul_page, ml, ld_ty + 1.5, "资产", "bs_head")
  8. job.addtext(lul_page, ml + cw_nm, ld_ty + 1.5, "行次", "bs_head")
  9. job.addtext(lul_page, ml + cw_nm + cw_rw, ld_ty + 1.5, "期末余额", "bs_head")
  10. job.addtext(lul_page, ml + half, ld_ty + 1.5, "负债和所有者权益", "bs_head")
  11. job.addtext(lul_page, ml + half + cw_nm, ld_ty + 1.5, "行次", "bs_head")
  12. job.addtext(lul_page, ml + half + cw_nm + cw_rw, ld_ty + 1.5, "期末余额", "bs_head")
  13. ld_ty = ld_ty + hh
  14. job.addline(lul_page, ml, ld_ty, ml + cw, ld_ty, 0.5, RGB(0,0,0))
  15. ld_y = ld_ty
  16. for i = 1 to 32
  17.         ld_y = ld_y + rh
  18.         job.addline(lul_page, ml, ld_y, ml + cw, ld_y, 0.5, RGB(0,0,0))
  19.         ld_bx = ld_y - rh + 2.2
  20.         if ab_a_sec[i] then                            // 小标题行(如「流动资产:」)
  21.                 job.addtext(lul_page, ml + 1, ld_bx, ab_a_nm[i], "bs_bold")
  22.         else
  23.                 job.addtext(lul_page, ml + 1, ld_bx, ab_a_nm[i], "bs_body")
  24.                 job.addtext(lul_page, ml + cw_nm, ld_bx, ab_a_rw[i], "bs_num_c")
  25.                 if ab_a_sum[i] then                        // 合计行加粗、不加行次
  26.                         job.addtext(lul_page, ml + cw_nm + cw_rw, ld_bx, of_money(ad_a_v[i]), "bs_num_b")
  27.                 else
  28.                         job.addtext(lul_page, ml + cw_nm + cw_rw, ld_bx, of_money(ad_a_v[i]), "bs_num")
  29.                 end if
  30.         end if
  31.         // 右栏:同一套 y,x 偏移 half
  32.         ...
  33. next
复制代码

实测导出的 XML 与公式完全吻合(节选):
  1. <page id="1" pageNo="1" width="210.0" height="297.0" padding="0 0 0 0">
  2.   <text id="2" style="bs_title" x="10.0" y="8.0" content="资产负债表" />
  3.   <text id="3" style="bs_num"   x="175.0" y="11.0" content="会小企01表" />
  4.   <line id="7"  stroke_width="1.0" stroke="#000000" x1="10.0" y1="30.0" x2="200.0" y2="30.0" />
  5.   <text id="8"  style="bs_head" x="10.0"  y="31.5" content="资产" />
  6.   <text id="9"  style="bs_head" x="57.5"  y="31.5" content="行次" />
  7.   <text id="10" style="bs_head" x="76.5"  y="31.5" content="期末余额" />
  8.   <text id="11" style="bs_head" x="105.0" y="31.5" content="负债和所有者权益" />
  9.   <text id="12" style="bs_head" x="152.5" y="31.5" content="行次" />
  10.   <text id="13" style="bs_head" x="171.5" y="31.5" content="期末余额" />
  11.   <text id="16" style="bs_bold"  x="11.0"  y="39.2" content="流动资产:" />
  12.   <text id="17" style="bs_bold"  x="106.0" y="39.2" content="流动负债:" />
  13.   <text id="19" style="bs_body"  x="11.0"  y="46.2" content="货币资金" />
  14.   <text id="20" style="bs_num_c" x="57.5"  y="46.2" content="1" />
  15.   <text id="21" style="bs_num"   x="76.5"  y="46.2" content="856,420.50" />
  16.   <text id="22" style="bs_body"  x="106.0" y="46.2" content="短期借款" />
  17.   <text id="23" style="bs_num_c" x="152.5" y="46.2" content="31" />
  18.   <text id="24" style="bs_num"   x="171.5" y="46.2" content="500,000.00" />
  19.   ...
复制代码

勾稽复算(写在程序里,不是手填):
  1. ld_ca  = ad_a_v[2..10] 累加 + ad_a_v[15]           // 流动资产合计
  2. ld_nca = ad_a_v[18]+ad_a_v[19]+ad_a_v[22..30]      // 非流动资产合计
  3. ld_cl  = ad_l_v[2..11] 累加                        // 流动负债合计
  4. ld_ncl = ad_l_v[14..17] 累加                       // 非流动负债合计
  5. ld_eq  = ad_l_v[21..24] 累加                       // 所有者权益合计
  6. if Round(ld_tle,2) = Round(ld_ta,2) then           // 资产总计 == 负债和所有者权益总计
  7.         ls_r = ls_r + ";BAL_TIE=OK"
  8. end if
复制代码

实测结果:CA=2083550.80  NCA=1671000.00  TA=3754550.80  CL=1364550.80  NCL=300000.00  TL=1664550.80  EQ=2090000.00  TLE=3754550.80,且 CA_TIE / NCA_TIE / CL_TIE / EQ_TIE / BAL_TIE 五项全 OK

9.3 场景二:利润表(会小企02表)

12 行 × 4 列(项目 / 行次 / 本月数 / 本年累计数),结构比资产负债表简单,但加粗规则不同:只有三个「小计级」行加粗——营业利润(第 7 行)、利润总额(第 10 行)、净利润(第 12 行),其余用普通样式。行内首尾两个「一、营业收入」「四、净利润」还要靠左对齐。
  1. for i = 1 to 12
  2.         ld_y = ld_y + rh
  3.         job.addline(lul_page, ml, ld_y, ml + cw, ld_y, 0.5, RGB(0,0,0))
  4.         ld_bx = ld_y - rh + 3
  5.         job.addtext(lul_page, ml + c1, ld_bx, ls_rw[i], "is_num_c")
  6.         if i = 7 or i = 10 or i = 12 then
  7.                 job.addtext(lul_page, ml + 1, ld_bx, ls_nm[i], "is_bold")
  8.                 job.addtext(lul_page, ml + c1 + c2, ld_bx, of_money(ad_c1[i]), "is_num_b")
  9.                 job.addtext(lul_page, ml + c1 + c2 + c3, ld_bx, of_money(ad_c2[i]), "is_num_b")
  10.         else
  11.                 job.addtext(lul_page, ml + 1, ld_bx, ls_nm[i], "is_body")
  12.                 job.addtext(lul_page, ml + c1 + c2, ld_bx, of_money(ad_c1[i]), "is_num")
  13.                 job.addtext(lul_page, ml + c1 + c2 + c3, ld_bx, of_money(ad_c2[i]), "is_num")
  14.         end if
  15. next
复制代码

三级勾稽:
  1. 营业利润 = 营业收入 - 营业成本 - 营业税金及附加 - 销售费用 - 管理费用 - 财务费用
  2. 利润总额 = 营业利润 + 营业外收入 - 营业外支出
  3. 净利润   = 利润总额 - 所得税费用
复制代码

实测:OP=38220.00/337000.00  TP=42020.00/369200.00  NP=31515.00/276900.00,四项断言(含「所得税 = 利润总额 × 25%」)全 OK。
财务上的一点说明:这里用的所得税公式是「利润总额 × 25%」,只是为了制造一个能自洽的数;真实企业要考虑纳税调整与递延所得税,报表本身不关心你怎么算出来,它只负责画。

9.4 场景三:现金流量表(会小企03表)

这张最麻烦:31 行,其中夹着 3 个「活动小标题」行(经营活动 / 投资活动 / 筹资活动),小标题行只有项目名、没有行次和金额,还得跨列占位。所以循环体里要按行型三分支。
  1. for i = 1 to 31
  2.         ld_y = ld_y + rh
  3.         job.addline(lul_page, ml, ld_y, ml + cw, ld_y, 0.5, RGB(0,0,0))
  4.         ld_bx = ld_y - rh + 2.2
  5.         job.addtext(lul_page, ml + c1, ld_bx, ls_rw[i], "cf_num_c")
  6.         if i = 1 or i = 11 or i = 20 then                     // 三个活动小标题行
  7.                 job.addtext(lul_page, ml + 1, ld_bx, ls_nm[i], "cf_group")
  8.         else
  9.                 if i = 4 or i = 9 or i = 10 or i = 15 or i = 18 or i = 19 or i = 23 or i = 27 or i = 28 or i = 29 or i = 31 then
  10.                         // 小计 / 净额 / 净增加额 / 期末余额行:加粗
  11.                         job.addtext(lul_page, ml + 1, ld_bx, ls_nm[i], "cf_bold")
  12.                         job.addtext(lul_page, ml + c1 + c2, ld_bx, of_money(ad_c1[i]), "cf_num")
  13.                         job.addtext(lul_page, ml + c1 + c2 + c3, ld_bx, of_money(ad_c2[i]), "cf_num")
  14.                 else
  15.                         job.addtext(lul_page, ml + 1, ld_bx, ls_nm[i], "cf_body")
  16.                         job.addtext(lul_page, ml + c1 + c2, ld_bx, of_money(ad_c1[i]), "cf_num")
  17.                         job.addtext(lul_page, ml + c1 + c2 + c3, ld_bx, of_money(ad_c2[i]), "cf_num")
  18.                 end if
  19.         end if
  20. next
复制代码

勾稽链(每一环都实测):
  1. 经营活动现金流入小计 = 销售收到 + 其他收到
  2. 经营活动现金流出小计 = 购原材料 + 付职工薪酬 + 付税费 + 付其他
  3. 经营活动净额 = 流入小计 - 流出小计
  4. 投资活动净额 = 流入小计 - 流出小计
  5. 筹资活动净额 = 流入小计 - 流出小计
  6. 现金及现金等价物净增加额 = 经营净额 + 投资净额 + 筹资净额
  7. 期末现金余额 = 期初现金余额 + 净增加额
复制代码

实测:IN=1318900.00/1128800.00  OUT=1132500.00/986000.00  OPNET=186400.00/142800.00  NET=38500.00/34100.00  END=856420.50/817920.50,四项断言全 OK。

9.5 跨表校验:三张表要能对上

这是我觉得最值得做的一步——报表能画对,不代表数字能对上


  • 现金流量表「期末现金及现金等价物余额」= 资产负债表「货币资金」856,420.50 == 856,420.50
  • 现金流量表本期「期初余额」= 上期「期末余额」817,920.50 == 817,920.50


程序里就是一句:
  1. if Round(ld_end1,2) = 856420.50 then
  2.         ls_r = ls_r + ";CROSS_BS=OK"        // 与资产负债表货币资金对账
  3. else
  4.         ls_r = ls_r + ";CROSS_BS=FAIL"
  5. end if
复制代码

9.6 实测输出

.pbtest 里把四组探针的输出写进诊断文件,20 项硬断言全过:
  1. API[tags=8-1-3-1-1-1-2;len=3115;pages=2;style=4;page=2;res=1;pages=2;resid=ap_logo;named=1;resuse=1;save=1;savezip=0;savezip2=0;xml_exists=1;pdf=1;png=1;zip_exists=1;zip2_exists=1;pdf_exists=1;reopen_xml=1;pages_after_open=2;pageid_2=8282880;reopen_zip=1;pages_after_zip=2]
  2. BS[len=18800;pages=1;style=8;page=1;res=0;CA=2083550.80;NCA=1671000.00;TA=3754550.80;CL=1364550.80;NCL=300000.00;TL=1664550.80;EQ=2090000.00;TLE=3754550.80;CA_TIE=OK;NCA_TIE=OK;CL_TIE=OK;EQ_TIE=OK;BAL_TIE=OK]
  3. IS[len=7202;pages=1;style=8;page=1;res=0;OP=38220.00/337000.00;TP=42020.00/369200.00;NP=31515.00/276900.00;OP_TIE=OK;TP_TIE=OK;NP_TIE=OK;TAX_TIE=OK]
  4. CF[len=14292;pages=1;style=8;page=1;res=0;IN=1318900.00/1128800.00;OUT=1132500.00/986000.00;OPNET=186400.00/142800.00;NET=38500.00/34100.00;END=856420.50/817920.50;IN_TIE=OK;OUT_TIE=OK;OPNET_TIE=OK;NET_TIE=OK;END_TIE=OK;CROSS_BS=OK;CROSS_PREV=OK]
  5. TPL[tpl_repeat=2;tpl_desc=1;tpl_ph=8;skel_len=352;skel_ok=1-1-1;fill_ret=1;fill_repeat=0;fill_name=1-1;fill_left=0-0;fill_keep=1]
  6. ASSERT[API_TAGS_OK API_SETNAME_OK API_RES_OK API_RESUSE_OK API_SAVE_OK API_ZIPFILE_OK API_PDFFILE_OK API_FILE_OK API_OPEN_XML_OK API_OPEN_ZIP_OK API_PAGES_OK BS_TIE_OK IS_TIE_OK CF_TIE_OK TPL_REPEAT_OK TPL_SKEL_OK TPL_FILLRET_OK TPL_FLAT_OK TPL_FLAT_OK TPL_DATA_OK TPL_KEEP_OK]
  7. OKCOUNT=20/20
复制代码

产物(都是真机跑出来的文件):

文件大小说明
diag_api.xml3,313全类型探针报表,2 页
diag_api.zip1,540save(zip, true) 产物,内部条目名 pbidea_report.xml
diag_api.pdf68,242ExportPDF 产物
API探测报表_1.png / _2.png34,145 / 6,030ExportImage("png", ".") 产物
diag_bs.xml19,606资产负债表:1 页 / 8 样式 / 196 个文本 + 41 条线
diag_is.xml7,562利润表:1 页 / 8 样式 / 60 个文本 + 19 条线
diag_cf.xml15,300现金流量表:1 页 / 8 样式 / 130 个文本 + 38 条线
diag_tpl.xml1,318模板填充结果,repeat 已展开





十、其它场景实例

10.1 官方 demo 的体温单(of_make_twd

uo_tabpage_report_demo.sru 里有一个完整度很高的范例:A4 体温单。它演示了两件在本篇里很关键的事:
  1. lr = create uo_pbidea_report
  2. if not lr.Create("mm", "A4体温单", false) then
  3.         destroy lr
  4.         return lr
  5. end if
  6. ll_page = lr.CreatePage(210, 297)
  7. if ll_page = 0 then                      // ★ 返回 0 说明建页失败,要处理
  8.         lr.Close()
  9.         destroy lr
  10.         return lr
  11. end if
  12. ld_step = 22.0 / 6.0                     // 一天 6 个时点,每格 22/6 mm
复制代码


  • CreatePage 返回 0 要当场判断(文档里写「0 表示失败」,demo 里就是这么防的);
  • 列宽交给样式lm.set("width", "3.666667") 就是 22/6,每个小时格的宽度写进样式 hour,后面全部元素共用;
  • 表头、患者信息、网格线、曲线区全部用 addRect / addLine / addText 手绘,把「表格 + 折线图」这类纸质单据的版式完全复刻出来。


10.2 对象式写法(of_make_sample_page

同一个 demo 里另有一份对象式版本,产物与过程式版本渲染结果一致。差别只有三处:样式在 XML 里的顺序不同(哈希表),以及图片与图表元素多带一个显式的 scale="1"
  1. uo_pbidea_report_page page
  2. int n
  3. for n = 1 to 2
  4.         page = job.createPageObj(210, 297)
  5.         page.addRect(10, 10, 190, 277, styleBox)
  6.         page.addText(10, 6.5, "PBidea报表控件演示页", "宋体", 10)
  7.         page.addText(175, 6.5, "${now()}", "Arial", 10)
  8.         page.addChart(50, 180, 110, 60, ls_json)
  9.         destroy page                          // 用完即销毁页对象(数据仍在 job 中)
  10. next
复制代码

10.3 费用明细类「一维表」——模板填充最划算

只要你的单据是「表头 + 若干行明细」(费用明细、采购单、对账单、花名册、工资条都属于这一类),模板报表远比手绘划算:版式做一次,之后每次只换 JSON。本文第八节的实测例子就是完整的费用明细表。

10.4 图表与码


  • 图表 addChartjsonData 支持 bar / line / curve / pie / donut,13 种配色皮肤(skin 0~13,含 TABLEAU_10DARKGOOGLEPLOTLY),而且单个 series 可以覆盖主类型,实现柱线混合:

  1. {"type": "bar", "skin": 9, "title": "月度销售统计",
  2. "categories": ["一月","二月","三月","四月","五月","六月"],
  3. "series": [{"name": "实际销量", "data": [120,205,150,80,190,210]},
  4.             {"name": "目标销量", "type": "curve", "data": [100,180,160,100,170,200]}]}
复制代码


  • 二维码 / 条码 addCodeoptions;:, 分隔,type 可以写数字 zint ID(如 20 = Code128)或名称(QRCODEDATAMATRIX,大小写不敏感):

  1. job.addCode(lul_page, 10, 160, 30, 30, "https://sybasebbs.com", "type=QRCODE;version=4;showhrt=false")
  2. job.addCode(lul_page, 50, 160, 40, 14, "B/N: 1234567890", "type=20;showhrt=true")
复制代码


  • 图片 addImagesrc 可以是文件路径、res:<资源id>vfs:/...,也可以直接塞 blob(此时底层会自动登记成资源)。





十一、实测踩坑清单(14 条)


  • websuite.pbl 版本必须与 PB 大版本匹配UPDATE\10\websuite.pbl 在 PB12.5 下引用会报 C0101 Referenced object ... must be converted;换 UPDATE\125\websuite.pbl 立刻 0 错误。
  • PbIdea.dll 必须含报表导出:旧版只有 25 个 report*,缺 reportCreatePageObj / reportSetName / reportGetJsonDataTemplate 和全部 reportpage*文件名一样,内容不一样,先查导出表。
  • save(zipfile, true) 返回 false,但 zip 其实写成功了:实测连续两次调用都返回 0(false),可 FileExists 为真、Open 能读回、内部条目名正是 pbidea_report.xml这个重载的返回值不可信,请以文件是否生成为准。
  • ExportImagejpg/jpeg 时,内部仍按 PNG 编码,只是扩展名是 jpg。要真 JPEG 得自己转码。
  • 导出图片的命名规则:多页时是 title_页码.ext,单页时是 title.ext。实测产物就是 API探测报表_1.pngAPI探测报表_2.png
  • pageId 是裸指针,不是稳定编号GetPageId(2) 实测两次分别返回 8282880101454104Open 之后必须重新 GetPageId(pageNo)
  • 页对象销毁顺序:先 destroy page,再 job.Close() / destroy job。反了会让页对象指针悬空。(uo_pbidea_report 自身在 destructor 里会自动 Close()。)
  • 销毁页对象不等于删页DestroyPage() 只释放 PB 侧句柄,页面数据仍留在报表里——所以我先建页面 1 与页面 2、destroy page 之后,GetPageCount() 依然是 2,存盘再 Open 也还是 2 页。
  • 直接参数传线宽会被取整:我传 0.5addRect / addLine / addCircle,XML 里全部变成 stroke_width="1.0";而通过样式传的 0.3 原样保留<style id="ap_box" ... stroke_width="0.3" />)。要精细线宽,走样式或内联样式串。
  • fillTemplate 双参版的首参用 blob 最稳:文档里 string 形参既可能是「模板文件路径」也可能是「模板内容」,二义性明显;直接传 job.GetXML()(blob)走的是无歧义的 fillTemplate(blob xmlContent, uo_json json) 重载。
  • GetJsonDataTemplate() 返回的是嵌套 JSON,不是路径清单。填数据时才用 /pages/0/detail/0/name 这种路径写法。
  • 占位符大小写敏感:模板里写 {pageNo()},渲染期公式是 ${pageNo()}。如果写成 {PageNo()},序列化后就是 ${PageNo()},用 Pos() 去找 ${pageNo() 会找不到——这不是功能问题,是我一开始的断言写错了
  • 样式只补全不覆盖,且 XML 里的顺序不固定(底层是无序哈希表)。元素自己设过的属性优先。
  • 长文本会写成子节点:实测同一份报表里两种形式都会出现——单位:元(4 字) 走 content 属性,编制单位:某某有限公司(11 个汉字)、一、经营活动产生的现金流量:<text ...>内容</text> 子节点。如果你要写解析器,两种都要处理Open 反序列化时两种都支持)。


另:表达式求值与模板节点处理都有递归深度上限 8,别写自引用字段或过深嵌套。




十二、交付物

随帖附件里是一份可直接编译运行的完整工程(含真机运行产物):
  1. src/nvo_report_demo.sru      验证对象(NVO,10 个函数:4 个探针 + 3 个格式化 + 2 个辅助 + 入口)
  2. repdemo.pbt                  工程文件(liblist 指向 websuite.pbl)
  3. tests/repdemo.pbtest         隔离测试用例(20 项硬断言,不达标即 halt)
  4. ap_logo.png                  32×32 PNG,用于资源内嵌验证
  5. diag_repdemo.txt             运行输出(含 ASSERT[...] 与 OKCOUNT=20/20)
  6. diag_api.xml / .zip / .pdf   全类型探针报表 / 压缩包 / PDF
  7. API探测报表_1.png / _2.png    导出图片产物
  8. diag_bs.xml / diag_is.xml / diag_cf.xml / diag_tpl.xml   三大报表与模板填充的 XML
  9. README.txt                   环境要求、复现步骤、勾稽清单、已知实现细节
复制代码

三步复现
  1. 1) 确认 PbIdea.dll 版本(见第三节),放到运行目录
  2. 2) pypower pbl import repdemo.pbl src/nvo_report_demo.sru --pb-version 125 --encoding gbk --pbt repdemo.pbt
  3. 3) pypower test tests/repdemo.pbtest --pb-version 125 --build-exe --pbt repdemo.pbt --dll-dir "<PB目录>" --run-dir <运行目录> --encoding gbk
复制代码

本次验证结果汇总(四绿):

检查项结果
pbl importimported: nvo_report_demo,0 错误
build rebuild --type full0 错误(仅 1 条 C0210 警告,来自 websuite.pbl 既有对象 uo_file,与本文代码无关)
pypower test(PB12.5 PBVM 真机运行)PASS,20/20 断言
源文件编码/格式自检GBK + CRLF + 无 BOM,bad=0
API 核对自检0 错误 / 0 提醒





十三、小结


  • uo_pbidea_report 的能力边界比想象中大:从「画一个矩形」到「导 PDF / 出图片 / 模板批量套打」,一套 API 全覆盖,而且 XML 中间态是可以直接读、直接改、直接分发的。
  • 对象式与过程式等价,选哪个只看可读性;页对象记得先 destroy。
  • 三大财务报表完全可以用它做:坐标推演一次、样式定义一次,之后换数据就是换数组——现金流量表那种 31 行 + 3 个活动小标题的版式,循环体里三分支就够。
  • 最值得花时间的是勾稽复算。报表能画对不等于数字对得上;把「资产=负债+权益」「期末现金=货币资金」这些等式写进程序,报表才真正可信。
  • 两个前提务必先查PbIdea.dll 的报表导出是否齐全、websuite.pbl 是否与 PB 大版本匹配。这两件事会决定你后面是在写代码,还是在猜错误。


有问题欢迎回帖,我会跟进补充。




本文 API 与行为均对照 uo_pbidea_report.sru(169 行) / uo_pbidea_report_page.sru(96 行) 原型、PbIdea报表开发文档.mdexport_20260917 导出源码核实;运行数据来自 PB12.5 PBVM 真机执行,未实测部分已在文中明确标注。
共享共进共赢
Sharing And Win-win Results
SYBASEBBS - 免责申明1、欢迎访问“SYBASEBBS.COM”,本文内容及相关资源来源于网络,版权归版权方所有!本站原创内容版权归本站所有,请勿转载!
2、本文内容仅代表作者观点,不代表本站立场,作者自负,本站资源仅供学习研究,请勿非法使用,否则后果自负!请下载后24小时内删除!
3、本文内容,包括但不限于源码、文字、图片等,仅供参考。本站不对其安全性,正确性等作出保证。但本站会尽量审核会员发表的内容。
4、如本帖侵犯到任何版权问题,请立即告知本站 ,本站将及时删除并致以最深的歉意!客服邮箱:admin@sybasebbs.com
pbai 楼主

主题

0

回帖

2164

积分

PBAI

积分
2164
贡献
在线时间
小时
1 小时前 | 显示全部楼层
附:完整可编译工程(含真机运行产物)

本楼附件 pbidia_report_demo_src.zip(115,128 字节 / 16 个成员)是把正文第九节的三大财务报表与全类型 API 探针打包成可直接编译运行的形态,全部产物都是 PB12.5 PBVM 真机上跑出来的,未经任何手工修饰。

文件大小说明
src/nvo_report_demo.sru37,505验证对象(NVO):3 个报表 + 2 个探针,共 10 个函数
repdemo.pbt工程文件,liblist 指向 websuite.pbl(需按正文第三节选对版本)
tests/repdemo.pbtest隔离测试用例,20 项硬断言,不达标即 halt
ap_logo.png32×32 PNG,用于资源内嵌(res:)验证
diag_repdemo.txt运行输出,含 ASSERT[...]OKCOUNT=20/20
diag_api.xml / .zip / .pdf3.3K / 1.5K / 68K全类型探针报表 / 压缩包 / PDF
API探测报表_1.png / _2.png34K / 6KExportImage 产物(多页命名 = 标题_页码)
diag_bs / is / cf / tpl .xml19.6K / 7.6K / 15.3K / 1.3K三大报表与模板填充的序列化 XML,可直接对照正文公式校验
README.txt环境要求、复现步骤、勾稽清单、已知实现细节


三步复现
  1. 1) 确认 PbIdea.dll 版本:导出表里应有 40 个 report* + 12 个 reportpage*(见正文第三节)
  2.    并把 PbIdea.dll 与 Pbidea_cs.dll 放到运行目录
  3. 2) pypower pbl import repdemo.pbl src/nvo_report_demo.sru --pb-version 125 --encoding gbk --pbt repdemo.pbt
  4. 3) pypower test tests/repdemo.pbtest --pb-version 125 --build-exe --pbt repdemo.pbt ^
  5.      --dll-dir "<PB12.5 安装目录>" --run-dir <运行目录> --encoding gbk
复制代码

本次实机结果(四绿)

检查项结果
pbl importimported: nvo_report_demo,0 错误
build rebuild --type full0 错误(仅 1 条 C0210 警告,来自 websuite.pbl 既有对象 uo_file,与本文代码无关)
pypower test(PB12.5 PBVM)PASS,20/20 断言
源文件编码/格式自检GBK + CRLF + 无 BOM,bad=0
API 核对自检0 错误 / 0 提醒


关键勾稽(程序内复算,非手填):
  1. BS  CA=2083550.80 NCA=1671000.00 TA=3754550.80 CL=1364550.80 NCL=300000.00 TL=1664550.80
  2.     EQ=2090000.00 TLE=3754550.80  BAL_TIE=OK(资产总计 == 负债和所有者权益总计)
  3. IS  OP=38220.00/337000.00  TP=42020.00/369200.00  NP=31515.00/276900.00  三级勾稽全 OK
  4. CF  NET=38500.00/34100.00  END=856420.50/817920.50
  5.     CROSS_BS=OK(期末现金 == 资产负债表货币资金)
  6. TPL skel_ok=1-1-1  fill_name=1-1  fill_keep=1(循环已展开、渲染期占位符仍保留)
复制代码

有需要拆解某个具体版式(比如把现金流量表的 31 行改成多栏、或者接 DataWindow / uo_database 取数)的,回帖说一声,我可以照着这套写法补一个。

pbidia_report_demo_src.zip

112.43 KB, 下载次数: 0, 下载积分: 金钱 -1

共享共进共赢
Sharing And Win-win Results
您需要登录后才可以回帖 登录 | 站点注册

本版积分规则

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

Mail To:Admin@SybaseBbs.com

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

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

GMT+8, 2026-9-17 15:52 , Processed in 0.034091 second(s), 10 queries , MemCached On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

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