背景
很久很久以前写了一篇类似的文章 阿里easyExcel – excel下载/导出/读取 (单元格自定义下拉选择、不支持图片) ,用了没多久就发现不好用,限制太多(以后遇到你就知道了),然后就有了现在迟到很久的文章,主要懒得写文章。
必看
此篇文章的单元格下拉支持 1级,2级,多级联动下拉等 ,比较复杂,需耐心看一下。
再写之前,先讲几个excel注意的点:
- 你的excel必须支持创建 名称管理器 ,如下图所示:
- excel必须支持
INDIRECT
,CONCATENATE
和VLOOKUP
函数,要是excel版本太太太太低,可能没有这些函数,检查方式如下,一般=后面加函数名就会有提示:
缺点或限制
- 当下拉的数据太多时,会导致创建excel的速度变慢
- 下拉的总数据不能超过
1048576
行,不能超过16384列
先简单看下效果
下拉的原理,怎么实现的下拉
创建一个sheet,然后把下拉数据放接某一列,如下:
当然也可以横着放,但是横着最多只能放 16384
列,而竖着可以放 1048576
行。
如果 只有1级下拉 那么不需要创建 名称管理器 ,如果是多级联动下拉,则除了最后1级,其余都要设置 名称管理器 , 名称管理器 怎么设置下拉自己去百度。下面直接放代码,看不懂慢慢看,不想看直接复制使用,不想讲解了哈哈哈。
另外,由于 名称管理器 对name的设置要求很高,有些字符无法设置,为了实现下拉数据的千奇百怪,所以需要做一层转换。下面展示代码。
代码
ExcelUtils 工具:
public class ExcelUtils {
/**
* 下载
*
* @param writeHandlers 处理器(可自定义,可为null)
* @param os 输出流
* @param clazz 操作对象字节
* @param data 数据
* @param sheetName 表名
*/
public static <T> void downLoad(List<WriteHandler> writeHandlers, OutputStream os, Class<T> clazz, List<T> data, String sheetName) {
ExcelWriterSheetBuilder builder = EasyExcelFactory.write(os, clazz).sheet(sheetName);
if (!CollectionUtils.isEmpty(writeHandlers)) {
writeHandlers.forEach(builder::registerWriteHandler);
}
builder.doWrite(data);
}
}
ExcelLinkageDropdown 多级下拉数据:
public class ExcelLinkageDropdown {
/**
* 是否允许设置其他的值。false:只能是下拉列表的值;true:允许列表之外的值
*/
private boolean isAllowOtherValue = false;
/**
* 表头名称(为bean对象时传字段名称,为map时且多个头用json:["头1","头2"])
*/
private String fieldName;
/**
* 第几列,为对象自动计算
*/
private Integer cellIndex;
/**
* 下拉内容,<上级,下级列表>,第一级的key为null,只有一级时key也为null
*/
private Map<String, List<String>> value = new HashMap<>();
/**
* 提示信息
*/
private String message = "只能选择列表中的值!!!";
//...get set 自己生成
}
DropdownWriteHandler下拉处理器:
/**
* 下拉处理器:单元格下拉列表格式
* 最大行:1048576 / 65536
* 最大列:16384 / 256
*/
public class DropdownWriteHandler extends AbstractVerticalCellStyleStrategy implements SheetWriteHandler {
private final Map<ExcelLinkageDropdown[], String> dropdowns = new HashMap<>(); //所有下拉值
private final Class<?> clazz; //操作的类
private final int headMax; //表头行数
public DropdownWriteHandler(Class<?> clazz) {
this.clazz = clazz;
Field[] fields = clazz.getDeclaredFields();
// 取表头行数
this.headMax = Arrays.stream(fields).filter(field -> field.isAnnotationPresent(ExcelProperty.class))
.map(field -> field.getAnnotation(ExcelProperty.class).value().length).reduce(Integer::max).orElse(0);
}
@Override
public void beforeSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
}
@Override
public void afterSheetCreate(WriteWorkbookHolder writeWorkbookHolder, WriteSheetHolder writeSheetHolder) {
Workbook book = writeWorkbookHolder.getCachedWorkbook();
Sheet sheet = writeSheetHolder.getSheet();
// 设置固定区域
sheet.createFreezePane(0, headMax, 0, headMax);
DataValidationHelper helper = sheet.getDataValidationHelper();
// 联动下拉校验
if (!CollectionUtils.isEmpty(dropdowns)) {
long l = System.currentTimeMillis();
for (Map.Entry<ExcelLinkageDropdown[], String> dropdownMap : dropdowns.entrySet()) {
ExcelLinkageDropdown[] dropdown = dropdownMap.getKey();
String key = StringUtils.isNotBlank(dropdownMap.getValue()) ? dropdownMap.getValue() :
"t_" + Arrays.stream(dropdown).map(t -> t.getCellIndex().toString()).collect(Collectors.joining("_"));
if (key.length() > 30) {
key = "s_" + IdGenerator.getInstance().getId();
}
//设置下拉及校验数据
setDropdownsAndValidationData(book, sheet, helper, dropdown, key);
}
long l2 = System.currentTimeMillis();
System.out.println("下拉耗时" + (l2 - l));
}
}
/**
* 设置下拉及校验数据
*/
private void setDropdownsAndValidationData(Workbook book, Sheet sheet, DataValidationHelper helper, ExcelLinkageDropdown[] dropdown, String key) {
// 设置多级下拉
if (book.getSheetIndex(key) == -1) {
buildDropdownSheet(book, dropdown, key);
}
//设置一级下拉
List<String> val = dropdown[0].getValue().get(null);
if (Objects.nonNull(val) && !val.isEmpty()) {
String ss = ExcelTools.getRangeByCel(2, 2, val.size()); // A和B被占用,从C开始
dropdownValidationData(String.format("='%s'!%s", key, ss), helper, this.headMax, 1000000, dropdown[0], sheet);
}
//二级及之后的下拉
for (int i = 1; i < dropdown.length; i++) {
String ci = CellReference.convertNumToColString(dropdown[i - 1].getCellIndex());
String format = "INDIRECT(CONCATENATE(\"_\",VLOOKUP($" + ci + (this.headMax + 1) + "," + key + "!A:B,2,0),\"_\",\"" + key + "\"))"; // A:B写死
dropdownValidationData(format, helper, this.headMax, 1000001, dropdown[i], sheet);
}
}
/**
* 验证下拉数据
*
* @param formula 公式
* @param firstRow 第一行
* @param lastRow 最后一行
* @param dropdown1 下拉数据
* @param sheet 表
*/
private static void dropdownValidationData(String formula, DataValidationHelper helper, int firstRow, int lastRow, ExcelLinkageDropdown dropdown1, Sheet sheet) {
DataValidationConstraint constraint = helper.createFormulaListConstraint(formula);
DataValidation dataValidation = helper.createValidation(constraint, new CellRangeAddressList(firstRow, lastRow, dropdown1.getCellIndex(), dropdown1.getCellIndex()));
dataValidation.setSuppressDropDownArrow(false);
if (dataValidation instanceof XSSFDataValidation) {
dataValidation.setSuppressDropDownArrow(true);
dataValidation.setShowErrorBox(!dropdown1.isAllowOtherValue()); // 输入无效值时是否显示错误框
dataValidation.setShowPromptBox(!dropdown1.isAllowOtherValue()); // 设置无效值时 是否弹出提示框
dataValidation.createPromptBox("温馨提示", dropdown1.getMessage()); // 设置无效值时的提示框内容
dataValidation.createErrorBox("温馨提示", dropdown1.getMessage()); // 设置无效值时的提示框内容
}
sheet.addValidationData(dataValidation);
}
/**
* 设置单级或多级联动下拉,按顺序(1级,2级,3级...),否则将出错
* 调用多次将设置多个多级联动
* 单级或1级的key为null
*
* @param head clazz为map时需要传头,否则传null
*/
public void setLinkageDropdown(List<List<String>> head, String excelName, ExcelLinkageDropdown... dropdowns) throws Exception {
if (Objects.isNull(dropdowns) || dropdowns.length == 0) {
throw new Exception("至少设置一个下拉参数");
}
boolean isMap = this.clazz.isAssignableFrom(Map.class);
if (isMap && CollectionUtils.isEmpty(head)) {
throw new Exception("head参数不能为空");
}
if (StringUtils.isNotBlank(excelName) && excelName.length() > 30) {
throw new Exception("excelName长度不能超过30");
}
if (isMap) {
for (ExcelLinkageDropdown dropdown : dropdowns) {
if (dropdown.getValue().isEmpty()) {
continue;
}
List<String> heads = head.stream().map(t -> String.join(",", t)).collect(Collectors.toList());
int i = heads.indexOf(dropdown.getFieldName());
dropdown.setCellIndex(i);
}
this.dropdowns.put(dropdowns, excelName);
return;
}
List<ExcelLinkageDropdown> ds = new ArrayList<>();
for (ExcelLinkageDropdown dropdown : dropdowns) {
if (dropdown.getValue().isEmpty()) {
continue;
}
Field field;
try {
field = this.clazz.getDeclaredField(dropdown.getFieldName());
} catch (Exception e) {
throw new Exception("填写的字段不存在:" + dropdown.getFieldName() + "," + e.getMessage());
}
int index = field.getAnnotation(ExcelProperty.class).index(); // 获取头的位置
if (index == -1) {
for (Field f : clazz.getDeclaredFields()) {
if (!f.isAnnotationPresent(ExcelProperty.class)) {
continue;
}
index++;
if (f.getName().equals(dropdown.getFieldName())) {
break;
}
}
}
dropdown.setCellIndex(index);
ds.add(dropdown);
}
this.dropdowns.put(ds.toArray(new ExcelLinkageDropdown[0]), excelName);
}
/**
* 设置单级或多级联动下拉,按顺序(1级,2级,3级...),否则将出错
* 调用多次将设置多个多级联动
* 单级或1级的key为null
*
* @param head clazz为map时需要传头,否则传null
*/
public void setLinkageDropdown(List<List<String>> head, ExcelLinkageDropdown... dropdowns) throws Exception {
this.setLinkageDropdown(head, null, dropdowns);
}
/**
* 构建下拉列表sheet页,用于下拉框展示的数据源
*
* @param book 工作簿
*/
private void buildDropdownSheet(Workbook book, ExcelLinkageDropdown[] dropdowns, String hiddenArea) {
//创建新的隐藏表
Sheet hideSheet = createNewHideSheet(book, hiddenArea);
// 设置map的key格式化, 设置一级下拉
this.setKeyFormat(dropdowns, hideSheet);
// 第4列开始将具体的数据写入到每一列中
int col = 3;
int startRow = 1; //开始行
CellStyle cellStyle = this.getFixRed(book);
for (int i = 1; i < dropdowns.length; i++) {
ExcelLinkageDropdown dropdown = dropdowns[i];
for (Map.Entry<String, List<String>> entry : dropdown.getValue().entrySet()) {
int rows = entry.getValue().size();
//起始行如果超出最大行数,则新增一列,起始行重新计算,当前列总行数重新计算;
if (startRow >= 1048576 || startRow + rows >= 1048576) {
startRow = 1;
col++;
}
//当前列超出最大列数,报错
if (col > 16384) {
throw new RuntimeException("当前列超出最大列数");
}
//设置下拉值和名称管理器
this.setNameName(book, hideSheet, hiddenArea, col, startRow, cellStyle, entry, rows);
//重新计算起始行
startRow = startRow + rows + 2;
}
}
}
/**
* 设置下拉值和名称管理器
*/
private void setNameName(Workbook book, Sheet hideSheet, String hiddenArea, int col, int startRow, CellStyle cellStyle, Map.Entry<String, List<String>> entry, int rows) {
Row row0 = Objects.isNull(hideSheet.getRow(startRow - 1)) ? hideSheet.createRow(startRow - 1) : hideSheet.getRow(startRow - 1);
Cell cell = row0.createCell(col);
cell.setCellValue(ExcelTools.replaceAscii(entry.getKey()));
cell.setCellStyle(cellStyle); //设置样式,区分下拉值
for (int j = 0; j < rows; j++) {
int r = j + startRow;
Row row = Objects.isNull(hideSheet.getRow(r)) ? hideSheet.createRow(r) : hideSheet.getRow(r);
row.createCell(col).setCellValue(entry.getValue().get(j));
}
// 添加名称管理器
String range = ExcelTools.getRangeByCel(col, startRow + 1, rows);
String nameName = "_" + ExcelTools.replaceAscii(entry.getKey()) + "_" + hiddenArea;
Name name = book.createName();
name.setNameName(nameName); // key不可重复
String formula = hiddenArea + "!" + range;
name.setRefersToFormula(formula);
}
/**
* 设置map的key格式化
*/
private void setKeyFormat(ExcelLinkageDropdown[] dropdowns, Sheet hideSheet) {
// 第1-2列设置匹配表
int rowId = 0; // 设置区域的头行
for (int i = 1; i < dropdowns.length; i++) {
ExcelLinkageDropdown d = dropdowns[i];
for (Map.Entry<String, List<String>> kv : d.getValue().entrySet()) {
// 原始key-第一列
Row row0 = hideSheet.createRow(rowId);
row0.createCell(0).setCellValue(kv.getKey()); // 第一列
row0.createCell(1).setCellValue(ExcelTools.replaceAscii(kv.getKey())); // 处理后的key-第二列
rowId++;
}
}
// 设置第1级
List<String> dropdownVal = dropdowns[0].getValue().get(null); // 得到第一级
Row r1 = Objects.isNull(hideSheet.getRow(0)) ? hideSheet.createRow(0) : hideSheet.getRow(0);
r1.createCell(2).setCellValue(dropdowns[0].getFieldName());
for (int i = 0; i < dropdownVal.size(); i++) {
Row r1_ = Objects.isNull(hideSheet.getRow(i + 1)) ? hideSheet.createRow(i + 1) : hideSheet.getRow(i + 1);
r1_.createCell(2).setCellValue(dropdownVal.get(i));
}
}
/**
* 创建新的隐藏表
*/
private Sheet createNewHideSheet(Workbook book, String hiddenArea) {
// 创建一个专门用来存放下拉的隐藏sheet页
Sheet hideSheet = book.createSheet(hiddenArea);
// 这一行作用是将此sheet隐藏
book.setSheetHidden(book.getSheetIndex(hideSheet), true);
//如果是SXSSFWorkbook类型,转XSSFWorkbook类型,否则sheet.getRow()可能为空
if (book instanceof SXSSFWorkbook) {
SXSSFWorkbook sxssfWorkbook = (SXSSFWorkbook) book;
hideSheet = sxssfWorkbook.getXSSFWorkbook().getSheetAt(book.getSheetIndex(hideSheet));
}
return hideSheet;
}
/**
* 红色固定样式
*/
private CellStyle getFixRed(Workbook book) {
CellStyle style = book.createCellStyle();
Font font = book.createFont();
font.setBold(true);
font.setColor(IndexedColors.RED.getIndex());
style.setFont(font);
return style;
}
}
工具:
public class ExcelTools {
/**
* 计算formula:纵向
*
* @param offset 偏移量,如果给0,表示从A列开始,1,就是从B列
* @param rowId 第几行开始
* @param rowCount 一共多少行
* @return 如果给入参 0,2,10. 表示从A2-A11。最终返回 $A$2:$A$11
*/
public static String getRangeByCel(int offset, int rowId, int rowCount) {
String columnLetter1 = CellReference.convertNumToColString(offset);
return String.format("$%s$%s:$%s$%s", columnLetter1, rowId, columnLetter1, rowId + rowCount - 1);
}
/**
* 把非(中文、英文、下划线、点)替换为 ascii码,因为excel不支持其他字符设置名称管理器
* 如:”审核订单(一级)(1)“ 替换为 ”审核订单.40.一级.41..40.1.41.“
*/
public static String replaceAscii(String str) {
if (StringUtils.isBlank(str)) {
return str;
}
StringBuilder sb = new StringBuilder();
for (String s : str.split("")) {
if (s.matches("[^\\d\\u4e00-\\u9fa5.a-zA-Z_]")) {
s = "." + (int) s.charAt(0) + ".";
}
sb.append(s);
}
return sb.toString();
}
}
测试
@HeadStyle(fillForegroundColor = 1)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ExcelProperty(value = {"一级"})
private String borth;
@ExcelProperty(value = {"二级"})
@HeadStyle(fillForegroundColor = 29)
private String name;
@ExcelProperty(value = {"三级"})
private String url;
@ExcelProperty(value = {"性别"})
private String sex;
// 日期校验
@ExcelProperty(value = "图片")
private WriteCellData<Void> imgs;
// 日期校验
@ExcelProperty(value = "字节图片")
private byte[] byteArray;
}
//测试excel多级联动下拉
@GetMapping("/test4")
public void test4(HttpServletResponse response, int n) throws Exception {
long l = System.currentTimeMillis();
Map<String, List<String>> m1 = new HashMap<>();
m1.put(null, new ArrayList<>());
for (int i = 0; i < n; i++) {
m1.get(null).add("A" + (i == 0 ? "" : i));
m1.get(null).add("AA" + (i == 0 ? "" : i));
m1.get(null).add("AAA" + (i == 0 ? "" : i));
m1.get(null).add("AAAA" + (i == 0 ? "" : i));
}
Map<String, List<String>> m2 = new HashMap<>();
for (String s : m1.get(null)) {
List<String> a = new ArrayList<>();
for (int i = 1; i < 11; i++) {
a.add(s + "-" + i + "B");
}
m2.put(s, a);
}
List<String> v2 = m2.values().stream().flatMap(List::stream).collect(Collectors.toList());
Map<String, List<String>> m3 = new HashMap<>();
for (String s : v2) {
int t = 50001;
List<String> a = new ArrayList<>();
for (int i = 1; i < t; i++) {
a.add(s + "-" + i + "C");
if (i > 1) {
t = 1;
}
}
m3.put(s, a);
System.out.println(s);
}
DropdownWriteHandler handler = new DropdownWriteHandler(User.class);
ExcelLinkageDropdown dropdown11 = new ExcelLinkageDropdown();
dropdown11.setFieldName("borth");
dropdown11.setValue(m1);
ExcelLinkageDropdown dropdown111 = new ExcelLinkageDropdown();
dropdown111.setFieldName("name");
dropdown111.setValue(m2);
ExcelLinkageDropdown dropdown1111 = new ExcelLinkageDropdown();
dropdown1111.setFieldName("url");
dropdown1111.setValue(m3);
Map<String, List<String>> sexMap = new HashMap<>();
sexMap.put(null, m3.values().stream().flatMap(List::stream).collect(Collectors.toList()));
ExcelLinkageDropdown sex = new ExcelLinkageDropdown();
sex.setFieldName("sex");
sex.setValue(sexMap);
long l2 = System.currentTimeMillis();
System.out.println("设置耗时" + (l2 - l));
handler.setLinkageDropdown(null, dropdown11, dropdown111, dropdown1111);
handler.setLinkageDropdown(null, sex);
long l3 = System.currentTimeMillis();
System.out.println("设置耗时" + (l3 - l2));
ExcelUtils.downLoad(Lists.newArrayList(handler), ExcelTools.getOutputStream("测试excel多级联动下拉", response), User.class, null, "测试excel多级联动下拉");
}
好了,又不想写了,就到此为止吧,有问题评论留言,看到能回就回。