文章目录
Java操作Word文档引言1、技术选型结论 2、基础文本填充2.1 引入依赖2.1.1. poi2.1.2. poi-ooxml2.1.3. poi-ooxml-schemas 总结2.2 业务思路2.3 业务层 OfficeService2.4 通用工具类 OfficeUtils2.5 控制层 OfficeController 3、表格3.1 准备模板3.2 业务层 OfficeService业务流程:名词解释: 3.2 Word工具类OfficeUtils3.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 业务层 OfficeServicel4.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 */@Servicepublic 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); }}
这里用到了SpringUtils 中的convertInputStreamToFileInputStream 方法来将模板文件从对应路径中读取
SpringUtils
package com.example.demo.uitls;import org.springframework.beans.BeansException;import org.springframework.context.ApplicationContext;import org.springframework.context.ApplicationContextAware;import org.springframework.core.io.Resource;import org.springframework.stereotype.Component;import java.io.*;/** * @author zyw */@Componentpublic class SpringUtils implements ApplicationContextAware { private static ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { if (SpringUtils.applicationContext == null) { SpringUtils.applicationContext = applicationContext; } } public static Object getBean(String name) { return applicationContext.getBean(name); } public static <T> T getBean(Class<T> requiredType) { return applicationContext.getBean(requiredType); } public static InputStream readResourceFile(String path) throws IOException { Resource resource = applicationContext.getResource(path); InputStream inputStream = resource.getInputStream(); return inputStream; } public static File getResourceFile(String path) throws IOException { Resource resource = applicationContext.getResource(path); return resource.getFile(); } /** * 将 InputStream 转换为 FileInputStream * @param inputStream * @return * @throws IOException */ public static FileInputStream convertInputStreamToFileInputStream(InputStream inputStream) throws IOException { // 从 inputStream 创建一个临时文件 File tempFile = File.createTempFile("temp", ".tmp"); tempFile.deleteOnExit(); // 确保程序退出时删除临时文件 // 将 inputStream 写入临时文件 try (FileOutputStream out = new FileOutputStream(tempFile)) { byte[] buffer = new byte[1024]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { out.write(buffer, 0, bytesRead); } } // 返回新的 FileInputStream 对象,从临时文件中读取数据 return new FileInputStream(tempFile); }}
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@Slf4jpublic 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@Componentpublic 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@Slf4jpublic 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: