案例
利用 POI
进行 word
模板替换已经有很成熟的方案了,开源的工具就有 easypoi,以及最近发现的工具 poi-tl,都是模板替换,用法大同小异。
团队内部现在总结发现,用 easypoi
进行 excel
导入导出,模板替换比较方便,但是在 word
方面目前来看 poi-tl
,优势更大,因为其既拥有 easypoi
的优势,又弥补了一些不足,比如图片的环绕,可以通过其插件解决,而且也提供自定义插件支持,可以更灵活。
但是最近有个需求,在word
模板中有图表的存在,现存的两个工具都无法满足,只能支持图片插入。因此决定通过自定义其引用插件
,来满足需求。
在自定义插件时,发现使用poi-tl
还是很方便和简单的,麻烦的是自己去操作图表,由于自己太菜以及对 poi
的认识还严重不足,走了很多路,所以这里记录下。
引用
学习 poi
完全是瞎撞,也不知道各位大佬是如何学习的,为了解决图表问题,能用的资料太少了,这里就贴一些:
- java使用poi在word中生成柱状图、折线图、饼图、柱状图+折线图组合图、动态表格、文本替换、图片替换、更新内置Excel数据、更新插入的文本框内容、合并表格单元格
- 官方 javaDoc
- 官方 示例源码
这里感谢下 我们都有 博主提供的示例源码。
使用记录
目前只是研究了下折线图,其他的还没来的及。
获取图表,定位模板图表
我们都有 博主的思路是通过,图表的编辑数据第一行第一列的交叉格子(org.apache.poi.xssf.usermodel.XSSFCell
),在这里输入标识符来定位,其方法提供是getZeroData
我将方法稍微调整了下,思路是一样的,直接贴代码:
// 获取所有图表
List charts = document.getCharts();
for (XWPFChart chart : charts) {
XSSFWorkbook workbook = chart.getWorkbook();
XSSFSheet sheet1 = workbook.getSheetAt(0);
XSSFCell cell = sheet1.getRow(0).getCell(0);
String firstCellValue = cell.getStringCellValue();
// optionalText() 为标识符获取方法
if (StrUtil.equals(firstCellValue, optionalText())) {
findCharts.add(chart);
}
}
定位代码后,就是操作了
渲染数据
数据结构
渲染数据之前是填入数据,我们都有 博主提供的数据结构,个人觉得太凌乱了,所以我总结了一下,定义如下
@Data
public class LineChartRenderData {
/**
* 图名
*/
private String title;
/**
* 折线
*/
private List lines;
/**
* 横坐标
*/
private List pointX;
@Data
public static class LineData {
/**
* excel title 折线图 标题
*/
private String title;
/**
* 值
*/
private Map points;
}
}
因为目前只争对了折现,所以用 LineData
来定义每条线的数据,以及其名称。pointX
表示每个点的坐标名称,title
表示整个图表名称。
绘制线条
绘制线条,其实就是在拼接xml
文件,这里简单写下这两天对于这块的理解:
-
XDDFLineChartData
类有个属性是chart(org.openxmlformats.schemas.drawingml.x2006.chart.CTLineChart)
,这里其实就是整个图的xml
实体化,我看到大部分获取的方式是这样的:
CTChart ctChart = chart.getCTChart();
CTPlotArea plotArea = ctChart.getPlotArea();
// 折线图
CTLineChart lineChart = plotArea.getLineChartArray(0);
List lines = data.getLines();
我在这里偷了个懒,直接用反射获取了,也不知道后期会不会有些其他问题:
CTLineChart lineChart = (CTLineChart) ReflectUtil.getFieldValue(lineChartData, "chart");
- 拿到
lineChart
后将获取线条集合然后将其清空lineChart.getSerList().clear()
- 拼接
xml
,绘制线条。其实主要是拼接
,大概层次就是:
对应的方法就是一系列的 addNew*
方法,其中需要注意的就是一些 order
,idx
,ptCount
这些标签都是计数类的,跟子标签数量都是有关系的,要多注意。还有就是 ctNumRef.addNewExtLst().addNewExt()
方法有问题,jar
包丢失,也不晓得是不是因为我的maven
有问题
- 这里重点讲下
和
以及
,
控制线条名称,
控制横坐标,每个点的名称,
则是数据了。
public static void renderChart(XWPFChart chart, LineChartRenderData data) {
// 获取图表中的 单个图 之所以是数组 可能存在复合图表
List series = chart.getChartSeries();
for (XDDFChartData chartData : series) {
// 折线图
XDDFLineChartData lineChartData = (XDDFLineChartData) chartData;
CTLineChart lineChart = (CTLineChart) ReflectUtil.getFieldValue(lineChartData, "chart");
// 清空
lineChart.getSerList().clear();
for (int i = 0; i < data.getLines().size(); i++) {
LineChartRenderData.LineData lineData = data.getLines().get(i);
int size = data.getPointX().size();
// 系列名称 单条线名称 对应
CTLineSer ctLineSer = lineChart.addNewSer();
ctLineSer.addNewIdx().setVal(i);
ctLineSer.addNewOrder().setVal(i);
CTSerTx tx = ctLineSer.addNewTx();
String lineRange = new CellRangeAddress(0, 0, i + 1, i + 1).formatAsString("Sheet1", true);
CTStrRef txCTStrRef = tx.addNewStrRef();
txCTStrRef.setF(lineRange);
CTStrData ctStrData = txCTStrRef.addNewStrCache();
CTStrVal txCTStrVal = ctStrData.addNewPt();
txCTStrVal.setV(lineData.getTitle());
txCTStrVal.setIdx(0);
ctStrData.addNewPtCount().setVal(1);
//
//
//
//
//
//
//
//
//
//
// 线条颜色
ctLineSer.addNewSpPr().addNewLn().addNewSolidFill().addNewSchemeClr().setVal(STSchemeColorVal.Enum.forInt(7));
//
//
//
ctLineSer.addNewMarker().addNewSymbol().setNil();
//
ctLineSer.addNewSmooth().setVal(false);
//
//
//
//
//
ctLineSer.addNewExtLst();
//
//
// Sheet1!$A$2:$A$3
//
//
//
// 类别 1
//
//
// 类别 2
//
//
//
//
// x 轴 字段名
CTAxDataSource cat = ctLineSer.addNewCat();
// y 轴 数据
CTNumDataSource val = ctLineSer.addNewVal();
// 渲染 X 轴
//
CTStrRef ctStrRef = cat.addNewStrRef();
// Sheet1!$A$2:$A$3
// excel 表格 x 轴 数据范围, 第一列 1 - size
String xRange = new CellRangeAddress(1, size, 0, 0).formatAsString("Sheet1", true);
ctStrRef.setF(xRange);
// X 轴数据
CTStrData strData = ctStrRef.addNewStrCache();
// 总数
strData.addNewPtCount().setVal(size);
//
// X1
//
for (int j = 0; j < size; j++) {
CTStrVal ctStrVal = strData.addNewPt();
ctStrVal.setIdx(j);
ctStrVal.setV(data.getPointX().get(j));
}
// 渲染 点位 数据
//
// Sheet1!$B$2:$B$3
//
// General
//
//
// 4.3
//
//
// 2.5
//
//
//
CTNumRef ctNumRef = val.addNewNumRef();
// excel 表格 y 轴 数据范围, 第 i + 1 列 1 - size
// CellRangeAddress 参数范围 https://blog.csdn.net/aerchi/article/details/7787891
// CellRangeAddress(起始行号,终止行号, 起始列号,终止列号)
String yRange = new CellRangeAddress(1, size, i + 1, i + 1).formatAsString("Sheet1", true);
ctNumRef.setF(yRange);
CTNumData numData = ctNumRef.addNewNumCache();
// 总数
numData.addNewPtCount().setVal(size);
numData.setFormatCode("General");
for (int j = 0; j < size; j++) {
String key = data.getPointX().get(j);
BigDecimal value = lineData.getPoints().get(key);
CTNumVal ctNumVal = numData.addNewPt();
ctNumVal.setIdx(j);
ctNumVal.setV(value.toString());
}
}
}
绘制excel
数据
这个就比较简单了,只是把 我们都有 博主的方法,按照新的数据结构处理了下,然后看了下评论加了点代码。方法大意就是操作excel
表格,将数据写进去。
public static void renderExcel(XWPFChart chart, LineChartRenderData data) throws IOException {
Workbook wb = new XSSFWorkbook();
Sheet sheet = wb.createSheet("Sheet1");
List lines = data.getLines();
//根据数据创建excel第一行标题行
for (int i = 0; i < lines.size(); i++) {
if (sheet.getRow(0) == null) {
sheet.createRow(0).createCell(i + 1).setCellValue(lines.get(i).getTitle());
} else {
sheet.getRow(0).createCell(i + 1).setCellValue(lines.get(i).getTitle());
}
}
// 渲染数据
for (int i = 0; i < data.getPointX().size(); i++) {
String key = data.getPointX().get(i);
Row row = sheet.createRow(i + 1);
row.createCell(0).setCellValue(key);
for (int j = 0; j < lines.size(); j++) {
row.createCell(j + 1).setCellValue(lines.get(j).getPoints().get(key).doubleValue());
}
}
List pxdList = chart.getRelations();
if (pxdList != null && pxdList.size() > 0) {
for (int i = 0; i < pxdList.size(); i++) {
// 判断为sheet再去进行更新表格数据
if (pxdList.get(i).toString().contains("sheet")) {
POIXMLDocumentPart xlsPart = pxdList.get(i);
OutputStream xlsOut = xlsPart.getPackagePart().getOutputStream();
try {
wb.write(xlsOut);
xlsOut.close();
break;
} finally {
if (wb != null) {
wb.close();
}
}
}
}
}
}
到此就可以输出文档了。
office 打不开
有点时候文档渲染成功了,可以用 WPS
打开,office
却打不开,不用怀疑,肯定是 xml
拼接的有问题,最简单的方式就是拿到渲染后的文件与目标文件对比,看哪个标签出问题了,然后。
最后
这种方式虽然可以插入图表,但是还存在很多问题,只能说提供了思路,目前存在的问题:
1. 无法更改图表的标题
- 生成图表的样式就相当于没了,线条颜色也会存在问题。
- 不够灵活,最好的替代方式还是通过模板来渲染,不更改样式,只提供数据
最后的最后放个源码地址:apache-poi-word-chart