Poi-tl根据word模板生成动态表格

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}}

测试渲染模板方法

@Slf4jpublicclassUserTemplateTest{@Data@NoArgsConstructor@AllArgsConstructorpublicstaticclassUser{privateStringusername;privateIntegerage;privateStringgender;privateStringregisterTime;privateStringphone;privateStringwx;privateStringemail;privateStringqq;}@TestpublicvoidtestExportUserTemplate(){// 2. 组装渲染数据Map<String,Object>data=buildRenderMap();// 4. 加载模板并渲染StringtemplatePath="C:\\Users\\24962\\Desktop\\dept\\user.docx";StringoutputPath="C:\\Users\\24962\\Desktop\\dept\\user_output.docx";try(XWPFTemplatetemplate=XWPFTemplate.compile(templatePath);FileOutputStreamfos=newFileOutputStream(outputPath)){template.render(data);// 使用原生POI清空表格首行缩进PoiTlUtil.clearAllTableIndent(template.getXWPFDocument());template.write(fos);log.info("导出成功,文件路径:{}",outputPath);}catch(Exceptione){log.error("导出失败:{}",e.getMessage(),e);}}privateMap<String,Object>buildRenderMap(){List<User>users=buildMockUsers();Map<String,Object>resultMap=newHashMap<>();resultMap.put("userCount",users.size());resultMap.put("userList",buildUserList(users));returnresultMap;}privateList<Map<String,Object>>buildUserList(List<User>users){List<Map<String,Object>>resultList=newArrayList<>();Map<String,Object>userMap;for(Useruser:users){userMap=newHashMap<>();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);}returnresultList;}privateTableRenderDatabuildUserPhoneTable(Useruser){RowRenderDatatableRow=PoiTlUtil.buildRowRenderDataByArr("手机号","微信","邮箱","QQ");RowRenderDatamockUserRows=PoiTlUtil.buildRowRenderDataByArr(user.getPhone(),user.getWx(),user.getEmail(),user.getQq());TableRenderDatatableRenderData=Tables.of(tableRow,mockUserRows).create();// 设置表格内文字首行无缩进PoiTlUtil.setTableAllCellNoIndentLeft(tableRenderData);returntableRenderData;}privateList<User>buildMockUsers(){List<User>userList=newArrayList<>();userList.add(newUser("张三",20,"男","2023-01-01","13800000000","123456","zhangsan@example.com","123456"));userList.add(newUser("李四",21,"女","2023-01-02","13800000001","123457","lisi@example.com","123457"));userList.add(newUser("王五",22,"男","2023-01-03","13800000002","123458","wangwu@example.com","123458"));returnuserList;}}

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

importcom.deepoove.poi.data.*;importcom.deepoove.poi.data.style.ParagraphStyle;importorg.apache.poi.xwpf.usermodel.*;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTInd;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;importorg.openxmlformats.schemas.wordprocessingml.x2006.main.CTPPr;importjava.math.BigInteger;importjava.util.ArrayList;importjava.util.List;/** * * @author eleven * @since 2026/7/2 18:30 */publicclassPoiTlUtil{/** * 段落样式 首行无缩进 无边距 */privatestaticfinalParagraphStyleLEFT_NO_INDENT_STYLE=ParagraphStyle.builder().withAlign(ParagraphAlignment.LEFT).withIndentFirstLineChars(0)// 取消字符首行缩进(核心).withIndentLeftChars(0)// 整体段落左边距 0.build();/** * 设置word表格无首行缩进 * @param tableRenderData 表格数据 */publicstaticvoidsetTableAllCellNoIndentLeft(TableRenderDatatableRenderData){// 遍历所有行(表头+数据行)List<RowRenderData>rows=tableRenderData.getRows();for(RowRenderDatarow:rows){List<CellRenderData>cells=row.getCells();for(CellRenderDatacell:cells){// 单元格内所有段落统一替换样式List<ParagraphRenderData>paragraphs=cell.getParagraphs();for(ParagraphRenderDatapara:paragraphs){para.setParagraphStyle(LEFT_NO_INDENT_STYLE);}}}}publicstaticRowRenderDatabuildRowRenderData(List<String>list){returnbuildRowRenderDataByArr(list.toArray(newString[]{}));}publicstaticRowRenderDatabuildRowRenderDataByArr(String...arr){//return Rows.of(arr).horizontalCenter().center().create();returnbuildRowRenderDataByArr2(arr);}// 通用构建无缩进单元格工具publicstaticCellRenderDatabuildNoIndentCell(Stringtext){ParagraphRenderDatapara=newParagraphRenderData().addText(text);para.setParagraphStyle(LEFT_NO_INDENT_STYLE);returnCells.of().addParagraph(para).create();}// 改造表头构建方法privatestaticRowRenderDatabuildRowRenderDataByArr2(String...texts){List<CellRenderData>cells=newArrayList<>();for(Stringtext:texts){cells.add(buildNoIndentCell(text));}returnRows.of(cells.toArray(newCellRenderData[]{})).create();}/** * 原生POI强制清除文档内所有表格单元格段落首行缩进、整体左缩进 * @param document poi-tl编译后得到的XWPFDocument */publicstaticvoidclearAllTableIndent(XWPFDocumentdocument){// 遍历文档内全部表格for(XWPFTabletable:document.getTables()){for(XWPFTableRowrow:table.getRows()){for(XWPFTableCellcell:row.getTableCells()){// 单元格内所有段落统一重置for(XWPFParagraphparagraph:cell.getParagraphs()){// 强制左对齐paragraph.setAlignment(ParagraphAlignment.LEFT);CTPctp=paragraph.getCTP();CTPPrppr=ctp.getPPr()==null?ctp.addNewPPr():ctp.getPPr();CTIndindent=ppr.isSetInd()?ppr.getInd():ppr.addNewInd();// 首行缩进置0(消除2字符缩进)indent.setFirstLine(BigInteger.ZERO);// 段落整体左侧缩进置0indent.setLeft(BigInteger.ZERO);}}}}}}

2. 全代码生成动态表头

姓名年龄联系方式性别注册时间
QQ微信邮箱

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

importcom.deepoove.poi.XWPFTemplate;importcom.deepoove.poi.data.*;importcom.deepoove.poi.data.MergeCellRule.Grid;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.util.HashMap;importjava.util.Map;/** * 使用 poi-tl {@link Tables#of} + {@link Tables.TableBuilder#mergeRule} 生成用户固定表头。 * * <pre> * 表头结构(2行 × 7列): * ┌──────┬──────┬───────────────┬──────┬──────────┐ * │ 姓名 │ 年龄 │ 联系方式 │ 性别 │ 注册时间 │ * │ │ ├────┬────┬────┤ │ │ * │ │ │ qq │微信│邮箱│ │ │ * └──────┴──────┴────┴────┴────┴──────┴──────────┘ * </pre> * * @author eleven * @since 2026/7/7 */publicclassUserFixedHeadUtil{publicstaticvoidmain(String[]args)throwsException{StringoutputPath="userFixedHead.docx";StringtemplatePath="userFixedHeadTemplate.docx";// 1. 构建合并规则MergeCellRulemergeRule=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(被横向合并覆盖)RowRenderDatarow0=Rows.of("姓名","年龄","联系方式",null,null,"性别","注册时间").center().textBold().create();// 行1: 子表头,纵向合并的列填 nullRowRenderDatarow1=Rows.of(null,null,"qq","微信","邮箱",null,null).center().textBold().create();// 3. 构建表格(注入合并规则)TableRenderDataheaderTable=Tables.of(row0,row1).mergeRule(mergeRule).create();Map<String,Object>map=newHashMap<>();map.put("userHeader",headerTable);try(FileInputStreamfis=newFileInputStream(templatePath);XWPFTemplatedoc=XWPFTemplate.compile(fis);FileOutputStreamfos=newFileOutputStream(outputPath)){doc.render(map);doc.write(fos);}catch(Exceptione){e.printStackTrace();}System.out.println("已生成: "+outputPath);}}

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

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

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

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

importcom.deepoove.poi.XWPFTemplate;importcom.deepoove.poi.config.Configure;importcom.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importlombok.experimental.Accessors;importorg.junit.jupiter.api.Test;importjava.io.FileInputStream;importjava.io.FileOutputStream;importjava.util.ArrayList;importjava.util.HashMap;importjava.util.List;importjava.util.Map;publicclassUserReportTest{@TestpublicvoiduserReportTest(){try{// 1. 组装业务数据List<User>userList=getUserList();// 2. 封装模板参数,key 必须和模板 {{userList}} 一致Map<String,Object>renderData=newHashMap<>();renderData.put("userList",userList);// 3. 加载模板并渲染导出Map<String,Object>map=newHashMap<>();try(FileInputStreamfis=newFileInputStream("user_template_hackloop.docx");FileOutputStreamfos=newFileOutputStream("output3.docx")){LoopRowTableRenderPolicypolicy=newLoopRowTableRenderPolicy();Configureconfig=Configure.builder().bind("userList",policy).build();XWPFTemplatedoc=XWPFTemplate.compile(fis,config);map.put("userList",getUserList());doc.render(map);doc.write(fos);}}catch(Exceptione){e.printStackTrace();}}privateList<User>getUserList(){List<User>userList=newArrayList<>();returnuserList;}}