基于Poi-tl导出Word(含行、列合并)

基于Poi-tl导出Word(含行、列合并)

poi-tl是一个基于Apache POI的Word模板引擎,也是一个免费开源的Java类库,你可以非常方便的加入到你的项目中,并且拥有着让人喜悦的特性。
—官方文档

各种导出word方案的对比

各种导出word方案的对比

前提

前提

Maven

<dependency>
  <groupId>com.deepoove</groupId>
  <artifactId>poi-tl</artifactId>
  <version>1.12.2</version>
</dependency>

历史版本关联

历史版本关联

模版的描述

模版的描述

话不多说,直接上代码

@Slf4j
public class ServerTablePolicy extends DynamicTableRenderPolicy {

    /**
     * 填充数据所在行数
     */
    private final int laborsStartRow;

    /**
     * 表格的列数
     */
    private final int headerColumnIndex;

    /**
     * 合并的列
     */
    private List<Integer> colArr;

    /**
     * 表格行高
     */
    private int height = 250;

    /**
     * 是否启用水平合并(列合并)
     */
    private boolean enableHorizontalMerge = false;

    /**
     * 是否启用垂直合并(行合并)
     */
    private boolean enableVerticalMerge = true;

    // 存储列对齐方式
    private Map<Integer, ParagraphAlignment> columnAlignments = new HashMap<>();

    public ServerTablePolicy(int laborsStartRow, int headerColumnIndex) {
        this.laborsStartRow = laborsStartRow;
        this.headerColumnIndex = headerColumnIndex;
    }

    public void setColArr(List<Integer> colArr) {
        this.colArr = colArr;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    public void setEnableHorizontalMerge(boolean enableHorizontalMerge) {
        this.enableHorizontalMerge = enableHorizontalMerge;
    }

    public void setEnableVerticalMerge(boolean enableVerticalMerge) {
        this.enableVerticalMerge = enableVerticalMerge;
    }

    /**
     * 设置指定列的对齐方式
     * @param columnIndex 列索引
     * @param alignment   对齐方式
     */
    public void setColumnAlignment(ParagraphAlignment alignment, int... columnIndex) {
        for (int index : columnIndex) {
            columnAlignments.put(index, alignment);
        }
    }

    @Override
    public void render(XWPFTable xwpfTable, Object tableData) throws Exception {
        if (null == tableData) {
            return;
        }
        ServerTableData serverTableData = (ServerTableData) tableData;
        List<RowRenderData> serverDataList = serverTableData.getServerDataList();
        if (CollectionUtils.isNotEmpty(serverDataList)) {
            // 先删除一行, demo中第一行是为了调整 三线表 样式
            xwpfTable.removeRow(laborsStartRow);

            // 行从中间插入, 因此采用倒序渲染数据
            for (int i = serverDataList.size() - 1; i >= 0; i--) {
                XWPFTableRow newRow = xwpfTable.insertNewTableRow(laborsStartRow);
                newRow.setHeight(height);
                for (int j = 0; j < headerColumnIndex; j++) {
                    newRow.createCell();
                }

                // 渲染一行数据
                TableRenderPolicy.Helper.renderRow(newRow, serverDataList.get(i));

                // 设置列对齐方式
                for (int j = 0; j < headerColumnIndex; j++) {
                    if (columnAlignments.containsKey(j)) {
                        XWPFTableCell cell = newRow.getCell(j);
                        ParagraphAlignment alignment = columnAlignments.get(j);
                        cell.getParagraphs().forEach(paragraph -> paragraph.setAlignment(alignment));
                    }
                }

                // 水平合并(列合并)
                if (enableHorizontalMerge) {
                    int startCol = 0;
                    while (startCol < headerColumnIndex - 1) {
                        String currentValue = serverDataList.get(i).getCells().get(startCol)
                                .getParagraphs().get(0).getContents().get(0).toString();
                        int endCol = startCol + 1;

                        // 向后查找连续相同的值
                        while (endCol < headerColumnIndex) {
                            String nextValue = serverDataList.get(i).getCells().get(endCol)
                                    .getParagraphs().get(0).getContents().get(0).toString();
                            if (!currentValue.equals(nextValue)) {
                                break;
                            }
                            endCol++;
                        }

                        // 如果找到连续相同的值,进行合并
                        if (endCol > startCol + 1) {
                            TableTools.mergeCellsHorizonal(xwpfTable, laborsStartRow, startCol, endCol - 1);

                            // 加粗处理
                            XWPFTableCell cell = newRow.getCell(startCol);
                            if (cell != null && cell.getParagraphArray(0) != null) {
                                List<XWPFRun> runs = cell.getParagraphArray(0).getRuns();
                                for (XWPFRun run : runs) {
                                    run.setBold(true);
                                }
                            }
                        }
                        startCol = endCol;
                    }
                }
            }

            // 垂直合并(行合并)
            if (enableVerticalMerge && colArr != null && !colArr.isEmpty()) {
                for (Integer colIndex : colArr) {
                    if (colIndex >= headerColumnIndex) {
                        continue;  // 跳过超出范围的列
                    }

                    List<String> strings = new ArrayList<>();
                    // 收集该列的所有值
                    for (int i = 0; i < serverDataList.size(); i++) {
                        try {
                            String value = serverDataList.get(i).getCells().get(colIndex)
                                    .getParagraphs().get(0).getContents().get(0).toString();
                            strings.add(value != null ? value : "");
                        } catch (Exception e) {
                            log.error("获取单元格值时发生错误: row={}, col={}", i, colIndex, e);
                            strings.add("");  // 添加空值作为占位符
                        }
                    }

                    // 检查收集的数据是否为空
                    if (strings.isEmpty()) {
                        continue;
                    }

                    // 获取需要合并的行组
                    Map<String, List<List<Integer>>> result = ConsecutiveDoublesUtil.getConsecutiveDoubles(strings);

                    // 执行合并
                    if (result != null && !result.isEmpty()) {
                        for (Map.Entry<String, List<List<Integer>>> entry : result.entrySet()) {
                            for (List<Integer> integers : entry.getValue()) {
                                if (integers != null && integers.size() > 1) {
                                    try {
                                        TableTools.mergeCellsVertically(xwpfTable, colIndex,
                                                integers.get(0) + laborsStartRow,
                                                integers.get(integers.size() - 1) + laborsStartRow);
                                    } catch (Exception e) {
                                        log.error("垂直合并单元格时发生错误: col={}, fromRow={}, toRow={}",
                                                colIndex, integers.get(0), integers.get(integers.size() - 1), e);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
以上代码段是基于官方提供的动态循环生成表格插件编写的自定义插件;主要实现:
	1、调用时可指定是否开启行/列合并
	2、指定都需要哪些行/列进行合并
	3、合并时的逻辑为相邻行/列值相同时,自动合并
public static void exportWord(HttpServletResponse response, List<CommonExportVo> commonExportVos) throws IOException {

        Map<String, Object> map = new HashMap<>();
        ServerTableData tableData = new ServerTableData();
        List<RowRenderData> tableDataList = new ArrayList<>(); // 初始化传入表格的行列表数据结构
        for (CommonExportVo tableDatum : commonExportVos) { // 为每行赋值
            tableDataList.add(Rows.of(一列值, 二列值, 三列值, 四列值, 五列值, 六列值, 七列值)
            .textFontFamily("宋体").textFontSize(12).center().create());
        }
        tableData.setServerDataList(tableDataList); // 设置表格数据
        map.put("tableData", tableData);

        // 初始化填充表格策略,从第二行开始填充,一共有7列数据;默认开启同一列如果存在连续的相同数据进行【行合并】,关闭列合并
        ServerTablePolicy tableServerPolicy = new ServerTablePolicy(2, 7);
        tableServerPolicy.setColumnAlignment(ParagraphAlignment.LEFT, 3, 4);
        tableServerPolicy.setColArr(Arrays.asList(0)); // 指定需要垂直合并的列

        /** 2、配置策略 */
        ConfigureBuilder builder = Configure.builder().useSpringEL(false).bind("tableData", tableServerPolicy);

        /** 3.渲染文档 */
        String templatePath = new ClassPathResource("forms/Template.docx").getFile().getAbsolutePath();

        InputStream resourceAsStream = new FileInputStream(new File(templatePath));
        XWPFTemplate template = XWPFTemplate.compile(Objects.requireNonNull(resourceAsStream), builder.build()).render(map);

        String fileName = "aaa.docx";

        // 输出到响应流,设置响应头
        // response.setContentType("application/octet-stream");
        // response.setHeader("Content-disposition", "attachment;filename=\"" + URLEncoder.encode(fileName, "UTF-8"));
        // OutputStream out = response.getOutputStream();

        // 输出到本地
        String outFilePath = "C:\\Users\\2908\\Desktop\\" + fileName;
        OutputStream out = new FileOutputStream(new File(outFilePath));

        BufferedOutputStream bos = new BufferedOutputStream(out);
        try {
            template.write(bos);
            bos.flush();
            out.flush();
        } catch (Exception exception) {
            log.error("导出Word失败!", exception);
            throw new IOException("导出Word失败!", exception);
        } finally {
            PoitlIOUtils.closeQuietlyMulti(template, bos, out);
        }
    }
以上是poi-tl的调用方法。

poi-tl的优点在于制作模版无需如FreeMaker一样繁琐的调整模版的后缀格式,它可以直接使用docx来制作模版;简单表格导出时十分简单明了,在需要导出的位置填写参数(具体参考官方文档poi-tl官方API)所见即所得!

对于复杂表格的处理poi-tl也展现出其扩展友好的特性:既有官方给定的通用插件,我们也可以根据需求来自定义插件实现我们需要的功能。当然编写插件还是需要POI的基础,对小白来说是一个相对困难的过程,可以参考现有插件的源码,或者Google下Apache POI的用法。

本篇关于poi-tl的分享就到这了,更多poi-tl的使用技巧还在等我继续钻研,期待下次再做分享!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薛定不饿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值