阿里easyExcel -- excel单元格自定义下拉选择(升级版)

背景

很久很久以前写了一篇类似的文章 阿里easyExcel -- excel下载/导出/读取 (单元格自定义下拉选择、不支持图片) ,用了没多久就发现不好用,限制太多(以后遇到你就知道了),然后就有了现在迟到很久的文章,主要懒得写文章。

必看

此篇文章的单元格下拉支持 1级,2级,多级联动下拉等 ,比较复杂,需耐心看一下。

再写之前,先讲几个excel注意的点:

  1. 你的excel必须支持创建 名称管理器 ,如下图所示:
  2. excel必须支持 INDIRECTCONCATENATEVLOOKUP 函数,要是excel版本太太太太低,可能没有这些函数,检查方式如下,一般=后面加函数名就会有提示:

缺点或限制

  1. 当下拉的数据太多时,会导致创建excel的速度变慢
  2. 下拉的总数据不能超过 1048576 行,不能超过16384列

先简单看下效果




下拉的原理,怎么实现的下拉

创建一个sheet,然后把下拉数据放接某一列,如下:

当然也可以横着放,但是横着最多只能放 16384 列,而竖着可以放 1048576 行。

如果 只有1级下拉 那么不需要创建 名称管理器 ,如果是多级联动下拉,则除了最后1级,其余都要设置 名称管理器名称管理器 怎么设置下拉自己去百度。下面直接放代码,看不懂慢慢看,不想看直接复制使用,不想讲解了哈哈哈。

另外,由于 名称管理器 对name的设置要求很高,有些字符无法设置,为了实现下拉数据的千奇百怪,所以需要做一层转换。下面展示代码。

代码

ExcelUtils 工具:

java 复制代码
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 多级下拉数据:

java 复制代码
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下拉处理器:

java 复制代码
/**
 * 下拉处理器:单元格下拉列表格式
 * 最大行: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;
    }
}

工具:

java 复制代码
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();
    }
}

测试

java 复制代码
@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;
}
java 复制代码
	//测试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多级联动下拉");
    }

好了,又不想写了,就到此为止吧,有问题评论留言,看到能回就回。

相关推荐
Aileen_0v07 分钟前
【玩转OCR | 腾讯云智能结构化OCR在图像增强与发票识别中的应用实践】
android·java·人工智能·云计算·ocr·腾讯云·玩转腾讯云ocr
桂月二二2 小时前
Java与容器化:如何使用Docker和Kubernetes优化Java应用的部署
java·docker·kubernetes
liuxin334455662 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
大G哥2 小时前
pytest自动化测试数据驱动yaml/excel/csv/json
json·excel·pytest
小马爱打代码2 小时前
设计模式详解(建造者模式)
java·设计模式·建造者模式
小奥超人2 小时前
Excel粘贴复制不完整的原因以及解决方法
windows·经验分享·microsoft·excel·办公技巧
code04号3 小时前
python脚本:批量提取excel数据
开发语言·python·excel
栗子~~3 小时前
idea 8年使用整理
java·ide·intellij-idea
2301_801483693 小时前
Maven核心概念
java·maven
Q_19284999063 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端