基于API的多张报表拼接
需求分析
随着用户报表需求的多样化,简单的报表样式很难满足繁多的需求。如,一张报表上面是网格报表下面是分栏报表。分析知道,通过报表系统可以使用主子表的方式,把网格报表放到子表中,但是需求里的分栏报表又不是简简单单的分栏,里面包含了多层的分组表头。如下图所示。
所以在分组报表这块使用了行属性中的“分组表头”来实现多表头的分栏报表。
不过由于报表中的行类型的界别限制,在表头区域上面的行无法设成数据区,所以用了标题区来代替。
可是新的问题也随之产生,对于子表中的数据量如果过多,标题区是无法分页的,导致整套的报表不能分页显示。
之前的实现思路是将一张报表拆分成两张,然后通过PageBuilder将两表中每一页都拆分成多个rat,再将第一张表的最后一页对应到第二张表的第一页前的空白行中,写入数据,这种方式可以很好的实现这种复杂功能。优点在于两张表的行高可以随意设置,不需要互相对应,代码量比较简单。缺点在于,分页导出等功能需要另外实现。
经过思考以及用户对导出分页等功能的需求,研究了以下替代方式,原理是通过api提前创建出一张空白的raq(注意:是raq),还是先将第一张表的最后页数据写入第二张表的开始,再通过PageBuilder将每一页拆分成多个rat,读取全部的行数,为之前创建的raq增加空白行,再遍历每张rat的单元格,将其属性写入到raq中,由于rat到raq的单元格写入有局限性,无法直接将INormalCell直接赋值到raq中,所以需要取得属性后,将属性赋值到raq中。并且对于合并单元格的赋值也需要利用到api动态合并单元格的功能。最后得到一个ReportDefine的结果报表对象,通过defineBean的形式通过标签来展现报表,分页导出等功能自然就能实现了,此方法优点在于用户体验度比较理性具有和以往报表页面中的操作功能,具有一定的通用性,缺点在于,报表的单元格高度需要强制要求,否则写入raq后的分页样式会不同,代码量也比较大。
实现步骤
1,首先根据传入的报表列数创建一张空白的报表raq(也就是ReportDefine,这里也可以创建计算好的报表rat,但是考虑到最后要使用标签形式读取,随意采用的写入raq中的方案)
ReportDefine rdStatic = new ReportDefine2(1, col);
//获取需要对该ReportDefine对象增加多少空白行,计算行数是通过计算两张报表的总和
rdStatic.addRow(allAddRow(report_sub, report));
2,创建好了N多空行的raq后,下面要做的就是将两张报表拆分整理写入这个raq中。首先是第一张报表(除最后一页)
1)计算子报表,也就是需求中上面的网格式报表(由于子表最后一页的数据要写入第二张表的前面,所以这一步无需计算最后一页的数据)
// 计算子表(只计算整页数的)
IReport ireport_sub = toReport(report_sub);
int pagecount_sub = allPage(ireport_sub);
2)通过PageBuilder按页进行拆分,然后逐页逐行的写入之前创建好的raq中。
* setICell ()这个方法是将rat单元格属性写入到raq中,在后面会介绍。
// 对子表进行按页拆分(除了最后一页)
for (int page = 1; page < pagecount_sub; page++) {
IReport ir = pb.getPage(page);
for (int i = 1; i <= ir.getRowCount(); i++) {
for (int j = 1; j <= ir.getColCount(); j++) {
INormalCell iCell = ir.getCell(i, (short) j);
INormalCell inc = rdStatic.getCell((i + (page – 1)
* ir.getRowCount()), (short) j);
setICell(iCell, inc);
}
}
}
3,写入第一张报表的最后一页,以及第二张报表第一页
1)获取第一张表最后一页行数,然后再第二张报表第一行前插入相应行数并且设置行类别
// 获取子表最后一页,在主表中增加相应行数,将数据写入
IReport ir_sub_end = pb.getPage(pagecount_sub);
ir_sub_end.getRowCount();
// 将主表中增加取得的行数并设置行属性为标题
ReportDefine rd = (ReportDefine) ReportUtils.read(report);
rd.insertRow(1, ir_sub_end.getRowCount());
for (int i = 1; i <= ir_sub_end.getRowCount(); i++) {
IRowCell rowcell = rd.getRowCell(i); // 根据行号获得行首格
rowcell.setRowType(IRowCell.TYPE_TITLE_HEADER);
}
2)将第一张表最后页数据写入第二张表之前插入的空行内。
IReport iReport = toReport(rd);
// 将数据写入主表空白单元格内
pb = new PageBuilder(iReport);
IReport ir_begin = pb.getPage(1);
for (int i = 1; i <= ir_begin.getRowCount(); i++) {
IRowCell rowcell = ir_begin.getRowCell(i);
rowcell.setRowType(IRowCell.TYPE_NORMAL);
}
for (int i = 1; i <= ir_sub_end.getRowCount(); i++) {
for (int j = 1; j <= ir_sub_end.getColCount(); j++) {
INormalCell iCell = ir_sub_end.getCell(i, (short) j);
ir_begin.setCell(i, (short) j, iCell);
}
}
3)这样第一张表的最后一页再加上第二张表的前面一段数据就合成了一整页的效果,一起写入到raq中。由于之前已经写入了一部分内容,所以循环写入的位置需要重新计算下。
// 将填好数据的主表第一页写入最终的ireport中
for (int i = 1; i <= ir_begin.getRowCount(); i++) {
for (int j = 1; j <= ir_begin.getColCount(); j++) {
INormalCell iCell = ir_begin.getCell(i, (short) j);
INormalCell inc = rdStatic.getCell(i
+ ireport_sub.getRowCount() – ir_sub_end.getRowCount(),
(short) j);
setICell(iCell, inc);
}
}
3,将第二张表剩下的部分写入到raq的最后面,同样是需要计算下循环写入的位置。
// 将主表中后几页写入最终的ireport中
int page_i = pb.getPage(2).getRowCount();
for (int page = 2; page <= allPage(iReport); page++) {
IReport ir = pb.getPage(page);
for (int i = 1; i <= ir.getRowCount(); i++) {
for (int j = 1; j <= ir.getColCount(); j++) {
INormalCell iCell = ir.getCell(i, (short) j);
int i1 = (ireport_sub.getRowCount()
– ir_sub_end.getRowCount() + ir_begin.getRowCount())
+ (page – 2) * page_i;
INormalCell inc = rdStatic.getCell(i + i1, (short) j);
setICell(iCell, inc);
}
}
}
4,由于rat和raq间有着一些微妙的区别,有些属性是无法复制过去的,例如合并单元格。所以采用了对报表本身增加标识,然后通过动态合并单元格的方法。
在报表中对合并单元格的数据前增加标识“#”
// 计算最终报表,去掉合并单元格标识
//进行有标识的单元格合并
public static void mergeReport(ReportDefine ireportStatic) throws Exception {
String dealFlag = “#”;// 报表中对数据进行了标识,为了是后面进行单元格合并时的判断标志
for (int i = ireportStatic.getRowCount(); i >= 1; i–) {// 遍历rat的行列
for (int j = ireportStatic.getColCount(); j >= 1; j–) {
INormalCell cell0 = ireportStatic.getCell(i, (short) j);// 逐行逐列取得单元格
byte extMode = cell0.getExtendMode();// 获取扩展区域
if (j != 1 && cell0.getValue() != null) {
if (cell0.getValue().toString().indexOf(dealFlag) >= 0) {
INormalCell cell1 = ireportStatic.getCell(i,
(short) (j – 1));
int mAreaDStartCol, mAreaDEndCol, mAreaUStartCol, mAreaUEndCol;
mAreaDStartCol = cell0.isMerged() ? cell0
.getMergedArea().getBeginCol() : j;
mAreaDEndCol = cell0.isMerged() ? cell0.getMergedArea()
.getEndCol() : j;
mAreaUStartCol = cell1.isMerged() ? cell1
.getMergedArea().getBeginCol()
: mAreaDStartCol – 1;
mAreaUEndCol = cell1.isMerged() ? cell1.getMergedArea()
.getEndCol() : mAreaDEndCol – 1;
if (cell1.getValue() != null
&& cell1.getValue().equals(cell0.getValue())) {
cell1.setValue(cell0.getValue());
Area area = new Area(i, (short) mAreaUStartCol, i,
(short) mAreaDEndCol);
ReportUtils.mergeReport(ireportStatic, area);
}
if (cell0.getValue().equals(dealFlag)) {
Area area = new Area(i, (short) mAreaUStartCol, i,
(short) mAreaDEndCol);
ReportUtils.mergeReport(ireportStatic, area);
}
}
}
}
}
// 将没有用到的单元格中的标识#去掉
for (int i = 1; i <= ireportStatic.getRowCount(); i++) {
for (int j = 1; j <= ireportStatic.getColCount(); j++) {
INormalCell cell2 = ireportStatic.getCell(i, (short) j);
if (cell2.getValue() != null
&& cell2.getValue().toString().indexOf(dealFlag) >= 0) {
cell2.setValue(cell2.getValue().toString().replace(
dealFlag, “”));
}
}
}
5,最后在jsp页面中直接调用此方法获取到最终的reportDefine,通过defineBean的方式展现即可。
request.setAttribute(reportDefine, mr.apiToReport(
“D://pp_sub.raq”“D://pp.raq”, 8));
<report:html name=“report1″ srcType=“defineBean”
beanName=“<%=reportDefine%>“ />
页面效果如下:
附1:文中采用的方法是将rat的数据写入到raq中,在操作过程中发现了一些问题。
虽然reportDefine(raq)实现了IReport(rat)接口,它俩的方法也都是完全一样,可是在将rat的单元格写入到raq中,原本认为直接通过set、get方法将INormalCell赋值过去即可,却无法达到预期效果,后台报错,所以采取了逐个赋值INormalCell中的属性的方法,方法如下:
// 设置单元格属性,从报表中将单元格属性赋给最终的ireport,此处需要不同属性分别要设置,具体属性名参照润乾API文档
public void setICell(INormalCell iCell, INormalCell inc) {
if (iCell != null) {
inc.setValue(iCell.getValue());
inc.setFontName(iCell.getFontName()); // 设置字体
inc.setFontSize(iCell.getFontSize()); // 设置字号
inc.setForeColor(iCell.getForeColor()); // 设置前景色
inc.setBackColor(iCell.getBackColor()); // 设置背景色
inc.setBBColor(iCell.getBBColor());
inc.setBBStyle(iCell.getBBStyle());
inc.setBBWidth(iCell.getBBWidth());
inc.setLBColor(iCell.getLBColor());
inc.setLBStyle(iCell.getLBStyle());
inc.setLBWidth(iCell.getLBWidth());
inc.setRBColor(iCell.getRBColor());
inc.setRBStyle(iCell.getRBStyle());
inc.setRBWidth(iCell.getRBWidth());
inc.setTBColor(iCell.getTBColor());
inc.setTBStyle(iCell.getTBStyle());
inc.setTBWidth(iCell.getTBWidth());
inc.setHAlign(iCell.getHAlign());
inc.setFontSize(iCell.getFontSize());
inc.setBold(iCell.isBold());
inc.setItalic(iCell.isItalic());
}
}
附件中提供了完整的测试用例,请用户下载试用。
总结
由上述可以看出,借助强大的API,看似无从下手的问题,也被巧妙的解决,帮助用户解决某些特殊的需求。