Poi-Tl根据word模板生成合并表头使用

poi-tl是一个导出word的工具类, 相较于easy-poi来说,文档可能是更全乎一点吧。 具体的标签什么的看文档就行了,主要是一个生成多级合并表头的方法。

1. 基础用法

1. 基础标签介绍

标签 描述 备注
{{text}} 纯文本
{{?var}} 循环块 需要配对 {{/var}}
{{#table}} 纯代码拼接的表格 需要传入一个TableRenderData对象

2. 使用示例

Word模板如下

用户统计:共 {{userCount}}名注册用户

用户信息块

复制代码
{{?userList}}
    姓名: {{username}}
    年龄: {{age}}
    性别: {{gender}}
    注册时间: {{registerTime}}
    联系方式如下:
    {{#userPhoneTable}}
{{/userList}}

测试渲染模板方法

java 复制代码
@Slf4j
public class UserTemplateTest {

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class User {
        private String username;
        private Integer age;
        private String gender;
        private String registerTime;
        private String phone;
        private String wx;
        private String email;
        private String qq;
    }


    @Test
    public void testExportUserTemplate() {

        // 2. 组装渲染数据
        Map<String, Object> data = buildRenderMap();


        // 4. 加载模板并渲染
        String templatePath = "C:\Users\24962\Desktop\dept\user.docx";
        String outputPath = "C:\Users\24962\Desktop\dept\user_output.docx";

        try (XWPFTemplate template = XWPFTemplate.compile(templatePath);
             FileOutputStream fos = new FileOutputStream(outputPath)) {
            template.render(data);
            // 使用原生POI清空表格首行缩进
            PoiTlUtil.clearAllTableIndent(template.getXWPFDocument());
            template.write(fos);
            log.info("导出成功,文件路径:{}", outputPath);
        } catch (Exception e) {
            log.error("导出失败:{}", e.getMessage(), e);
        }
    }

    private Map<String, Object> buildRenderMap() {
        List<User> users = buildMockUsers();
        Map<String, Object> resultMap = new HashMap<>();
        resultMap.put("userCount", users.size());
        resultMap.put("userList", buildUserList(users));
        return resultMap;
    }

    private List<Map<String, Object>> buildUserList(List<User> users) {
        List<Map<String, Object>> resultList = new ArrayList<>();
        Map<String, Object> userMap;
        for (User user : users) {
            userMap = new HashMap<>();
            userMap.put("username", user.getUsername());
            userMap.put("age", user.getAge());
            userMap.put("gender", user.getGender());
            userMap.put("registerTime", user.getRegisterTime());
            userMap.put("userPhoneTable", buildUserPhoneTable(user));
            resultList.add(userMap);
        }
        return resultList;
    }

    private TableRenderData buildUserPhoneTable(User user) {
        RowRenderData tableRow = PoiTlUtil.buildRowRenderDataByArr("手机号", "微信", "邮箱", "QQ");
        RowRenderData mockUserRows = PoiTlUtil.buildRowRenderDataByArr(user.getPhone(), user.getWx(), user.getEmail(), user.getQq());
        TableRenderData tableRenderData = Tables.of(tableRow, mockUserRows).create();
        // 设置表格内文字首行无缩进
        PoiTlUtil.setTableAllCellNoIndentLeft(tableRenderData);
        return tableRenderData;
    }

    private List<User> buildMockUsers() {
        List<User> userList = new ArrayList<>();
        userList.add(new User("张三", 20, "男", "2023-01-01", "13800000000", "123456", "zhangsan@example.com", "123456"));
        userList.add(new User("李四", 21, "女", "2023-01-02", "13800000001", "123457", "lisi@example.com", "123457"));
        userList.add(new User("王五", 22, "男", "2023-01-03", "13800000002", "123458", "wangwu@example.com", "123458"));
        return userList;
    }
}

消除表格文字首行缩进工具类

java 复制代码
import com.deepoove.poi.data.*;
import com.deepoove.poi.data.style.ParagraphStyle;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

/**
 *
 * @author eleven
 * @since 2026/7/2 18:30
 */
public class PoiTlUtil {
    /**
     * 段落样式 首行无缩进 无边距
     */
    private static final ParagraphStyle LEFT_NO_INDENT_STYLE = ParagraphStyle.builder()
            .withAlign(ParagraphAlignment.LEFT)
            .withIndentFirstLineChars(0) // 取消字符首行缩进(核心)
            .withIndentLeftChars(0)    //  整体段落左边距 0
            .build();

    /**
     * 设置word表格无首行缩进
     * @param tableRenderData   表格数据
     */
    public static void setTableAllCellNoIndentLeft(TableRenderData tableRenderData) {

        // 遍历所有行(表头+数据行)
        List<RowRenderData> rows = tableRenderData.getRows();
        for (RowRenderData row : rows) {
            List<CellRenderData> cells = row.getCells();
            for (CellRenderData cell : cells) {
                // 单元格内所有段落统一替换样式
                List<ParagraphRenderData> paragraphs = cell.getParagraphs();
                for (ParagraphRenderData para : paragraphs) {
                    para.setParagraphStyle(LEFT_NO_INDENT_STYLE);
                }
            }
        }
    }

    public static RowRenderData buildRowRenderData(List<String> list) {
        return buildRowRenderDataByArr(list.toArray(new String[]{}));
    }

    public static RowRenderData buildRowRenderDataByArr(String... arr) {
        //return Rows.of(arr).horizontalCenter().center().create();
        return buildRowRenderDataByArr2(arr);
    }
    // 通用构建无缩进单元格工具
    public static CellRenderData buildNoIndentCell(String text) {
        ParagraphRenderData para = new ParagraphRenderData().addText(text);
        para.setParagraphStyle(LEFT_NO_INDENT_STYLE);
        return Cells.of().addParagraph(para).create();
    }

    // 改造表头构建方法
    private static RowRenderData buildRowRenderDataByArr2(String... texts) {
        List<CellRenderData> cells = new ArrayList<>();
        for (String text : texts) {
            cells.add(buildNoIndentCell(text));
        }
        return Rows.of(cells.toArray(new CellRenderData[]{})).create();
    }


    /**
     * 原生POI强制清除文档内所有表格单元格段落首行缩进、整体左缩进
     * @param document poi-tl编译后得到的XWPFDocument
     */
    public static void clearAllTableIndent(XWPFDocument document) {
        // 遍历文档内全部表格
        for (XWPFTable table : document.getTables()) {
            for (XWPFTableRow row : table.getRows()) {
                for (XWPFTableCell cell : row.getTableCells()) {
                    // 单元格内所有段落统一重置
                    for (XWPFParagraph paragraph : cell.getParagraphs()) {
                        // 强制左对齐
                        paragraph.setAlignment(ParagraphAlignment.LEFT);
                        CTP ctp = paragraph.getCTP();
                        CTPPr ppr = ctp.getPPr() == null ? ctp.addNewPPr() : ctp.getPPr();
                        CTInd indent = ppr.isSetInd() ? ppr.getInd() : ppr.addNewInd();
                        // 首行缩进置0(消除2字符缩进)
                        indent.setFirstLine(BigInteger.ZERO);
                        // 段落整体左侧缩进置0
                        indent.setLeft(BigInteger.ZERO);
                    }
                }
            }
        }
    }
}

2. 全代码生成动态表头

| 姓名 | 年龄 | 联系方式 ||| 性别 | 注册时间 |

姓名 年龄 QQ 微信 邮箱 性别 注册时间

word里边写一个{{#userHeader }}标签用来占位就行

java 复制代码
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.data.*;
import com.deepoove.poi.data.MergeCellRule.Grid;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.HashMap;
import java.util.Map;

/**
 * 使用 poi-tl {@link Tables#of} + {@link Tables.TableBuilder#mergeRule} 生成用户固定表头。
 *
 * <pre>
 * 表头结构(2行 × 7列):
 * ┌──────┬──────┬───────────────┬──────┬──────────┐
 * │ 姓名 │ 年龄 │  联系方式      │ 性别 │ 注册时间  │
 * │      │      ├────┬────┬────┤      │          │
 * │      │      │ qq │微信│邮箱│      │          │
 * └──────┴──────┴────┴────┴────┴──────┴──────────┘
 * </pre>
 *
 * @author eleven
 * @since 2026/7/7
 */
public class UserFixedHeadUtil {

    public static void main(String[] args) throws Exception {
        String outputPath = "userFixedHead.docx";
        String templatePath = "userFixedHeadTemplate.docx";

        // 1. 构建合并规则
        MergeCellRule mergeRule = MergeCellRule.builder()
                // "联系方式" 横向合并 cols 2→4(行0)
                .map(Grid.of(0, 2), Grid.of(0, 4))
                // 左侧列纵向合并(行0→1)
                .map(Grid.of(0, 0), Grid.of(1, 0))  // 姓名
                .map(Grid.of(0, 1), Grid.of(1, 1))  // 年龄
                // 右侧列纵向合并(行0→1)
                .map(Grid.of(0, 5), Grid.of(1, 5))  // 性别
                .map(Grid.of(0, 6), Grid.of(1, 6))  // 注册时间
                .build();

        // 2. 构建表头数据行
        //  行0: 主表头,cols 3~4 为 null(被横向合并覆盖)
        RowRenderData row0 = Rows.of(
                "姓名", "年龄",
                "联系方式", null, null,
                "性别", "注册时间"
        ).center().textBold().create();

        //  行1: 子表头,纵向合并的列填 null
        RowRenderData row1 = Rows.of(
                null, null,
                "qq", "微信", "邮箱",
                null, null
        ).center().textBold().create();

        // 3. 构建表格(注入合并规则)
        TableRenderData headerTable = Tables.of(row0, row1)
                .mergeRule(mergeRule)
                .create();
        Map<String, Object> map = new HashMap<>();
        map.put("userHeader", headerTable);
        try (FileInputStream fis = new FileInputStream(templatePath);
             XWPFTemplate doc = XWPFTemplate.compile(fis);
             FileOutputStream fos = new FileOutputStream(outputPath)) {
            doc.render(map);
            doc.write(fos);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("已生成: " + outputPath);
    }
}

3. 固定表头根据数据生成表格

这种固定表头的就有意思了,需要把循环标签写到循环数据的上一行,有这种合并单元格的写在拆分的上一行。 word里边就写成下边的表格这样就行了。

| 姓名 | 年龄 | 联系方式 ||| 性别 | 注册时间 |
| 姓名 | 年龄 | QQ | 微信 | 邮箱 {{userList}} | 性别 | 注册时间 |
| name | age | qq | wx | email | age | regTime |
| name | age | age | regTime |
|----------|---------|---------|-------------|-----------------|---------|-------------|

针对这个表格呢,进行渲染表格的时候就有些不一样了,需要在compile的时候指定config配置

java 复制代码
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import org.junit.jupiter.api.Test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class UserReportTest {
    @Test
    public void userReportTest() {
        try {
            // 1. 组装业务数据
            List<User> userList = getUserList();

            // 2. 封装模板参数,key 必须和模板 {{userList}} 一致
            Map<String, Object> renderData = new HashMap<>();
            renderData.put("userList", userList);

            // 3. 加载模板并渲染导出
            Map<String, Object> map = new HashMap<>();
            try (FileInputStream fis = new FileInputStream("user_template_hackloop.docx");
                 FileOutputStream fos = new FileOutputStream("output3.docx")) {
                LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
                Configure config = Configure.builder()
                        .bind("userList", policy)
                        .build();
                XWPFTemplate doc = XWPFTemplate.compile(fis, config);
                map.put("userList", getUserList());
                doc.render(map);
                doc.write(fos);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private List<User> getUserList() {
        List<User> userList = new ArrayList<>();
        return userList;
    }

}
相关推荐
梨子同志1 小时前
Spring Framework
后端
梨子同志1 小时前
Spring Boot
后端
Fanta丶1 小时前
4.Activiti流程定义部署
后端
Yeats_Liao3 小时前
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
java·后端·架构
梨子同志3 小时前
Docker
后端
woxiangkaile3 小时前
kernel6.1 新增驱动
后端
高明珠4 小时前
麒麟 V10 SP1 踩坑:`./startup.sh` 报“解释器错误: 权限不够“,根因是 KySec
后端
用户8358086187914 小时前
基于Redis的分布式锁优化实践:从Redlock到自适应自旋的实现与思考
后端