基于EasyExcel导出Excel后,通过对合并单元格的简单规则配置,实现如下图所示的单元格合并效果:
效果截图
原表格数据如下:
通过配置单元格合并规则后,生成的合并后的表格如下:
注:其中第三列,没有配置合并规则,数据保持不动
自定义合并规则类
如下代码类,是处理单元格合并的核心类,主要是按行计算相同数据进行单元格合并。
package com.shanhy.project.service.impl;
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
import com.shanhy.common.utils.JsonUtils;
import com.shanhy.project.vo.ExcelMergeStrategyModel;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.util.CellRangeAddress;
import java.util.ArrayList;
import java.util.List;
/**
* Excel 行合并策略
*/
@Slf4j
public class CustomLoopMergeStrategy implements RowWriteHandler {
//上一行
private Row beforeRow = null;
//合并规则(多个)
private List<ExcelMergeStrategyModel> strategyList;
//总行数(不含表头)
private int dataRowTotalSize;
//当前已经处理的行数(不含表头)
private int dataRowCurrentSize = 0;
private CustomLoopMergeStrategy() {
}
/**
* 构造方法
*
* @param loopMergeStrategyJson 合并规则JSON,其中 columnName 为要被自动计算合并的列,relativeColumnNames 表示目标合并列需要参照的相关列(值全部相同则会触发合并目标目标列的单元格)
* 示例:
* String loopMergeStrategyJson =
* [
* {
* "columnName": "A",
* "relativeColumnNames": "A"
* },
* {
* "columnName": "B",
* "relativeColumnNames": "A,B"
* },
* {
* "columnName": "C",
* "relativeColumnNames": "A,B,C"
* },
* {
* "columnName": "D",
* "relativeColumnNames": "A,B,C,D"
* }
* ];
* @param dataRowTotalSize 所有数据的行数
*/
public CustomLoopMergeStrategy(String loopMergeStrategyJson, int dataRowTotalSize) {
//记录excel行数
this.dataRowTotalSize = dataRowTotalSize;
//解析json 获取合并规则
this.strategyList = JsonUtils.jsonToList(loopMergeStrategyJson, ExcelMergeStrategyModel.class);
}
@SneakyThrows
@Override
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
//表头直接跳过
if (isHead) {
return;
}
//记录数据行数
this.dataRowCurrentSize++;
//记录beforeRow(第一行数据行 beforeRow = row)
if (beforeRow == null) {
beforeRow = row;
return;
}
// 需要被合并的单元格,从第二行开始要被删除,否则会出现最终导出Excel后“选中合并的单元格下面的状态栏显示的数量不是1的情况“
List<String> removeCellName = new ArrayList<>();
// 循环所有规则并做相关处理
for (ExcelMergeStrategyModel strategy : strategyList) {
//执行 rowDataCompare() 根据当前规则进行判断 返回 true 和 false,返回true表示该列标记为可合并列
if (this.rowDataCompare(row, beforeRow, strategy.getRelativeColumnNames().split(","))) {
//mergeFlag:如果第一次出现重复数据,则进行合并标记(一般为即将合并列的第二行数据时标记)
if (!strategy.isMergeFlag()) {
strategy.setMergeFlag(true);
strategy.setMergeStartRowIndex(row.getRowNum() - 1);
} else {
removeCellName.add(strategy.getColumnName());
}
//判断是否最后一行 (最后一行直接执行合并)
if (this.dataRowTotalSize == this.dataRowCurrentSize) {
this.addMergedRegion(strategy, row, row.getRowNum());
removeCellName.add(strategy.getColumnName());
// 处理完最后一行,直接结束
return;
}
} else {
if (strategy.isMergeFlag()) {
//添加合并范围
this.addMergedRegion(strategy, row, beforeRow.getRowNum());
removeCellName.add(strategy.getColumnName());
}
}
}
// 清理当前行对应的列,避免单元格合并后计数不是1的问题
if (!removeCellName.isEmpty()) {
if (this.dataRowTotalSize == this.dataRowCurrentSize) {
this.removeCell(row, removeCellName);
} else {
this.removeCell(beforeRow, removeCellName);
}
}
beforeRow = row;
}
/**
* 清理单元格的值
*
* @param row 行对象
* @param columnNameList 单元格序号名称列表
*/
private void removeCell(Row row, List<String> columnNameList) {
for (String columnName : columnNameList) {
row.removeCell(row.getCell(excelNum2Digit(columnName) - 1));
}
}
/**
* 添加合并范围
*
* @param strategy 规则对象
* @param row 当前行
* @param lastRowNum 合并单元格的最后一行序号
*/
private void addMergedRegion(ExcelMergeStrategyModel strategy, Row row, int lastRowNum) {
// 产生合并规则,从 mergeStartRowIndex 合并至 currentIndex - 1
int currentCellIndex = excelNum2Digit(strategy.getColumnName()) - 1;
CellRangeAddress cellRangeAddress = new CellRangeAddress(strategy.getMergeStartRowIndex(), lastRowNum, currentCellIndex, currentCellIndex);
row.getSheet().addMergedRegion(cellRangeAddress);
strategy.setMergeFlag(false);
strategy.setMergeStartRowIndex(-1);
}
/**
* 比较两行数据中的指定列的数据是否相同
*
* @param currentRow 当前行
* @param beforeRow 上一行
* @param relativeColumnNames 数值对比计算相对列
* @return 所有相对列数值是否全部相同
*/
private boolean rowDataCompare(Row currentRow, Row beforeRow, String[] relativeColumnNames) {
if (beforeRow != null) {
//取出规则进行判断
for (String columnName : relativeColumnNames) {
log.info("xxxxxxxxxxxx>>>>>>>>>>>{}", columnName);
//当前列
int cellIndex = excelNum2Digit(columnName) - 1;
//判断当前行当前列的数据 和上一行规则内指定列的单元格数据 是否相同
//例:第二行 A列的和B列的单元格数据要和 第一行的A列的和B列的单元格数据相同
if (!currentRow.getCell(cellIndex).getStringCellValue().equals(beforeRow.getCell(cellIndex).getStringCellValue())) {
return false;
}
}
//相对列的值全部相同返回true
return true;
} else {
return false;
}
}
/**
* Excel 列号转数字 (A = 1 B = 2)
*
* @param excelNum Excel 列号
* @return 数字
*/
private int excelNum2Digit(String excelNum) {
char[] chs = excelNum.toCharArray();
int digit = 0;
/*
* B*26^2 + C*26^1 + F*26^0
* = ((0*26 + B)*26 + C)*26 + F
*/
for (char ch : chs) {
digit = digit * 26 + (ch - 'A' + 1);
}
return digit;
}
/**
* 数字转 Excel 列号
*
* @param digit 数字
* @return Excel 列号
*/
private String digit2ExcelNum(int digit) {
/*
* 找到 digit 所处的维度 len, 它同时表示字母的位数
* power 表示 26^n, 这里 n 分别等于 1, 2, 3
* pre 表示 前 n 个维度的总和, 即 26^1 + 26^2 + 26^3
*/
int len = 0, power = 1, pre = 0;
for (; pre < digit; pre += power) {
power *= 26;
len++;
}
// 确定字母位数
char[] excelNum = new char[len];
/*
* pre 包含 digit 所处的维度
* pre - power 则是 digit 前面的维度总和
* digit 先减去前面维度和
*/
digit -= pre - power;
/*
* 比较难以理解的是这里为什么要自减 1
* 其实是相对 (digit / power + 'A') 这句代码来的
* 本应该是 (digit / power + 'A' - 1),
* digit / power 的结果是完整的维度个数, 它加上 'A' - 1 后需要再加一
* 当最后剩下的 6 个加上 'A' - 1 是应当的, 不需要做修改
* 而当 (digit / power + 'A') 中没有减 1 后,
* digit / power 的结果不需要再加一了
* 相对于 digit / power 的结果, 最后剩下的 6 需要减 1
*/
digit--;
for (int i = 0; i < len; i++) {
power /= 26;
excelNum[i] = (char) (digit / power + 'A');
digit %= power;
}
return String.valueOf(excelNum);
}
public void setDataRowTotalSize(int dataRowTotalSize) {
this.dataRowTotalSize = dataRowTotalSize;
}
}
@Data
class ExcelMergeStrategyModel {
/**
* 合并列,区分大小写只允许大写(例:A)
* */
private String columnName;
/**
* 参考列(例:A,B,C)
* */
private String relativeColumnNames;
/**
* 是否 合并标签
* */
private boolean mergeFlag = false;
/**
* 合并起始行
* */
private int mergeStartRowIndex = -1;
}
测试代码
@Slf4j
public class ExcelLoopMergeStrategyTest {
public static void main(String[] args) {
String s = "";
String fileName = "e:\\MergeExcel-Demo1.xlsx";
// String loopMergeStrategyJson =
// [
// {
// "columnName": "A",
// "relativeColumnNames": "A"
// },
// {
// "columnName: "B",
// "relativeColumnNames: "A,B"
// }
// ];
String loopMergeStrategyJson = "[\n" + " {\n" + " \"columnName\": \"A\",\n" + " \"relativeColumnNames\": \"A\"\n" + " },\n" + " {\n" + " \"columnName\": \"B\",\n" + " \"relativeColumnNames\": \"A,B\"\n" + " }\n" + "]";
CustomLoopMergeStrategy loopMergeStrategy = new CustomLoopMergeStrategy(loopMergeStrategyJson, 10);
// 这里 需要指定写用哪个class去写,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
EasyExcel.write(fileName, DemoMergeData.class).registerWriteHandler(loopMergeStrategy).sheet("模板").doWrite(data());
}
private static List<DemoMergeData> data() {
List<DemoMergeData> list = ListUtils.newArrayList();
for (int i = 0; i < 10; i++) {
DemoMergeData data = new DemoMergeData();
if (i <= 7) {
data.setStr1("字符串One");
} else {
data.setStr1("字符串One-" + i);
}
if (i <= 5) {
data.setStr2("字符串Two");
} else {
data.setStr2("字符串Two-" + i);
}
if (i < 2) {
data.setStr3("字符串Three");
} else {
data.setStr3("字符串Three-" + i);
}
list.add(data);
}
return list;
}
}
@Data
class DemoMergeData {
@ExcelProperty("标题1")
private String str1;
@ExcelProperty("标题2")
private String str2;
@ExcelProperty("标题3")
private String str3;
}
(END)