自定义单元格合并策略:
/**
* 自定义单元格合并策略
*
* @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进行的校验:
/**
* 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());
}
导致错误产生的代码:
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自带的两种合并方法:
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);
}
合并策略的使用:
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);
}
}