最终的目标(不包含颜色)
很显然,行也在合并,列也在合并,没关系,我们首先制作一个模板,然后使用EasyExcel增加自定义的处理器,就可以实现。
制作Excel模板
准备数据
定义两个实体,一个对应表头的数据,这里只有一个time。一个对应列表中的数据,通常这些都是我们的业务数据,通过数据库统计而来,然后经过我们处理。
import lombok.Data;
@Data
public class FormData {
private String time;
}
import lombok.Data;
@Data
public class UserSum {
private String province;
private String city;
private String gender;
private Integer studentNum;
}
引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>3.1.1</version>
</dependency>
合并处理器
合并的核心代码如下,向sheet中增加CellRangeAddress对象
sheet.addMergedRegion(new CellRangeAddress(startRow, endRow, column, column));
需要注意以下几点:
A、CellRangeAddress的四个参数分别是:起始行、结束行、起始列、结束列
B、一个sheet页,我们添加的CellRangeAddress是不能有行列交叉的,或者说任何两个合并单元格不能包含相同的原始单元格,比如合并单元格( 3、4、1、1 )与合并单元格(3、6、1、1)包含相同的单元格(第3行第1列、第4行第1列)
C、最终合并单元格的数据,是合并前从左往右、从上往下第一个单元格的数据,所以我们在组装数据的时候一定要给这个单元格赋值,其他单元格有没有值不影响,最后合并单元格才会有值,比如合并单元格(2、3、4、5),最后展现的就是合并前(第2行第4列)的数据。
import com.alibaba.excel.write.handler.RowWriteHandler;
import com.alibaba.excel.write.handler.context.RowWriteHandlerContext;
import com.alibaba.fastjson2.JSONObject;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.DateUtil;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public class MyExcelHandler implements RowWriteHandler {
private static final Logger logger = LoggerFactory.getLogger(MyExcelHandler.class);
private final Set<Integer> columnsToMerge;//需要进行合并的列
private final boolean mergeAllColumns;//是否所有列都需要合并
private final Map<Integer, Object> previousValues;//记录上一行的值
private final Map<Integer, Integer> previousRowIndices;//记录合并的起始位置
public MyExcelHandler() {
this.columnsToMerge = new HashSet<>();
this.mergeAllColumns = true;
this.previousValues = new HashMap<>();
this.previousRowIndices = new HashMap<>();
}
public MyExcelHandler(Set<Integer> columnsToMerge) {
this.columnsToMerge = columnsToMerge;
this.mergeAllColumns = false;
this.previousValues = new HashMap<>();
this.previousRowIndices = new HashMap<>();
}
@Override
public void afterRowDispose(RowWriteHandlerContext context) {
// 获取当前操作的Sheet对象
Sheet sheet = context.getWriteSheetHolder().getSheet();
// 获取当前行索引
int currentRowIndex = context.getRowIndex();
if (currentRowIndex < 3) {
return;
}
logger.info("当前行号:{}", currentRowIndex);
// 获取当前行对象
Row currentRow = context.getRow();
// 遍历当前行的所有单元格
for (Cell cell : currentRow) {
// 获取当前单元格的列索引
int columnIndex = cell.getColumnIndex();
logger.info("当前单元格序号:{}", columnIndex);
// 如果不合并所有列且当前列不在合并列列表中,则跳过当前列
if (!mergeAllColumns && !columnsToMerge.contains(columnIndex)) {
continue;
}
// 获取当前单元格的值
Object currentValue = getCellValue(cell);
if (currentValue != null && currentValue.equals("总人数")) {
sheet.addMergedRegion(new CellRangeAddress(currentRowIndex, currentRowIndex, columnIndex, columnIndex+1));
}
logger.info("当前单元格的值:{}", currentValue);
logger.info("上一行的值:{}", JSONObject.toJSONString(previousValues));
// 如果当前值不为空
if (currentValue != null) {
// 如果当前值与前一值相同,则合并单元格
if (currentValue.equals(previousValues.get(columnIndex))) {
logger.info("当前单元格的值,与上一行序号相同单元格值一样,开始合并");
logger.info("........................................");
// 合并单元格
mergeColumns(sheet, previousRowIndices.get(columnIndex), currentRowIndex, columnIndex);
} else {
logger.info("当前单元格的值,与上一行序号相同单元格值不一样,替换上一行的值");
// 否则,更新前一值和前一行索引为当前值和当前行索引
previousValues.put(columnIndex, currentValue);
previousRowIndices.put(columnIndex, currentRowIndex);
logger.info("替换之后");
logger.info("{}", JSONObject.toJSONString(previousValues));
logger.info("{}", JSONObject.toJSONString(previousRowIndices));
}
}
}
}
private void mergeColumns(Sheet sheet, int startRow, int endRow, int column) {
logger.info("开始行:{},结束行:{},第{}列", startRow, endRow, column);
// 遍历工作表中所有的合并区域
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
CellRangeAddress region = sheet.getMergedRegion(i);
// 检查当前合并区域是否与指定的起始行和列匹配
if (region.getFirstRow() == startRow && region.getFirstColumn() == column) {
// 如果匹配,移除当前合并区域,以便可以重新设置它的范围
sheet.removeMergedRegion(i);
// 调整合并区域的结束行为指定的结束行
region.setLastRow(endRow);
// 重新添加调整后的合并区域
sheet.addMergedRegion(region);
// 返回,完成合并
return;
}
}
// 如果没有找到匹配的合并区域,则创建一个新的合并区域并添加到工作表中
logger.info("创建全新单元格合并:{},{},{},{}", startRow, endRow, column, column);
sheet.addMergedRegion(new CellRangeAddress(startRow, endRow, column, column));
}
private Object getCellValue(Cell cell) {
if (cell == null) {
return null;
}
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue();
case NUMERIC:
return DateUtil.isCellDateFormatted(cell) ? cell.getDateCellValue() : cell.getNumericCellValue();
case BOOLEAN:
return cell.getBooleanCellValue();
case FORMULA:
return cell.getCellFormula();
default:
return null;
}
}
}
上面代码简单解释一下:
afterRowDispose方法,每次导入一行数据后调用
1、因为两个合并单元格不能包含同一个原始单元格,所以在合并时候,会不断的做判断、删除、重新创建合并单元格,这个代码体现在mergeColumns方法中。
2、代码中下面一段
if (currentValue != null && currentValue.equals("总人数")) {
sheet.addMergedRegion(new CellRangeAddress(currentRowIndex, currentRowIndex, columnIndex, columnIndex+1));
}
是针对总人数单独处理,发现总人数,就合并连续两列。
开始使用处理器导入数据
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import com.alibaba.excel.write.metadata.fill.FillConfig;
import com.alibaba.fastjson2.JSONArray;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public class MainTest {
private static FormData formData;
private static List<UserSum> userSumList;
public static void main(String[] args) throws Exception {
initData();
File fileTemplate = new File("E:\\技术沉淀\\Template20240711.xlsx");
InputStream excelTemplateStream = new FileInputStream(fileTemplate);//模板
File fileOutPut = new File("E:\\技术沉淀\\OutPut20240711.xlsx");
OutputStream outputStream = new FileOutputStream(fileOutPut);
Set<Integer> needMergeCols = new HashSet<>();
needMergeCols.add(0);//第一列需要合并
needMergeCols.add(1);//第二列需要合并
needMergeCols.add(2);//第二列需要合并
//工作簿对象
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(excelTemplateStream)
.registerWriteHandler(new MyExcelHandler(needMergeCols)).build();
//工作表对象
WriteSheet writeSheet = EasyExcel.writerSheet().build();
//组合填充换行
excelWriter.fill(formData, writeSheet);
FillConfig fillConfig = FillConfig.builder().forceNewRow(Boolean.TRUE).build();
excelWriter.fill(userSumList, fillConfig, writeSheet);
try {
excelWriter.finish();
} catch (Exception e) {
System.out.println(ExceptionUtils.getStackTrace(e));
}
}
/*******************************************
* @Description: 初始化数据
******************************************/
private static void initData() {
formData = new FormData();
formData.setTime("2024-09-01");
userSumList = new ArrayList<>();
String jsonStr = "[{\"city\":\"达州市\",\"province\":\"四川省\",\"gender\":\"男\",\"studentNum\":12},{\"city\":\"达州市\",\"province\":\"四川省\",\"gender\":\"女\",\"studentNum\":2},{\"city\":\"达州市\",\"province\":\"四川省\",\"gender\":\"合计\",\"studentNum\":14},{\"city\":\"成都市\",\"province\":\"四川省\",\"gender\":\"男\",\"studentNum\":4},{\"city\":\"成都市\",\"province\":\"四川省\",\"gender\":\"女\",\"studentNum\":13},{\"city\":\"成都市\",\"province\":\"四川省\",\"gender\":\"合计\",\"studentNum\":17},{\"city\":\"总人数\",\"province\":\"四川省\",\"gender\":\"\",\"studentNum\":31},{\"city\":\"武汉市\",\"province\":\"湖北省\",\"gender\":\"男\",\"studentNum\":2},{\"city\":\"武汉市\",\"province\":\"湖北省\",\"gender\":\"合计\",\"studentNum\":2},{\"province\":\"湖北省\",\"city\":\"总人数\",\"gender\":\"\",\"studentNum\":2}]";
userSumList = JSONArray.parseArray(jsonStr, UserSum.class);
}
}
引用模板、使用自定义合并处理器:
ExcelWriter excelWriter = EasyExcel.write(outputStream).withTemplate(excelTemplateStream) .registerWriteHandler(new MyExcelHandler(needMergeCols)).build();
填充表单数据:
excelWriter.fill(formData, writeSheet);
填充数据列表:
excelWriter.fill(userSumList, fillConfig, writeSheet);
最终生成文件
如下,和期待的有点差异,设计模板的时候增加一下居中,就可以了。
希望对大家有帮助。