Jamie Kabzinski
Jamie Kabzinski

Reputation: 1

How do I maintain cell widths when merging cells in a word table using apache poi?

I'm creating a word document with a table using Java and Apache POI.

I can create the table easily enough, set each column with different widths and then merge the cells to produce the desired effect (see images below) however when I open the word document some of the cells have been adjusted so that their edges snap together. I have found that adding an additional row to the beginning of the table and leaving all cells unmerged keeps the rest of the rows intact, but removing this row later using table.removeRow(0); affects the rest of the rows. If I open the word document and manually delete the row, the cells stay where they are. Is there anything I can do to preserve the layout of the cells?

correct layout with an additional unmerged top row

the result after removing the top row

This is the function that creates the word doc and table:

public static void createWord() {
    // Blank Document
    XWPFDocument document = new XWPFDocument();
    
    CTSectPr sectPr = document.getDocument().getBody().addNewSectPr();
    CTPageMar pageMar = sectPr.addNewPgMar();
    pageMar.setLeft(BigInteger.valueOf(300L));
    pageMar.setTop(BigInteger.valueOf(300L));
    pageMar.setRight(BigInteger.valueOf(300L));
    pageMar.setBottom(BigInteger.valueOf(300L));

    XWPFParagraph paragraph = document.createParagraph();
    paragraph.setSpacingBefore(0);
    paragraph.setSpacingAfter(0);
    
    // determine the number of rows and columns required 
    int rows = 3;
    int cols = 6; 
    
    // create table
    XWPFTable table = document.createTable(rows+1, cols);
    CTTblPr tblPr = table.getCTTbl().getTblPr();
    if (null == tblPr) {
        tblPr = table.getCTTbl().addNewTblPr();
    }

    // set table width
    CTTblWidth width = table.getCTTbl().addNewTblPr().addNewTblW();
    width.setType(STTblWidth.PCT);
    width.setW(BigInteger.valueOf(5000)); // 5000 * 1/50 = 100%
    
    //set row height
    for(XWPFTableRow row:table.getRows()) {
        row.setHeight(22);
    }
    
    // set width of each column
    for (int row = 0; row <= rows; row++) {
        setCellWidthPercentage(table, row, 0, 0.188);
        setCellWidthPercentage(table, row, 1, 0.125);
        setCellWidthPercentage(table, row, 2, 0.063);
        setCellWidthPercentage(table, row, 3, 0.25);
        setCellWidthPercentage(table, row, 4, 0.25);
        setCellWidthPercentage(table, row, 5, 0.125);
    }
    
    mergeCellHorizontally(table, 1, 0, 2);
    mergeCellHorizontally(table, 2, 0, 1);
    mergeCellHorizontally(table, 2, 2, 4);
    mergeCellHorizontally(table, 3, 1, 3);
    
    // remove first row (comment out this line to see issue)
    table.removeRow(0);
    
    // Write the Document in file system
    try {
        File docFile = new File("C:\\doc.docx");
        docFile.createNewFile();
        FileOutputStream out = new FileOutputStream(docFile, false); 
        
        document.write(out);
        out.close();
        document.close();           
    } catch(Exception ex) {
        ex.printStackTrace();
    }
}

I'm using the code below to merge cells horizontally:

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);
        }
        
        // Try getting the TcPr. Not simply setting an new one every time.
        CTTcPr tcPr = cell.getCTTc().getTcPr();
        
        if (tcPr != null) {
            tcPr.setHMerge(hmerge);
        } else {
            // only set an new TcPr if there is not one already
            tcPr = CTTcPr.Factory.newInstance();
            tcPr.setHMerge(hmerge);
            cell.getCTTc().setTcPr(tcPr);
        }
    }
}

and this function to assign width values to the columns before merging:

private static void setCellWidthPercentage(XWPFTable table, int row, int col, double width) {
    // prevent out of bounds exception
    if (row < 0 || row >= table.getRows().size()) return;
    if (col < 0 || col >= table.getRow(row).getTableCells().size()) return;
    
    // assign widths in units of 1/50 of a percentage
    CTTblWidth tblW = table.getRow(row).getCell(col).getCTTc().addNewTcPr().addNewTcW();
    tblW.setType(STTblWidth.PCT);
    tblW.setW(BigInteger.valueOf(Math.round(width * 50)));
}

Thanks in advance!

Upvotes: 0

Views: 1518

Answers (1)

Axel Richter
Axel Richter

Reputation: 61852

The problem you see is that Word renders tables in respect of the column width settings of the row which has the most columns in it. If other rows contradict the column width settings of that row, then their column width setting will be ignored. And after merging cells you are not correcting the column width settings. For example after mergeCellHorizontally(table, 0, 0, 2); column 0 in row 0 is up to column 2 now. So column 0 now need width of formerly columns 0 + 1 + 2. But since you are not correcting that, it stays width of formerly column 0 only and gets ignored while rendering if that contradicts the width settings of the row having the most columns.

So the main problem is that your code lacks correcting the column width settings in the rows after merging cells.

I have shown this already in how to set specific cell width in different row in apache poi table?.

But there are more issues.

First the method mergeCellHorizontally should merge cells horizontally by setting grid span instead of using CTHMerge. This is much more compatible to all kinds of word processing applications which open *.docx files than using CTHMerge.

Second there always should be used the last apache poi version. Current apache poi 4.1.2 provides XWPFTable.setWidth and XWPFTableCell.setWidth. So no own set-width-methods are necessary.

And third you should create a table grid for the table with widths of the columns. This is necessary for Libreoffice/OpenOffice to accept the column widths. Unfortunately this needs calculating the column widths in unit twentieths of a point (1/1440 of an inch) since TblGrid - GridCol does not accepts percent values.

The following complete example shows all this and creates the table you want.

import java.io.FileOutputStream;

import java.math.BigInteger;

import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun;

import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTTcPr;

public class CreateWordTableMergedCells {

 //merging horizontally by setting grid span instead of using CTHMerge
 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));
  }
  // Cells which join (merge) the first one, must be removed
  for(int colIndex = toCol; colIndex > fromCol; colIndex--) {
   table.getRow(row).getCtRow().removeTc(colIndex);
   table.getRow(row).removeCell(colIndex);
  }
 }

 public static void main(String[] args) throws Exception {

  XWPFDocument document= new XWPFDocument();

  XWPFParagraph paragraph = document.createParagraph();
  XWPFRun run=paragraph.createRun();  
  run.setText("The table:");

  // determine the number of rows and columns required 
  int rows = 3;
  int cols = 6; 

  //create table
  XWPFTable table = document.createTable(rows, cols);

  //set table width
  table.setWidth("100%"); 

  double[] columnWidths = new double[] { // columnWidths in percent
   0.188, 0.125, 0.062, 0.25, 0.25, 0.125
  };

  //create CTTblGrid for this table with widths of the columns. 
  //necessary for Libreoffice/Openoffice to accept the column widths.
  //values are in unit twentieths of a point (1/1440 of an inch)
  int w100Percent = 6*1440; // twentieths of a point (1/1440 of an inch); 6 inches
  //first column
  table.getCTTbl().addNewTblGrid().addNewGridCol().setW(BigInteger.valueOf(
   Math.round(w100Percent*columnWidths[0])));
  //other columns
  for (int c = 1; c < cols; c++) {
   table.getCTTbl().getTblGrid().addNewGridCol().setW(BigInteger.valueOf(
    Math.round(w100Percent*columnWidths[c])));
  }

  // set width of each column in each row
  for (int r = 0; r < rows; r++) {
   for (int c = 0; c < cols; c++) {
    table.getRow(r).getCell(c).setWidth("" + (columnWidths[c]*100.0) + "%");
   }
  }

  //using the merge method
  mergeCellHorizontally(table, 0, 0, 2); // after that column 0 is up to column 2
  //column 0 now need width of formerly columns 0 + 1 + 2
  table.getRow(0).getCell(0).setWidth("" + ((columnWidths[0]+columnWidths[1]+columnWidths[2])*100.0) + "%");

  mergeCellHorizontally(table, 1, 0, 1); // after that column 0 is up to column 1
  //column 0 now need width of formerly columns 0 + 1
  table.getRow(1).getCell(0).setWidth("" + ((columnWidths[0]+columnWidths[1])*100.0) + "%");
  mergeCellHorizontally(table, 1, 1, 3); // formerly col 2 is now col 1 and after that formerly column 2 is up to column 4
  //current column 1 now need width of formerly columns 2 + 3 + 4
  table.getRow(1).getCell(1).setWidth("" + ((columnWidths[2]+columnWidths[3]+columnWidths[4])*100.0) + "%");
 
  mergeCellHorizontally(table, 2, 1, 3); // after that column 1 is up to column 3
  //column 1 now need width of formerly columns 1 + 2 + 3
  table.getRow(2).getCell(1).setWidth("" + ((columnWidths[1]+columnWidths[2]+columnWidths[3])*100.0) + "%");

  paragraph = document.createParagraph();

  FileOutputStream out = new FileOutputStream("create_table.docx"); 
  document.write(out);
  out.close();
 }
}

Upvotes: 1

Related Questions