文章目录
- Java操作Word文档
- 引言
- 1、技术选型
- 结论
- 2、基础文本填充
- 2.1 引入依赖
- 2.1.1. poi
- 2.1.2. poi-ooxml
- 2.1.3. poi-ooxml-schemas
- 总结
- 2.2 业务思路
- 2.3 业务层 OfficeService
- 2.4 通用工具类 OfficeUtils
- 2.5 控制层 OfficeController
- 3、表格
- 3.1 准备模板
- 3.2 业务层 OfficeService
- 业务流程:
- 名词解释:
- 3.2 Word工具类OfficeUtils
- 3.3 导出效果
- 3.4 动态表格
- 4、自定义图表
- 4.1 思路
- 4.1.1 概述
- 4.1.2 支持的图表类型
- 4.1.3 特性
- 4.2 准备模板
- 4.3 导入依赖
- 4.4 图表生成工具类 ChartWithChineseExample
- 步骤 1: 准备字体文件
- 步骤 2: 注册字体到`FontFactory`
- 步骤 3: 设置图表具体位置的字体
- 柱状图:
- 饼图:
- 折线图:
- 完整代码:
- 4.5 业务层 OfficeServicel
- 4.6 导出效果
Java操作Word文档
在日常开发中,经常遇到需要自动化处理Word文档的需求,比如批量生成报告、填写模板内容等。Java作为一种广泛应用的编程语言,提供了多种方式来操作Word文档。本文将详细介绍如何使用Java处理Word文档,并通过实战示例带你入门。
引言
Word文档本质上是一个遵循Open XML标准的ZIP压缩包,包含了一系列XML文件和其他资源(如图片)。因此,操作Word文档的关键在于解析和修改这些XML文件。Java开发者可以选择多种库来实现这一目标,包括但不限于Apache POI、docx4j、iText以及Spire.Doc for Java等。下面,我们将逐一探讨这些工具,并给出具体示例。
1、技术选型
工具 | 优点 | 缺点 | 简介 |
---|---|---|---|
Apache POI | 开源免费、社区活跃、功能完善。 | 对于复杂的Word样式处理支持有限。 | Apache POI是Apache软件基金会的一个项目,提供了一套用于读写Microsoft Office格式档案的Java API,包括Word、Excel等。对于Word文档,主要使用的是POI的HWPF(处理.doc 文件)和XWPF(处理.docx 文件)模块。 |
docx4j | 功能强大,支持复杂Word操作,如样式、表格、图片插入等。 | 学习曲线相对较陡峭,文档相对不够丰富。 | docx4j是一个开源库,专为操作.docx (Open XML)格式的Word文档设计,提供了丰富的API来处理XML内容。 |
iText | 如果你的项目已经使用了iText处理PDF,那么使用它来生成简单的Word文档会比较方便。 | Word处理功能不如Apache POI或docx4j全面。 | 虽然iText主要用于PDF处理,但它也支持生成Word(.docx )文档,尽管功能相比专门的Word处理库较为有限。 |
Spire.Doc for Java | 功能强大,支持度高,文档和客户服务较完善。 | 需要付费使用,免费版有功能限制。 | Spire.Doc for Java是一个商业库,专注于Word文档的处理,提供了丰富的功能,包括创建、读取、编辑、转换Word文档等。 |
结论
选择合适的库取决于你的具体需求和项目条件。如果你需要处理大量复杂的Word文档且预算允许,Spire.Doc可能是最佳选择。而对于开源解决方案,Apache POI适合初学者和基本需求,而docx4j则更适合处理高级场景。iText虽能生成Word,但更擅长PDF处理。无论哪种选择,掌握基本的API使用和理解Word的内部结构都是关键。希望本文能帮助你在Java项目中有效操作Word文档。
本篇文章主要以Apache POI来实现具体业务。
2、基础文本填充
2.1 引入依赖
2.1.1. poi
- 基础库:
poi
是最基础的Apache POI库,包含了处理老版本Office文件格式(如.xls
、.doc
)的类和方法。它不直接支持.xlsx
或.docx
等基于XML的文件格式。这个库主要用于处理二进制文件格式,并且是其他更特定库的基础。
2.1.2. poi-ooxml
- XML支持:
poi-ooxml
是针对基于XML的Office Open XML格式(.xlsx
、.docx
、.pptx
等)的扩展库。它依赖于poi
库,并添加了处理Open XML文件所需的所有额外类和方法。当你需要读写新格式的Office文件时,这个库是必不可少的。它包含了解析和生成Open XML文档所需的API。
2.1.3. poi-ooxml-schemas
- XML模式与验证:
poi-ooxml-schemas
包含了Office Open XML格式的完整XML模式定义。这些模式定义对于验证生成的Open XML文档是否符合官方规范非常重要,确保了文档的兼容性和正确性。这个依赖项不是直接用于编写代码操作POI的API,而是作为后台支持,帮助POI库正确解析和验证XML结构。
总结
- 如果你只处理老版本的Office文件(
.xls
,.doc
),可能只需要poi
库。 - 处理
.xlsx
,.docx
等XML格式的文件时,你需要同时引入poi
和poi-ooxml
,因为后者依赖前者,并且提供了处理这些新格式的功能。 poi-ooxml-schemas
虽然不是每次都需要,但对于确保生成的文档结构正确,特别是在复杂的文档处理场景下,是非常推荐加入的依赖,因为它提供了详细的XML模式验证能力。
在Maven或Gradle项目中,通常你会同时声明这三个依赖(如果处理XML格式文件的话),以确保所有必要的组件都已就绪。
<properties>
<poi-ooxml.version>4.1.2</poi-ooxml.version>
<poi.version>3.17</poi.version>
</properties>
<dependencies>
<!-- poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- poi-ooxml -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<!-- 读写Microsoft Office poi-ooxml-schemas -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-schemas</artifactId>
<version>${poi-ooxml.version}</version>
</dependency>
</dependencies>
2.2 业务思路
-
报告模板设计
使用Word文档作为模板,预先设计好报告的布局,包括封面页、基本信息页、测量数据表格、历史数据图表等部分。在需要填充数据的地方,设定占位符或者使用特定标记(例如
{{username}}
、{{weight}}
等)。 -
使用Apache POI生成Word文档
利用Apache POI库(特别是poi
和poi-ooxml
)来读取模板Word文件,并根据整理好的数据集替换模板中的占位符:
-
加载模板文档。
-
遍历文档,查找并替换所有的占位符。
-
利用
poi-ooxml
和图表生成库(如JFreeChart结合Apache POI导出图表)生成历史测量数据的柱状图,并嵌入Word文档中。 -
文件存储与接口设计
- 生成的Word文档可以临时保存在服务器的文件系统或云存储中。
- 设计一个RESTful API,接收生成报告的请求,处理逻辑后,返回文件的下载链接或Base64编码的文件内容给前端。
- 考虑到安全性,可以设置链接的有效期,过期自动删除临时文件。
将模板文件放置resourcs/templates文件夹下,
2.3 业务层 OfficeService
package com.example.demo.service.impl;
import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import com.example.demo.uitls.Office2PdfService;
import com.example.demo.uitls.OfficeUtils;
import com.example.demo.uitls.SpringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.FileInputStream;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
/**
* OfficeServiceImpl :
*
* @author zyw
* @create 2024-06-24 15:41
*/
@Service
public class OfficeServiceImpl implements OfficeService {
/**
* 个人健康报告模板
*/
public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";
@Resource
private ResourceLoader resourceLoader;
@Resource
private Office2PdfService office2PdfService;
@Override
public XWPFDocument getHealthReport(HealthReportQuery query) {
try {
FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
// 替换文本数据构建
OfficeUtils.paragraphTextFilling(xwpfDocument,OfficeUtils.objectToMap(query));
return xwpfDocument;
} catch (Exception e) {
return null;
}
}
@Override
public void getHealthReportWord(XWPFDocument document, HealthReportQuery query, HttpServletResponse response) {
OfficeUtils.processingWordResponses("健康问卷-" + query.getName(), OfficeUtils.writeDocumentToInputStream(document), response);
}
}
2.4 通用工具类 OfficeUtils
package com.example.demo.uitls;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.lang.reflect.Field;
import java.io.*;
import java.util.*;
/**
* OfficeUtils : Office工具类
*
* @author zyw
* @create 2024-06-24 16:35
*/
public class OfficeUtils {
/**
* 对象转Map
* @param obj
* @return
*/
public static Map<String, String> objectToMap(Object obj) {
Map<String, String> map = new HashMap<>();
Class<?> clazz = obj.getClass();
// 获取类中所有声明的字段(包括私有、受保护、默认、公共)
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true); // 设置字段可访问(如果是私有的)
try {
Object value = field.get(obj);
String key = "${" + field.getName() + "}"; // 构造key,以${name}形式
map.put(key, String.valueOf(value));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return map;
}
/**
* 段落文本填充
*
* @param document 文档
* @param insertTextMap 填充内容
*/
public static void paragraphTextFilling(XWPFDocument document, Map<String, String> insertTextMap) {
Set<String> set = insertTextMap.keySet();
Iterator<XWPFParagraph> itPara = document.getParagraphsIterator();
while (itPara.hasNext()) {
// 获取文档中当前的段落文字信息
XWPFParagraph paragraph = itPara.next();
List<XWPFRun> run = paragraph.getRuns();
// 遍历段落文字对象
for (int i = 0; i < run.size(); i++) {
// 获取段落对象
if (run.get(i) == null) { //段落为空跳过
continue;
}
String sectionItem = null;
try {
// 检查段落中是否包含文本框
sectionItem = run.get(i).getText(run.get(i).getTextPosition()); //段落内容
} catch (Exception e) {
}
if (sectionItem == null) {
continue;
}
// 遍历自定义表单关键字,替换Word文档中的内容
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
// 当前关键字
String key = iterator.next();
// 替换内容
sectionItem = sectionItem.replace(key, String.valueOf(insertTextMap.get(key)));
}
run.get(i).setText(sectionItem, 0);
}
}
}
/**
* 处理Word响应
*
* @param downloadName 下载文件名
* @param inputStream 文件输入流
* @param response 响应
*/
public static void processingWordResponses(String downloadName,
InputStream inputStream,
HttpServletResponse response) {
try {
// 设置响应的Content-Type
response.setContentType("application/octet-stream");
response.setCharacterEncoding("utf-8");
// 设置Content-Disposition头部,指示浏览器下载文件,文件名为document.docx
downloadName = new String(downloadName.getBytes("UTF-8"), "ISO-8859-1");
response.setHeader("Content-Disposition", "attachment;filename=" + downloadName + ".docx");
// 获取响应的输出流
OutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[4096];
int bytesRead = -1;
// 将InputStream中的内容写入到OutputStream中
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
// 关闭流
inputStream.close();
outputStream.close();
}catch (Exception e){
}
}
/**
* word转InputStream
*
* @param document
* @return
*/
public static InputStream writeDocumentToInputStream(XWPFDocument document) {
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
document.write(byteArrayOutputStream);
byteArrayOutputStream.close();
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
2.5 控制层 OfficeController
package com.example.demo.controller;
import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;
/**
* OfficeController : Office办公文件控制器
*
* @author zyw
* @create 2024-06-24 15:40
*/
@Tag(name = "Office办公文件控制器")
@RestController
@RequestMapping("/office")
public class OfficeController {
@Resource
private OfficeService officeService;
@GetMapping("/getHealthReportWord")
@Operation(summary = "获取健康报告Word", description = "获取健康报告")
@Parameters({
@Parameter(name = "name", description = "姓名", required = true, in = ParameterIn.QUERY),
@Parameter(name = "gender", description = "性别", required = true, in = ParameterIn.QUERY),
@Parameter(name = "age", description = "年龄", required = true, in = ParameterIn.QUERY)
})
public void getHealthReportWord(HealthReportQuery query, HttpServletResponse response) {
officeService.getHealthReportWord(officeService.getHealthReport(query), query, response);
}
}
可以看到我们通过接口传输的三个参数均已渲染到了指定${}位置
3、表格
需求:我们需要根据输入的身高、体重、运动能力在文档中动态展示所处的健康状态
3.1 准备模板
- 在基本信息中我们需要将姓名、性别、头像等基本信息填入模板中已存在的表格的指定单元格里
- 在"3、您目前的体育运动水平"和"4、您的体重指数"两个标题下我们需要动态生成"体力活动水平标尺"和"体重指数标尺",同时展示出所处的健康状态
3.2 业务层 OfficeService
业务流程:
-
模板中已存在的表格,可以通过遍历文档所有表格获取:List tables = document.getTables();
-
模板中未存在的表格,我们通过找到模板中所需插入动态表格的上一个段落,再其下创建新的段落以及表格实现;
名词解释:
- XWPFDocument: 这个类代表一个Word文档。它是操作Word文件的入口点,允许你创建新的文档,读取现有的文档,添加或删除段落、表格、图片等元素。
- XWPFTable: 表示Word文档中的表格。你可以使用这个类来创建新的表格,获取或设置表格的属性(比如宽度、边框样式),以及操作表格中的行和单元格。
- XWPFParagraph: 代表文档中的一个段落。段落可以包含文本、图片、表格等多种元素。你可以使用这个类来创建新的段落,设置对齐方式、缩进、间距等格式,以及添加或删除段落中的文本或其它内容。
- XWPFTableCell: 单元格类,表示表格中的一个单元格。你可以通过这个类来设置单元格的内容(包括文本和嵌入的对象)、样式(如背景色、边框)以及合并或拆分单元格等。
- XWPFRun: 运行对象,是段落中最基本的文本处理单位。一个段落可以由一个或多个run组成,每个run可以有不同的字体样式、颜色、大小等。当你需要在同一个段落中应用不同的格式时,就会用到多个run。例如,改变文本颜色、加粗或斜体等操作都是通过对特定的run进行设置来实现的。
综上所述,这些类共同构成了操作Word文档的框架,让你能够在Java程序中灵活地创建和修改复杂的Word文档结构。
package com.example.demo.service.impl;
import com.example.demo.dto.HealthReportQuery;
import com.example.demo.service.OfficeService;
import com.example.demo.uitls.Office2PdfService;
import com.example.demo.uitls.OfficeUtils;
import com.example.demo.uitls.SpringUtils;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.*;
import org.apache.xmlbeans.XmlCursor;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Service;
import java.io.*;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.List;
import java.util.Objects;
/**
* OfficeServiceImpl :
*
* @author zyw
* @create 2024-06-24 15:41
*/
@Service
@Slf4j
public class OfficeServiceImpl implements OfficeService {
/**
* 个人健康报告模板
*/
public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";
private static final String HEADER_1_3 = "3、您目前的体育运动水平";
private static final String HEADER_1_4 = "4、您的体重指数";
@Resource
private ResourceLoader resourceLoader;
@Resource
private Office2PdfService office2PdfService;
@Override
public XWPFDocument getHealthReport(HealthReportQuery query) {
try {
FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
// 替换文本数据构建
OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
// 在基本信息表格中填充数据
fillInTable(xwpfDocument, query);
// 插入体育运动水平表格
int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
handleTableOne(xwpfDocument, index3, query.getSportsLevel());
// 插入体重指数表格
int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
return xwpfDocument;
} catch (Exception e) {
return null;
}
}
/**
* 填充基本信息表格
*
* @param document
* @param query
*/
public void fillInTable(XWPFDocument document, HealthReportQuery query) throws IOException, InvalidFormatException {
// 获取表格对象集合
List<XWPFTable> tables = document.getTables();
// 获取模板中第一个表格
XWPFTable xwpfTable = tables.get(0);
xwpfTable.getRow(0).getCell(1).setText(query.getName());
xwpfTable.getRow(0).getCell(3).setText(query.getGender());
xwpfTable.getRow(1).getCell(1).setText(query.getAge() + "岁");
xwpfTable.getRow(1).getCell(3).setText(query.getNativePlace());
xwpfTable.getRow(2).getCell(1).setText(query.getHeight() + "cm");
xwpfTable.getRow(2).getCell(3).setText(query.getWeight() + "kg");
xwpfTable.getRow(3).getCell(1).setText(String.valueOf(query.getPhone()));
xwpfTable.getRow(4).getCell(1).setText(query.getAddress());
// 在第一行第五列插入图片
XWPFTableCell cell04 = xwpfTable.getRow(0).getCell(4);
XWPFParagraph xwpfParagraph = cell04.getParagraphs().get(0);
// 通过URL获取图片数据
InputStream inputStream = new URL("http://127.0.0.1:1030/zyw/static/2024/06/25/lbxx_20240625141543A001.png").openStream();
XWPFRun run = xwpfParagraph.createRun();
run.addPicture(inputStream,
Document.PICTURE_TYPE_PNG, "头像",
Units.toEMU(150), Units.toEMU(150));
// 设置垂直居中
cell04.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
for (XWPFParagraph para : cell04.getParagraphs()) {
//居中
para.setAlignment(ParagraphAlignment.CENTER);
}
inputStream.close();
}
/**
* 表格1 (体育运动水平)
*
* @param document 文档
* @param index 索引
* @param sportsLevel 个人运动水平
*/
public void handleTableOne(XWPFDocument document, Integer index, String sportsLevel) {
// 获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
// 在目标段落后添加一个新的段落
XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph.setWordWrap(true); // 设置自动换行
// 创建表格
XmlCursor cursor = paragraph.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table = document.insertNewTbl(cursor);
// 去除表格边框设置表格宽度
OfficeUtils.setTableWidthToRemoveBorder(table, 7920);
// 设置表格内容
XWPFTableRow row0 = OfficeUtils.createRow(table, 0);
OfficeUtils.setRowHeight(row0, 2);
XWPFTableCell cell01 = OfficeUtils.createCell(row0, 0);
XWPFRun run1 = cell01.getParagraphs().get(0).createRun();
// 设置字体为宋体
run1.setFontFamily("宋体");
// 设置字号为四号(12磅)
run1.setFontSize(12);
run1.setText("您目前的体力活动:");
OfficeUtils.setTheLandscapeHeader(cell01, 0.25);
for (int i = 1; i <= 3; i++) {
OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row0, i), 0.13);
}
if (StringUtils.isNotBlank(sportsLevel)) {
XWPFTableCell cell;
switch (sportsLevel) {
case "体力活动不足":
cell = row0.getCell(1);
cell.setText("不足");
//大红色
cell.setColor("FF0000");
break;
case "体力活动中等":
cell = row0.getCell(2);
cell.setText("中等");
//天蓝色
cell.setColor("4E95D9");
break;
case "体力活动充分":
cell = row0.getCell(3);
cell.setText("充分");
//绿色
cell.setColor("00FF00");
break;
default:
}
} else {
XWPFTableCell cell = row0.getCell(1);
cell.setText("暂无分析数据");
}
// 在目标段落后添加第二个新的段落
XWPFParagraph paragraph2 = document.insertNewParagraph(paragraphs.get(index + 2).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph2.setWordWrap(true); // 设置自动换行
// 创建表格
XmlCursor cursor2 = paragraph2.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table2 = document.insertNewTbl(cursor2);
// 去除表格边框设置表格宽度
OfficeUtils.setTableWidthToRemoveBorder(table2, 7920);
XWPFTableRow row1 = OfficeUtils.createRow(table2, 0);
OfficeUtils.setRowHeight(row1, 2);
XWPFTableCell cell11 = OfficeUtils.createCell(row1, 0);
XWPFRun run2 = cell11.getParagraphs().get(0).createRun();
// 设置字体为宋体
run2.setFontFamily("宋体");
// 设置字号为四号(12磅)
run2.setFontSize(12);
run2.setText("体力活动水平标尺:");
OfficeUtils.setTheLandscapeHeader(cell11, 0.25);
XWPFTableCell cell12 = OfficeUtils.createCell(row1, 1);
cell12.setText("不足");
OfficeUtils.setsTheCellWidth(cell12, 0.13);
//大红色
cell12.setColor("FF0000");
XWPFTableCell cell13 = OfficeUtils.createCell(row1, 2);
cell13.setText("中等");
OfficeUtils.setsTheCellWidth(cell13, 0.13);
//天蓝色
cell13.setColor("4E95D9");
XWPFTableCell cell14 = OfficeUtils.createCell(row1, 3);
cell14.setText("充分");
OfficeUtils.setsTheCellWidth(cell14, 0.13);
//绿色
cell14.setColor("00B050");
}
/**
* 表格2
*
* @param document 文档
* @param index 索引
* @param height 身高
* @param weight 体重
*/
public void handleTableTwo(XWPFDocument document, Integer index, Double height, Double weight) {
// 获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
// 在目标段落后添加一个新的段落
XWPFParagraph paragraph1 = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph1.setWordWrap(true); // 设置自动换行
// 创建表格
XmlCursor cursor1 = paragraph1.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table1 = document.insertNewTbl(cursor1);
// 去除表格边框设置表格宽度
OfficeUtils.setTableWidthToRemoveBorder(table1, 7920);
// 设置表格内容
XWPFTableRow row00 = OfficeUtils.createRow(table1, 0);
OfficeUtils.setRowHeight(row00, 2);
XWPFTableCell cell001 = OfficeUtils.createCell(row00, 0);
XWPFRun run01 = cell001.getParagraphs().get(0).createRun();
// 设置字体为宋体
run01.setFontFamily("宋体");
// 设置字号为四号(12磅)
run01.setFontSize(12);
run01.setText("您的体重:");
OfficeUtils.setTheLandscapeHeader(cell001, 0.15);
for (int i = 1; i <= 4; i++) {
OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row00, i), 0.13);
}
XWPFTableCell cell011 = row00.getCell(1);
if (Objects.nonNull(weight)) {
cell011.setText(weight + "kg");
} else {
cell011.setText("暂未获得");
}
// 在目标段落后添加第二个新的段落
XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 2).getCTP().newCursor().newCursor());
// 在目标段落后添加新的段落
// 设置段落的样式和属性,实现换行
paragraph.setWordWrap(true); // 设置自动换行
// 创建表格
XmlCursor cursor = paragraph.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table = document.insertNewTbl(cursor);
// 去除表格边框设置表格宽度
OfficeUtils.setTableWidthToRemoveBorder(table, 7920);
// 设置表格内容
XWPFTableRow row0 = OfficeUtils.createRow(table, 0);
OfficeUtils.setRowHeight(row0, 2);
XWPFTableCell cell01 = OfficeUtils.createCell(row0, 0);
XWPFRun run1 = cell01.getParagraphs().get(0).createRun();
// 设置字体为宋体
run1.setFontFamily("宋体");
// 设置字号为四号(12磅)
run1.setFontSize(12);
run1.setText("您的体重指数:");
OfficeUtils.setTheLandscapeHeader(cell01, 0.15);
for (int i = 1; i <= 4; i++) {
OfficeUtils.setsTheCellWidth(OfficeUtils.createCell(row0, i), 0.13);
}
Double bmi = calculateBmi(height, weight);
if (Objects.nonNull(bmi)) {
XWPFTableCell cell = null;
if (bmi <= 18.5) {
cell = row0.getCell(1);
//天蓝色
cell.setColor("4E95D9");
} else if (bmi <= 24) {
cell = row0.getCell(2);
//绿色
cell.setColor("00B050");
} else if (bmi <= 28) {
cell = row0.getCell(3);
//黄色
cell.setColor("FFC000");
} else {
cell = row0.getCell(4);
//大红色
cell.setColor("FF0000");
}
cell.setText(String.valueOf(bmi));
}
// 在目标段落后添加第三个新的段落
XWPFParagraph paragraph2 = document.insertNewParagraph(paragraphs.get(index + 3).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph2.setWordWrap(true); // 设置自动换行
// 创建表格
XmlCursor cursor2 = paragraph2.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table2 = document.insertNewTbl(cursor2);
// 去除表格边框设置表格宽度
OfficeUtils.setTableWidthToRemoveBorder(table2, 7920);
XWPFTableRow row1 = OfficeUtils.createRow(table2, 0);
OfficeUtils.setRowHeight(row1, 2);
XWPFTableCell cell11 = OfficeUtils.createCell(row1, 0);
XWPFRun run2 = cell11.getParagraphs().get(0).createRun();
// 设置字体为宋体
run2.setFontFamily("宋体");
// 设置字号为四号(12磅)
run2.setFontSize(12);
run2.setText("体重指数标尺:");
OfficeUtils.setTheLandscapeHeader(cell11, 0.15);
XWPFTableCell cell12 = OfficeUtils.createCell(row1, 1);
cell12.setText("0~18.5");
OfficeUtils.setsTheCellWidth(cell12, 0.13);
//天蓝色
cell12.setColor("4E95D9");
XWPFTableCell cell13 = OfficeUtils.createCell(row1, 2);
cell13.setText("18.6~24");
OfficeUtils.setsTheCellWidth(cell13, 0.13);
//绿色
cell13.setColor("00B050");
XWPFTableCell cell14 = OfficeUtils.createCell(row1, 3);
cell14.setText("24.1~28");
OfficeUtils.setsTheCellWidth(cell14, 0.13);
//黄色
cell14.setColor("FFC000");
XWPFTableCell cell15 = OfficeUtils.createCell(row1, 4);
cell15.setText("<28");
OfficeUtils.setsTheCellWidth(cell15, 0.13);
//大红色
cell15.setColor("FF0000");
}
/**
* 计算BMI
*
* @param height 身高
* @param weight 体重
* @return
*/
public Double calculateBmi(Double height, Double weight) {
height = height / 100;
if (height <= 0 || weight <= 0) {
log.error("身高和体重必须是正数!");
return null;
}
DecimalFormat df = new DecimalFormat("#.##");
return Double.parseDouble(df.format(weight / (height * height)));
}
@Override
public void getHealthReportWord(XWPFDocument document, HealthReportQuery query, HttpServletResponse response) {
OfficeUtils.processingWordResponses("健康报告-" + query.getName(), OfficeUtils.writeDocumentToInputStream(document), response);
}
}
图片这里我使用的是在本地文件服务上上传的一张图片,关于文件服务的搭建可以阅读下面这篇博客:
Java实现对象存储的4种方式(本地对象存储、MINIO、阿里云OSS、FastDFS)
3.2 Word工具类OfficeUtils
抽出公共部分代码,编写静态方法到工具类中,解耦合。
这里仅列出这部分功能涉及的静态方法,上诉功能已列出的方法这里不展示。
package com.example.demo.uitls;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import java.lang.reflect.Field;
import java.io.*;
import java.math.BigInteger;
import java.util.*;
/**
* OfficeUtils : Office工具类
*
* @author zyw
* @create 2024-06-24 16:35
*/
public class OfficeUtils {
/**
* 设置表格宽度去除边框
* @param table 表格
* @param width 宽度值
*/
public static void setTableWidthToRemoveBorder(XWPFTable table,Integer width) {
// 去除表格边框
CTTblPr tblPr2 = table.getCTTbl().getTblPr();
CTTblBorders borders2 = tblPr2.addNewTblBorders();
borders2.addNewBottom().setVal(STBorder.NONE);
borders2.addNewTop().setVal(STBorder.NONE);
borders2.addNewLeft().setVal(STBorder.NONE);
borders2.addNewRight().setVal(STBorder.NONE);
borders2.addNewInsideH().setVal(STBorder.NONE);
borders2.addNewInsideV().setVal(STBorder.NONE);
// 设置表格整体样式
tblPr2.addNewTblW().setW(BigInteger.valueOf(width)); // 设置表格宽度
}
/**
* 设置表格单元格宽度及文本居中
*
* @param cell 单元格
* @param width 宽度占比
*/
public static void setTheLandscapeHeader(XWPFTableCell cell, double width) {
setsTheCellWidth(cell, width);
// 获取单元格属性对象
CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
// 设置垂直对齐方式为居中
CTVerticalJc vJc = tcPr.isSetVAlign() ? tcPr.getVAlign() : tcPr.addNewVAlign();
vJc.setVal(STVerticalJc.CENTER);
}
/**
* 设置表格单元格宽度
*
* @param cell 单元格
* @param width 宽度占比
*/
public static void setsTheCellWidth(XWPFTableCell cell, double width) {
// 假设A4纸宽约为210mm,1mm=360EMU,则A4宽约为7920EMU
int emuFor30Percent = (int) (7920 * width);
CTTblWidth ctTblWidth = cell.getCTTc().addNewTcPr().addNewTcW();
// 设置宽度为2000EMU,你可以根据需要调整这个值
ctTblWidth.setW(BigInteger.valueOf(emuFor30Percent));
// 设置宽度类型为字符单位(也可以是其他单位,如百分比等)
ctTblWidth.setType(STTblWidth.PCT);
// 设置垂直居中
cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
for (XWPFParagraph para : cell.getParagraphs()) {
//居中
para.setAlignment(ParagraphAlignment.CENTER);
}
}
/**
* 设置表格行的高度
*
* @param row 行
* @param heightCm 高度占比
*/
public static void setRowHeight(XWPFTableRow row, double heightCm) {
int emuForHeight = (int) (360 * heightCm);
CTTrPr trPr = row.getCtRow().addNewTrPr();
CTHeight ht = trPr.addNewTrHeight();
ht.setVal(BigInteger.valueOf(emuForHeight));
}
/**
* 创建表格行
*
* @param table 表格
* @param index 行索引
* @return
*/
public static XWPFTableRow createRow(XWPFTable table, int index) {
return Objects.isNull(table.getRow(index)) ? table.createRow() : table.getRow(index);
}
/**
* 创建单元格
*
* @param row 行
* @param index 列索引
* @return
*/
public static XWPFTableCell createCell(XWPFTableRow row, int index) {
return Objects.isNull(row.getCell(index)) ? row.createCell() : row.getCell(index);
}
/**
* 获取文本在文档中的索引
*
* @param doc 文档
* @param text 文本标识
* @return
*/
public static int findParagraphIndexByText(XWPFDocument doc, String text) {
// 获取所有段落
List<XWPFParagraph> paragraphs = doc.getParagraphs();
// 查找目标段落
int targetParagraphIndex = -1;
for (int i = 0; i < paragraphs.size(); i++) {
if (paragraphs.get(i).getText().contains(text)) {
targetParagraphIndex = i;
break;
}
}
return targetParagraphIndex;
}
}
3.3 导出效果
Word效果:
PDF效果:
3.4 动态表格
/**
* 个人健康报告模板
*/
public static final String PERSONAL_HEALTH_REPORT_TEMPLATE = "classpath:templates/HealthReport.docx";
private static final Pattern pattern = Pattern.compile("(\\d+、[^\\d]+)");
private static final String HEADER_1_1 = "您的基本信息";
private static final String HEADER_1_3 = "您目前的体育运动水平";
private static final String HEADER_1_4 = "您的体重指数";
private static final String HEADER_2_1 = "营养成分摄入比例";
private static final String HEADER_2_2 = "心率血氧检查";
private static final String HEADER_2_3 = "睡眠质量趋势";
private static final String HEADER_3_1 = "专家建议";
@Override
public XWPFDocument getHealthReport(HealthReportQuery query) {
try {
FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
// 替换文本数据构建
OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
// 在基本信息表格中填充数据
fillInTable(xwpfDocument, query);
// 插入体育运动水平表格
int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
handleTableOne(xwpfDocument, index3, query.getSportsLevel());
// 插入体重指数表格
int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
// 插入历史体重
int index5 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_1);
insertChartOne(xwpfDocument, index5);
// 插入心率检查
int index6 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_2);
insertChartTwo(xwpfDocument, index6);
// 插入睡眠质量趋势
int index7 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_3);
insertChartThree(xwpfDocument, index7);
// 插入体格检查动态列表
int index8 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_3_1);
dynamicList(xwpfDocument, index8);
return xwpfDocument;
} catch (Exception e) {
log.info("获取健康报告失败", e);
return null;
}
}
/**
* 动态列表 专家建议
*
* @param document 文档
* @param index 索引
*/
public void dynamicList(XWPFDocument document, Integer index) {
// 数据源
Map<String, String> map1 = new HashMap<>();
map1.put("code", "⽢油三酯增⾼");
map1.put("content", "1、建议限酒,低脂、低胆固醇饮⻝,如少吃油腻及煎烤类⻝物,少吃动物内脏等,多⻝蔬菜⽔果。加强运动,促进脂质代谢。2、每三-六个⽉复查⾎脂和肝脏B超⼀次,复查前请低脂饮⻝三天。如⾎脂持续增⾼,请在医⽣指导下使⽤调脂药物。");
Map<String, String> map2 = new HashMap<>();
map2.put("code", "肌酐增⾼");
map2.put("content", "1、肌酐是临床常规肾功能试验之⼀。肌酐是肌酸的代谢产物,98%的肌酸存在于肌⾁,为肌⾁收缩时的能量来源,释放能量后变为肌酐,由肾脏排泄。2、肌酐增⾼⻅于肾脏损害,急、慢性肾功能不全及⼼功能不全等。3、建议到医院肾内科就诊进⼀步检查,明确诊断。");
Map<String, String> map3 = new HashMap<>();
map3.put("code", "屈光不正");
map3.put("content", "注意⽤眼卫⽣,定期眼科随访。");
List<Map<String, String>> list = List.of(map1, map2, map3);
for (int i = 0; i < list.size(); i++) {
XWPFParagraph xwpfParagraph = OfficeUtils.insertNewParagraph(document.getParagraphs(), document, index + i);
// 创建表格
XmlCursor cursor = xwpfParagraph.getCTP().newCursor();
// 在指定游标位置插入表格
XWPFTable table = document.insertNewTbl(cursor);
CTTblPr tblPr = table.getCTTbl().getTblPr();
// 设置表格整体样式
tblPr.addNewTblW().setW(BigInteger.valueOf(7920)); // 设置表格宽度
XWPFTableRow dataRow = OfficeUtils.createRow(table, 0);
// 创建第一列
XWPFTableCell cell1 = OfficeUtils.createCell(dataRow, 0);
// 在段落中创建一个新的文本运行
XWPFRun run1 = cell1.getParagraphs().get(0).createRun();
// 设置字体为宋体
run1.setFontFamily("宋体");
// 设置字号为四号(14磅)
run1.setFontSize(14);
// 添加文本内容
run1.setText(list.get(i).get("code"));
OfficeUtils.setTheLandscapeHeader(cell1, 0.2);
// 使用十六进制颜色码,这里是灰色
cell1.setColor("C0C0C0");
// 创建第二列
XWPFTableCell cell2 = OfficeUtils.createCell(dataRow, 1);
// 清空单元格内容(可选,如果需要)
cell2.removeParagraph(0);
// 截断内容为多个段落
Matcher matcher = pattern.matcher(list.get(i).get("content").trim());
List<String> matches = new ArrayList<>();
while (matcher.find()) {
matches.add(matcher.group());
}
if (matches.size() == 0) {
matches.add(list.get(i).get("content").trim());
}
for (int j = 0; j < matches.size(); j++) {
XWPFParagraph para = cell2.addParagraph();
if (j != 0){
para = cell2.addParagraph();
}
// 添加文本内容
para.createRun().setText(matches.get(j));
para.setAlignment(ParagraphAlignment.LEFT); // 设置对齐方式
}
OfficeUtils.setsTheCellWidthLeft(cell2, 0.8);
}
}
4、自定义图表
4.1 思路
JFreeChart :
JFreeChart是一个开源的Java图表库,专为JAVA平台设计,用于生成高质量的2D图表。
4.1.1 概述
- JFreeChart是一个完全使用JAVA语言编写的图表绘制类库。
- 它最初由David Gilbert创建,自2001年以来一直在持续开发和更新,目前已成为Java社区中广泛使用的图表库之一。
- JFreeChart是一个开源项目,遵循GNU通用公共许可证(LGPL),允许在专有应用程序中使用。
4.1.2 支持的图表类型
- JFreeChart支持多种图表类型,包括但不限于:
- 饼图(Pie charts)
- 柱状图(Bar charts)
- 散点图(Scatter plots)
- 时序图(Time series)
- 甘特图(Gantt charts)
- 线形图(Line charts)
- 气泡图(Bubble charts)
- 热力图(Heatmaps)
4.1.3 特性
- 定制能力:提供大量的定制选项,包括颜色、字体、标签、图例、网格线、数据点等,以满足各种设计需求。
- 数据源:接受各种数据结构作为输入,如数组、列表或CategoryDataset和TimeSeriesDataset对象。
- 输出类型:支持多种输出类型,包括Swing组件、图像文件(PNG、JPEG)、矢量图形文件格式(PDF、EPS、SVG)等。
- 交互性:具有一定的交互功能,如缩放、平移等。
通过 JFreeChart 创建图表,将图表转换为图像格式(如PNG或JPEG),然后将图像解析成InputStream 写入到Word文档的相应位置中。
4.2 准备模板
4.3 导入依赖
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.3</version>
</dependency>
4.4 图表生成工具类 ChartWithChineseExample
在使用org.jfree.chart
库生成图表时,如果遇到中文无法正常显示的问题,通常是字体设置的问题。JFreeChart默认使用的字体可能不支持中文字符。要解决这个问题,你需要指定一个支持中文的字体。以下是解决此问题的一般步骤:
步骤 1: 准备字体文件
首先,你需要一个支持中文的TrueType字体文件(.ttf
),如宋体(SimSun.ttf
)、微软雅黑(msyh.ttf
)等。这些字体文件通常可以在Windows系统的C:\Windows\Fonts
目录下找到,或者你可以从互联网上下载。
字体文件包可以从这里下载:office字体文件包
步骤 2: 注册字体到FontFactory
在你的Java程序中,使用FontFactory.register()
方法注册你的中文字体文件。例如,如果你有SimSun.ttf
这个字体文件,可以这样做:
/**
* 注册中文字体
*/
public static void registerChineseFont() {
// 注册中文字体(这里假设已经将字体文件放置在项目的resources目录下)
InputStream fontStream = ChartWithChineseExample.class.getResourceAsStream("/font/SIMSUN.TTC"); // 路径根据实际情况调整
try {
Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.PLAIN, 12);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
ge.registerFont(customFont);
} catch (FontFormatException | IOException e) {
e.printStackTrace();
}
}
步骤 3: 设置图表具体位置的字体
柱状图:
// 示例字体为宋体,常规,14号
Font axisLabelFont = new Font("SimSun", Font.PLAIN, 14);
// X轴
chart.getCategoryPlot().getDomainAxis().setLabelFont(axisLabelFont);
chart.getCategoryPlot().getDomainAxis().setTickLabelFont(axisLabelFont);
// Y轴
chart.getCategoryPlot().getRangeAxis().setLabelFont(axisLabelFont);
chart.getCategoryPlot().getRangeAxis().setTickLabelFont(axisLabelFont);
饼图:
chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
// 获取饼图的plot对象,以便进行进一步定制
PiePlot3D plot = (PiePlot3D) chart.getPlot();
// 设置标签字体
plot.setLabelFont(new Font("SimSun", Font.PLAIN, 14));
// 设置无数据信息字体(如果需要)
plot.setNoDataMessageFont(new Font("SimSun", Font.PLAIN, 18));
折线图:
// 设置字体
chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
CategoryPlot plot = (CategoryPlot) chart.getPlot();
plot.getDomainAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getRangeAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getDomainAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getRangeAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
完整代码:
package com.example.demo.uitls;
import lombok.extern.slf4j.Slf4j;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot3D;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
/**
* ChartWithChineseExample : 图表生成工具类
*
* @author zyw
* @create 2024-06-25 16:20
*/
@Slf4j
@Component
public class ChartWithChineseExample {
// 柱状图临时文件名
public final static String BAR_CHART_FILE_NAME = "BAR_CHART.png";
// 饼图临时文件名
public final static String PIE_CHART_FILE_NAME = "PIE_CHART.png";
// 折线图临时文件名
public final static String LINE_CHART_FILE_NAME = "LINE_CHART.png";
public static InputStream lineChartGeneration(String title, String x, String y, DefaultCategoryDataset dataset) {
registerChineseFont();
JFreeChart chart = ChartFactory.createLineChart(
title, // 图表标题
x, // X轴标签
y, // Y轴标签
dataset, // 数据集
PlotOrientation.VERTICAL, // 图表方向
true, // 是否显示图例
true, // 是否生成工具提示
false // 是否生成URL链接
);
chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 14));
// 示例字体为宋体,常规,14号
Font axisLabelFont = new Font("SimSun", Font.PLAIN, 14);
// X轴
chart.getCategoryPlot().getDomainAxis().setLabelFont(axisLabelFont);
chart.getCategoryPlot().getDomainAxis().setTickLabelFont(axisLabelFont);
// Y轴
chart.getCategoryPlot().getRangeAxis().setLabelFont(axisLabelFont);
chart.getCategoryPlot().getRangeAxis().setTickLabelFont(axisLabelFont);
try {
// 将图表转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
ImageIO.write(chartImage, "png", outputStream);
byte[] chartBytes = outputStream.toByteArray();
// 将字节数组转换为InputStream
InputStream inputStream = new ByteArrayInputStream(chartBytes);
return inputStream;
} catch (IOException e) {
log.error("折线图图生成异常");
return null;
}
}
/**
* 饼图生成
*
* @param title 标题
* @param dataset 数据集
* @return
*/
public static InputStream pieChartGeneration(String title, DefaultPieDataset dataset) {
registerChineseFont();
// 使用数据集创建饼图
JFreeChart chart = ChartFactory.createPieChart3D(
title, // 图表标题
dataset, // 数据集
true, // 是否显示图例
true, // 是否生成工具提示
false // 是否生成URL链接
);
chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
// 获取饼图的plot对象,以便进行进一步定制
PiePlot3D plot = (PiePlot3D) chart.getPlot();
// 设置标签字体
plot.setLabelFont(new Font("SimSun", Font.PLAIN, 14));
// 设置无数据信息字体(如果需要)
plot.setNoDataMessageFont(new Font("SimSun", Font.PLAIN, 18));
try {
// 将图表转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
ImageIO.write(chartImage, "png", outputStream);
byte[] chartBytes = outputStream.toByteArray();
// 将字节数组转换为InputStream
InputStream inputStream = new ByteArrayInputStream(chartBytes);
return inputStream;
} catch (IOException e) {
log.error("饼图生成异常");
return null;
}
}
/**
* 注册中文字体
*/
public static void registerChineseFont() {
// 注册中文字体(这里假设已经将字体文件放置在项目的resources目录下)
InputStream fontStream = ChartWithChineseExample.class.getResourceAsStream("/font/SIMSUN.TTC"); // 路径根据实际情况调整
try {
Font customFont = Font.createFont(Font.TRUETYPE_FONT, fontStream).deriveFont(Font.PLAIN, 12);
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
ge.registerFont(customFont);
} catch (FontFormatException | IOException e) {
e.printStackTrace();
}
}
/**
* 创建柱状图表
*
* @param dataset 数据集
* @return
*/
public static InputStream createChartPanel(String title, String x, String y, DefaultCategoryDataset dataset) {
registerChineseFont();
// 创建图表
JFreeChart chart = ChartFactory.createBarChart(
title, // 图表标题
x, // X轴标签
y, // Y轴标签
dataset,
PlotOrientation.VERTICAL,
true, // 是否显示图例
true, // 是否使用工具提示
false // 是否生成URL链接
);
// 设置字体
chart.getTitle().setFont(new Font("SimSun", Font.BOLD, 18));
chart.getLegend().setItemFont(new Font("SimSun", Font.PLAIN, 12));
CategoryPlot plot = (CategoryPlot) chart.getPlot();
plot.getDomainAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getRangeAxis().setLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getDomainAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
plot.getRangeAxis().setTickLabelFont(new Font("SimSun", Font.PLAIN, 12));
try {
// 将图表转换为字节数组
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
BufferedImage chartImage = chart.createBufferedImage(400, 300); // 设置图像的宽高
ImageIO.write(chartImage, "png", outputStream);
byte[] chartBytes = outputStream.toByteArray();
// 将字节数组转换为InputStream
InputStream inputStream = new ByteArrayInputStream(chartBytes);
return inputStream;
} catch (IOException e) {
log.error("柱状图生成异常");
return null;
}
}
}
4.5 业务层 OfficeServicel
在word中遍历所有段落,找到需要插入图表的段落索引。
此处省略上诉已展示代码。
/**
* OfficeServiceImpl :
*
* @author zyw
* @create 2024-06-24 15:41
*/
@Service
@Slf4j
public class OfficeServiceImpl implements OfficeService {
private static final String HEADER_2_1 = "营养成分摄入比例";
private static final String HEADER_2_2 = "心率血氧检查";
private static final String HEADER_2_3 = "睡眠质量趋势";
@Override
public XWPFDocument getHealthReport(HealthReportQuery query) {
try {
FileInputStream fileInputStream = SpringUtils.convertInputStreamToFileInputStream(resourceLoader.getResource(PERSONAL_HEALTH_REPORT_TEMPLATE).getInputStream());
XWPFDocument xwpfDocument = new XWPFDocument(fileInputStream);
// 替换文本数据构建
OfficeUtils.paragraphTextFilling(xwpfDocument, OfficeUtils.objectToMap(query));
// 在基本信息表格中填充数据
fillInTable(xwpfDocument, query);
// 插入体育运动水平表格
int index3 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_3);
handleTableOne(xwpfDocument, index3, query.getSportsLevel());
// 插入体重指数表格
int index4 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_1_4);
handleTableTwo(xwpfDocument, index4, query.getHeight(), query.getWeight());
// 插入历史体重
int index5 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_1);
insertChartOne(xwpfDocument, index5);
// 插入心率检查
int index6 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_2);
insertChartTwo(xwpfDocument, index6);
// 插入睡眠质量趋势
int index7 = OfficeUtils.findParagraphIndexByText(xwpfDocument, HEADER_2_3);
insertChartThree(xwpfDocument, index7);
return xwpfDocument;
} catch (Exception e) {
log.info("获取健康报告失败", e);
return null;
}
}
/**
* 获取文本在文档中的索引
*
* @param doc 文档
* @param text 文本标识
* @return
*/
public static int findParagraphIndexByText(XWPFDocument doc, String text) {
// 获取所有段落
List<XWPFParagraph> paragraphs = doc.getParagraphs();
// 查找目标段落
int targetParagraphIndex = -1;
for (int i = 0; i < paragraphs.size(); i++) {
if (paragraphs.get(i).getText().contains(text)) {
targetParagraphIndex = i;
break;
}
}
return targetParagraphIndex;
}
/**
* 插入图表 1
*
* @param document
* @param index
* @throws Exception
*/
public void insertChartOne(XWPFDocument document, Integer index) throws Exception {
// 填充图表数据
DefaultPieDataset<String> dataset = new DefaultPieDataset<String>();
dataset.setValue("碳水化合物(30%)", 30);
dataset.setValue("蛋白质(30%)", 30);
dataset.setValue("脂肪(25%)", 25);
dataset.setValue("纤维等营养素(15%)", 15);
// 创建图表示例
InputStream chartPanel = ChartWithChineseExample.pieChartGeneration("营养成分摄入比例", dataset);
// 获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
// 在目标段落后添加一个新的段落
XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph.setWordWrap(true); // 设置自动换行
// 设置段落水平居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 设置段落内文字(这里是空格)垂直居中
paragraph.setVerticalAlignment(TextAlignment.CENTER);
// 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
XWPFRun run = paragraph.createRun();
run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.BAR_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
}
/**
* 插入图表2 心率血氧
*
* @param document
* @param index
*/
public void insertChartTwo(XWPFDocument document, Integer index) throws Exception {
// 填充图表数据
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(77, "心率", "2024-06-23");
dataset.addValue(85, "心率", "2024-06-24");
dataset.addValue(99, "心率", "2024-06-25");
dataset.addValue(92.76, "血氧饱和度", "2024-06-23");
dataset.addValue(98.74, "血氧饱和度", "2024-06-24");
dataset.addValue(94.2, "血氧饱和度", "2024-06-25");
// 创建图表示例
InputStream chartPanel = ChartWithChineseExample.createChartPanel("心率和血氧饱和度图表", "日期", "心率(次/分)、血氧饱和度(%)", dataset);
// 获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
// 在目标段落后添加一个新的段落
XWPFParagraph paragraph = document.insertNewParagraph(paragraphs.get(index + 1).getCTP().newCursor().newCursor());
// 设置段落的样式和属性,实现换行
paragraph.setWordWrap(true); // 设置自动换行
// 设置段落水平居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 设置段落内文字(这里是空格)垂直居中
paragraph.setVerticalAlignment(TextAlignment.CENTER);
// 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
XWPFRun run = paragraph.createRun();
run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.PIE_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
}
/**
* 插入图表3 睡眠质量趋势
*
* @param document
* @param index
* @throws Exception
*/
public void insertChartThree(XWPFDocument document, Integer index) throws Exception {
// 填充图表数据
DefaultCategoryDataset dataset = new DefaultCategoryDataset();
dataset.addValue(7.8, "起床时间", "06/18");
dataset.addValue(8, "起床时间", "06/19");
dataset.addValue(7.5, "起床时间", "06/20");
dataset.addValue(8.3, "起床时间", "06/21");
dataset.addValue(9, "起床时间", "06/22");
dataset.addValue(9.5, "起床时间", "06/23");
dataset.addValue(23, "睡眠时间", "06/18");
dataset.addValue(24, "睡眠时间", "06/19");
dataset.addValue(22.6, "睡眠时间", "06/20");
dataset.addValue(23.2, "睡眠时间", "06/21");
dataset.addValue(21.8, "睡眠时间", "06/22");
dataset.addValue(23.7, "睡眠时间", "06/23");
// 创建图表示例
InputStream chartPanel = ChartWithChineseExample.lineChartGeneration("睡眠质量趋势", "日期", "睡眠时间", dataset);
// 获取所有段落
List<XWPFParagraph> paragraphs = document.getParagraphs();
// 在目标段落后添加一个新的段落
XWPFParagraph paragraph = OfficeUtils.insertNewParagraph(paragraphs, document,index);
// 设置段落的样式和属性,实现换行
paragraph.setWordWrap(true); // 设置自动换行
// 设置段落水平居中
paragraph.setAlignment(ParagraphAlignment.CENTER);
// 设置段落内文字(这里是空格)垂直居中
paragraph.setVerticalAlignment(TextAlignment.CENTER);
// 调整行距以确保图片上下居中,这一步可能需要根据实际情况调整
XWPFRun run = paragraph.createRun();
run.addPicture(chartPanel, XWPFDocument.PICTURE_TYPE_PNG, ChartWithChineseExample.LINE_CHART_FILE_NAME, Units.toEMU(400), Units.toEMU(300));
}
}
4.6 导出效果
Word: