poi根据模板导出word(包含图片、动态生成表格、合并单元格)

本文介绍了一种使用Java生成Word文档的方法,包括文本替换、图片插入及表格数据动态填充等功能。通过示例代码展示了如何利用Apache POI库实现这些功能。
该文章已生成可运行项目,

模板样式:


运行结果:


需要的jar包:

<!-- poi  Excel、Word操作-->
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi</artifactId>
        <version>3.9</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml</artifactId>
        <version>3.7</version>
    </dependency>
    <dependency>
        <groupId>org.apache.poi</groupId>
        <artifactId>poi-ooxml-schemas</artifactId>
        <version>3.9</version>
    </dependency>

文件目录结构:

Test类:

package cn.gl.word;

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 Test {
	
	public static void main(String[] args) throws Exception {
		//需要进行文本替换的信息
		Map<String, Object> data = new HashMap<String, Object>();
		data.put("${date}", "2018-03-06");
		data.put("${name}", "东方明珠");
		data.put("${address}", "上海黄浦江附近");
		data.put("${communityValue}", "");
		data.put("${safetyCode}", "东方社区");
		data.put("${picture2}", "");
		data.put("${picture3}", "");
		data.put("${buildingValue2}", "漫展提示");
		data.put("${patrolPhoto1}", "");
		data.put("${patrolPhoto2}", "");
		data.put("${buildingValue3}", "中国标语");
		
		//图片,如果是多个图片,就新建多个map
		Map<String,Object> picture1 = new HashMap<String, Object>();
		picture1.put("width", 100);
		picture1.put("height", 150);
		picture1.put("type", "jpg");
		picture1.put("content", WorderToNewWordUtils.inputStream2ByteArray(new FileInputStream("template/c1.jpg"), true));
		data.put("${picture1}",picture1);
		
		//需要进行动态生成的信息
		Map<String,Object> mapList = new HashMap<String, Object>();
		
		//第一个动态生成的数据列表
		List<String[]> list01 = new ArrayList<String[]>();
		list01.add(new String[]{"A","美女好看"});
		list01.add(new String[]{"A","美女好多"});
		list01.add(new String[]{"B","漫展人太多"});
		list01.add(new String[]{"C","妹子穿的很清凉"});
		
		//第二个动态生成的数据列表
		List<String> list02 = new ArrayList<String>();
		list02.add("1、民主");
		list02.add("2、富强");
		list02.add("3、文明");
		list02.add("4、和谐");

		mapList.put("list01", list01);
		mapList.put("list02", list02);

		CustomXWPFDocument doc = WorderToNewWordUtils.changWord("template/demo.docx",data,mapList);
		FileOutputStream fopts = new FileOutputStream("D:/呵呵.docx");
		doc.write(fopts);
		fopts.close();
	}
}

WorderToNewWordUtils类:

package cn.gl.word;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.poi.POIXMLDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTHMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTVMerge;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.STMerge;

public class WorderToNewWordUtils {
	/**
	 * 根据模板生成word文档
	 * @param inputUrl 模板路径
	 * @param textMap 需要替换的文本内容
	 * @param mapList 需要动态生成的内容
	 * @return
	 */
    public static CustomXWPFDocument changWord(String inputUrl, Map<String, Object> textMap, Map<String, Object> mapList) {
    	CustomXWPFDocument document = null;
        try {
            //获取docx解析对象
            document = new CustomXWPFDocument(POIXMLDocument.openPackage(inputUrl));
            
            //解析替换文本段落对象
            WorderToNewWordUtils.changeText(document, textMap);
            
            //解析替换表格对象
            WorderToNewWordUtils.changeTable(document, textMap, mapList);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return document;
    }

    /**
     * 替换段落文本
     * @param document docx解析对象
     * @param textMap 需要替换的信息集合
     */
    public static void changeText(CustomXWPFDocument document, Map<String, Object> textMap){
        //获取段落集合
        List<XWPFParagraph> paragraphs = document.getParagraphs();

        for (XWPFParagraph paragraph : paragraphs) {
            //判断此段落时候需要进行替换
            String text = paragraph.getText();
            System.out.println(text);
            if(checkText(text)){
                List<XWPFRun> runs = paragraph.getRuns();
                for (XWPFRun run : runs) {
                    //替换模板原来位置
                    Object ob = changeValue(run.toString(), textMap);
                    if (ob instanceof String){
                        run.setText((String)ob,0);
                    }
                }
            }
        }
    }

    /**
     * 替换表格对象方法
     * @param document docx解析对象
     * @param textMap 需要替换的信息集合
     * @param mapList 需要动态生成的内容
     */
    public static void changeTable(CustomXWPFDocument document, Map<String, Object> textMap,Map<String, Object> mapList){
        //获取表格对象集合
        List<XWPFTable> tables = document.getTables();
        
        //循环所有需要进行替换的文本,进行替换
        for (int i = 0; i < tables.size(); i++) {
            XWPFTable table = tables.get(i);
            if(checkText(table.getText())){
                List<XWPFTableRow> rows = table.getRows();
                //遍历表格,并替换模板
                eachTable(document,rows, textMap);
            }
        }
        
        List<String[]> list01 = (List<String[]>) mapList.get("list01");
        List<String> list02 = (List<String>) mapList.get("list02");
        //操作word中的表格
        for (int i = 0; i < tables.size(); i++) {
            //只处理行数大于等于2的表格,且不循环表头
            XWPFTable table = tables.get(i);
            //第二个表格使用daList,插入数据
            if (null != list01 && 0 < list01.size() && i == 1){
                insertTable(table, null,list01,2);

                List<Integer[]> indexList = startEnd(list01);
                for (int c=0;c<indexList.size();c++){
                    //合并行
                    mergeCellVertically(table,0,indexList.get(c)[0]+1,indexList.get(c)[1]+1);
                }
            }
            //第四个表格使用tableList,插入数据
            if (null != list02 && 0 < list02.size() && i == 3){
                insertTable(table, list02,null,4);
            }
        }
    }

    /**
     * 遍历表格
     * @param rows 表格行对象
     * @param textMap 需要替换的信息集合
     */
    public static void eachTable(CustomXWPFDocument document,List<XWPFTableRow> rows ,Map<String, Object> textMap){
        for (XWPFTableRow row : rows) {
            List<XWPFTableCell> cells = row.getTableCells();
            for (XWPFTableCell cell : cells) {
                //判断单元格是否需要替换
                if(checkText(cell.getText())){
                    List<XWPFParagraph> paragraphs = cell.getParagraphs();
                    for (XWPFParagraph paragraph : paragraphs) {
                        List<XWPFRun> runs = paragraph.getRuns();
                        for (XWPFRun run : runs) {
                            Object ob = changeValue(run.toString(), textMap);
                            if (ob instanceof String){
                                run.setText((String)ob,0);
                            }else if (ob instanceof Map){
                                run.setText("",0);
                                Map pic = (Map)ob;
                                int width = Integer.parseInt(pic.get("width").toString());
                                int height = Integer.parseInt(pic.get("height").toString());
                                int picType = getPictureType(pic.get("type").toString());
                                byte[] byteArray = (byte[]) pic.get("content");
                                ByteArrayInputStream byteInputStream = new ByteArrayInputStream(byteArray);
                                try {
                                    int ind = document.addPicture(byteInputStream,picType);
                                    document.createPicture(ind, width , height,paragraph);
                                } catch (Exception e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * 为表格插入数据,行数不够添加新行
     * @param table 需要插入数据的表格
     * @param tableList 第四个表格的插入数据
     * @param daList 第二个表格的插入数据
     * @param type 表格类型:1-第一个表格 2-第二个表格 3-第三个表格 4-第四个表格
     */
    public static void insertTable(XWPFTable table, List<String> tableList,List<String[]> daList,Integer type){
        if (2 == type){
            //创建行和创建需要的列
            for(int i = 1; i < daList.size(); i++){
                XWPFTableRow row = table.insertNewTableRow(1);//添加一个新行
                row.createCell();//添加第一个列
                row.createCell();//添加第二个列
            }

            //创建行,根据需要插入的数据添加新行,不处理表头
            for(int i = 0; i < daList.size(); i++){
                List<XWPFTableCell> cells = table.getRow(i+1).getTableCells();
                for(int j = 0; j < cells.size(); j++){
                    XWPFTableCell cell02 = cells.get(j);
                    cell02.setText(daList.get(i)[j]);
                }
            }
        }else if (4 == type){
            //插入表头下面第一行的数据
            for(int i = 0; i < tableList.size(); i++){
                XWPFTableRow row = table.createRow();
                List<XWPFTableCell> cells = row.getTableCells();
                cells.get(0).setText(tableList.get(i));
            }
        }
    }

    /**
     * 判断文本中时候包含$
     * @param text 文本
     * @return 包含返回true,不包含返回false
     */
    public static boolean checkText(String text){
        boolean check  =  false;
        if(text.indexOf("$")!= -1){
            check = true;
        }
        return check;
    }

    /**
     * 匹配传入信息集合与模板
     * @param value 模板需要替换的区域
     * @param textMap 传入信息集合
     * @return 模板需要替换区域信息集合对应值
     */
    public static Object changeValue(String value, Map<String, Object> textMap){
        Set<Entry<String, Object>> textSets = textMap.entrySet();
        Object valu = "";
        for (Entry<String, Object> textSet : textSets) {
            //匹配模板与替换值 格式${key}
            String key = textSet.getKey();
            if(value.indexOf(key)!= -1){
                valu = textSet.getValue();
            }
        }
        return valu;
    }

    /**
     * 将输入流中的数据写入字节数组
     * @param in
     * @return
     */
    public static byte[] inputStream2ByteArray(InputStream in,boolean isClose){
        byte[] byteArray = null;
        try {
            int total = in.available();
            byteArray = new byte[total];
            in.read(byteArray);
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            if(isClose){
                try {
                    in.close();
                } catch (Exception e2) {
                    System.out.println("关闭流失败");
                }
            }
        }
        return byteArray;
    }

    /**
     * 根据图片类型,取得对应的图片类型代码
     * @param picType
     * @return int
     */
    private static int getPictureType(String picType){
        int res = CustomXWPFDocument.PICTURE_TYPE_PICT;
        if(picType != null){
            if(picType.equalsIgnoreCase("png")){
                res = CustomXWPFDocument.PICTURE_TYPE_PNG;
            }else if(picType.equalsIgnoreCase("dib")){
                res = CustomXWPFDocument.PICTURE_TYPE_DIB;
            }else if(picType.equalsIgnoreCase("emf")){
                res = CustomXWPFDocument.PICTURE_TYPE_EMF;
            }else if(picType.equalsIgnoreCase("jpg") || picType.equalsIgnoreCase("jpeg")){
                res = CustomXWPFDocument.PICTURE_TYPE_JPEG;
            }else if(picType.equalsIgnoreCase("wmf")){
                res = CustomXWPFDocument.PICTURE_TYPE_WMF;
            }
        }
        return res;
    }

    /**
     * 合并行
     * @param table
     * @param col 需要合并的列
     * @param fromRow 开始行
     * @param toRow 结束行
     */
    public static void mergeCellVertically(XWPFTable table, int col, int fromRow, int toRow) {
        for(int rowIndex = fromRow; rowIndex <= toRow; rowIndex++){
            CTVMerge vmerge = CTVMerge.Factory.newInstance();
            if(rowIndex == fromRow){
                vmerge.setVal(STMerge.RESTART);
            } else {
                vmerge.setVal(STMerge.CONTINUE);
            }
            XWPFTableCell cell = table.getRow(rowIndex).getCell(col);
            CTTcPr tcPr = cell.getCTTc().getTcPr();
            if (tcPr != null) {
                tcPr.setVMerge(vmerge);
            } else {
                tcPr = CTTcPr.Factory.newInstance();
                tcPr.setVMerge(vmerge);
                cell.getCTTc().setTcPr(tcPr);
            }
        }
    }
    
    //列合并  ,有点问题,用不了
    public static void mergeCellHorizontally(XWPFTable table, int row, int fromCol, int toCol) {  
        for(int colIndex = fromCol; colIndex <= toCol; colIndex++){  
            CTHMerge hmerge = CTHMerge.Factory.newInstance();  
            if(colIndex == fromCol){
                hmerge.setVal(STMerge.RESTART);  
            } else {  
                hmerge.setVal(STMerge.CONTINUE);  
            }
            XWPFTableCell cell = table.getRow(row).getCell(colIndex);  
            CTTcPr tcPr = cell.getCTTc().getTcPr();  
            if (tcPr != null) {
                tcPr.setHMerge(hmerge);  
            } else {  
                tcPr = CTTcPr.Factory.newInstance();  
                tcPr.setHMerge(hmerge);  
                cell.getCTTc().setTcPr(tcPr);  
            }  
        }  
    }

    /**
     * 获取需要合并单元格的下标
     * @return
     */
    public static List<Integer[]> startEnd(List<String[]> daList){
        List<Integer[]> indexList = new ArrayList<Integer[]>();

        List<String> list = new ArrayList<String>();
        for (int i=0;i<daList.size();i++){
            list.add(daList.get(i)[0]);
        }
        Map<Object, Integer> tm = new HashMap<Object, Integer>();
        for (int i=0;i<daList.size();i++){
            if (!tm.containsKey(daList.get(i)[0])) {
                tm.put(daList.get(i)[0], 1);
            } else {
                int count = tm.get(daList.get(i)[0]) + 1;
                tm.put(daList.get(i)[0], count);
            }
        }
        for (Map.Entry<Object, Integer> entry : tm.entrySet()) {
            String key = entry.getKey().toString();
            String value = entry.getValue().toString();
            if (list.indexOf(key) != (-1)){
                Integer[] index = new Integer[2];
                index[0] = list.indexOf(key);
                index[1] = list.lastIndexOf(key);
                indexList.add(index);
            }
        }
        return indexList;
    }
}

CustomXWPFDocument类:

package cn.gl.word;

import java.io.IOException;
import java.io.InputStream;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.xmlbeans.XmlException;
import org.apache.xmlbeans.XmlToken;
import org.openxmlformats.schemas.drawingml.x2006.main.CTNonVisualDrawingProps;
import org.openxmlformats.schemas.drawingml.x2006.main.CTPositiveSize2D;
import org.openxmlformats.schemas.drawingml.x2006.wordprocessingDrawing.CTInline;
  
/**
 * 自定义 XWPFDocument,并重写 createPicture()方法
 */
public class CustomXWPFDocument extends XWPFDocument {  
    public CustomXWPFDocument(InputStream in) throws IOException {  
        super(in);  
    }  
  
    public CustomXWPFDocument() {  
        super();  
    }  
  
    public CustomXWPFDocument(OPCPackage pkg) throws IOException {  
        super(pkg);  
    }  
  
    /**
     * @param id
     * @param width 宽
     * @param height 高
     * @param paragraph  段落
     */
    public void createPicture(int id, int width, int height,XWPFParagraph paragraph) {  
        final int EMU = 9525;  
        width *= EMU;  
        height *= EMU;  
        String blipId = getAllPictures().get(id).getPackageRelationship().getId();  
        CTInline inline = paragraph.createRun().getCTR().addNewDrawing().addNewInline();  
        
        System.out.println(blipId+":"+inline);
        
        String picXml = ""  
                + "<a:graphic xmlns:a=\"http://schemas.openxmlformats.org/drawingml/2006/main\">"  
                + "   <a:graphicData uri=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">"  
                + "      <pic:pic xmlns:pic=\"http://schemas.openxmlformats.org/drawingml/2006/picture\">"  
                + "         <pic:nvPicPr>" + "            <pic:cNvPr id=\""  
                + id  
                + "\" name=\"Generated\"/>"  
                + "            <pic:cNvPicPr/>"  
                + "         </pic:nvPicPr>"  
                + "         <pic:blipFill>"  
                + "            <a:blip r:embed=\""  
                + blipId  
                + "\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\"/>"  
                + "            <a:stretch>"  
                + "               <a:fillRect/>"  
                + "            </a:stretch>"  
                + "         </pic:blipFill>"  
                + "         <pic:spPr>"  
                + "            <a:xfrm>"  
                + "               <a:off x=\"0\" y=\"0\"/>"  
                + "               <a:ext cx=\""  
                + width  
                + "\" cy=\""  
                + height  
                + "\"/>"  
                + "            </a:xfrm>"  
                + "            <a:prstGeom prst=\"rect\">"  
                + "               <a:avLst/>"  
                + "            </a:prstGeom>"  
                + "         </pic:spPr>"  
                + "      </pic:pic>"  
                + "   </a:graphicData>" + "</a:graphic>";  
  
        inline.addNewGraphic().addNewGraphicData();  
        XmlToken xmlToken = null;  
        try {  
            xmlToken = XmlToken.Factory.parse(picXml);  
        } catch (XmlException xe) {  
            xe.printStackTrace();  
        }  
        inline.set(xmlToken); 
        
        inline.setDistT(0);    
        inline.setDistB(0);    
        inline.setDistL(0);    
        inline.setDistR(0);    
        
        CTPositiveSize2D extent = inline.addNewExtent();  
        extent.setCx(width);  
        extent.setCy(height);  
        
        CTNonVisualDrawingProps docPr = inline.addNewDocPr();    
        docPr.setId(id);    
        docPr.setName("图片" + id);    
        docPr.setDescr("测试"); 
    }  
}  


本文章已经生成可运行项目
<think>我们参考了多个引用,特别是引用[2]和引用[4]中关于POI操作Word表格的内容。 用户需求:使用Java POI根据模板Word表格生成表格的方法。 核心步骤: 1. 读取模板文件,获取XWPFDocument对象。 2. 在模板中找到需要操作的表格(可能需要遍历文档中的表格,并根据某些特征识别目标表格,例如表格中的占位文本)。 3. 根据业务数据动态生成表格行(或修改现有表格的内容)。 4. 将修改后的文档写入输出流(如文件输出流或响应输出流以下载)。 根据引用[2],我们可以使用poi-tl(基于POI模板引擎)来简化操作,特别是处理动态表格行。但用户问题中明确提到使用Java POI,所以我们将分别展示两种方式: 方式一:使用原生POI API操作表格(引用[4]提供了基础,引用[3]有简单示例) 方式二:使用poi-tl(引用[2]中提到的模板引擎)来处理模板中的表格(更推荐,因为更简单) 由于用户要求根据模板生成表格,并且模板中已经有一个表格结构,我们假设模板中有一个表格,其中一行作为模板行,我们需要复制该行并填充数据。 我们先展示原生POI API的方式(较为繁琐): 步骤: a. 读取模板文档 b. 定位到目标表格(假设第一个表格) c. 获取模板行(通常是第二行,第一行是表头,但具体看模板设计) d. 移除模板行(但保留其样式和结构)或保留模板行并复制多份 e. 根据数据列表,为每个数据项创建新行,并填充数据 f. 保存文档 示例代码(原生POI): 引用[4]提供了操作表格的基本类:XWPFTable, XWPFTableRow, XWPFTableCell。 然而,由于原生POI操作表格较为复杂,且用户问题中引用[2]提到了poi-tl,而poi-tl可以非常方便地处理动态表格(通过区块对标签和表格行插件),因此我们更推荐使用poi-tl。 下面我们将分别给出两种方法的简要示例: 方法一:原生POI(假设模板表格只有一行数据行,我们将其作为模板行) 注意:此方法需要处理样式复制等细节,代码量较大。 方法二:poi-tl(使用模板引擎,通过声明式方式) 由于用户明确要求使用Java POI,但引用[2]中提到的poi-tl也是基于POI的,且是专门为模板导出设计的,因此我们重点介绍poi-tl方式(因为它更符合模板生成的需求)。 下面我们详细说明poi-tl的使用方法(根据引用[2]的描述): 步骤: 1. 在模板中定义表格占位符:例如,在模板中需要动态生成表格的位置,放置一个占位符{{#contractTable}},然后在表格中设计好表头和一行数据行(数据行中使用占位符如[departmentName]等)。 2. 在Java代码中,准备数据模型(Map),其中key为占位符名称,value为数据列表(List<Map<String, Object>>或List<自定义对象>)。 3. 使用poi-tl的API加载模板并渲染数据。 示例模板设计(Word模板): 表格第一行:表头(例如:部门、目标金额、总金额...) 表格第二行:作为模板行,每个单元中写上占位符,如:[departmentName], [targetMoney]等(注意:这里的占位符用[]括起来,并且不在{{}}内,但整个表格区域的占位符是{{contractTable}},它作用于整个表格行循环) 在poi-tl中,表格行循环的语法是:{{var}} ... {{/var}},但在引用[2]中描述的表格行插件使用的是:{{contractTable}},然后在数据行中定义字段占位符(用方括号包裹)。实际上,poi-tl的表格行插件是通过自定义标签策略实现的。 具体实现(poi-tl): 1. 引入依赖(引用[2]没有给出,但通常为): <dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.10.0</version> </dependency> 2. 编写模板(假设模板文件为template.docx): 在文档中插入一个表格表格有两行:第一行是表头,第二行是数据行(模板行)。在第二行的每个单元中,按照数据字段写入占位符,例如: 第一列单元:`[departmentName]` 第二列单元:`[targetMoney]` 等等。 然后,选中整个表格(或者表格的某几行,具体看需求),在表格上方或下方(实际上是在表格外面)添加一个文本占位符{{contractTable}},这个占位符表示从这里开始到结束的表格行需要循环。但是注意:poi-tl的表格行循环插件要求将占位符放在表格的下一行(紧挨着表格的段落)?实际上,根据poi-tl的文档,它支持在表格内部使用行循环标签。 3. 数据准备: Map<String, Object> data = new HashMap<>(); List<Map<String, Object>> rows = new ArrayList<>(); for (int i = 0; i < 5; i++) { Map<String, Object> row = new HashMap<>(); row.put("departmentName", "部门" + i); row.put("targetMoney", 1000 + i); // ... 其他字段 rows.add(row); } data.put("contractTable", rows); 4. 代码渲染: XWPFTemplate template = XWPFTemplate.compile("template.docx").render(data); template.writeAndClose(new FileOutputStream("output.docx")); 注意:在poi-tl中,表格行循环是通过配置插件来实现的。上面的写法需要确认模板中的占位符是否按照poi-tl的表格行插件规则编写。实际上,引用[2]中提到的占位符式是:在表格中,占位符contractTable对应的值是一个列表,列表中的每个对象对应一行,对象中的字段名与模板行中的方括号内的字段名一致。 如果使用poi-tl,我们还需要注意图片、区块对等特殊标签的处理(如引用[2]中提到的图片使用{{@var}},区块对使用{{?var}}...{{/var}})。 因此,我们更推荐使用poi-tl,因为它简化了动态表格的生成。 如果用户坚持使用原生POI,我们可以给出一个基本步骤(不完整代码,仅示意): // 1. 加载模板 XWPFDocument doc = new XWPFDocument(new FileInputStream("template.docx")); // 2. 获取第一个表格(假设目标表格是第一个) XWPFTable table = doc.getTables().get(0); // 3. 获取模板行(假设第二行为模板行,第一行为表头) XWPFTableRow templateRow = table.getRow(1); // 4. 移除模板行(如果不移除,则第一行是表头,第二行是模板行,我们保留表头,然后从第二行开始插入新行) table.removeRow(1); // 移除模板行 // 5. 循环数据,创建新行 for (int i = 0; i < dataList.size(); i++) { // 创建新行,复制模板行的样式和单元结构 XWPFTableRow newRow = table.insertNewTableRow(1 + i); // 插入到第i+1行(表头之后) // 复制模板行的所有单元(包括样式) for (int j = 0; j < templateRow.getTableCells().size(); j++) { XWPFTableCell newCell = newRow.addNewTableCell(); // 复制样式(这里需要深度复制样式,包括字体等,比较复杂) // 复制内容:这里假设模板行的每个单元有特定的文本,我们要替换为数据 // 注意:模板行中可能包含占位符,但我们这里没有占位符,所以需要根据数据填充 String value = ...; // 根据当前行数据和列位置获取值 newCell.setText(value); // 如果需要复制样式,可以参考:复制模板行单元的样式到newCell(需要处理段落、字体等) } } // 6. 保存 FileOutputStream out = new FileOutputStream("output.docx"); doc.write(out); out.close(); 注意:原生POI操作表格样式复制非常繁琐,需要复制单元的样式(如背景色、边框、字体等)以及段落样式。因此,对于复杂表格,强烈建议使用poi-tl。 总结:根据用户的需求,我们推荐使用poi-tl来实现,因为它是基于POI模板引擎,专门为此类场景设计。 下面我们给出poi-tl的完整示例代码(包括依赖)和模板设计说明: 依赖(pom.xml): <dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.10.0</version> </dependency> 模板设计(template.docx): 创建一个表格包含表头行(比如:部门、目标金额)和一行数据行(数据行中填写占位符,如:`[departmentName]`和`[targetMoney]`)。注意,这个数据行就是将要被循环渲染的行。 然后,我们需要为整个表格设置一个循环区域(实际上poi-tl会自动识别,因为我们把占位符{{contractTable}}放在了一个特定的位置?)。根据poi-tl文档,表格行循环的占位符应该放在表格的下一行(在表格后面的段落中)?但引用[2]的描述似乎是将占位符直接放在表格内部作为行的标记。实际上,poi-tl提供了多种方式,我们可以使用行循环标签({{#var}})在表格内部。 更准确的做法(参考poi-tl官方文档): 在表格中,从第二行开始,在每一行的第一个单元中,使用{{#var}}来标记循环的开始,在最后一行的最后一个单元使用{{/var}}来标记结束。但这样比较麻烦。 另一种简单的方式是使用表格行插件(RowRenderPolicy): 我们需要在模板中,将占位符{{contractTable}}放在表格的下面(紧挨着表格的段落),然后通过配置策略将该占位符绑定到表格的行循环。 但是,引用[2]中的描述并没有配置策略,而是直接使用{{contractTable}}作为表格行循环的占位符,并且在数据行中使用了[fieldName]这样的占位符。这实际上是poi-tl的默认表格行插件机制吗? 实际上,poi-tl支持通过自定义模板语法来实现,但默认情况下,表格行循环使用区块对标签({{?var}} ... {{/var}})并配合指令({{@}})来定义行循环。不过,poi-tl也内置了表格行循环的插件,可以通过以下方式配置: ```java Configure config = Configure.builder().bind("contractTable", new MiniTableRenderPolicy()).build(); ``` 但更常见的做法是使用区块对(Loop表格行)标签(引用[2]中提到了区块对标签)。因此,我们按照区块对标签的方式: 模板设计: 表格第一行:表头 表格第二行:在第二行的每个单元中,使用{{#var.fieldName}},并且整个第二行被区块对标签包围: 在第二行上面创建一个段落(在表格内部),写上{{?contractTable}}(注意:这个段落必须在表格内部,且是第二行之前的一个段落?实际上不对,区块对标签应该跨段落) 实际上,在表格中使用区块对标签的正确位置是在表格的单元内。我们可以在第二行的第一个单元的开头(或第一个单元内)写上{{?contractTable}},在第二行的最后一个单元内写上{{/contractTable}},然后将第二行中间单元的占位符改为{{item.fieldName}}。 但是,这样就会将第二行作为一个循环体,然后循环渲染多行。 示例模板表格): 表头行:<tr><th>部门</th><th>目标金额</th></tr> 数据行:<tr> <td>{{?contractTable}}{{item.departmentName}}</td> <td>{{item.targetMoney}}{{/contractTable}}</td> </tr> 然而,这样写区块对标签跨越了两个单元,可能不符合要求。我们可以将开始标签放在第一个单元的开头,结束标签放在最后一个单元的末尾。 另一种推荐的方式:使用行循环指令(poi-tl 1.8.0以上支持): 在表格中,我们可以在数据行所在的行(整个行)设置一个指令标签:{{@contractTable}},然后在每个单元中直接使用字段名,如{{departmentName}},然后poi-tl会自动将这一行作为循环行。 根据引用[2]的描述,他们使用了{{contractTable}}作为占位符,并且在单元中使用[fieldName](方括号)作为字段占位符。这可能是他们自定义的语法,或者是使用了某种插件。因此,为了简化,我们按照poi-tl官方文档的示例来进行。 由于篇幅和复杂性,我们直接给出poi-tl官方推荐的一种表格行循环方式(使用区块对): 模板表格: 第一行(表头):部门 | 目标金额 第二行(数据行): 第一个单元:{{?contractTable}}{{departmentName}} 第二个单元:{{targetMoney}}{{/contractTable}} 注意:区块对的开始和结束标签必须在同一个单元内?实际上,poi-tl支持跨单元的区块对。 但是,更常用的做法是使用表格行循环插件(MiniTableRenderPolicy),它要求将占位符放在表格后面,然后通过策略绑定(见poi-tl的文档示例:https://deepoove.com/poi-tl/)。 由于引用[2]并没有给出确切的做法,我们按照poi-tl的官方示例进行: 示例(使用poi-tl的循环表格行): 模板文件: 表格如下: 表头:部门、目标金额 下面一行(数据行):部门列单元写[departmentName],目标金额列单元写[targetMoney] 然后,在表格之后(紧挨着表格的段落)写一个占位符{{contractTable}}(这里注意,这个占位符必须在表格后面,并且单独一行)。 代码: Map<String, Object> data = new HashMap<>(); // 准备表格数据 List<Map<String, Object>> tableData = new ArrayList<>(); for (int i = 0; i < 5; i++) { Map<String, Object> row = new LinkedHashMap<>(); row.put("departmentName", "部门"+i); row.put("targetMoney", 10000 + i * 1000); tableData.add(row); } data.put("contractTable", tableData); // 注意:这里需要配置表格行循环策略 Configure config = Configure.builder() .bind("contractTable", new LoopRowTableRenderPolicy()).build(); // LoopRowTableRenderPolicy是自定义策略吗?实际上,poi-tl默认提供了表格渲染策略,但这里我们需要使用行循环策略。 实际上,poi-tl默认的区块对标签就可以循环渲染表格行,但这里我们使用MiniTableRenderPolicy(用于渲染一个表格)并不适用循环。所以,我们可以使用循环区块对标签({{?var}})来实现表格行的循环。 鉴于引用[2]中的描述,他们似乎没有配置额外的策略,而是直接使用区块对标签和表格行插件。因此,我们换一种更简单的方式:使用区块对标签循环表格行(不需要配置策略)。 模板设计(使用区块对): 表格的第一行:表头 表格的第二行:整个行用区块对标签包围(开始标签在行首,结束标签在行尾): 在第二行的第一个单元的开头写上:{{?contractTable}} 在第二行的最后一个单元的末尾写上:{{/contractTable}} 单元中的内容使用{{字段名}},例如:{{departmentName}}, {{targetMoney}} 代码中数据模型: data.put("contractTable", tableData); // tableData为List<Map<String, Object>>或List<Object> 这样,poi-tl会自动复制这一行,循环渲染。 因此,我们采用区块对的方式(官方示例:https://deepoove.com/poi-tl/#example-loop-table-row): 模板: 表头行 {{?contractTable}} 数据行:{{departmentName}} | {{targetMoney}} {{/contractTable}} 注意:这里的区块对标签是写在表格的单元内。具体位置: 第二行,第一个单元:输入 {{?contractTable}} 和 {{departmentName}} 第二个单元:输入 {{targetMoney}} 然后在第二行最后一个单元的末尾(可以在最后那个单元中,在内容后面)写上 {{/contractTable}} 但是,这样写区块对标签跨越了多个单元?实际上,区块对标签可以跨单元,但必须开始和结束标签在同一个表格行内。 因此,我们这样设计模板(第二行): 单元1:{{?contractTable}}{{departmentName}} 单元2:{{targetMoney}}{{/contractTable}} 这样,区块对标签在第二行内部。 代码示例(poi-tl): XWPFTemplate template = XWPFTemplate.compile("template.docx").render(data); template.writeToFile("output.docx"); 不需要额外配置策略。 综上所述,我们推荐使用poi-tl的区块对标签来实现表格行的循环生成。 最后,我们给出一个完整的poi-tl区块对表格行循环的示例: 模板文件(template.docx): 表格: 第一行(表头):部门 | 目标金额 第二行: 第一个单元:{{?contractTable}}{{departmentName}} 第二个单元:{{targetMoney}}{{/contractTable}} 注意:区块对的开始和结束标签必须在同一行内。 Java代码: import com.deepoove.poi.XWPFTemplate; import java.io.FileOutputStream; import java.util.*; public class PoiTLExample { public static void main(String[] args) throws Exception { Map<String, Object> data = new HashMap<>(); List<Map<String, Object>> contractTable = new ArrayList<>(); Map<String, Object> row1 = new HashMap<>(); row1.put("departmentName", "研发部"); row1.put("targetMoney", "100000"); contractTable.add(row1); Map<String, Object> row2 = new HashMap<>(); row2.put("departmentName", "市场部"); row2.put("targetMoney", "200000"); contractTable.add(row2); data.put("contractTable", contractTable); XWPFTemplate template = XWPFTemplate.compile("template.docx").render(data); template.writeAndClose(new FileOutputStream("output.docx")); } } 运行后,模板中的第二行会根据contractTable列表循环生成两行数据。 注意:如果模板中第二行本身是隐藏的模板行,不希望被显示,那么可以在区块对标签外再套一层区块对,用于控制整行的显示?但实际上,区块对标签内的内容会被循环,而区块对标签本身会被替换(不显示)。所以,这样写没有问题。 因此,我们提供了两种方式,但推荐使用poi-tl的区块对方式。 如果用户希望看到原生POI的方式,我们也可以给出一个简化版的代码(不处理样式复制): // 原生POI示例(不处理样式复制,仅文本) XWPFDocument doc = new XWPFDocument(new FileInputStream("template.docx")); // 获取第一个表格 XWPFTable table = doc.getTables().get(0); // 获取模板行(假设第一行为表头,第二行为模板行) XWPFTableRow templateRow = table.getRow(1); // 移除模板行 table.removeRow(1); // 准备数据 List<String[]> dataList = Arrays.asList( new String[]{"研发部", "100000"}, new String[]{"市场部", "200000"} ); for (String[] rowData : dataList) { XWPFTableRow newRow = table.createRow(); // 在末尾添加一行 // 复制模板行的单元数量 for (int i = 0; i < templateRow.getTableCells().size(); i++) { XWPFTableCell newCell = newRow.getCell(i); if (newCell == null) { newCell = newRow.addNewTableCell(); } newCell.setText(rowData[i]); } } // 保存 FileOutputStream out = new FileOutputStream("output.docx"); doc.write(out); out.close(); doc.close(); 注意:这个原生POI示例没有复制样式,所以生成的表格行没有模板行的样式(如字体、颜色、对齐等)。如果需要复制样式,需要操作XWPFRun的样式,比较复杂。 综上,我们强烈推荐使用poi-tl。 相关问题: 1. 如何在使用poi-tl时处理表格样式? 2. 使用原生POI如何复制表格行的样式? 3. 如何在Word模板中设计复杂的动态表格(例如合并单元)? 请根据实际需求选择方法。 </think>### Java POI根据Word模板生成表格的实现方法 使用Apache POI根据Word模板生成表格主要分为两种方式:**原生POI API操作**和**模板引擎(如poi-tl)**。以下是具体实现方法: --- ### 一、原生POI API操作表格(适合简单表格) #### 核心步骤: 1. **读取模板**:加载包含表格Word模板文件 2. **定位表格**:获取文档中的目标表格对象 3. **操作单元**:修改或新增行列数据 4. **输出文档**:保存生成的新文件 #### 代码示例: ```java import org.apache.poi.xwpf.usermodel.*; import java.io.FileInputStream; import java.io.FileOutputStream; public class PoiTableGenerator { public static void main(String[] args) throws Exception { // 1. 加载模板 XWPFDocument doc = new XWPFDocument( new FileInputStream("template.docx") ); // 2. 获取第一个表格(索引0) XWPFTable table = doc.getTables().get(0); // 3. 操作表格数据(示例:修改第二行第一列) table.getRow(1).getCell(0).setText("新数据"); // 4. 添加新行 XWPFTableRow newRow = table.createRow(); newRow.getCell(0).setText("新增行-列1"); newRow.addNewTableCell().setText("新增行-列2"); // 5. 保存文件 FileOutputStream out = new FileOutputStream("output.docx"); doc.write(out); out.close(); doc.close(); } } ``` #### 关键对象说明: | 类 | 作用 | |-------------------|-------------------------------| | `XWPFDocument` | 代表整个Word文档 | | `XWPFTable` | 操作表格的容器 | | `XWPFTableRow` | 表格行对象 | | `XWPFTableCell` | 表格单元对象 | --- ### 二、使用poi-tl模板引擎(推荐复杂表格) #### 优势: - 支持模板语法(如循环、条件判断) - 自动处理样式复制 - 简化动态内容生成 #### 实现步骤: 1. **模板设计**: - 在Word模板中使用占位符:`{{#data}}`标记表格循环区域 - 单元内使用字段名:`[fieldName]` 2. **添加依赖**: ```xml <dependency> <groupId>com.deepoove</groupId> <artifactId>poi-tl</artifactId> <version>1.12.1</version> </dependency> ``` 3. **代码实现**: ```java import com.deepoove.poi.XWPFTemplate; import java.util.*; public class PoiTLExample { public static void main(String[] args) throws Exception { // 准备数据模型 Map<String, Object> data = new HashMap<>(); List<Map<String, String>> rows = new ArrayList<>(); // 添加行数据 rows.add(new HashMap<String, String>() {{ put("name", "张三"); put("score", "90"); }}); rows.add(new HashMap<String, String>() {{ put("name", "李四"); put("score", "85"); }}); data.put("tableData", rows); // 对应模板中的{{tableData}} // 生成文档 XWPFTemplate template = XWPFTemplate .compile("template.docx") .render(data); template.writeToFile("output.docx"); template.close(); } } ``` #### 模板示例 (`template.docx`): ```markdown | 姓名 | 成绩 | |------------|--------| {{#tableData}} | [name] | [score]| {{/tableData}} ``` --- ### 三、关键注意事项 1. **样式继承**: - 原生POI需手动复制样式:`newCell.getCTTc().setTcPr(templateCell.getCTTc().getTcPr().copy())` 2. **动态行处理**: ```java // 原生POI动态添加多行 for (Object rowData : dataList) { XWPFTableRow newRow = table.createRow(); // 填充单元数据... } ``` 3. **合并单元**: - 使用`table.addMergedRegion(startRow, endRow, startCol, endCol)` 4. **图片/图表处理**: - 需特殊占位符:`{{@imageField}}`(poi-tl) - 图表数据需通过内嵌Excel更新[^5] --- ### 四、方案对比 | **方法** | 适用场景 | 复杂度 | 维护性 | |----------------|----------------------------|--------|--------| | 原生POI API | 简单表格、少量动态内容 | ★★★☆☆ | ★★☆☆☆ | | poi-tl引擎 | 复杂模板、循环表格、多元素混合 | ★★☆☆☆ | ★★★★★ | ---
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值