利用 POI 修改插入图表

案例

利用 POI 进行 word 模板替换已经有很成熟的方案了,开源的工具就有 easypoi,以及最近发现的工具 poi-tl,都是模板替换,用法大同小异。

团队内部现在总结发现,用 easypoi 进行 excel 导入导出,模板替换比较方便,但是在 word 方面目前来看 poi-tl,优势更大,因为其既拥有 easypoi 的优势,又弥补了一些不足,比如图片的环绕,可以通过其插件解决,而且也提供自定义插件支持,可以更灵活。

但是最近有个需求,在word模板中有图表的存在,现存的两个工具都无法满足,只能支持图片插入。因此决定通过自定义其引用插件,来满足需求。

在自定义插件时,发现使用poi-tl还是很方便和简单的,麻烦的是自己去操作图表,由于自己太菜以及对 poi 的认识还严重不足,走了很多路,所以这里记录下。

引用

学习 poi 完全是瞎撞,也不知道各位大佬是如何学习的,为了解决图表问题,能用的资料太少了,这里就贴一些:

  1. java使用poi在word中生成柱状图、折线图、饼图、柱状图+折线图组合图、动态表格、文本替换、图片替换、更新内置Excel数据、更新插入的文本框内容、合并表格单元格
  2. 官方 javaDoc
  3. 官方 示例源码

这里感谢下 我们都有 博主提供的示例源码。

使用记录

目前只是研究了下折线图,其他的还没来的及。

获取图表,定位模板图表

我们都有 博主的思路是通过,图表的编辑数据第一行第一列的交叉格子(org.apache.poi.xssf.usermodel.XSSFCell),在这里输入标识符来定位,其方法提供是getZeroData

示例

我将方法稍微调整了下,思路是一样的,直接贴代码:

// 获取所有图表
List<XWPFChart> 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<LineData> lines;
    /**
     * 横坐标
     */
    private List<String> pointX;


    @Data
    public static class LineData {
        /**
         * excel title 折线图 标题
         */
        private String title;
        /**
         * 值
         */
        private Map<String, BigDecimal> points;

    }
}

因为目前只争对了折现,所以用 LineData 来定义每条线的数据,以及其名称。pointX表示每个点的坐标名称,title表示整个图表名称。

绘制线条

绘制线条,其实就是在拼接xml文件,这里简单写下这两天对于这块的理解:

  1. XDDFLineChartData类有个属性是 chart(org.openxmlformats.schemas.drawingml.x2006.chart.CTLineChart) ,这里其实就是整个图的xml实体化,我看到大部分获取的方式是这样的:
CTChart ctChart = chart.getCTChart();
CTPlotArea plotArea = ctChart.getPlotArea();
// 折线图
CTLineChart lineChart = plotArea.getLineChartArray(0);
List<LineChartRenderData.LineData> lines = data.getLines();

我在这里偷了个懒,直接用反射获取了,也不知道后期会不会有些其他问题:

CTLineChart lineChart = (CTLineChart) ReflectUtil.getFieldValue(lineChartData, "chart");
  1. 拿到 lineChart 后将获取线条集合然后将其清空lineChart.getSerList().clear()
  2. 拼接 xml,绘制线条。其实主要是拼接<c:ser></c:ser>,大概层次就是:
<c:ser>
    <c:idx val="0"/>
    
    <c:order val="0"/>
    
    <c:tx> </c:tx>
    
    <c:spPr> </c:spPr>
    
    <c:marker> </c:marker>
    
    <c:cat> </c:cat>
    
    <c:val> </c:val>
    
    <c:smooth val="0"/>
    
    <c:extLst> </c:extLst>
</c:ser>

对应的方法就是一系列的 addNew*方法,其中需要注意的就是一些 orderidxptCount这些标签都是计数类的,跟子标签数量都是有关系的,要多注意。还有就是 ctNumRef.addNewExtLst().addNewExt()方法有问题,jar包丢失,也不晓得是不是因为我的maven有问题

  1. 这里重点讲下<c:tx></c:tx><c:cat></c:cat>以及<c:val></c:val><c:tx></c:tx>控制线条名称,<c:cat></c:cat>控制横坐标,每个点的名称,<c:val></c:val>则是数据了。
public static void renderChart(XWPFChart chart, LineChartRenderData data) {
    // 获取图表中的 单个图 之所以是数组 可能存在复合图表
    List<XDDFChartData> 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();
        // 系列名称 单条线名称 对应  <c:tx></c:tx>
        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);
        // <c:spPr></c:spPr>
        // <c:spPr>
        //      <a:ln w="28575" cap="rnd">
        //        <a:solidFill>
        //          <a:schemeClr val="accent1"/>
        //        </a:solidFill>
        //        <a:round/>
        //      </a:ln>
        //      <a:effectLst/>
        // </c:spPr>
        // 线条颜色
        ctLineSer.addNewSpPr().addNewLn().addNewSolidFill().addNewSchemeClr().setVal(STSchemeColorVal.Enum.forInt(7));
        //     <c:marker>
        //      <c:symbol val="none"/>
        //    </c:marker>
        ctLineSer.addNewMarker().addNewSymbol().setNil();
        //     <c:smooth val="0"/>
        ctLineSer.addNewSmooth().setVal(false);
        // <c:extLst>
        //      <c:ext uri="{C3380CC4-5D6E-409C-BE32-E72D297353CC}" xmlns:c16="http://schemas.microsoft.com/office/drawing/2014/chart">
        //        <c16:uniqueId val="{00000000-2505-4B5B-950D-4F4D615D4F57}"/>
        //      </c:ext>
        //    </c:extLst>
        ctLineSer.addNewExtLst();
    
    
        // <c:cat>
        //      <c:strRef>
        //        <c:f>Sheet1!$A$2:$A$3</c:f>
        //        <c:strCache>
        //          <c:ptCount val="2"/>
        //          <c:pt idx="0">
        //            <c:v>类别 1</c:v>
        //          </c:pt>
        //          <c:pt idx="1">
        //            <c:v>类别 2</c:v>
        //          </c:pt>
        //        </c:strCache>
        //      </c:strRef>
        //    </c:cat>
        // <c:cat></c:cat> x 轴 字段名
        CTAxDataSource cat = ctLineSer.addNewCat();
        // <c:val></c:val> y 轴 数据
        CTNumDataSource val = ctLineSer.addNewVal();
    
        // 渲染 X 轴
        // <c:cat> <c:strRef>   </c:strRef> </c:cat>
        CTStrRef ctStrRef = cat.addNewStrRef();
        // <c:f>Sheet1!$A$2:$A$3</c:f>
        // 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);
        //   <c:pt idx="0">
        //      <c:v>X1</c:v>
        //    </c:pt>
        for (int j = 0; j < size; j++) {
            CTStrVal ctStrVal = strData.addNewPt();
            ctStrVal.setIdx(j);
            ctStrVal.setV(data.getPointX().get(j));
        }
        // 渲染 点位 数据
        // <c:numRef>
        //        <c:f>Sheet1!$B$2:$B$3</c:f>
        //        <c:numCache>
        //          <c:formatCode>General</c:formatCode>
        //          <c:ptCount val="2"/>
        //          <c:pt idx="0">
        //            <c:v>4.3</c:v>
        //          </c:pt>
        //          <c:pt idx="1">
        //            <c:v>2.5</c:v>
        //          </c:pt>
        //        </c:numCache>
        //      </c:numRef>
        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<LineChartRenderData.LineData> 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<POIXMLDocumentPart> 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. 无法更改图表的标题

  1. 生成图表的样式就相当于没了,线条颜色也会存在问题。
  2. 不够灵活,最好的替代方式还是通过模板来渲染,不更改样式,只提供数据

最后的最后放个源码地址:apache-poi-word-chart

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 字不如表,表不入图 一、认识图表信息时代,工作汇总、方案描述、调研报告.……均要求用数据说话。数据如何“说话”,秘...
    职场龙叔阅读 636评论 0 1
  • 1.1 前言 easypoi功能如同名字easy,主打的功能就是容易,让一个没见接触过poi的人员就可以方便的写出...
    afterturn阅读 667评论 0 0
  • 使用首先需要了解他的工作原理 1.POI结构与常用类 (1)创建Workbook和Sheet (2)创建单元格 (...
    长城ol阅读 8,397评论 2 25
  • 一、认识图表 直观形象:用图表让数据开口说话 信息时代,工作汇总、方案描述、调研报告……均要求用数据说话。数据如何...
    爱喝茉莉花茶的女孩阅读 1,778评论 0 8
  • Technical Q&A QA1649 WARNING: The Copy Bundle Resources b...
    Dayon阅读 386评论 0 0