Apache POI Word - 表格

Last Modified: 2023/11/08

概述

本文的目标是使用 POI Word API 制作一张样式相对丰富的表格,通过阅读本文能够掌握基本的表格操作。 在开始之前我们先看一张表格,然后我们通过代码来实现该表格。

这张表格看起来不复杂,实现起来却不那么容易,仔细观察该表格,它至少有以下特性需要我们实现:

  • 表格的边框是点状的
  • 表格的第一行有橙色的填充色
  • 表格的第一行合并了单元格
  • 表格的第一行的高度相对较高,需要设置行高并且表格中的文字是水平垂直居中的

下面我们开始使用 Apache POI Word API 来实现这张表格。

创建表格

import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;

import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;

public class CreateTable {

    public static void main(String[] args) throws IOException {
        try (XWPFDocument doc = new XWPFDocument()) {
            try (FileOutputStream out = new FileOutputStream("D:\\tmp\\table.docx")) {
                // 创建一个一行两列的表格
                XWPFTable table = doc.createTable(1, 2);
                // 主要逻辑待实现 ...
                // 主要逻辑实现完成后,保存文档
                doc.write(out);
            }
        }
    }
}

创建 table 相当简单,有一点需要说明,表格明明是两行两列的,为什么创建的是一行两列的呢?因为后面可以通过 table.createRow() 来动态添加行。当然一开始就创建两行两列也是完全可以的。

创建表格之后,一般需要给表格添加一个 grid(网格),grid 规定了表格每一列的宽度,这对以后合并单元格也很有作用。

CTTblGrid grid = table.getCTTbl().addNewTblGrid();
grid.addNewGridCol().setW(BigInteger.valueOf((long) (4 / 2.54 * 1440)));
grid.addNewGridCol().setW(BigInteger.valueOf((long) (8 / 2.54 * 1440)));

grid 的宽度单位是 twips, twip 是 point 的 1/20。一英寸有 1440 twips,假设我们的表格第一列 4cm,第二列 8cm,那么需要先除以 2.54 得到英寸再乘以 1440 得到 twips。

给表格设置边框

表格边框包括上下左右四个边框,外加横向和竖向的网格线。设置边框实际上是设置边框的样式、粗细、Space 和 边框颜色。边框样式可以使用 XWPFTable.XWPFBorderType 枚举来指定。这里指定的边框的宽度均为 4,边框宽度的单位是 “1/8 point”,也就是边框宽等于 4*1/8 = 0.5 point。

table.setLeftBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");
table.setRightBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");
table.setTopBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");
table.setBottomBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");
able.setInsideHBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");
table.setInsideVBorder(XWPFTable.XWPFBorderType.DOTTED, 4, 0, "000000");

注:point 翻译过来是点,那 0.5 point 究竟多宽呢?由于知道 1 twip = 1/20 point,1 inch = 1440 twips,大家可自行换算 1 point 大概是多少英寸。 POI 很多 API 要求的单位不一致,时常需要换算,这一点不太友好。

设置单元格填充色

XWPFTableRow row1 = table.getRow(0);
row1.getCell(0).setColor("ffa500");
row1.getCell(1).setColor("ffa500");

获取表格的第一行使用 table.getRow(0),这很容易理解,那么是不是意味着获取第二行可以使用 table.getRow(1) 呢?答案是 NO,因为当前表格只有一行两列,因此 table.getRow(1) 会报错。需要先使用 table.createRow() 再创建一行之后才可以使用 table.getRow(1)

给第一行设置行高

假设我们想将表格的行高设置为 2cm,但是 setHeight() 方法接收的数值单位为 twips,因此需要将厘米转换为 twips。

row1.setHeight((int) (2 / 2.54 * 1440));

单元格填充文字

row1.getCell(0).setText("row 1 cell 1");
row1.getCell(1).setText("row 1 cell 2");
// 创建第二行
XWPFTableRow row2 = table.createRow();
row2.getCell(0).setText("row 2 cell 1");
row2.getCell(1).setText("row 2 cell 2");

单元格文字水平垂和直居中

row1.getCell(0).setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
row1.getCell(0).getParagraphs().get(0).setAlignment(ParagraphAlignment.CENTER);

注意到这里只设置了第一个单元格,第二个单元格则没有设置,因为第二个单元格会被合并掉,因此没必要做过多设置。

兼容性

到这里表格基本快要完成了,但是还有一些问题,特别是在 wps 上表格的宽度居然不是 12cm,记得之前我们有设置 grid,其中第一列 4cm,第二列 8cm,但是在 wps 中依然不能正常显示表格的宽度。

为了兼容 wps,需要给第一行的每个单元格再设置一遍宽度

row1.getCell(0).setWidth(Long.toString((long) (4 / 2.54 * 1440)));
row1.getCell(1).setWidth(Long.toString((long) (8 / 2.54 * 1440)));

1440 是一个神奇的数字,一看到它基本就知道单位是 twips。但是这个 API,居然接收的是字符串。。。

在未合并单元格之前,运行程序,生成的表格如下

合并单元格

合并单元格有两种方式:一种通过设置 GridSpan,另外一种 CTHMerge(水平方向合并) 或 CTVMerge(垂直方向合并) 对象来完成。

GridSpan 水平方向合并

public static void mergeCellHorizontally(XWPFTable table, int row, int fromCol, int toCol) {
    XWPFTableCell cell = table.getRow(row).getCell(fromCol);
    // Try getting the TcPr. Not simply setting an new one every time.
    CTTcPr tcPr = cell.getCTTc().getTcPr();
    if (tcPr == null) tcPr = cell.getCTTc().addNewTcPr();
    // The first merged cell has grid span property set
    if (tcPr.isSetGridSpan()) {
        tcPr.getGridSpan().setVal(BigInteger.valueOf(toCol - fromCol + 1));
    } else {
        tcPr.addNewGridSpan().setVal(BigInteger.valueOf(toCol - fromCol + 1));
    }
    // 被合并的单元格需要被移除
    for (int colIndex = toCol; colIndex > fromCol; colIndex--) {
        table.getRow(row).removeCell(colIndex); // use only this for apache poi versions greater than 4.1.1
        // table.getRow(row).getCtRow().removeTc(colIndex); // use this for apache poi versions up to 4.1.1
    }
}

这段代码来自 stackoverflow: how-to-horizontally-merge-xwpftable-using-poi-in-java

这里有两点需要注意:

  • 第一,这种方式需要给表格添加一个 grid,这点在之前已经做过了;
  • 第二,被合并的单元格需要被移除。但这个还有兼容性问题,在 POI 4.1.1 以及这之前需要使用 removeTc(),4.1.1 之后则需使用 removeCell()

但是这段代码依然不够完美,在 wps 中依然有显示问题,正常情况下合并后的单元格宽度应该是所有参与合并的单元格的宽度之和,但是在 wps 中宽度仅仅是第一个单元格的宽度。因此这段代码还需要改良。

改良后的代码请参考这里:TableTools.java

使用 CTHMerge 水平合并

public static void mergeCellHorizontally(XWPFTable table, int row, int fromCol, int toCol) {
  for (int colIndex = fromCol; colIndex <= toCol; colIndex++) {
    XWPFTableCell cell = table.getRow(row).getCell(colIndex);
    CTHMerge hmerge = CTHMerge.Factory.newInstance();
    if (colIndex == fromCol) {
      // The first merged cell is set with RESTART merge value
      hmerge.setVal(STMerge.RESTART);
    } else {
      // Cells which join (merge) the first one, are set with CONTINUE
      hmerge.setVal(STMerge.CONTINUE);
      // and the content should be removed
      clearCell(cell);
    }
    // Try getting the TcPr. Not simply setting an new one every time.
    CTTcPr tcPr = cell.getCTTc().getTcPr();
    if (tcPr == null) tcPr = cell.getCTTc().addNewTcPr();
    tcPr.setHMerge(hmerge);
  }
}

private static void clearCell(XWPFTableCell cell) {
  for (int i = cell.getParagraphs().size(); i > 0; i--) {
      cell.removeParagraph(0);
  }
  cell.addParagraph();
}

其实就是给每个单元格设置一个 HMerge 属性,第一个单元格 HMerge 的 value 为 STMerge.RESTART,其他被合并的单元格的 HMerge 的 value 为 STMerge.CONTINUE。

有了这个合并方法之后,合并表格第一行的两个单元格就很容易了

mergeCellHorizontally(table, 0, 0, 1);

不过还有一个坑需要提醒,这个方法只能在第二行创建完后才能调用,如果提前调用,合并之后的表格就变成一行一列了,那么再使用 table.createRow() 创建的新行也就只有一列,这个时候使用 row2.getCell(1) 就越界了。

使用 CTVMerge 垂直合并

public static void mergeCellVertically(XWPFTable table, int col, int fromRow, int toRow) {
  for (int rowIndex = fromRow; rowIndex <= toRow; rowIndex++) {
    XWPFTableCell cell = table.getRow(rowIndex).getCell(col);
    CTVMerge vmerge = CTVMerge.Factory.newInstance();
    if (rowIndex == fromRow) {
      // The first merged cell is set with RESTART merge value
      vmerge.setVal(STMerge.RESTART);
    } else {
      // Cells which join (merge) the first one, are set with CONTINUE
      vmerge.setVal(STMerge.CONTINUE);
      // and the content should be removed
      clearCell(cell);
    }
    // Try getting the TcPr. Not simply setting an new one every time.
    CTTcPr tcPr = cell.getCTTc().getTcPr();
    if (tcPr == null) tcPr = cell.getCTTc().addNewTcPr();
    tcPr.setVMerge(vmerge);
  }
}

如果需要完整代码参考这里:CreateTable.java

有问题吗?点此反馈!

温馨提示:反馈需要登录