往期系列传送门:
(需要完整代码的直接看最后位置!!!)
前言:
补充Word中图表的知识:
每个图表在word中都有一个内置的Excel,用于操作数据。
内置Excel有类别、系列、值三个概念:
poi可以获取word中的图表对象,通过这个图表对象来操作Excel的系列、类别、值。这样是不是思路很清晰了,直接看代码:
public void exportWord() throws Exception {
//获取word模板
InputStream is = WordController.class.getResourceAsStream("/template/wordChartTemplate.docx");
try {
ZipSecureFile.setMinInflateRatio(-1.0d);
// 获取docx解析对象
XWPFDocument document = new XWPFDocument(is);
// 解析替换第一个图表数据,根据具体业务封装数据
List<Number[]> singleBarValues = new ArrayList<>();
// 第一个系列的值
singleBarValues.add(new Number[]{3232, 5222, 2424, 2346, 3123});
// 第二个系列的值
singleBarValues.add(new Number[]{42.2, 24.41, 31.73, 74.83, 53.28});
// x轴的值
String[] xValues = new String[]{"山东省", "河南省", "北京市", "天津市", "陕西省"};
changeChart1(document, singleBarValues, xValues);
// 输出新文件
FileOutputStream fos = new FileOutputStream("D:\\test\\test.docx");
document.write(fos);
document.close();
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static XWPFChart changeChart1(XWPFDocument document, List<Number[]> singleBarValues, String[] xValues) {
//获取word中所有图表对象
List<XWPFChart> charts = document.getCharts();
//获取第一个单系列柱状图
XWPFChart docChart = charts.get(0);
//系列信息
String[] seriesNames = {"出生人口数","出生率"};
//分类信息
String[] cats = xValues;
// 业务数据
singleBarValues.add(singleBarValues.get(0));
singleBarValues.add(singleBarValues.get(1));
//获取图表数据对象
XDDFChartData chartData = null;
//word图表均对应一个内置的excel,用于保存图表对应的数据
//excel中 第一列第二行开始的数据为分类信息
//CellRangeAddress(1, categories.size(), 0, 0) 四个参数依次为 起始行 截止行 起始列 截止列。
//excel中分类信息的范围
String catDataRange = docChart.formatRange(new CellRangeAddress(1, cats.length, 0, 0));
//根据分类信息的范围创建分类信息的数据源
XDDFDataSource<?> catDataSource = XDDFDataSourcesFactory.fromArray(cats, catDataRange, 0);
//更新数据
for (int i = 0; i < seriesNames.length; i++) {
chartData = docChart.getChartSeries().get(i);
//excel中各系列对应的数据的范围
String valDataRange = docChart.formatRange(new CellRangeAddress(1, cats.length, i + 1, i + 1));
//根据数据的范围创建值的数据源
Number[] val = singleBarValues.get(i);
XDDFNumericalDataSource<Number> valDataSource = XDDFDataSourcesFactory.fromArray(val, valDataRange, i + 1);
//获取图表系列的数据对象
XDDFChartData.Series series = chartData.getSeries(0);
//替换系列数据对象中的分类和值
series.replaceData(catDataSource, valDataSource);
//修改系列数据对象中的标题
// CellReference cellReference = docChart.setSheetTitle(seriesNames[i], 1);
// series.setTitle(seriesNames[i], cellReference);
//更新图表数据对象
docChart.plot(chartData);
}
//图表整体的标题 传空值则不替换标题
// if (!Strings.isNullOrEmpty(title)) {
// docChart.setTitleText(title);
// docChart.setTitleOverlay(false);
// }
return docChart;
}
导出效果:
重点!重点!重点!
看下面这个图表用上述方法能成功导出吗?
像这种x轴带有分类的通过上述方法已经不行了,原因是在用
chartData = docChart.getChartSeries().get(i);
这个方法获取系列对象时报空指针异常。可能是poi不支持这个格式的x轴。
那我们只能换个角度来处理了,之前说过Word中每个图表都是一个内置Excel控制,那Poi操作Excel我可太会了,不熟悉的可以看之前Poi处理Excel的文章。
果然,我们可以通过图表对象拿到XSSFWorkbook
//获取word中所有图表对象
List<XWPFChart> charts = doc.getCharts();
//获取第一个单系列柱状图
XWPFChart singleBarChar = charts.get(1);
XSSFWorkbook workbook = singleBarChar.getWorkbook();
XSSFSheet sheet = workbook.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
// 更新内置excel数据
for (int i = 1; i <= lastRowNum; i++) {
XSSFRow row = sheet.getRow(i);
XSSFCell cell = row.getCell(2);
cell.setCellValue(Double.parseDouble(n1[i - 1].toString()));
XSSFCell cell1 = row.getCell(3);
cell1.setCellValue(Double.parseDouble(n2[i - 1].toString()));
}
处理完后,发现导出的Word虽然内置的Excel数据替换成我们的数据了,页面显示的还是之前模板数据,必须点击编辑后页面数据才显示Excel中的值。
现在问题成了,怎么刷新内置Excel数据,简单图表通过docChart.plot(chartData);方法直接刷新,现在拿不到了怎么办?
上一篇(Poi实现根据word模板导出-文本段落篇)说到,poi是将word解析成xml操作的,那Word页面显示的图表也应该是xml来生成的,我们只需把对应标签数据也改成我们的数据,那页面数据和内置Excel数据不就保持一致了。
通过Debug看下一图表对象
可以发现果然有标签是控制值显示的,下面我们只需按照标签顺序找到位置替换值就可以了。
// 内置excel数据不会更新到页面,需要刷新页面数据
CTChart ctChart = singleBarChar.getCTChart();
CTPlotArea plotArea = ctChart.getPlotArea();
// 第一列数据
CTBarChart barChartArray = plotArea.getBarChartArray(0);
List<CTBarSer> serList = barChartArray.getSerList();
CTBarSer ctBarSer = serList.get(0);
List<CTNumVal> ptList = ctBarSer.getVal().getNumRef().getNumCache().getPtList();
for (int i = 0; i < ptList.size(); i++) {
ptList.get(i).setV(String.valueOf(n1[i]));
}
// 第二列数据
CTLineChart lineChartArray = plotArea.getLineChartArray(0);
List<CTLineSer> lineSers = lineChartArray.getSerList();
CTLineSer ctLineSer = lineSers.get(0);
List<CTNumVal> ptList1 = ctLineSer.getVal().getNumRef().getNumCache().getPtList();
for (int i = 0; i < ptList1.size(); i++) {
ptList1.get(i).setV(String.valueOf(n2[i]));
}
导出效果:
完整代码:
package com.javacoding.controller;
import cn.hutool.core.util.RandomUtil;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xddf.usermodel.chart.*;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.apache.poi.xwpf.usermodel.XWPFChart;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.openxmlformats.schemas.drawingml.x2006.chart.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.*;
import java.util.*;
@RestController
@RequestMapping("/word")
public class WordController {
@RequestMapping("/export")
public void exportWord() throws Exception {
//获取word模板
InputStream is = WordController.class.getResourceAsStream("/template/wordChartTemplate.docx");
try {
ZipSecureFile.setMinInflateRatio(-1.0d);
// 获取docx解析对象
XWPFDocument document = new XWPFDocument(is);
// 解析替换文本段落对象
// 替换word模板中占位符数据,根据业务自行封装
Map<String, String> params = new HashMap<>();
params.put("area1", "山东省");
params.put("area2", "河南省");
params.put("area3", "北京市");
params.put("area4", "天津市");
params.put("area5", "陕西省");
params.put("areaRate1", "42.15");
params.put("areaRate2", "22.35");
params.put("areaRate3", "42.35");
params.put("areaRate4", "23.11");
params.put("areaRate5", "15.34");
changeText(document, params);
// 解析替换第一个图表数据,根据具体业务封装数据
List<Number[]> singleBarValues = new ArrayList<>();
// 第一个系列的值
singleBarValues.add(new Number[]{3232, 5222, 2424, 2346, 3123});
// 第二个系列的值
singleBarValues.add(new Number[]{42.2, 24.41, 31.73, 74.83, 53.28});
// x轴的值
String[] xValues = new String[]{"山东省", "河南省", "北京市", "天津市", "陕西省"};
changeChart1(document, singleBarValues, xValues);
// 解析替换第二个图表数据,根据具体业务封装数据
List<Number[]> singleBarValues2 = new ArrayList<>();
Number[] n1 = new Number[32];
Number[] n2 = new Number[32];
for (int i = 0; i < 32; i++) {
n1[i] = RandomUtil.randomInt(1000000);
n2[i] = RandomUtil.randomDouble(1);
}
singleBarValues2.add(n1);
singleBarValues2.add(n2);
changeChart2(document, singleBarValues2);
// 输出新文件
FileOutputStream fos = new FileOutputStream("D:\\test\\test.docx");
document.write(fos);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void changeChart2(XWPFDocument doc, List<Number[]> singleBarValues2) throws IOException, InvalidFormatException {
// 业务数据
Number[] n1 = singleBarValues2.get(0);
Number[] n2 = singleBarValues2.get(1);
//获取word中所有图表对象
List<XWPFChart> charts = doc.getCharts();
//获取第一个单系列柱状图
XWPFChart singleBarChar = charts.get(1);
XSSFWorkbook workbook = singleBarChar.getWorkbook();
XSSFSheet sheet = workbook.getSheetAt(0);
int lastRowNum = sheet.getLastRowNum();
// 更新内置excel数据
for (int i = 1; i <= lastRowNum; i++) {
XSSFRow row = sheet.getRow(i);
XSSFCell cell = row.getCell(2);
cell.setCellValue(Double.parseDouble(n1[i - 1].toString()));
XSSFCell cell1 = row.getCell(3);
cell1.setCellValue(Double.parseDouble(n2[i - 1].toString()));
}
// 内置excel数据不会更新到页面,需要刷新页面数据
CTChart ctChart = singleBarChar.getCTChart();
CTPlotArea plotArea = ctChart.getPlotArea();
// 第一列数据
CTBarChart barChartArray = plotArea.getBarChartArray(0);
List<CTBarSer> serList = barChartArray.getSerList();
CTBarSer ctBarSer = serList.get(0);
List<CTNumVal> ptList = ctBarSer.getVal().getNumRef().getNumCache().getPtList();
for (int i = 0; i < ptList.size(); i++) {
ptList.get(i).setV(String.valueOf(n1[i]));
}
// 第二列数据
CTLineChart lineChartArray = plotArea.getLineChartArray(0);
List<CTLineSer> lineSers = lineChartArray.getSerList();
CTLineSer ctLineSer = lineSers.get(0);
List<CTNumVal> ptList1 = ctLineSer.getVal().getNumRef().getNumCache().getPtList();
for (int i = 0; i < ptList1.size(); i++) {
ptList1.get(i).setV(String.valueOf(n2[i]));
}
}
private static XWPFChart changeChart1(XWPFDocument document, List<Number[]> singleBarValues, String[] xValues) {
//获取word中所有图表对象
List<XWPFChart> charts = document.getCharts();
//获取第一个单系列柱状图
XWPFChart docChart = charts.get(0);
//系列信息
String[] seriesNames = {"出生人口数","出生率"};
//分类信息
String[] cats = xValues;
// 业务数据
singleBarValues.add(singleBarValues.get(0));
singleBarValues.add(singleBarValues.get(1));
//获取图表数据对象
XDDFChartData chartData = null;
//word图表均对应一个内置的excel,用于保存图表对应的数据
//excel中 第一列第二行开始的数据为分类信息
//CellRangeAddress(1, categories.size(), 0, 0) 四个参数依次为 起始行 截止行 起始列 截止列。
//excel中分类信息的范围
String catDataRange = docChart.formatRange(new CellRangeAddress(1, cats.length, 0, 0));
//根据分类信息的范围创建分类信息的数据源
XDDFDataSource<?> catDataSource = XDDFDataSourcesFactory.fromArray(cats, catDataRange, 0);
//更新数据
for (int i = 0; i < seriesNames.length; i++) {
chartData = docChart.getChartSeries().get(i);
//excel中各系列对应的数据的范围
String valDataRange = docChart.formatRange(new CellRangeAddress(1, cats.length, i + 1, i + 1));
//根据数据的范围创建值的数据源
Number[] val = singleBarValues.get(i);
XDDFNumericalDataSource<Number> valDataSource = XDDFDataSourcesFactory.fromArray(val, valDataRange, i + 1);
//获取图表系列的数据对象
XDDFChartData.Series series = chartData.getSeries(0);
//替换系列数据对象中的分类和值
series.replaceData(catDataSource, valDataSource);
//修改系列数据对象中的标题
// CellReference cellReference = docChart.setSheetTitle(seriesNames[i], 1);
// series.setTitle(seriesNames[i], cellReference);
//更新图表数据对象
docChart.plot(chartData);
}
//图表整体的标题 传空值则不替换标题
// if (!Strings.isNullOrEmpty(title)) {
// docChart.setTitleText(title);
// docChart.setTitleOverlay(false);
// }
return docChart;
}
private void changeText(XWPFDocument document, Map<String, String> params) {
//获取段落集合
List<XWPFParagraph> paragraphs = document.getParagraphs();
for (XWPFParagraph paragraph : paragraphs) {
//判断此段落时候需要进行替换
String text = paragraph.getText();
if(checkText(text)){
List<XWPFRun> runs = paragraph.getRuns();
for (XWPFRun run : runs) {
//替换模板原来位置
String value = changeValue(run.toString(), params);
if (Objects.nonNull(value)) {
run.setText(value, 0);
}
}
}
}
}
/**
* 判断文本中时候包含$
* @param text 文本
* @return 包含返回true,不包含返回false
*/
public static boolean checkText(String text){
boolean check = false;
if(text.indexOf("$")!= -1){
check = true;
}
return check;
}
/**
* 匹配传入信息集合与模板
* @param value 模板需要替换的区域
* @param textMap 传入信息集合
* @return 模板需要替换区域信息集合对应值
*/
public static String changeValue(String value, Map<String, String> textMap){
Set<Map.Entry<String, String>> textSets = textMap.entrySet();
for (Map.Entry<String, String> textSet : textSets) {
//匹配模板与替换值 格式${key}
String key = "${"+textSet.getKey()+"}";
if(value.indexOf(key)!= -1){
value = value.replace(key, textSet.getValue());
}
}
//模板未匹配到区域替换为空
if(checkText(value)){
value = "";
}
return value;
}
}