Apache POI Word - Table

Last Modified: 2023/11/14

Overview

The goal of this article is to create a relatively rich-styled table using the POI Word API, and by reading this article, you will be able to grasp the basic operations of tables. Before we begin, let's take a look at a table, and then we will implement the table using code.

This table may not look complicated, but it is not that easy to implement. By closely observing the table, it has the following features that we need to implement:

  • The table has dotted borders.
  • The first row of the table has an orange background color.
  • The first row of the table has merged cells.
  • The first row of the table has a relatively higher height, requiring setting the row height, and the text in the table is horizontally and vertically centered.

Now let's start using the Apache POI Word API to implement this table.

Create table

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")) {
                // Create a table with one row and two columns
                XWPFTable table = doc.createTable(1, 2);
                // The main part to be implemented
                // After the main part implementation is completed, save the document
                doc.write(out);
            }
        }
    }
}

Creating a table is quite simple, but there is one thing to clarify. Although the table is supposed to have two rows and two columns, why are we creating a table with only one row and two columns? It's because we can dynamically add rows later using table.createRow(). However, it is also possible to create the table initially with two rows and two columns.

After creating the table, it is generally necessary to add a grid to the table. The grid defines the width of each column in the table, which is also useful for merging cells in the future.

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)));

The width unit of the grid is twips, where a twip is 1/20 of a point. There are 1440 twips in one inch. Assuming our table's first column is 4cm and the second column is 8cm, we need to divide them by 2.54 to get inches and then multiply by 1440 to get twips.

Set the border for the table

The table border includes four borders for the top, bottom, left, and right sides, as well as horizontal and vertical gridlines. Setting the border involves specifying the border style, thickness, space, and color. The border style can be specified using the XWPFTable.XWPFBorderType enumeration. Here, the border width is set to 4, and the unit for border width is "1/8 point," which means the border width is equal to 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");

Note: How wide is 0.5 point? Since we know that 1 twip = 1/20 point and 1 inch = 1440 twips, therefore, you should be able to imagine approximately how long 1 point is. It is worth mentioning that POI has inconsistent unit requirements for many APIs, which often require conversion. This aspect is not very user-friendly.

Set the cell background color

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

To retrieve the first row of the table, use table.getRow(0). This is easy to understand. However, does it mean that you can retrieve the second row using table.getRow(1)? The answer is NO because the current table only has one row and two columns. Therefore, table.getRow(1) will throw an error. You need to first use table.createRow() to create an additional row before using table.getRow(1).

Set line height for the first row

Suppose we want to set the row height of the table to 2cm, but the setHeight() method accepts values in twips. Therefore, we need to convert centimeters to twips.

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

Set text to cell

row1.getCell(0).setText("row 1 cell 1");
row1.getCell(1).setText("row 1 cell 2");
// create the second row
XWPFTableRow row2 = table.createRow();
row2.getCell(0).setText("row 2 cell 1");
row2.getCell(1).setText("row 2 cell 2");

Center the text horizontally and vertically in the cells

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

Notice that only the first cell is set here, and the second cell is not set because it will be merged. Therefore, there is no need for additional settings for the second cell.

Compatibility

The table is almost complete at this point, but there are still some issues, especially with the table width not being 12cm on WPS. Remember that we set the grid with the first column as 4cm and the second column as 8cm, but the table width is still not displayed correctly in WPS.

To ensure compatibility with WPS, we need to set the width for each cell in the first row again.

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

1440 is a magical number that immediately indicates the unit is twips. However, this API unexpectedly accepts a string...

Before merging the cells, running the program will generate the following table:

Merge cells

There are two ways to merge cells: one is by setting the GridSpan property, and the other is by using the CTHMerge (for horizontal merging) or CTVMerge (for vertical merging) objects.

Merge horizontally using 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));
    }
    // The merged cells need to be removed
    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
    }
}

The code snippet comes from stackoverflow: how-to-horizontally-merge-xwpftable-using-poi-in-java

There are two points to note here:

First, this method requires adding a grid to the table, which has already been done earlier.

Second, the merged cells need to be removed. However, there is a compatibility issue. In POI 4.1.1 and earlier versions, you need to use removeTc() to remove the cell, while in POI 4.1.1 and later versions, you should use removeCell().

However, this code is still not perfect as there are still display issues in WPS. Normally, the width of the merged cell should be the sum of the widths of all the cells involved in the merge. However, in WPS, the width is only based on the width of the first cell. Therefore, this code still needs improvement.

Please refer to this link for the improved code: TableTools.java

Merge horizontally using 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();
}

In fact, it involves setting an HMerge attribute for each cell. The value of HMerge for the first cell is set to STMerge.RESTART, while for the other merged cells, the value of HMerge is set to STMerge.CONTINUE.

With this merge method, it becomes easy to merge the two cells in the first row of the table.

mergeCellHorizontally(table, 0, 0, 1);

However, there is one pitfall that needs to be mentioned. This method can only be called after creating the second row. If it is called prematurely, the merged table will become a single cell in one row. Consequently, when using table.createRow() to create a new row, it will also have only one cell. At this point, using row2.getCell(1) will result in an index out of bounds error.

Merge vertically using 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);
  }
}

If you need the complete code, please refer to this link:CreateTable.java

Feedback

Notice:Feedback requires logging into the system first.