基于模板创建Jaspersoft报表:JRXML分页与中文PDF导出

发布时间:2026/11/2 10:26:58
基于模板创建Jaspersoft报表:JRXML分页与中文PDF导出

先说结论:Jaspersoft Studio 这个报表设计器我用了有几年,从最早只会拖文本框,到后来做几十页的复杂报表,中间踩过的坑大多集中在“起步方式”上。很多人第一次打开它,面对空白画布和新文档,会本能地选择 File → New → Blank A4,然后从零开始画标题、画表头、调边距。结果往往是画了半天,导出一看,页边距对不上,表头每页都多出一块,长文本被砍掉一半。这篇内容就围绕“基于模板创建报表”这个最基础但最容易被跳过的环节展开,讲清楚模板到底给了我们什么、JRXML 的内部结构长什么样、从模板出发的第一份报表该怎么一步步跑通、中文导出为什么会变成方块、以及一整套排查思路。适合刚接触报表工具的后端同学、数据分析岗的同事,也适合已经会拖控件但总觉得“报表长得丑、导不出去”的中级使用者。读完你至少能做到:选对模板、改对参数、拖对字段、导出一份在 PDF 和 Excel 里都不丢格式的报表。

1. 为什么我坚持从模板起手,而不是空白画布

1.1 模板给的不是一张纸,是一整套调好的分页参数

刚接触这个工具的时候,我也觉得模板是个“花架子”,不就是个带标题栏的空壳吗?后来才明白,模板真正值钱的地方在于它预先算好了一堆你大概率不会主动去算的参数。报表引擎内部用的是点(pt)作为长度单位,一张 A4 纸是 595 × 842 pt,这个数字不是随便来的,它来自 210mm × 297mm 按 72 DPI 换算的结果。模板在创建时就帮你把页面宽度、左右边距、上下边距、可用内容宽度全部对齐好了。比如某款内置的 A4 模板默认左右边距各 20pt,那么可用画布宽度就是 595 − 40 = 555pt。你后面每一列的宽度加起来不能超过 555pt,超了就会自动跑到下一页去,很多人抱怨“明明只差一点点,怎么多出来一页空白”,根子就在这里。

除了页面尺寸,模板还会把各类 Band(分区)的初始高度设成合理值。标题区、页头区、列头区、明细区、页脚区各占多少高度,这些值看起来不起眼,但直接决定了你的第一版报表看上去是“规整”还是“松垮”。自己从空白画布起手,这些数字全是 0 或者默认的极小值,你得挨个双击标尺去拉,拉完还不一定记得住比例。我个人的习惯是:哪怕最终要重画,也先从模板生成一份,把它当成“参数基准”,改起来比从零调快得多。

还有一层隐性收益,模板会顺带把一些通用样式(Style)带进来。比如列头的背景色、边框线宽、字体字号、对齐方式,都以命名样式的方式挂在报表里。这些样式不是硬编码在每个文本框上,而是可以被复用的。后面你要统一改字号,改一处样式定义,几十个字段一起变,比挨个选中修改省事太多。这也是我一直劝新手别跳过模板环节的原因——你省下的不是那五分钟的画图时间,而是后面几十次重复劳动。

1.2 打开 JRXML 源码,把报表的“骨相”看一遍

设计器是所见即所得的,但真正决定报表行为的是它背后那份 XML,也就是 .jrxml 文件。在项目树里右键任意一份报表,选“Open With → XML Editor”,或者直接在文件系统里用文本编辑器打开,你会看到结构非常清晰的层级。理解这个结构,比会拖控件重要得多,因为绝大多数“预览正常、导出异常”的问题,都能在这一层找到答案。

一份从模板生成的报表,骨架大致是这样:

<jasperReport name="demo_report" pageWidth="595" pageHeight="842" leftMargin="20" rightMargin="20" topMargin="30" bottomMargin="30" columnWidth="555" whenNoDataType="AllSectionsNoDetail"> <property name="com.jaspersoft.studio.data.defaultdataadapter" value="One Empty Record"/> <style name="Base" fontName="SansSerif" fontSize="10" pdfFontName="Helvetica"/> <style name="TableHeader" style="Base" backcolor="#E8E8E8" isBold="true"/> <field name="orderNo" class="java.lang.String"/> <field name="amount" class="java.math.BigDecimal"/> <parameter name="reportTitle" class="java.lang.String"/> <variable name="totalAmount" class="java.math.BigDecimal" calculation="Sum"> <variableExpression>$F{amount}</variableExpression> </variable> <title> <band height="60"> <textField> <reportElement x="0" y="0" width="555" height="30"/> <textElement textAlignment="Center" fontSize="16" isBold="true"/> <textFieldExpression>$P{reportTitle}</textFieldExpression> </textField> </band> </title> <columnHeader> <band height="24">...</band> </columnHeader> <detail> <band height="20">...</band> </detail> <summary> <band height="30">...</band> </summary> </jasperReport>

看几眼就能发现几个关键点。pageWidth和columnWidth是必须对上的,columnWidth通常等于pageWidth减去左右边距。whenNoDataType这个属性决定了查不到数据时怎么表现,模板默认给了AllSectionsNoDetail,意思是除了明细区之外其他区照常输出,标题和表头还在,只是没有数据行。如果不设这个属性,查询结果为空时你会得到一张彻底的白纸,连标题都没有,这在小批量调试时非常容易误判成“报表坏了”。

<style>的继承关系也值得看一眼,TableHeader用style="Base"继承了基础样式的字体和字号,只覆盖了背景色和加粗。这种继承链在报表里是生效的,选中一个文本框,点属性面板里的“Style”下拉框就能挂上去。很多人不知道有这个机制,于是把每一列的表头都手动设一遍背景色,一旦客户说“颜色再浅一点”,就得改二十次。

1.3 什么时候该放弃模板,自己建基准

话说回来,模板也不是万能的。内置模板多数偏欧美版面习惯,页边距给得比较宽,字号偏小,行高偏紧凑。如果你做的是国内常见的单据类报表,比如对账单、发货单,很可能需要更窄的页边距、更大的字号、更明显的表头底色。这种情况下我的做法是:第一次从模板起手,把所有参数调到满意,然后把这份调好的报表另存为一份“自建模板”,以后所有同类报表都从它派生。

具体操作不复杂,把这份 jrxml 放进设计器可识别的模板目录,重启后新建报表向导里就会出现你自己的模板分类。这一步的价值在于把“团队约定”固化了:字号、行高、表头颜色、页脚格式、页码写法全部统一,后面换人接手也不会走样。我待过的团队里,凡是老老实实做了这件事的,报表风格一致性和维护效率都明显好一截。

2. 动手前先理清三个概念,否则模板也救不了你

2.1 Band 分区模型:报表本质上是若干条横向切片

很多人对报表的第一误解是“它像网页一样从上往下流式排版”。不是的,报表引擎的模型是“带区(Band)+ 分页”。报表被切成若干条横贯页面的带状区域,每条有自己的高度,内容在带内绝对定位。默认的带区顺序是:Background(背景,每页都绘制,在最底层)、Title(标题,只在第一页出现一次)、Page Header(页眉,每页顶部)、Column Header(列头,每页第一行数据前出现)、Detail(明细,每行数据迭代一次)、Column Footer(列尾)、Page Footer(页脚,每页底部)、Summary(汇总,最后一页最后出现一次)、No Data(无数据时显示)。

理解这个模型的关键在于“迭代”二字。只有 Detail 带区会随着数据行重复,其他带区要么只出现一次,要么每页出现一次。这解释了一个高频困惑:为什么我把某个字段拖到 Page Header 里,它只显示了第一行的值?因为 Page Header 每页只画一次,它拿的是“当前上下文的第一条数据”,并不是每行都画。想让字段逐行显示,必须放进 Detail。

列头区(Column Header)的作用也常被误读。它不是“表头只出现一次”,而是“每页表头都重复”。对于多页报表,这是必须的,否则翻到第二页就不知道哪列是哪列了。如果你确实只想在第一页显示表头,那就该把它放在 Title 区,而不是 Column Header。这个区分看起来细枝末节,但我在实际项目里见过太多次“第二页没表头”的低级返工。

2.2 字段、参数、变量:三种取值方式的适用边界

报表里能取的值基本分三类,用三个前缀区分。搞不清这三者的边界,模板再漂亮也只是个壳子。

类型语法数据来源典型用途生命周期
字段 Field$F{name}数据源返回的当前行某一列明细数据、订单号、金额随 Detail 每行变化
参数 Parameter$P{name}调用方传入,或报表内部声明报表标题、查询条件、动态 SQL 片段整份报表固定不变
变量 Variable$V{name}由表达式按规则累计或计算合计、计数、页码、分组小计按计算类型(Sum/Count/Average)累加

字段是最直观的,它对应查询结果集里的一列,也对应数据源对象的一个属性。用 JDBC 数据源时,字段名就是 SQL 里的列别名;用 JavaBean 数据源时,字段名是 getter 去掉 get 后首字母小写。模板里已经声明好的字段类型写在class属性上,java.lang.String、java.math.BigDecimal、java.util.Date这几种最常见。类型错了不会报编译错,但会在运行时抛转换异常,或者干脆显示成空白,这个坑后面第 5 节还会提到。

参数用来解决“值不由数据决定、而由外部决定”的场景。最典型的就是报表标题,你不可能把标题写死在设计器里,因为同一份模板要给不同场景用。声明一个reportTitle参数,预览时在参数面板填值,导出时由调用方传入,一份模板能吃遍多个业务场景。另一个高频用法是拼动态 SQL,参数里放一段where子句片段,用$P!{}的写法直接文本替换进查询,注意这里必须用感叹号版本,否则会被当成预编译参数处理,语法上过不去。

变量是三者里最容易被低估的。它不只是“合计”,还包括页码$V{PAGE_NUMBER}、总页数$V{PAGE_NUMBER}配合isResetPageNumber、分组小计$V{groupSum}、序号$V{rowIndex}(用calculation="Count"配合内置的REPORT_COUNT也能出)。变量的核心属性是calculation和resetType,前者决定是求和、计数还是取平均,后者决定什么时候清零。默认resetType是 Report,即整份报表累加一次;设成 Page 就是每页重置,页码就是这么来的;设成 Group 就是每个分组重置,小计就是这么来的。

2.3 数据源和查询:先定 shape,再定样式

一个很实际的顺序问题:应该先选数据源还是先排版?我的建议是分两段走。第一段用“空数据源”或者“一条空记录”先把版式跑出来,确认分页、列宽、表头、页脚都对;第二段再切到真实数据源,把字段对上去。原因是真实查询往往需要连接配置、需要网络、需要权限,一出问题你就分不清是版式问题还是数据问题。

模板向导里会直接让你选数据源,常见的选项有:空数据源(Empty datasource)、一条空记录(One Empty Record)、数据库 JDBC 连接、JSON 文件、XML 文件、JavaBean 集合。选“一条空记录”是我最推荐的新手起点,它会产生一行全为 null 的数据,让 Detail 区至少渲染一次,你能看到表头下面确实有一行,但内容是空的。这比纯空数据源友好,因为纯空数据源会让 Detail 区完全不渲染,你会怀疑自己是不是字段没拖对。

切到 JDBC 数据源后,重点是“Dataset and Query”对话框里的那套流程:先选连接,把查询语句贴进去,点“Read Fields”,工具会自动执行一次查询(通常加limit或者只取少量行)并把结果集的列名和类型读出来,映射成字段列表。这一步有个小技巧,查询里尽量给每个计算列写明确的别名,别用sum(amount)这种无别名写法,否则生成的字段名会是一串不好用的表达式结果。别名写成total_amount,字段名就是total_amount,后面拖拽引用时清爽很多。

3. 基于模板创建报表的完整实操

3.1 新建向导六步走,每一步填什么

打开设计器,菜单 File → New → Jasper Report,向导会依次问六件事,我按顺序说清楚每一项该怎么填,以及为什么。

第一步是选模板。列表里通常分两大类,一类是空白版式(Blank A4、Blank Letter 等),另一类是示例模板(带表格、带图表的样例)。新手直接选 Blank A4,不要一上来就选示例模板,因为示例模板里塞了子报表、图表、多语言资源,你改不动也删不干净。选空白 A4 的意义就是拿到一套正确的页面参数和几个空带区,干干净净。

第二步是填文件名和存放位置。文件名建议用“业务域_报表名”的形式,全小写加下划线,比如order_detail_report,不要用中文和空格。工具本身支持中文文件名,但后续在代码里加载、在服务器上部署、在构建脚本里打包时,中文名会带来一堆不必要的转义麻烦。存放位置选项目里的 reports 目录,别扔在根目录,否则项目一大就找不着。

第三步是选数据源。按上一节说的,第一版选“One Empty Record”。如果你已经有 JDBC 连接配置好了,也可以直接选,但先把查询写成select 1 as id这种极小语句,别一上来就查大表。

第四步是配置查询(仅当选择了数据库类数据源时出现)。这里可以先跳过,后面在 Dataset and Query 对话框里改。

第五步是选字段(同样仅数据库数据源)。也可以跳过。

第六步是完成。点 Finish 后,主编辑区会打开设计视图,左侧是 Outline 树,中间是设计画布,右侧是属性面板和组件面板。

这里有个细节值得说:向导完成后,很多人找不到“带区”在哪里显示。默认视图下带区是画布上一条条横向的灰色区域,高度为 0 的带区会缩成一条线,几乎看不见。这时候看 Outline 树最清楚,那里把 Title、Page Header、Column Header、Detail 等逐个列出来了,双击任意一项,画布会自动滚动定位过去。我一般建议新手把 Outline 面板固定在左边,它是理解带区模型最快的入口。

3.2 打开模板后的第一件事:核对四个数

向导生成完,别急着拖控件,先花两分钟核对四个数值:页面宽度、左右边距、上下边距、可用内容宽度。这四个数在画布上选中空白处,右侧属性面板的“Report”分组里能看到,也可以在 Outline 里选中根节点查看。

假设客户要 A4 纵向、左边距 15pt、右边距 15pt、上边距 20pt、下边距 20pt,那么:页面宽度 595pt,可用宽度 = 595 − 15 − 15 = 565pt;可用高度 = 842 − 20 − 20 = 802pt。这两个数决定了你后面所有内容的布局边界。列宽总和不能超过 565,一页内所有带区高度之和不能超过 802(扣除页眉页脚重复占用的部分)。

列宽怎么分?我的经验是先列业务字段,按内容长度估权重,再换算成 pt。比如一份订单明细,字段是订单号(约 20 字符)、客户名(约 12 字符)、下单时间(19 字符固定)、金额(12 字符)、备注(剩余)。按 10 号字估算,一个中文字宽约 10pt,一个西文字符约 5.5pt。订单号占 20 × 5.5 ≈ 110pt,客户名占 12 × 10 ≈ 120pt,时间占 19 × 5.5 ≈ 105pt,金额占 12 × 5.5 ≈ 66pt,加起来 401pt,剩 164pt 给备注。这个粗算当然不精确,但比“先随便拖,导出发现溢出再返工”高效得多。实际调的时候,我会把估算值再压 10% 作为初始宽度,留出余量给边框和内边距。

接下来调带区高度。列头区高度取决于表头文字是否换行,单行表头 20pt 到 24pt 够用,两行就得 32pt 往上。明细区高度取决于行内容的高度上限,纯单行文本 18pt 到 20pt,如果某列允许换行(勾选 Stretch With Overflow),那这行的高度会动态撑开,明细区的基础高度可以设小一点,比如 16pt。页脚区通常放页码和打印时间,20pt 到 24pt 足矣。

3.3 把字段拖进 Detail 区,四个必须注意的细节

字段拖拽是最爽的一步,也是埋坑最多的一步。从 Outline 或者右侧的字段列表里把一个字段拖进 Detail 区,工具会自动生成一个文本字段,表达式是$F{字段名}。看着简单,但有四个细节必须当场处理。

第一个是宽度和溢出行为。拖进去的文本框默认宽度可能是 100pt,跟你的列宽对不上。先把 x 和 width 调准,然后看属性面板里的“Text Field”分组,把“Stretch With Overflow”勾上。不勾的话,内容超长会被直接截断,页面上看不出任何提示,导出 PDF 才发现后半句没了。勾上之后还有个配套属性叫“Stretch Type”,默认是“No Stretch”,如果希望同一行的其他单元格跟着一起撑高对齐,可以设成“Relative To Tallest Object”,这样一行里有一个格换行了,其他格也跟着变高,视觉上是一条完整的行。

第二个是对齐方式。金额类字段必须右对齐,数字右对齐是财务类报表的基本素养,左对齐的数字上下对不齐小数点,看着非常业余。日期居中或左对齐都行,文本左对齐。设置位置在文本元素的“Text Adjust”下面的对齐属性里,或者在属性面板顶部的对齐图标。别忘了同时给列头设成相同的对齐方式,否则表头和数据会错位。

第三个是空值处理。字段值是 null 时,默认会输出空白,这本身没问题,但如果你在表达式里做了字符串拼接,null 会导致整个表达式返回 null,最后显示成空白,排查起来很费劲。稳妥的写法是用$F{name} == null ? "" : $F{name},或者用条件表达式。金额字段如果 null 要显示 0,就写$F{amount} == null ? BigDecimal.ZERO : $F{amount},注意类型要匹配,声明是 BigDecimal 就不能返回 int。

第四个是格式化。金额千分位、日期格式、百分比,这些不要靠数据库查出来再拼字符串,应该在报表层用 pattern 处理。选中文本字段,属性面板里有“Pattern”输入框,金额填#,##0.00,日期填yyyy-MM-dd HH:mm:ss,百分比填0.00%。用 pattern 的好处是导出 Excel 时能保留数值类型,而不是变成文本,这对下游做二次计算很关键。我见过太多报表把金额格式化成字符串输出,结果业务方在 Excel 里求和全是 0,返工重做。

3.4 跑通第一个闭环:编译、预览、导出

版式调得差不多了,就该跑第一个闭环。设计器顶部有一排标签页,切到“Preview”就会触发编译和填充。第一次编译可能报错,常见的是表达式语法错误、字段类型不匹配、字体找不到。编译成功后,预览区会渲染出结果,顶部还有一排导出按钮,可以直接导 PDF、Excel、Word、HTML、CSV。

这里有个流程上的建议:先用“一条空记录”预览,切到真实数据源再预览一次,最后才导出 PDF。三段式的顺序能帮你快速定位问题层级。如果空记录预览就白屏,那是版式或带区问题;如果空记录正常但真实数据源白屏,那是查询或字段映射问题;如果预览正常但 PDF 异常,那是字体或编码问题。把问题分层,排查效率能差好几倍。

导出 PDF 时的一个基础检查:打开导出的文件,翻到第二页,看列头是否还在,看页码是否正确递增,看最后一行有没有被切到下一页。分页问题几乎不可能在预览的第一屏发现,必须翻页看。我一般会造 60 到 80 条测试数据,正好跨两三页,这样能一次性暴露分页、列头重复、页脚位置、汇总区位置这几类问题。

3.5 表格类版式:用 Table 组件还是 Group 分组

模板生成后你会发现,右侧组件面板里有个“Table”组件。很多新手第一反应是用它来做明细表,因为看起来“像表格”。但我的建议是:能用 Detail 区手画的,就别用 Table 组件。

原因是 Table 组件内部其实是一个子数据集(SubDataset)驱动的独立渲染单元,它有自己的字段映射、自己的列定义、自己的分页逻辑。好处是列宽自动分配、自带表头重复、适合列数很多的宽表;坏处是字段要重新映射一遍、样式要单独设、出问题时错误信息藏在子数据集里非常难查。对于列数在 10 列以内、版式比较规整的报表,直接在 Detail 区摆文本框,配合 Column Header 做表头,反而更简单可控。

什么时候必须上 Table 或者 Group?第一种是列数超过 15 列、需要横向扩展或者列动态隐藏的宽表;第二种是需要按某个维度分组、每组带小计和组头组尾的报表。后一种其实用的是 Group 功能而不是 Table 组件,在 Outline 树里右键报表根节点选“Add Group”,指定分组表达式(比如$F{department}),工具会自动生成 Group Header 和 Group Footer 两个带区,配合resetType="Group"的变量就能做出漂亮的分组小计。这个组合我在做部门汇总、月度分类统计这类报表时用得最多,比 Table 组件省心。

4. 中文输出与字体:模板环节最容易翻车的地方

4.1 预览里好好的,导出 PDF 全是方块

这个问题几乎是所有中文使用者的第一课。现象很一致:设计器里预览,中文显示正常;导出 PDF,所有中文字符变成方块或者空白;导出 Excel 和 HTML 却又是好的。原因不复杂,PDF 渲染走的是内置的字体机制,默认字体只覆盖了有限的字符集,中文不在其中。设计器预览用的是系统字体,所以看着没问题,一进 PDF 就露馅。

解决路径是给报表引擎注册一个支持中文的字体,并且让 PDF 导出时使用“嵌入”模式。具体分两步:先在工具里把字体加进去,再把字体打包成扩展放到运行环境里。

4.2 字体扩展的完整操作步骤

第一步,在工具的偏好设置里找到字体配置页(大致路径是 Window → Preferences → Jaspersoft Studio → Fonts),点“Add”从本机选择一个中文字体文件,通常是一个 .ttf。选完后填三项:字体名称(自己起个易记的名字,比如CN-Sans)、PDF 字体名称、PDF 编码。PDF 编码这一项必须选Identity-H,这是支持中文这类多字节字符的关键,选错了照样出方块。同时把“Embed this font in PDF document”勾上,这样 PDF 里会把字体子集打包进去,换台机器打开也不会变形。

第二步,同一页面里点“Export”,把这套字体配置导出成一个 jar 包。这个 jar 里装的是字体文件和一份 XML 描述,是给运行环境用的。把 jar 放到你的项目依赖路径里,如果是部署到报表服务端,就放进服务端的库目录。放好之后重启设计器,在字段的属性面板里,字体下拉列表中就能看到CN-Sans这个选项了。

这里有个容易被忽略的点:字体改了之后,所有原本用默认字体的文本元素都要重新指定一遍。因为字体是挂在样式或者元素上的,不会自动继承新加的字体。省事的做法是在报表里定义一个名为Base的基础样式,字体设成CN-Sans,然后让所有其他样式都继承它。这样只需改一处,全报表生效。模板里一般已经有Base样式,你要做的就是改它的fontName属性。

4.3 用样式模板统一多份报表的字体和表头

当报表数量多起来,一份一份改字体会非常痛苦。这时候可以上“样式模板”(.jrtx 文件)。它的本质是把一组样式定义抽出来单独存成一个文件,多份报表引用同一个文件。做法是:在项目里新建一个样式模板文件,把Base、TableHeader、TableCell、TotalRow这几个常用样式全定义进去,字体统一设成CN-Sans;然后在每份报表的属性里指定引用的样式模板路径。

这么做的额外好处是“版本统一”。某天客户说表头颜色要换成另一种,你只改样式模板这一处,引用它的几十份报表全部跟着变,不用挨个打开改。这个机制我在做统一视觉规范的报表集时用得很顺手,代价只是前期多花半小时整理样式。

还有个小提醒:样式模板文件要跟着报表一起部署,不能只留在本地。部署时路径写相对路径,别写本机的绝对路径,否则换台机器就找不到了。

5. 常见问题与排查实录

5.1 从现象到原因的速查表

我把这几年遇到的高频问题整理成一张表,基本覆盖了新手期 80% 的卡点。判断思路永远是“先分层,再看现象”:是设计期就报错,还是编译期报错,还是预览期空白,还是导出后才异常。

现象最可能的原因处理方式
预览白屏,什么都没有数据源为空且未设whenNoDataType把该属性设为AllSectionsNoDetail,或改用“一条空记录”数据源
表头只显示一次,第二页没有表头放在了 Title 区移到 Column Header 区
明细只显示一行,后面没了字段拖到了 Column Header 或 Page Footer移到 Detail 区
内容超出右边界,多出空白页列宽总和大于可用宽度重新分配列宽,总和 ≤pageWidth − 左右边距
长文本被截断未勾选 Stretch With Overflow勾选该属性,必要时设 Stretch Type
导出 PDF 中文变方块未配置中文字体扩展按第 4 节配置,编码选 Identity-H
金额在 Excel 里无法求和用了 pattern 之外的字符串拼接保留数值类型,用 Pattern 格式化
运行时抛类型转换异常字段class声明与实际类型不符在 Dataset and Query 里重新 Read Fields
合计值一直是 0变量的 resetType 或 calculation 设错检查是否为 Sum,resetType 是否为 Report
页码始终显示 1用了$V{PAGE_NUMBER}但未在页脚求值页脚处放两个文本字段,其中一个用$V{PAGE_NUMBER}配合求值时机
编译报找不到字段字段名拼写与查询别名不一致对照 SQL 别名逐个核对,注意大小写
分组小计串到下一组分组表达式或 resetType 不匹配确保变量 resetType 设为 Group 并指定分组名

表里有一项要单独说明:页码问题。$V{PAGE_NUMBER}这个内置变量的值取决于“求值时机”。放在 Page Footer 里,它在渲染当前页时求值,得到的是当前页码;如果你想显示“第 X 页 / 共 Y 页”,那个 Y 需要把文本字段的“Evaluation Time”属性设成 Report,这样它在整份报表渲染完才求值,拿到的是总页数。这两个字段长得一样,属性不同,效果差很远,我第一次做分页页码时在这上面耗了快一个小时。

5.2 我踩过的几个坑,写下来省得你再踩

第一个坑是测试数据太少。刚开始做报表时,我用一条数据测通了就交付,结果上线后 300 条数据一跑,分页、列头重复、汇总位置全乱。后来我养成习惯,本地准备一份至少 80 条的测试数据集,专门用来验分页。80 条这个数字是我按“A4 纸、每页约 25 行”倒推的,能稳定跨三四页,足够暴露分页问题。

第二个坑是在 Detail 区用了绝对定位却期望它自动流式排版。报表引擎是绝对定位的,你在 Detail 区放两个文本框,y 坐标重叠了它们就重叠显示,不会像网页那样自动往下挤。所以同一行里每个元素的 y 和 height 必须手动对齐,我一般会把同一行的所有元素先设成相同的 y 和 height,再调 x 和 width,这样视觉上一定是一条整齐的线。

第三个坑是把业务逻辑塞进报表表达式。有人喜欢在表达式里写复杂的字符串处理、条件判断、甚至调用工具类方法。这在技术上行得通,但维护成本极高,因为报表文件改起来没有代码舒服,还没法写单元测试。我的原则是:能在 SQL 或者服务层算好的,就不要放到报表里算。报表层只做展示和轻量聚合,超过三行的表达式就该挪走。

第四个坑是忽略导出格式的差异。同一份报表导 PDF 和导 Excel,表现可能完全不同。PDF 是绝对定位渲染,你画成什么样就是什么样;Excel 是网格模型,合并单元格、行高、列宽都会被重新计算,绝对定位的浮层元素可能错位。做需要导 Excel 的报表时,我一般会先把所有元素对齐到网格,避免大量重叠和浮动,导出来才勉强能看。如果客户对 Excel 格式要求高,那就得单独做一版面向 Excel 的模板,指望一份模板通吃所有格式,最终大概率两头不讨好。

第五个坑,也是最隐性的一个:样式模板路径写成绝对路径。本地开发一切正常,部署到别的环境立刻字体丢失、样式全无。这个问题的排查成本很高,因为报错信息很含糊。后来我定了个规矩,凡是涉及外部文件引用,一律用相对路径,并且在部署清单里单独列一行“样式模板文件是否随包发布”。

最后分享一个提高效率的小习惯:每做完一份报表,在报表属性里写一段注释,记录这份模板基于哪个模板派生、改过哪些关键参数、依赖哪个字体扩展、测试数据量是多少。这份注释不会影响渲染,但半年后你或者别人接手时,能省下大量“这参数为什么是这个值”的考古时间。我现在的项目里,每份报表开头都挂着这么一段说明,翻起来比查文档快。