EasyExcel动态标题,设置自定义列宽(二)
书接上篇,上篇只是自己简单做的deme,但是真正到项目里,又出现情况了T-T
直接上测试代码,然后看看效果(根据业务改编的,并不是我司业务)
public void test(HttpServletResponse response) {
List<List<String>> headList = new ArrayList<>();
List<String> head0 = new ArrayList<>();
head0.add("公司名称");
headList.add(head0);
List<String> head3 = new ArrayList<>();
head3.add("截至当前交易总额");
head3.add("小计");
headList.add(head3);
List<String> head4 = new ArrayList<>();
head4.add("截至当前交易总额");
head4.add("一、线上交易额");
headList.add(head4);
List<String> head5 = new ArrayList<>();
head5.add("截至当前交易总额");
head5.add("二、线下交易额");
headList.add(head5);
List<String> head6 = new ArrayList<>();
head6.add("截至当前交易总额");
head6.add("三、第三方平台交易额");
head6.add("小计");
headList.add(head6);
List<String> head7 = new ArrayList<>();
head7.add("截至当前交易总额");
head7.add("三、第三方平台交易额");
head7.add("美团");
headList.add(head7);
List<String> head8 = new ArrayList<>();
head8.add("截至当前交易总额");
head8.add("三、第三方平台交易额");
head8.add("饿了么");
headList.add(head8);
for (int i = 0; i < 3; i++) {
Date calcMonthTime = DateUtil.offsetMonth(DateUtil.date(), i);
String calcMonthStr = DateUtil.format(calcMonthTime, DatePattern.NORM_MONTH_PATTERN) + "月交易额";
List<String> monthHead1 = new ArrayList<>();
monthHead1.add(calcMonthStr);
monthHead1.add("小计");
monthHead1.add("收入");
headList.add(monthHead1);
List<String> monthHead2 = new ArrayList<>();
monthHead2.add(calcMonthStr);
monthHead2.add("小计");
monthHead2.add("支出");
headList.add(monthHead2);
List<String> monthHead3 = new ArrayList<>();
monthHead3.add(calcMonthStr);
monthHead3.add("一、线上交易额");
monthHead3.add("收入");
headList.add(monthHead3);
List<String> monthHead4 = new ArrayList<>();
monthHead4.add(calcMonthStr);
monthHead4.add("一、线上交易额");
monthHead4.add("支出");
headList.add(monthHead4);
List<String> monthHead5 = new ArrayList<>();
monthHead5.add(calcMonthStr);
monthHead5.add("二、线下交易额");
monthHead5.add("收入");
headList.add(monthHead5);
List<String> monthHead6 = new ArrayList<>();
monthHead6.add(calcMonthStr);
monthHead6.add("二、线下交易额");
monthHead6.add("支出");
headList.add(monthHead6);
List<String> monthHead7 = new ArrayList<>();
monthHead7.add(calcMonthStr);
monthHead7.add("三、第三方平台交易额");
monthHead7.add("收入");
headList.add(monthHead7);
List<String> monthHead8 = new ArrayList<>();
monthHead8.add(calcMonthStr);
monthHead8.add("三、第三方平台交易额");
monthHead8.add("支出");
headList.add(monthHead8);
}
try {
// 设置响应头
response.setContentType("application/vnd.ms-excel");
response.setCharacterEncoding("utf-8");
String fileName = "test";
// 设置防止中文名乱码
String excelName = URLEncoder.encode(fileName, "utf-8");
response.setHeader("Content-disposition", "attachment;filename=" + excelName + ".xlsx");
// 先仅仅写入头,再以不写入头的方式写入数据
ExcelWriterBuilder writerBuilder = EasyExcel.write(response.getOutputStream()).head(headList);
writerBuilder.sheet("test")
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.doWrite(new ArrayList<>());
} catch (IOException e) {
throw new BizException("导出失败");
}
}
运行出的结果如下图
看出来啥问题了吗,虽然可以自适应,但这个列宽也太大了!!!
研究一下官方这个自适应策略的代码
package com.alibaba.excel.write.style.column;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
/**
* Take the width of the longest column as the width.
* <p>
* This is not very useful at the moment, for example if you have Numbers it will cause a newline.And the length is not
* exactly the same as the actual length.
*
* @author Jiaju Zhuang
*/
public class LongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell,
Head head,
Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (!needSetWidth) {
return;
}
Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
Integer columnWidth = dataLength(cellDataList, cell, isHead);
if (columnWidth < 0) {
return;
}
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
}
WriteCellData<?> cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
}
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
经过debug,发现他的处理方案其实是取的这一列的最宽的宽度,在合并单元格之前。
可以看这个B列,"小计"非常宽,他其实最终取到的宽度是"小计"的父级标题"截至当前交易总额"的宽度,并不是"小计"的宽度,但其实如果父级标题是一样的,最终的展示效果是合并起来的,子标题总计的长度远远超过父标题了,"小计"不应该这么宽
将导出文件的标题复制一份,然后取消单元格合并,就发现确实是这样,如下图
官方的代码也比较简单,很容易就能看懂,简单说明一下
直接给出我改的代码,新建一个类CustomLongestMatchColumnWidthStyleStrategy
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.Head;
import com.alibaba.excel.metadata.data.WriteCellData;
import com.alibaba.excel.util.MapUtils;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.poi.ss.usermodel.Cell;
/**
* 自定义 自适应宽度
*
* @author FangCheng
* @since 2023/08/25 10:19
**/
public class CustomLongestMatchColumnWidthStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
/**
* 官方给的这个cache存储的是:第几个sheet,第几列,最大宽度
*/
private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);
/**
* 存储表头宽度 第几个sheet,第几行,第几列,真实宽度
*/
private final Map<Integer, Map<Integer, Map<Integer, Integer>>> widthCache = MapUtils.newHashMapWithExpectedSize(8);
/**
* 存储表头key 第几个sheet,第几行,第几列,key
*/
private final Map<Integer, Map<Integer, Map<Integer, String>>> headerCache = MapUtils.newHashMapWithExpectedSize(8);
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell,
Head head,
Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
if (!needSetWidth) {
return;
}
Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
// 取当前sheet页数据
Map<Integer, Map<Integer, Integer>> currentSheetRowColumnWidthMap = widthCache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
Map<Integer, Map<Integer, String>> currentSheetRowColumnKeyMap =
headerCache.computeIfAbsent(writeSheetHolder.getSheetNo(), key -> new HashMap<>(16));
// 取当前行数据
Map<Integer, Integer> currentRowColumnWidthMap = currentSheetRowColumnWidthMap.computeIfAbsent(cell.getRowIndex(), key -> new HashMap<>(16));
Map<Integer, String> currentRowColumnKeyMap = currentSheetRowColumnKeyMap.computeIfAbsent(cell.getRowIndex(), key -> new HashMap<>(16));
Integer columnWidth = dataLength(cellDataList, cell, isHead);
if (columnWidth < 0) {
return;
}
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
if (isHead) {
// 表头数据,才存入
currentRowColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
currentRowColumnKeyMap.put(cell.getColumnIndex(), cell.getStringCellValue());
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
// 是表头,且存在多级表头(当前行号不为0),才处理,进行宽度重写
if (isHead && cell.getRowIndex() > 0) {
// 取前一行的数据信息
Map<Integer, String> previousRowColumnKeyMap = currentSheetRowColumnKeyMap.get(cell.getRowIndex() - 1);
Map<Integer, Integer> previousRowColumnWidthMap = currentSheetRowColumnWidthMap.get(cell.getRowIndex() - 1);
// 是最后一列才处理。依据上一行表头的size判断
if (previousRowColumnKeyMap.size() - 1 == cell.getColumnIndex()) {
List<List<Integer>> sameParentColumnIndices = getSameParentColumnIndex(previousRowColumnKeyMap);
for (List<Integer> sameParentColumnIndex : sameParentColumnIndices) {
Integer firstIndex = sameParentColumnIndex.get(0);
// 父级的最小长度,即单个父级的长度
Integer parentColumnMinWidth = previousRowColumnWidthMap.get(firstIndex);
// 父级的最大长度,即N个父级的长度
int parentColumnMaxWidth = previousRowColumnWidthMap.get(firstIndex) * sameParentColumnIndex.size();
Integer childrenColumnsWidth = 0;
for (Integer index : sameParentColumnIndex) {
childrenColumnsWidth += currentRowColumnWidthMap.get(index);
}
/*
*一共三种情况:
* 1、子表头的总长比 父表头单个都要小
* 2、子表头的总长比 父表头单个要大,但是比父表头N个要小
* 3、子表头的总长比 父表头N个都要大
*/
if (parentColumnMinWidth < childrenColumnsWidth && parentColumnMaxWidth > childrenColumnsWidth) {
// 2、子表头的总长比 父表头单个要大,但是比父表头N个要小,将表头处理成应该有的宽度
for (Integer index : sameParentColumnIndex) {
writeSheetHolder.getSheet().setColumnWidth(index, currentRowColumnWidthMap.get(index) * 256);
}
} else if (parentColumnMinWidth > childrenColumnsWidth) {
for (Integer index : sameParentColumnIndex) {
// 、子表头的总长比 父表头单个都要小,要根据比例重新计算宽度,如果算的宽度比正常还要小,那么就不修改
int newWidth = parentColumnMinWidth * currentRowColumnWidthMap.get(index) / childrenColumnsWidth;
if (newWidth > currentRowColumnWidthMap.get(index)) {
writeSheetHolder.getSheet().setColumnWidth(index, newWidth * 256);
currentRowColumnWidthMap.put(index, newWidth);
}
}
} else if (childrenColumnsWidth > parentColumnMaxWidth) {
// 3、子表头的总长比 父表头N个都要大,将表头处理成应该有的宽度
for (Integer index : sameParentColumnIndex) {
writeSheetHolder.getSheet().setColumnWidth(index, currentRowColumnWidthMap.get(index) * 256);
}
}
}
}
}
}
/**
* 取上一级表头,连续相等的key,所在的列
*
* @author FangCheng
* @since 2023/08/25 11:28
* @param previousRowColumnKeyMap previousRowColumnKeyMap
* @return java.util.List<java.util.List < java.lang.Integer>>
*/
private List<List<Integer>> getSameParentColumnIndex(Map<Integer, String> previousRowColumnKeyMap) {
List<List<Integer>> result = new ArrayList<>();
int calcIndex = 0;
for (Integer columnIndex : previousRowColumnKeyMap.keySet()) {
if (calcIndex >= columnIndex) {
continue;
}
String headerValue = previousRowColumnKeyMap.get(columnIndex);
List<Integer> sameIndex = new ArrayList<>();
sameIndex.add(columnIndex);
for (Integer nextColumnIndex : previousRowColumnKeyMap.keySet()) {
// hashMap的key是int自增的,所以可以这样循环处理
if (columnIndex >= nextColumnIndex) {
continue;
}
String nextHeaderValue = previousRowColumnKeyMap.get(nextColumnIndex);
if (ObjectUtil.equals(headerValue, nextHeaderValue)) {
sameIndex.add(nextColumnIndex);
calcIndex = nextColumnIndex;
} else {
// 如果下一个不相等,应直接跳出此循环
break;
}
}
if (sameIndex.size() > 1) {
result.add(sameIndex);
}
}
return result;
}
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
}
WriteCellData<?> cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
}
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
default:
return -1;
}
}
}
注释写的还比较详细吧,简单说一下思路
1、模仿官方的写法增加了两个map,存储真实的宽度及key值(单元格里的内容)。宽度map用来改写单元格宽度,key值map用来判断哪些单元格需要合并(问题点其实就在这里,没有处理合并的单元格宽度)
2、判断是表头才写入这两个map
3、判断是表头,且存在多级表头(当前行号不为0),才处理,进行宽度重写
4、当前单元格为最后一个单元格才进行改写,一行的宽度一起进行改写
5、分别处理三种场景,writeSheetHolder.getSheet().setColumnWidth改写单元格宽度,并修改宽度map(下一级表头需要使用修改后的数据才行)
-
1、子表头的总长比 父表头单个都要小
即这种情况,子表头总共才7个汉字,而父表头有10个汉字,按照默认的自适应策略,每个单元格都将是10个汉字的宽度。需要处理成:要根据比例重新计算宽度,如果算的宽度比正常还要小,那么就不修改。 -
2、子表头的总长比 父表头单个要大,但是比父表头N个要小
即这种情况,二级标题26个汉字,比父标题8个汉字多,又比8*4=32个汉字少。需要处理成:直接取子表头的宽度。 -
3、子表头的总长比 父表头N个都要大
这种情况也很好理解:直接取子表头的宽度。
修改导出时使用的策略
writerBuilder.sheet("test")
.registerWriteHandler(new CustomLongestMatchColumnWidthStyleStrategy())
.doWrite(new ArrayList<>());
看效果吧
是不是比之前舒服很多呢
更多推荐
所有评论(0)