承接上篇
第三章主要说了交叉表的简单使用以及列出了在交叉表制作过程中可能会遇到的实际场景,还有就是实际场景中出现的一些坑,为了方便演示所以案例数据比较简单,希望小伙伴们认真阅读,细节很多,各位伙伴多注意一下!报表之路本就繁琐,不能一蹴而就,万丈高楼平地起,耐心制作,jasperreport还你一个完美的报表!!!
本章准备开始写分组交叉表,案例的话我准备还是以清华为例,毕竟是梦想,遥不可及但不可或缺。
还是提醒一下小伙伴们,简单的部分我选择直接讲解不会花太多时间,因为百度都有,基本看到教程的你已经是属性jasperreport的一些基本元素再来学习,否则会有点吃力!切记!!!切记!!!切记!!!
本章案例描述
分组大家并不陌生,意思是一个大学分成几个院系(这是分组的关键词),一个院系中存在着很多学员(这是交叉表),后面我都会以组相称。
案例:清华之下存在三个组(系),No1:计算机科学与技术(csGroup);No2:软件工程(seGroup);No3:物联网(webGroup);每个组(系)的人数都是150,具体分成三个班级:一班(40人)、二班(50人)、三班(60人);这里我们沿用上一章的数据,因为方便我们数据呈现!!!
数据准备
1. 分组的相关类
分组的交叉表需要一个分组唯一字段,比如你是按照什么来分组的,我们这里是按照系名称来分组的,所以我们重新写个属性类,里面包含分组名称和组下的数据list:
1.1 QHGroupStudentInfo(清华分组学生数据详情)
public class QHGroupStudentInfo {
/**
* 分组的名称
*/
private String groupName;
/**
* 组下的学生数据
*/
private List<QHComputerStudent> studentList;
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
public List<QHComputerStudent> getStudentList() {
return studentList;
}
public void setStudentList(List<QHComputerStudent> studentList) {
this.studentList = studentList;
}
}
这个QHGroupStudentInfo里面包含了分组名称和该分组之下的数据,到时候这个QHGroupStudentInfo会以一个list的形式注入到模板中,然后以groupName为分组标志进行分组,组内会自动显示当前组下的数据。
1.2 QHComputerStudent(学生数据类)— 上一章已经提到过
public class QHComputerStudent {
/**
* 性别
*/
String gender;
/**
* 班级
*/
String grade;
/**
* 人数
*/
Integer personNumber;
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getGrade() {
return grade;
}
public void setGrade(String grade) {
this.grade = grade;
}
public Integer getPersonNumber() {
return personNumber;
}
public void setPersonNumber(Integer personNumber) {
this.personNumber = personNumber;
}
}
这个类不多说了,存放实际数据!
1.3 StudentInfo — 核心数据类(用于注入模板)
public class StudentInfo {
private List<QHComputerStudent> qhComputerStudentList;
// 新增的数据
private List<QHGroupStudentInfo> groupStudentInfoList;
public List<QHComputerStudent> getQhComputerStudentList() {
return qhComputerStudentList;
}
public void setQhComputerStudentList(List<QHComputerStudent> qhComputerStudentList) {
this.qhComputerStudentList = qhComputerStudentList;
}
public List<QHGroupStudentInfo> getGroupStudentInfoList() {
return groupStudentInfoList;
}
public void setGroupStudentInfoList(List<QHGroupStudentInfo> groupStudentInfoList) {
this.groupStudentInfoList = groupStudentInfoList;
}
}
核心数据类新增了分组交叉表的数据List,之后这个数据也会作为数据源注入到模板中显示,list中会存在多条组名不同的数据,分别为上面提到的计科(csGroup),软工(seGroup)以及物联网(webGroup)
1.4 StudentFactory — 构造分组数据
我们需要构造这三个组的数据,因为上一章已经构造过学生数据了,我们可以直接拿过来使用了,不然数据构造很多比较繁琐不太好演示,类如下:
public class StudentFactory {
/**
* 获取清华分组数据
*/
public static List<QHGroupStudentInfo> getQHGroupStudentInfo(){
List<QHGroupStudentInfo> qhGroupStudentInfoList = new ArrayList<>();
// 计科组
QHGroupStudentInfo ceGroup = new QHGroupStudentInfo();
ceGroup.setGroupName("计算机科学与技术");
ceGroup.setStudentList(getQHComputerStudent());
// 软工组
QHGroupStudentInfo seGroup = new QHGroupStudentInfo();
seGroup.setGroupName("软件工程");
seGroup.setStudentList(getQHComputerStudent());
// 物联网组
QHGroupStudentInfo webGroup = new QHGroupStudentInfo();
webGroup.setGroupName("物联网");
webGroup.setStudentList(getQHComputerStudent());
qhGroupStudentInfoList.add(ceGroup);
qhGroupStudentInfoList.add(seGroup);
qhGroupStudentInfoList.add(webGroup);
return qhGroupStudentInfoList;
}
/**
* 获取测试学生数据(这里已经改回之前的列头相同的数据)
*/
public static List<QHComputerStudent> getQHComputerStudent(){
List<QHComputerStudent> infoList = new ArrayList<>();
QHComputerStudent student1 = new QHComputerStudent();
student1.setGender("男");
student1.setGrade("一班");
student1.setPersonNumber(20);
QHComputerStudent student2 = new QHComputerStudent();
student2.setGender("男");
student2.setGrade("二班");
student2.setPersonNumber(20);
QHComputerStudent student3 = new QHComputerStudent();
student3.setGender("男");
student3.setGrade("三班");
student3.setPersonNumber(30);
QHComputerStudent student4 = new QHComputerStudent();
student4.setGender("女");
student4.setGrade("一班");
student4.setPersonNumber(20);
QHComputerStudent student5 = new QHComputerStudent();
student5.setGender("女");
student5.setGrade("二班");
student5.setPersonNumber(30);
QHComputerStudent student6 = new QHComputerStudent();
student6.setGender("女");
student6.setGrade("三班");
student6.setPersonNumber(30);
infoList.add(student1);
infoList.add(student2);
infoList.add(student3);
infoList.add(student4);
infoList.add(student5);
infoList.add(student6);
return infoList;
}
/**
* 主数据源注入属性
*/
public static List<StudentInfo> getStudentInfo(){
List<StudentInfo> studentInfoList = new ArrayList<>();
StudentInfo studentInfo = new StudentInfo();
// 测试简易交叉表数据
studentInfo.setQhComputerStudentList(getQHComputerStudent());
// 测试分组交叉表
studentInfo.setGroupStudentInfoList(getQHGroupStudentInfo());
studentInfoList.add(studentInfo);
return studentInfoList;
}
}
打了断点的数据展示,有三组,组名和数据!!!
1.5 导出代码不变
@GetMapping(value = "/reportStudent")
public void reportStudent(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
OutputStream os = response.getOutputStream();
String reportName ="student.jasper";
response.setContentType("application/pdf; charset=utf-8");
response.setDateHeader("Expires", 0);
JasperReport jasperReport = (JasperReport)JRLoader.loadObject(resourceLoader.getResource("classpath:/jasper/student/"+reportName).getInputStream());
String subPath = resourceLoader.getResource("classpath:/jasper/student/").getURL().toString();
JRBeanCollectionDataSource studentFactory = new JRBeanCollectionDataSource(StudentFactory.getStudentInfo());
HashMap<String, Object> hashMap = new HashMap<>(16);
hashMap.put("subPath", subPath);
hashMap.put("studentFactory", studentFactory);
JasperPrint jasperPrint = JasperFillManager.fillReport(jasperReport, hashMap, new JREmptyDataSource());
// 页面预览pdf
JasperExportManager.exportReportToPdfStream(jasperPrint,os);
// 导出pdf到具体路径
JasperExportManager.exportReportToPdfFile(jasperPrint,"D:/report/测试.pdf");
// 导出word到具体路径
List<JasperPrint> test1 = new ArrayList<>();
test1.add(jasperPrint);
JRDocxExporter exporter = new JRDocxExporter();
exporter.setExporterInput(SimpleExporterInput.getInstance(test1));
exporter.setExporterOutput(new SimpleOutputStreamExporterOutput(new File("D:/report/测试.doc")));
exporter.exportReport();
System.out.println("导出word文件成功");
} catch (Exception e) {
e.printStackTrace();
}
}
所有的属性全部封装到了studentFactory中,无需修改代码,到这差不多数据层已经结束,可以开始制作模板了!!
2. 分组模板制作
2.1 模板分析
分组数据略微和正常的单个交叉表的层级不太相同,我们上一章提到过数据的传输过程是通过studentFactory中的学生变量传递到交叉表中,交叉表拿到变量中对应的属性进行赋值;但是分组交叉表的数据我们可以想象一下,也是通过studentFactory中的分组list变量 groupStudentInfoList 传递到子模板中,但是 groupStudentInfoList 中不像之前的学生数据类直接对应的属性变量,而是由分组名称 + 分组数据对象list组成,所以似乎还需要进一步往对象内部挖掘;这里比前面的学生数据类多了一层。
解决方法:我们需要在子模板中进一步创建子模板,向子子模板(没打错字)中传递分组变量groupStudentInfoList ,在子子模板中接收groupStudentInfoList 中的分组名称 + 分组数据对象list;OK — 思路清晰!
2.2 新建demo2的子模板(子报表)
第一步: 打开demo2.jrxml,新建一个detail(选中detail1直接右击add),拖动差不多大的位置。最后别忘记保存!!!
第二步:拖动子模板(subreport)到detail2中,选择新建一个新的报告(BlankA4即可),选择你的项目目录(别选错位置了),给它个名字(我的叫demo2_sub1.jrxml),然后点击finish即可。最后别忘记保存!!!
第三步:双击demo2_sub1模板,删除不必要的部分,只保留demo2_sub1模板的detail部分,因为我们只要展示数据,最后别忘记保存!!!
到这差不多demo2的子模板新建成功!
2.3 完善demo2_sub1模板的配置
第一步:老生常谈的内容了,越界设置,点击子模板打开属性设置如下
第二步:子模板数据源注入,打开demo2.jrxml,首先在field中新建一个属性:groupStudentInfoList — 类型为java.util.List,用于传递给子模板的属性
第三步:demo2_sub1子子模板添加数据源 — 通过它的父模板demo2传递,点击子子模板demo2_sub1,选择subreport,注入数据源new net.sf.jasperreports.engine.data.JRBeanCollectionDataSource($F{groupStudentInfoList}),这里也是一样,需要一层数据源的包装,这样就可以把groupStudentInfoList 传到子子模板里面去了
第四步:demo2_sub1子子模板中添加field,用于展示分组交叉表的数据,groupName(分组名)和studentList(分组下的数据),双击打开demo2_sub1,在field中新建两个变量groupName和studentList,这两个变量是由第三步的数据源传递过来的,注意类型和属性类中一致!!!
第五步:新建子子交叉表的数据集,用于展示分组内的学生数据,默认创建空数据集;然后在创建好的数据集中填写需要展示的交叉表的属性field(gender、grade和personNumber),注意field和属性类中的类型一致,这里的设置和上一章一样!!!可以参考!!!这里不多做介绍了
** 第六步:新建子子模板的交叉表,选择创建好的数据集dataset1(和上一章一样的设置),默认横向列头班级,纵向列头性别,主体内容是人数,不需要总计!!!(注意参考上一章的设置,不多说了)。**
** 完善设置交叉表的设置,双击交叉表,加上左上角列头;所有内容字体设置为微软雅黑,10号字体,居中,加上边框;边框可以适当拉一拉大小;设置列头默认不排序,按照list数据的顺序展示;设置字体太大越界需要自动拉伸高度,默认数据为null为空白;**
字体列头居中设置:
越界设置:
列头默认不排序设置:
第七步:设置交叉表的数据源:new net.sf.jasperreports.engine.data.JRBeanCollectionDataSource($F{studentList}),点击finish完成
** 设置完成别忘记保存了!!!!保存保存保存保存**
2.4 新建分组group(单独拿出来写)
- 点击demo2_sub1子子模板,新建create group --> 选择根据组名groupName分组 --> 点击下一步 --> 点击finish,可以看到detail组件的上面和下面出现了分组区域,里面可以填写你分组的字段!!!
- 将分组字段groupName拖动到你的分组区域头中,删除Group footer,暂时要也没啥用,如图:
并且设置组名字体,微软雅黑,字体大小适中就行,位置稍微调整一下,我们只做演示,没太大要求!
到这里差不多子子模板设置的内容差不多了!!!记得保存
2.5 设置子子模板demo2_sub1的路径
第一步:之前说过,子模板的子模板也需要加载,所以需要识别父模板的路径,还是用统一的subPath,我们需要将subPath传递到子模板demo2中,跟之前一样:打开主模板student.jrxml --> 选择demo2 --> 给demo2添加一个subPath路径参数(和上一章背景图片路径一样的操作)
第二步:demo2中新建一个用于接收subPath的参数;
第三步:设置子子模板demo2_sub1的路径,点击子子模板 --> 找到subreport —>修改路径表达式$P{subPath}+“demo2_sub1.jasper”,点击保存,这样子子模板路径也设置好了
3. 重新编译模板进行测试
重新编译一下模板放入项目中测试,项目中目录也要重新编译一下(rebuild)(编译也不多说了,见前一章节~!)
启动项目,访问测试地址 —> http://localhost:8080/hkfire/reportStudent,这里我们之前已经提前准备好属性类:
第一组:
第二组:
第三组:
可以看出我们分组的数据已经展示出来了,pdf和word也成功出来了!!!但是有个问题,样式很丑!同样的交叉表的样式,这样看着很不舒服,为啥我的分组名称和分组的数据离的那么远,就不能差不多靠在一起吗?对不对!!!,而且交叉表也没有居中显示,显得很奇怪,下面我们来尝试解决以上的所有问题!!!
4. 解决导出样式存在的问题
4.1 解决交叉表数据居中显示的问题
其实实际情况之下,我们的交叉表可能居左、居右或者居中都有可能,这个跟交叉表的位置有关系;可以看到我现在的交叉表在子子模板demo2_sub1的靠近中间的位置,而在子模板中,demo2_sub1子子模板的位置处于中间,所以两者叠加导致交叉表数据靠右显示(应该不难理解);所以我们可以将子子模板的交叉表贴边处理,不置于中间位置;
拖动子子模板中的交叉表的位置和边缘重合(边缘的蓝线),如图:
重新编译模板,启动项目测试,可以看到现在交叉表的显示差不多就在中间。这里需要根据实际情况调整交叉表的位置;
4.2 关于分组名和分组数据相距过远的问题
我们分组名和分组数据相距很远,造成这样的原因是模板中(我图中画圈的部分)留有很大一部分空白,空白部分模板不会自动剔除,模板会默认认为你是需要这些距离的,所以实际情况就是导出的样式出现了大片空白,所以为了避免不必要的空白我们直接将交叉表的拖拽与边线重合(上下都重合);
如图,现在是detail和交叉表之间没有任何多余的空白处(与蓝线重合,蓝线代表着边缘),保存一下,再次编译测试一下效果:
效果如图:
可以发现,现在分组和分组数据之间没有多余的空格了,只要我们不主动添加空白,它就不会给你补充空白!!!
4.3 关于交叉表到底需要多大的占位空间问题
4.2 其实已经去掉了很大的空白,但是还有个问题,假如没有数据我们出现什么效果;先把所有学生数据注释掉,此时学生数据list为空,启动项目测试:
重启启动访问,测试如图:
我们发现,分组名之间会有一部分空白部分,这部分占位是由于模板中交叉表本身的占位大小,虽然交叉表没有数据,但是它占据了一部分空间;
但是我们知道交叉表是动态的,后面可能会有几十条几百条数据,这样的大小就够了吗?答案肯定是不够的,动态和静态之间永远不可能统一,那么交叉表要多大的空间合适?
或者说交叉表动态的,它需要空间吗?答案是它真的不需要,我们可以直接把交叉表的高度设置成1,长度不变(长度决定了一行可以显示多少个数据,如果你想多显示一点,可以设置的长一些)
交叉表高度直接变成1,相当于交叉表变成了一条直线,完全不占用空间,如图:
这样子感觉就跟交叉表不见了一样,其实还是在的,然后相应的也缩小detail本身和交叉表重合
重新编译访问,测试如图:
到这算是把交叉表占位的空白部分以及组件之间的空白解决了!!!
4.4 关于每页页脚的空白问题
打开数据注释,恢复测试数据使用,打印预览,如图中圈出的页脚部分留了部分空白,这部分的空白是页面本身的上下间距造成的,默认模板上下间距是20,如果你嵌套了好几个模板可能导致模板间距叠加会变的很大,所以我们这里取消子子模板的间距,让其上下间距都为0,这样会减小页眉页脚的间距;
设置如图:点击子子模板demo2_sub1的灰色部分,双击page format设置top和bottom都为0,表示上下间距都为0,点击ok保存!!!
重新编译模板,测试一下:可以看到页脚的边距小很多了,实际情况根据不同的需要,自行设置边距即可,还有左右间距,包括列头的位置都可以设置到合理的位置(我这里都是居中显示的)
4.5 关于交叉表数据很多的情况之下换行之后带来的一系列问题
现在的数据比较少,我们多写点测试数据,简单写个for循环多写一些测试数据,这里我给男生女生分别新增了27条数据,重新启动项目预览:
如图所示,这里数据太多我就不一一截图,截部分,可以看到我们的数据是自动换行的,它会默认往下换行,列头也会保留,每一行显示的数据条数也是由你的交叉表长度决定的,可以自己设置;同时换行的数据会自动往下拉伸,第二组的数据等第一组数据显示完成之后才会打印出来;
坑注意:因为交叉表的自动拉伸会挤压不同组件之间的内容,但是如果你的交叉表之下是有内容的,会重合!例如,我在交叉表之下写个测试数据,如图:将测试数据和交叉表放在了同一个组件detail中,此时重新编译测试:
现象是交叉表覆盖了我们测试内容,所以记住如果你的交叉表之下有内容,请放到新的detail中,保证交叉表所在的detail中它的下面没有任何内容,因为动态交叉表不确定它的高度,没准就覆盖了你的内容!!!新建detail2,将测试内容放到新的detail中,再次编译测试:
我们可以看到测试内容,放到了数据的末尾,所以这个细节小伙伴记得注意:保持交叉表所在组件的干净,我这里的建议是每一个内容(无论是标题还是交叉表、图表等等)都放到一个单独detail中,保证所有内容的整洁性,可以避免很多问题,很重要哦!!!!!
5. 小彩蛋:页码的配置
因为我们现在已经预览很多页了,但是缺少每一页的页码值,这里的封面、目录和尾页是不需要页码值的,只在正文主体内容中才会存在页码值。在模板中,jasperreport自带了提供页码值的变量
V
M
A
S
T
E
R
C
U
R
R
E
N
T
P
A
G
E
,
一
般
页
码
值
都
是
放
在
页
脚
内
,
所
以
我
们
新
建
一
个
组
件
p
a
g
e
f
o
o
t
e
r
,
然
后
拖
动
变
量
V{MASTER_CURRENT_PAGE},一般页码值都是放在页脚内,所以我们新建一个组件page footer,然后拖动变量
VMASTERCURRENTPAGE,一般页码值都是放在页脚内,所以我们新建一个组件pagefooter,然后拖动变量V{MASTER_CURRENT_PAGE}到page footer中,设置字体微软雅黑10号字体,居中,设置变量加载时间为master;
到这先重新编译demo2.jrxml(我们这里添加的页码值是在demo2中进行的,所以重新编译demo2)测试一下,发现页码是从3开始计算的,这是因为默认$V{MASTER_CURRENT_PAGE}把封面和目录页也算进去了,所以实际情况页码值需要减去2,重新编译测试
重新编译测试demo2测试:成功!!!!
6. 总结summary
差不多本章把交叉表的所有内容以及出现的问题都列举了出来,还有一些实际碰到的样式问题需要小伙伴自己去慢慢调整模板的位置,最好达到想要的效果,我写的东西也是边写边思考,不想遗漏任何可能出现的问题,如果大家有疑问欢迎评论区留言我可以在补充说明!
写一篇文章不易,交叉表算是特别复杂的存在了,后面就准备开始写饼图和柱形图了,这两个都依赖于交叉表的数据实现,最大的交叉表难关已经过去,后面自然顺风顺水,当然图表也存在很多的坑,到时候会给小伙伴们一一列举,文中案例说明仅做参考,问题也是我实际开发中遇到的问题,运气很好都解决了,希望大家看完能有所收获,码子不易,都是一个一个字打出来的,细节的部分很多,希望学习报表的你耐得住寂寞,细心一些。