2

I'm using Apache POI and I am running into a weird problem. I can auto-size my rows, but only if there are no merged cells in that row. Here's an example:

new FileOutputStream('test.xlsx').withStream { OutputStream os ->
    Workbook workbook = new XSSFWorkbook();
    Sheet sheet = workbook.createSheet();

    CellStyle wrapStyle = workbook.createCellStyle();
    wrapStyle.setWrapText(true);

    Row row = sheet.createRow(0); row.setRowStyle(wrapStyle);

    Cell cell = row.createCell(0); cell.setCellStyle(wrapStyle);
    cell.setCellValue("Very long text that needs to be wrapped")

    cell = row.createCell(1); cell.setCellStyle(wrapStyle);
    cell.setCellValue("Short text");

    cell = row.createCell(2); cell.setCellStyle(wrapStyle);
    cell.setCellValue("");

    // These two lines break row auto-height!
    //
    CellRangeAddress cellRangeAddress = new CellRangeAddress(0, 0, 1, 2);
    sheet.addMergedRegion(cellRangeAddress);

    workbook.write(os);
}

This code generates the following document: enter image description here

However, as soon as I comment out the lines that merge the two cells, the output looks like this: enter image description here

Is this a bug? Does anyone know of a workaround?

Val Blant
  • 1,664
  • 2
  • 24
  • 34
  • What do you mean by autosize? sheet.setColumnWidth? –  Jun 10 '16 at 09:44
  • @RC. I mean that the row height should be adjusted dynamically as the text is being wrappped, as shown in the 2nd image. My entire code is shown above. – Val Blant Jun 10 '16 at 10:01
  • possible duplicate: http://stackoverflow.com/questions/19145628/auto-size-height-for-rows-in-apache-poi –  Jun 10 '16 at 10:03

3 Answers3

1

After more research, turns out that this is a problem with Excel itself, not POI. Excel does indeed lose its ability to auto-fit rows to content for all rows that have merged cells in them. For more info see:

http://excel.tips.net/T003207_Automatic_Row_Height_For_Merged_Cells_with_Text_Wrap.html http://blog.contextures.com/archives/2012/06/07/autofit-merged-cell-row-height/

The workaround is based on predicting the number of lines in the largest cell of the row, and then adjusting the row height manually. The code is based on this discussion:

http://mail-archives.apache.org/mod_mbox/poi-user/200906.mbox/%3C24216153.post@talk.nabble.com%3E

RowInfo and NestedCellInfo below are my custom data structures that keep track of the sheet layout. You should be able to replace these with your equivalents and helper functions.

private void adjustRowHeights(Sheet sheet, List<RowInfo> rows, SortedSet<Integer> createdColumnNumbers) {
    SortedMap<Integer, Float> columnWidthsInPx = [] as TreeMap;
    createdColumnNumbers.each {
        columnWidthsInPx.put(it,  sheet.getColumnWidthInPixels(it));
    }
    rows.each { RowInfo rowInfo ->
        if ( rowInfo.hasMergedCells ) {
            Row excelRow = sheet.getRow(rowInfo.rowIndex);

        // Find the column with the longest text - that's the one that will determine
        // the row height
        //
        NestedCellInfo longestCell = rowInfo.getCellWithLongestContent();
        String cellText = longestCell.getText();
        if ( cellText != null && cellText.size() > 5 ) {
            int colIdx = rowInfo.cells.indexOf(longestCell);

            // Figure out available width in pixels, taking colspans into account
            //
            float columnWidthInPx = columnWidthsInPx[colIdx];
            int numberOfMergedColumns = longestCell.colSpan;
            (numberOfMergedColumns - 1).times {
                columnWidthInPx += columnWidthsInPx[colIdx + it];
            }

            // Setup the font we'll use for figuring out where the text will be wrapped
            //
            XSSFFont cellFont = longestCell.getCellFont();
            int fontStyle = Font.PLAIN;
            if ( cellFont.getBold() ) fontStyle = Font.BOLD; 
            if ( cellFont.getItalic() ) fontStyle = Font.ITALIC;

            java.awt.Font currFont = new java.awt.Font(
                cellFont.getFontName(), 
                fontStyle, 
                cellFont.getFontHeightInPoints());

            AttributedString attrStr = new AttributedString(cellText);
            attrStr.addAttribute(TextAttribute.FONT, currFont);

            // Use LineBreakMeasurer to count number of lines needed for the text
            //
            FontRenderContext frc = new FontRenderContext(null, true, true);
            LineBreakMeasurer measurer = new LineBreakMeasurer(attrStr.getIterator(), frc);
            int nextPos = 0;
            int lineCnt = 0;
            while (measurer.getPosition() < cellText.length()) {
                nextPos = measurer.nextOffset( columnWidthInPx );
                lineCnt++;
                measurer.setPosition(nextPos);
            }

            if ( lineCnt > 1 ) {
                excelRow.setHeight((short)(excelRow.getHeight() * lineCnt * /* fudge factor */ 0.7));
            }
        }
    }
}

This solution is far from perfect, but it allowed me to move forward.

Val Blant
  • 1,664
  • 2
  • 24
  • 34
1

By using the last part from Val Blant I've did something that's easier to use, and yet much complex. Please be aware that there is a single line of code that adds one additional line to the cell height-wise, for personal reasons. If you do not wish that, please remove it. Also feel free to change it to a non-static, I've had to use static methods due to the company I'm working for making the specific class static.

PS: It's my first post on stackoverflow, please be gentle. :)

Solution:

public static Boolean isCellMerged(Cell cell) {
        Sheet sheet = cell.getSheet();
        for (CellRangeAddress mergedRegionRange : sheet.getMergedRegions()) {
            Integer cellColumn = cell.getColumnIndex();
            Integer cellRow = cell.getRowIndex();
            if (mergedRegionRange.containsColumn(cellColumn) && mergedRegionRange.containsRow(cellRow)) {
                return true;
            }
        }
        return false;
    }

public static List<List<Cell>> getCellsInRowsInsideRegionRange(Cell cell) {
        Sheet sheet = cell.getSheet();
        List<List<Cell>> mergedRowList = new ArrayList<>();
        List<Cell> mergedCellsList = new ArrayList<>();

        //Nejdříve musíme zjistit sloučenou sekci dané buňky
        for (CellRangeAddress mergedRegionRange : sheet.getMergedRegions()) {
            Integer cellColumn = cell.getColumnIndex();
            Integer cellRow = cell.getRowIndex();
            if (mergedRegionRange.containsColumn(cellColumn) && mergedRegionRange.containsRow(cellRow)) {

                //Protože CellRangeAddress nemá moc metod, musíme si pomoci sami a získat z ní buňky a řádky
                for (Row row : sheet) {
                    for (Cell iteratedCell : row) {
                        Integer iteratedCellColumn = iteratedCell.getColumnIndex();
                        Integer iteratedCellRow = iteratedCell.getRowIndex();
                        if (mergedRegionRange.containsColumn(iteratedCellColumn) && mergedRegionRange.containsRow(iteratedCellRow)) {
                            //Rozdělování jednotlivých řádků
                            //Není-li řádek bez buněk...
                            if (!mergedCellsList.isEmpty()) {

                                //Tak buňku přidáme do Listu buněk...
                                mergedCellsList.add(iteratedCell);

                            } else {
                                //Pokud se jedná o první buňku prvního řádku, tak přidáme rovnou
                                mergedCellsList.add(iteratedCell);
                            }
                        }
                    }
                    //Vložíme List buněk daného řádku do Listu řádků
                    if (!mergedCellsList.isEmpty()) {
                        mergedRowList.add(mergedCellsList);
                    }

                    //A vyresetujeme list buněk (začneme tak nanovo novým řádkem)
                    mergedCellsList = null;
                    mergedCellsList = new ArrayList<>();
                }
                //Vrátíme výsledný List řádků, obsahující Listy buněk ve sloučené sekci.
                if (!mergedRowList.isEmpty()) {
                    return mergedRowList;
                } else {
                    return null;
                }
            }
        }
        return null;
    }

    public static void adjustRowHeightForRowWithNonMergedCells(Row row) {
        row.setHeight((short) -1);
    }

public static void adjustRowHeightForRowWithMergedCells(Row row) {
        Sheet sheet = row.getSheet();
        Cell longestTextCell = null;

        //Potřebujeme získat buňku s nejdelším textem
        for (Cell iteratedCell : row) {
            String iteratedTextString = iteratedCell.getStringCellValue();

            if (longestTextCell != null && StringUtils.isNotBlank(longestTextCell.getStringCellValue())) {
                if (iteratedTextString.length() > longestTextCell.getStringCellValue().length()) {
                    longestTextCell = iteratedCell;
                }
            } else {
                longestTextCell = iteratedCell;
            }

        }

        //Z textově nejobsáhlejší buňky potřebujeme dostat údaje
        String longestText = "";

        if (StringUtils.isNotBlank(longestTextCell.getStringCellValue()) && longestTextCell != null) {
            longestText = longestTextCell.getStringCellValue();

            //Protože textově nejobsáhlejší buňka nemusí nutně být sloučeného typu, je zapotřebí to všude ošetřit
            Boolean isLongestTextCellMerged = isCellMerged(longestTextCell);
            Float longestCellWidthInPixels = 0f;
            Float longestMergedCellWidthInPixels = 0f;

            //Získat šířku nesloučené nejobsáhlejší buňky je jednoduché
            if (!isLongestTextCellMerged) {
                Integer longestCellColumnIndex = longestTextCell.getColumnIndex();
                longestCellWidthInPixels = sheet.getColumnWidthInPixels(longestCellColumnIndex);

            } else {

                //Musíme přijít na šířku sloučené buňky namísto buňky uvnitř sloučené buňky
                List<List<Cell>> cellsInMergedRegion = getCellsInRowsInsideRegionRange(longestTextCell);
                longestMergedCellWidthInPixels = 0f;

                //Projdeme řádky
                for (List<Cell> iteratedCell2List : cellsInMergedRegion) {
                    Float iteratedMergedCell2WidthInPixels = 0f;

                    //Projdeme jednotlivé buňky ve sloučené buňce na řádku a sečteme jejich šířky
                    for (Cell iteratedCell2 : iteratedCell2List) {
                        Integer iteratedCell2ColumnIndex = iteratedCell2.getColumnIndex();
                        Float iteratedCell2ColumnWidthInPixels = sheet.getColumnWidthInPixels(iteratedCell2ColumnIndex);

                        iteratedMergedCell2WidthInPixels = iteratedMergedCell2WidthInPixels + iteratedCell2ColumnWidthInPixels;
                    }

                    //Získáme šířku nejširší sloučené buňky na řádku
                    if (iteratedMergedCell2WidthInPixels > longestMergedCellWidthInPixels) {
                        longestMergedCellWidthInPixels = iteratedMergedCell2WidthInPixels;
                    }

                    //Resetujeme sčítání
                    iteratedMergedCell2WidthInPixels = 0f;
                }
            }

            //Uložíme si nejširší buňku dle toho, zda je sloučená či nikoliv
            Float longestWidthInPixels;
            if (isLongestTextCellMerged) {
                longestWidthInPixels = longestMergedCellWidthInPixels;
            } else {
                longestWidthInPixels = longestCellWidthInPixels;
            }

            //Potřebujeme font
            Workbook wb = sheet.getWorkbook();
            Short fontIndex = longestTextCell.getCellStyle().getFontIndex();
            Font excelFont = wb.getFontAt(fontIndex);

            //Potřebujeme i jeho styl
            Integer excelFontStyle = java.awt.Font.PLAIN;
            if (excelFont.getBold()) excelFontStyle = java.awt.Font.BOLD;
            if (excelFont.getItalic()) excelFontStyle = java.awt.Font.ITALIC;

            //Potřebujeme získat skutečný font i s velikostí
            java.awt.Font currentFont = new java.awt.Font(excelFont.getFontName(), excelFontStyle, excelFont.getFontHeightInPoints());

            //Získáme řetězec s vlastností
            AttributedString attributedString = new AttributedString(longestText);
            attributedString.addAttribute(TextAttribute.FONT, currentFont);

            //Použijeme LineBreakMeasurer k zjištění kolik řádků bude text potřebovat
            FontRenderContext fontRenderContext = new FontRenderContext(null, true, true);
            LineBreakMeasurer measurer = new LineBreakMeasurer(attributedString.getIterator(), fontRenderContext);

            Integer nextPosition = 0;
            Integer lineCount = 0;

            while (measurer.getPosition() < longestText.length()) {
                nextPosition = measurer.nextOffset(longestWidthInPixels);

                //Také musíme ošetřit případ manuálně zadaných LineBreaků pro všechny možné techtle mechtle :-S
                String textLine = StringUtils.substring(longestText, measurer.getPosition(), nextPosition);
                Boolean containsNewLine = StringUtils.containsIgnoreCase(textLine, "\r") || StringUtils.containsIgnoreCase(textLine, "\\r") || StringUtils.containsIgnoreCase(textLine, "\n") || StringUtils.containsIgnoreCase(textLine, "\\n");

                if (containsNewLine) {

                    if (StringUtils.containsIgnoreCase(textLine, "\r\n") || StringUtils.containsIgnoreCase(textLine, "\\r\\n")) {
                        lineCount = lineCount + StringUtils.countMatches(textLine, "\n");
                    } else {

                        if (StringUtils.containsIgnoreCase(textLine, "\r") || StringUtils.containsIgnoreCase(textLine, "\\r")) {
                            lineCount = lineCount + StringUtils.countMatches(textLine, "\r");
                        }
                        if (StringUtils.containsIgnoreCase(textLine, "\n") || StringUtils.containsIgnoreCase(textLine, "\\n")) {
                            lineCount = lineCount + StringUtils.countMatches(textLine, "\n");
                        }

                    }

                    lineCount = lineCount + StringUtils.countMatches(textLine, "\\r?\\n");
                }

                lineCount++;
                measurer.setPosition(nextPosition);
            }

            //Máme počet řádků, zbývá konečný dopočet výšky řádku a jeho použití
            if (lineCount > 1) {

                Float fontHeight = currentFont.getLineMetrics(longestText, fontRenderContext).getHeight();

                //Pro jistotu přidáme jeden řádek navíc, člověk nikdy neví...
                lineCount = lineCount + 1;

                //Potřebujeme získat poslední řádek
                Row lastRow = null;

                if (isCellMerged(longestTextCell)) {
                    List<List<Cell>> mergedCellsInRows = getCellsInRowsInsideRegionRange(longestTextCell);
                    Integer lastRowInMergedSectionIndex = mergedCellsInRows.size() - 1;
                    List<Cell> lastRowInMergedSection = mergedCellsInRows.get(lastRowInMergedSectionIndex);
                    lastRow = lastRowInMergedSection.get(0).getRow();
                } else {
                    lastRow = longestTextCell.getRow();
                }

                //Je potřeba ošetřit velikosti, pokud má sloučená buňka vícero řádků
                Float cellsMergedAboveHeight = 0f;
                if (isCellMerged(longestTextCell)) {
                    if (getCellsInRowsInsideRegionRange(longestTextCell).size() > 1) {
                        List<List<Cell>> mergedCellsInRows = getCellsInRowsInsideRegionRange(longestTextCell);
                        for (List<Cell> rowsWithCells : mergedCellsInRows){
                            if (!lastRow.equals(rowsWithCells.get(0).getRow())){
                                cellsMergedAboveHeight = cellsMergedAboveHeight + rowsWithCells.get(0).getRow().getHeight();
                            }
                        }
                    }
                }
                //Vzorec je ((Velikost fontu krát počet řádků plus (počet řádků krát volný prostor mezi řádky)) krát přepočet Excelu) mínus výška sloučených buněk nad posledním řádkem.
                Short finalRowHeight = (short) (((fontHeight * lineCount + (lineCount * 15))* 10) - cellsMergedAboveHeight);

                //A výsledek nastavíme na poslední řádek, protože jinak to przní sloupce vlevo a vpravo od vyšších řádků
                lastRow.setHeight(finalRowHeight);

            }
        }
    }
1

I was having a similar problem where row height was not adjusting for merged cells. I had to write a custom function for adjusting height of that particular row. Here is my code:

I have fixed column width in my sheet to be 24, and default font size to be 11. I needed adjustment in row height whenever my font size > 11 and cell text length is overflowing the column width or isn't visible properly due to larger length.

    private void adjustRowHeightMergedCells(final XSSFCell mergedCell) {

        DataFormatter dataFormatter = new DataFormatter();
        int defaultCharWidth = SheetUtil.getDefaultCharWidth(mergedCell.getRow().getSheet().getWorkbook());
        XSSFRow row = mergedCell.getRow();

        // Getting merged cell width value
        double cellValueWidth = SheetUtil.getCellWidth(mergedCell, defaultCharWidth, dataFormatter, true);

        // If cell width value > 24 (Default value), calculate how much extra row height is needed
// This happends when text length is larger than the column width (24)
        float extraRowHeightDueToCellTextLength = (float) Math.floor(cellValueWidth / 24) * row.getHeightInPoints();
        float extraRowHeightDueToCellTextFontSize = 0;

        // If cell font size > 11 (Default value), calculate how much extra row height is needed
        short cellTextFontSize = mergedCell.getCellStyle().getFont().getFontHeightInPoints();
        if (cellTextFontSize > 11) {
            extraRowHeightDueToCellTextFontSize = (cellTextFontSize-ExcelConstants.DEFAULT_FONT_SIZE) * (5f/3f); 
        }
// 5f/3f in above calculation is custom number which assumed by thinking that for font size 11, my row height shud be 15, and for example custom font size 20, row height shud be 30, hence the factor 5f/ 3f( per extra 1 point increase in font size above 11, row height shud be increased by 5/3)
        // Larger of two adjustment values will be taken and added to current row height
        float extraRowHeightAdjustment = Math.max(extraRowHeightDueToCellTextFontSize,extraRowHeightDueToCellTextLength);
        if(extraRowHeightAdjustment > 0) {
            row.setHeightInPoints(row.getHeightInPoints() + extraRowHeightAdjustment);
        }
    }

Kinda hacky solution but works for my situation, you can modify it per your requirements.

Anurag
  • 107
  • 1
  • 6