OrderID
City
Date
Value
2. 第i页/共n页(Page i of n)的实现
很多时候我们都需要在报表的开头就显示哪些需要计算完整个报表才能得到的值,就像这种页码形式每一页都要显示,但总页数却要在整个文档计算完后才能得到。而引擎是一页一页的填充报表的,总页数只能在到达报表结束时才能计算出来,怎么实现这种形式的页码呢?
非常简单。报表引擎允许我们指定文本域表达式的准确执行时机,要显示总页数,我们只需要在页面上放置一个使用PAGE_NUMBER报表变量的文本域,并指定该变量的计算时机为"Report",这样报表会在整个报表结束时在执行该表达式,而这时PAGE_NUMBER才表示整个报表的总页数。
在报表设计文档中实现该功能如下:
<textField evaluationTime="Now"><reportElement x="280" y="10" width="300" height="20"/>
<textElement textAlignment="Left">
<font fontName="Helvetica" size="14"/>
</textElement>
<textFieldExpression >
"第 " + $V{PAGE_NUMBER} + " 页/共 "
</textFieldExpression>
</textField>
<textField evaluationTime="Report">
<reportElement x="580" y="10" width="275" height="20"/>
<textElement textAlignment="Left">
<font fontName="Helvetica" size="14"/>
</textElement>
<textFieldExpression >
$V{PAGE_NUMBER} + "页"
</textFieldExpression>
</textField>
第一个<textField>的evaluationTime是"Now",也是该属性的默认值,表示在报表填充当前区域或列(column)时就处理该表达式,这得到的是当前也的值。要同时显示当前页和总页数,第二个<textField>就必须将evaluationTime设置成"Report",这样该表达式只有在报表结束时才处理。
evaluationTime属性有五个可选值:Now、Report、Page、Column和Group。具体含义轻参考速查手册。
3. 区域内容大于一页
有时一个特定的区域内容可能大于一页,我们必须在区域内部结构中找到一个合适的位置来分页。当然这种问题并不设计报表所有的区域类型,我觉得页或者列的头尾区域应该不会大于一页,但是谁知道呢。
如果标题区域和summary区域大于一页,你应该将该区域内容设置放到子报表中,并按下面将要介绍的技巧处理该子报表。对于页头或列头也用同样的方法,,但是对于页尾和列尾就不行了,因为引擎用固定高度来处理它们,对于它们的元素则不允许拉伸。
经过上面的排除法,剩下的就只有组头组尾和数据区域了。组头和组尾可以相同对待,我们先从数据区域入手。
如果我们有一个数据区域包含太多的元素使得我们必须分成多页来显示,我们就需要找到一个合适的方法来分页,但是报表引擎没有提供"分页符"这样的元素,怎么办呢?
报表引擎只为<group>元素提供了isStartNewPage属性来明确设定分页位置。也许我们可以用它来解决这些问题?
通常,报表引擎在被填充区域无法挤在剩余的页空间里时会创建一个新页或列,这也是我们要在报表设计被编译时确认区域是否可以在页内完全显示原因,以使区域不会再比页更高。没有这样的限制,报表引擎将会很困惑(confused)并发生控制之外的动作。因此,为了解决这个问题,我们首先要找到一个如何通过该区域高度检查的方法。
我们可以通过将数据区域的内容分割到多个区域中,使每个区域的高度都小于页高,这样报表设计就合法了。用什么样的区域来分割呢?当然是组头和组尾区域了。我们可以将数据区域的一些元素放置在特定分组的组头区域,另一些元素仍然放置在数据区域,剩下的元素可以放置在组尾区域。这样处理唯一的条件是组头尾区域必须总是伴随着数据区域一起显示,三个区域的行为看起来像一个普通的大数据区域。这是最简单的划分方法,因为我们可以创建一个"无用"分组把数据源中的每条数据分为一组,这样的分组表达式可以像下面这样定义:
<groupExpression>$V{REPORT_COUNT}</groupExpression>如果你有一个巨大的数据区域,一个无用分组没法满足你的要求,你可以加入任意多个你认为需要的无用分组,所有的分组表达式都是一样的"无用"表达式。
问题解决了。
噢,差点忘了当组头和组尾区域大于一页是的情况!没问题,同样的方法就可以了。你只需要创建一个新的分组,使用和需要分割分组同样的分组表达式,并将原来的组头和组尾元素分割放置到新分组的组头和组尾中,这两个分组会用同样的组边界就像一个分组一样,所以分组元素可以任意放置在两个分组的组头和组尾中。
这样区域高度检查也可以通过了。
4. 创建HTML、XLS、CSV格式有好的报表。
创建报表时,引擎使用报表元素的绝对位置来排列每一页的内容,绝对位置排列报表元素可以对输出文档的内容进行完全控制。PDF格式支持在一个文档页上按绝对位置排列文本和图形,这也是它被广泛应用于各种平台的原因。你可以完全相信,一个PDF文档一经创建,不管用什么平台的浏览工具都将得到同样的显示结果。报表引擎使用的文档格式(net.sf.jasperreports.engine.JasperPrint对象)也使用绝对位置排列页面元素。每个元素的绝对位置和大小通过x, y, width和height属性来确定。
然而,其他其他文档格式如HTML、XLS等并不支持按照绝对位置来排列文本和图形元素,这些文档的内容都被安排在表格(grid or table)结构中。当然,有人也许会说HTML元素的绝对位置排列可以借助CSS来实现,但是CSS的标准功能定义在各种浏览器之间差异太大,这样同样的HTML文档就不能在任何场合有同样的显示结果了。
因此报表引擎内建的用来产生HTML、XLS和、CSV文档的导出工具使用了特殊的算法来来排列文档中的元素到特定的表格。当报表设计非常复杂或使用了很大的模块,从绝对位置排列到表格的转换会产生带有很多无用行和列的复杂表格,这些行和列都是为了填充元素之间的空白或者为了完成元素的对齐格式而加入的。
这里有几个非常简单的原则来指导大家用导出工具得到最优化的HTML、XLS和CSV文档。
A. 最小化表格中的行和列的数量(最小化切割)。
你必须最大限度的将你的元素放在坐标轴上(表格线上),并除去元素之间的空白。如下两图所比较:
a) 低劣的格式
b) 表格有好的格式
在报表产生是确认没有元素互相重叠。因为如果有两个元素有一部分重叠,而在结果表格结构中它们却没有办法公用一个单元格,这样将会得到无法预料的结果。
尤其是当你想得到单页文档时,确认你禁用了页头和页尾。你也可以最小化页面顶端和底部的空白,以便在导出HTML、XLS和CSV文档时,你的文档可以被显示在一个没有分页符的单独区块中。
报表引擎允许你为报表元素设定任意的颜色。然而对于导出到XLS格式的报表,你必须知道XLS文件只支持一个有限的颜色集合。下面是一个Excel的调色板,列出了40种XLS文件可以使用的颜色。如果你最终要将报表导出成XLS文件,确定你只使用了这些颜色:
#FFFF00 YELLOW
#969696 GREY_40_PERCENT
#99CC00 LIME
#CC99FF LAVENDER
#FFFF99 LIGHT_YELLOW
#008000 GREEN
#C0C0C0 GREY_25_PERCENT
#339966 SEA_GREEN
#FF9900 LIGHT_ORANGE
#003366 DARK_TEAL
#FFFFFF WHITE
#99CCFF PALE_BLUE
#00FFFF TURQUOISE
#008080 TEAL
#000080 DARK_BLUE
#FFCC00 GOLD
#33CCCC AQUA
#333300 OLIVE_GREEN
#808080 GREY_50_PERCENT
#003300 DARK_GREEN
#808000 DARK_YELLOW
#FF00FF PINK
#FF99CC ROSE
#3366FF LIGHT_BLUE
#FF6600 ORANGE
#993300 BROWN
#993366 PLUM
#800080 VIOLET
#333399 INDIGO
#000000 BLACK
#00CCFF SKY_BLUE
#333333 GREY_80_PERCENT
#CCFFFF LIGHT_TURQUOISE
#CCFFCC LIGHT_GREEN
#666699 BLUE_GREY
#0000FF BLUE
#00FF00 BRIGHT_GREEN
#800000 DARK_RED
如果你的报表设计中使用了这些颜色之外的颜色,报表引擎将会采用特殊的算法找到一个RGB值最接近的颜色来替换,但是替换的结果往往会出乎预料:)
你可以通过使用特殊参数当作容器从子报表返回值。这里有一个例子:
问题:我想从在主报表中得到子报表中的记录总数。
解决办法:
在主报表中,定义一个特殊的容器参数,用它来存放将从子报表返回的总记录数:
<parameter >
<defaultValueExpression>
new java.util.HashMap()
</defaultValueExpression>
</parameter>
将该参数传给子报表,以使它可以被子报表用来存放我们想要的值:
<subreportParameter >
<subreportParameterExpression>
$P{ReturnedValuesMap}
</subreportParameterExpression>
</subreportParameter>
在子报表模板中,声明该容器参数。虽然它们使用的名字相同,主报表的参数和子报表参数是完全不同的实体。
<parameter />
在子报表中,为summary区域的一个不可见直线元素添加一个无效的<printWhenExpression>表达式来将我们需要返回的值放到容器中。
<line>
<reportElement x="0" y="0" width="0" height="0">
<printWhenExpression>
($P{ReturnedValuesMap}.put(
"MY_RETURNED_VALUE", $V{REPORT_COUNT}) == null
)?Boolean.FALSE:Boolean.FALSE
</printWhenExpression>
</reportElement>
</line>
当然这个功能也可以在scriptlet来实现,之所以用上面的方法只是因为简单。
返回到主报表,如果要显示该返回值,只需要从从其参数中取出来:
<textField evaluationTime="Group" evaluationGroup="DetailGroup">
<reportElement x="335" y="50" width="175" height="15"/>
<textFieldExpression >
$P{ReturnedValuesMap}.get("MY_RETURNED_VALUE")
</textFieldExpression>
</textField>
你也许会问为什么文本域使用了evaluationTime="group"。这是因为子报表的返回值有一个问题:它们是在当前区域的所有元素都被处理之后才返回的。如果你想在子报表所在的主报表区域显示它的返回值,你最终将看到它们返回的太晚了。
所以,为了能在主报表中的子报表所在的同一区域显示返回值,我们必须延迟文本域表达式的处理时机。这是通过加入一个无效的分组让表达式在分组结束时处理来实现的,而这个无效分组的效果就是数据源中的每条记录就是一组,而且它自己没有组头和组尾区域。
<group >
<groupExpression>$V{REPORT_COUNT}</groupExpression>
</group>
这个无效分组达到了我们的目的,因为我们的子报表和文本区域都放置在主报表的数据区域,并且它对每条记录分组。如果子报表放置在不同的报表区域中,这个无效分组需要根据实际情况调整。如果你不需要在子报表所在区域显示返回值,就不需要这样的无效分组。
7. 伪造标题和summary区域
标题和summary区域时报表的特殊区域,它们并不跟随它们所在页的页头和页尾,当它们超出当前页时尤其明显。
很多时候,我们需要让标题区域在第一页紧跟在页头区域之后,让summary区域在每一页跟随这页头和页尾。
我们可以通过创建伪造的标题和summary区域来实现。伪造标题可以是一个组头区域,这是一个将整个报表看成一个组的无效分组,它有一个空分组表达式。组头只在报表开始时打印一次。伪造summary区域同样使用这种无效分组的组尾区域。
如果你的报表设计中使用了这些颜色之外的颜色,报表引擎将会采用特殊的算法找到一个RGB值最接近的颜色来替换,但是替换的结果往往会出乎预料:)
你可以通过使用特殊参数当作容器从子报表返回值。这里有一个例子:
问题:我想从在主报表中得到子报表中的记录总数。
解决办法:
在主报表中,定义一个特殊的容器参数,用它来存放将从子报表返回的总记录数:
<parameter >
<defaultValueExpression>
new java.util.HashMap()
</defaultValueExpression>
</parameter>
将该参数传给子报表,以使它可以被子报表用来存放我们想要的值:
<subreportParameter >
<subreportParameterExpression>
$P{ReturnedValuesMap}
</subreportParameterExpression>
</subreportParameter>
在子报表模板中,声明该容器参数。虽然它们使用的名字相同,主报表的参数和子报表参数是完全不同的实体。
<parameter />
在子报表中,为summary区域的一个不可见直线元素添加一个无效的<printWhenExpression>表达式来将我们需要返回的值放到容器中。
<line>
<reportElement x="0" y="0" width="0" height="0">
<printWhenExpression>
($P{ReturnedValuesMap}.put(
"MY_RETURNED_VALUE", $V{REPORT_COUNT}) == null
)?Boolean.FALSE:Boolean.FALSE
</printWhenExpression>
</reportElement>
</line>
当然这个功能也可以在scriptlet来实现,之所以用上面的方法只是因为简单。
返回到主报表,如果要显示该返回值,只需要从从其参数中取出来:
<textField evaluationTime="Group" evaluationGroup="DetailGroup">
<reportElement x="335" y="50" width="175" height="15"/>
<textFieldExpression >
$P{ReturnedValuesMap}.get("MY_RETURNED_VALUE")
</textFieldExpression>
</textField>
你也许会问为什么文本域使用了evaluationTime="group"。这是因为子报表的返回值有一个问题:它们是在当前区域的所有元素都被处理之后才返回的。如果你想在子报表所在的主报表区域显示它的返回值,你最终将看到它们返回的太晚了。
所以,为了能在主报表中的子报表所在的同一区域显示返回值,我们必须延迟文本域表达式的处理时机。这是通过加入一个无效的分组让表达式在分组结束时处理来实现的,而这个无效分组的效果就是数据源中的每条记录就是一组,而且它自己没有组头和组尾区域。
<group >
<groupExpression>$V{REPORT_COUNT}</groupExpression>
</group>
这个无效分组达到了我们的目的,因为我们的子报表和文本区域都放置在主报表的数据区域,并且它对每条记录分组。如果子报表放置在不同的报表区域中,这个无效分组需要根据实际情况调整。如果你不需要在子报表所在区域显示返回值,就不需要这样的无效分组。
7. 伪造标题和summary区域
标题和summary区域时报表的特殊区域,它们并不跟随它们所在页的页头和页尾,当它们超出当前页时尤其明显。
很多时候,我们需要让标题区域在第一页紧跟在页头区域之后,让summary区域在每一页跟随这页头和页尾。
我们可以通过创建伪造的标题和summary区域来实现。伪造标题可以是一个组头区域,这是一个将整个报表看成一个组的无效分组,它有一个空分组表达式。组头只在报表开始时打印一次。伪造summary区域同样使用这种无效分组的组尾区域。