easyExcle单元格合并

自定义单元格合并策略:

java 复制代码
/**
 * 自定义单元格合并策略
 *
 * @create: 2023-11-15 13:41
 **/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Slf4j
public class EasyExcelCustomMergeStrategy implements RowWriteHandler {

    /**
     * 总数
     */
    private Integer totalNum;

    //合并行计数
    private int count;

    //要合并的列 从0开始
    private int[] mergeColumnIndex;

    //已合并单元数
    private int mergedTotalCount = 0;

    public EasyExcelCustomMergeStrategy(int[] mergeColumnIndex) {
        this.mergeColumnIndex = mergeColumnIndex;
    }
    
    // 每写入一行会执行一次afterRowDispose,存在合并行时进行合并方法mergeSameRow,否则什么也不做
    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        //当前行索引
        int curRowNum = row.getRowNum();

        if (mergeColumnIndex != null && mergeColumnIndex.length > 0 && !isHead) {
            //当前行第一列单元格
            Cell curA1Cell = row.getCell(0);
            Object curA1Data = curA1Cell.getCellTypeEnum() == CellType.STRING ? curA1Cell.getStringCellValue() : curA1Cell.getNumericCellValue();
            //上一行第一列单元格
            Cell preA1Cell = row.getSheet().getRow(curRowNum - 1).getCell(0);
            Object preA1Data = preA1Cell.getCellTypeEnum() == CellType.STRING ? preA1Cell.getStringCellValue() : preA1Cell.getNumericCellValue();

            if (curA1Data.equals(preA1Data)) {
                count++;
            } else {
                if (count > 0) {
                    for (int columnIndex : mergeColumnIndex) {
                        mergeSameRow(writeSheetHolder, curRowNum, count, columnIndex);
                    }
                    //for (int i = 0; i < mergeColumnIndex.length; i++) {
                    //
                    //}
                    count = 0;
                }
            }
            // 最后一行存在合并时,需要单独调用合并,否则不会执行mergeSameRow
            if (curRowNum == totalNum && count > 0) {
                for (int columnIndex : mergeColumnIndex) {
                    mergeSameRow(writeSheetHolder, curRowNum + 1, count, columnIndex);
                }
            }
        }
    }

    /**
     * 按列合并单元格
     *
     * @param writeSheetHolder 写出处理
     * @param curRowIndex      当前行索引,有n行固定行就加n
     * @param needMergeNum     需要合并的行
     * @param curColIndex      需要合并的列
     */
    private void mergeSameRow(WriteSheetHolder writeSheetHolder, int curRowIndex, int needMergeNum, int curColIndex) {
        Sheet sheet = writeSheetHolder.getSheet();
        try {
            Field sh = sheet.getClass().getDeclaredField("_sh");
            sh.setAccessible(true);
            XSSFSheet shSheet = (XSSFSheet) sh.get(sheet);
            CTWorksheet worksheet = shSheet.getCTWorksheet();
            CTMergeCells ctMergeCells = mergedTotalCount > 0 ? worksheet.getMergeCells() : worksheet.addNewMergeCells();
            CTMergeCell ctMergeCell = ctMergeCells.addNewMergeCell();

            CellRangeAddress cellAddresses = new CellRangeAddress(curRowIndex - needMergeNum - 1, curRowIndex - 1, curColIndex, curColIndex);
            ctMergeCell.setRef(cellAddresses.formatAsString());
            mergedTotalCount++;
        } catch (Exception e) {
            log.error("导出出错!", e);
            throw new BusinessException("导出出错!\n");
        }
    }
}

easyExcel提供了两种合并单元格方法:addMergedRegion和addMergedRegionUnsafe,在使用中发现会遇到不同的问题:

如果将需要合并的数据一次性传入afterRowDispose方法进行合并,在合并过程中使用addMergedRegionUnsafe方法合并单元格可能会导致工作簿损坏,而使用addMergedRegion会进行单元格是否重复合并的校验,会直接抛出异常。

使用addMergedRegionUnsafe方法合并单元格导致的问题:

使用addMergedRegion进行的校验:

java 复制代码
/**
 * adds a merged region of cells (hence those cells form one)
 *
 * @param region (rowfrom/colfrom-rowto/colto) to merge
 * @param validate whether to validate merged region
 * @return index of this region
 * @throws IllegalArgumentException if region contains fewer than 2 cells
 * @throws IllegalStateException if region intersects with an existing merged region
 * or multi-cell array formula on this sheet
 */
private int addMergedRegion(CellRangeAddress region, boolean validate) {
    if (region.getNumberOfCells() < 2) {
        // 区域包含少于 2 个单元格
        throw new IllegalArgumentException("Merged region " + region.formatAsString() + " must contain 2 or more cells");
    }
    region.validate(SpreadsheetVersion.EXCEL97);

    if (validate) {
        // throw IllegalStateException if the argument CellRangeAddress intersects with
        // a multi-cell array formula defined in this sheet
        // 此工作表上的多单元格数组公式
        validateArrayFormulas(region);
    
        // Throw IllegalStateException if the argument CellRangeAddress intersects with
        // a merged region already in this sheet
        // 区域与现有合并区域相交
        validateMergedRegions(region);
    }

    return _sheet.addMergedRegion(region.getFirstRow(),
            region.getFirstColumn(),
            region.getLastRow(),
            region.getLastColumn());
}

导致错误产生的代码:

java 复制代码
public class EasyExcelCustomMergeStrategy implements RowWriteHandler {

    //合并坐标集合
    private List<CellRangeAddress> cellRangeAddress;
    //从哪行开始
    private int beginRow;
    //合并行数
    private int mergeRows;

    public PiFillMergeStrategy(List<CellRangeAddress> cellRangeAddress, int beginRow, int mergeRows) {
        this.cellRangeAddress = cellRangeAddress;
        this.beginRow = beginRow;
        this.mergeRows = mergeRows;
    }

    @Override
    public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
        if (CollectionUtil.isNotEmpty(cellRangeAddress)) {
            if (row.getRowNum() >= beginRow && row.getRowNum() <= beginRow + mergeRows) {
                for (CellRangeAddress item : cellRangeAddress) {
                    writeSheetHolder.getSheet().addMergedRegionUnsafe(item);
                }
            }
        }
    }

}

因此合并时循环每一条数据判断是否与上一条数据是否相同,相同时再逐条进行合并,写法使用XSSFSheet写法或使用easyExcel自带的两种合并方法:

java 复制代码
private void mergeSameRow(WriteSheetHolder writeSheetHolder, int curRowIndex, int needMergeNum, int curColIndex) {
    Sheet sheet = writeSheetHolder.getSheet();
    CellRangeAddress cellAddresses = new CellRangeAddress(curRowIndex - needMergeNum - 1, curRowIndex - 1, curColIndex, curColIndex);
    sheet.addMergedRegionUnsafe(cellAddresses);
    // 或使用
    // sheet.addMergedRegion(cellAddresses);
}

合并策略的使用:

java 复制代码
public void download(HttpServletResponse response) {
    /**
    *  此处为业务数据
    *
    *
    *
    */
    String subject = "XXX";
    String fileName = subject.concat(".xlsx");
    //定义多级表头
    List<List<String>> headList = new ArrayList<>();
    //定义数据体
    List<List<Object>> dataList = new ArrayList<>();
    this.getHeadAndData(headList, dataList);

    // 定义合并单元格 (根据业务需求自定义)
    List<Integer> mergeColumnIndex = new ArrayList<>();
    int mergeCol = headList.size() - offerVOList.size() - 3;
    for (int i = 0; i < mergeCol; i++) {
        mergeColumnIndex.add(i);
    }
    for (int j = mergeCol + 3; j < headList.size(); j++) {
        mergeColumnIndex.add(j);
    }
    // 生成自定义策略
    EasyExcelCustomMergeStrategy customMergeStrategy = new EasyExcelCustomMergeStrategy(mergeColumnIndex.stream().filter(Objects::nonNull).mapToInt(i->i).toArray());
    customMergeStrategy.setTotalNum(dataList.size());

    try {
        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
        EasyExcel.write(response.getOutputStream())
                .excelType(ExcelTypeEnum.XLSX)
                .head(headList)
                .sheet(subject)
                .registerWriteHandler(customMergeStrategy)//调用策略
                .doWrite(dataList);
    } catch (Exception e) {
        throw new Exception(String.format("生成文件(%s)失败", fileName), e);
    }
}
相关推荐
顾北川_野4 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航7 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
远望清一色13 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself23 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq041527 分钟前
J2EE平台
java·java-ee
XiaoLeisj34 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
杜杜的man38 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*39 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
半桶水专家39 分钟前
go语言中package详解
开发语言·golang·xcode
llllinuuu40 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang