Java Excel 多级菜单联动原理与实现(可扩展)

您所在的位置:网站首页 excel做成下拉菜单 Java Excel 多级菜单联动原理与实现(可扩展)

Java Excel 多级菜单联动原理与实现(可扩展)

2023-12-05 06:38| 来源: 网络整理| 查看: 265

Java Excel级联菜单实现(可扩展)

为什么要写这篇文章呢,因为看到了有人在提问如何用Java做Excel的级联菜单效果。帖子详情:http://spring4all.com/forum-post/575 我之前也遇到过同样的场景,当时查了很多文档才搞定,为了让更多人可以直接使用代码,节省时间,所以决定写这篇文章。

实现效果

先上效果图,这里演示我做了三级下拉菜单的联动,不过我实现的版本可以支持任意级菜单的联动 在这里插入图片描述

实现原理介绍 名称管理器

Excel中有名称管理器的概念,什么意思呢。可以简单理解为,一个名称对应一组数据序列,举个例子,一个省会对映多个市,省名则是名称管理器的名字,对应的数据序列则是相关的市级城市名。

在创建下拉关系对映规则时,需要先根据数据的对映关系,创建所有名称管理器,然后再用数据有效性绑定名称管理器的名字就行了。 在这里插入图片描述 可以在公式->名称管理器内查看我这边创建的名称管理器和对映关系。

数据有效性

名称管理器创建好了,怎么使用呢?这就要用到Excel的数据有效性了。 你可以任意选中一个单元格,在菜单栏上点击数据->有效性 在这里插入图片描述 在数据有效性内设置如下 在这里插入图片描述 来源参数说明 =INDIRECT() :Excel内置函数,可以返回单元格的值引用 dataSheet:数据页的名称 $A$1:数据页的单元格位置 $A$1 是什么值呢,其实就是province 在这里插入图片描述 还记得名称管理器的province对应的数据序列吗,会被引用到这个单元格上,于是效果如下 在这里插入图片描述 好了,基本原理就是这样,接下来我们看下怎么使用代码生成

设计与约定 易使用

只需要用到两个注解,分别是ExcelFile、ExcelValidation ExcelFile代表需要生成文件 目前Excel是在本地创建的,不过可以根据我的源码修改成上传到服务器,如果你需要帮助,可以给我留言 ExcelValidation是打在字段上的,用来标注字段是否需要生成校验。 目前获取数据源的的方式比较单一,只支持静态无参方法,如果你的项目整合了Spring,也可以改从Bean方法内获取数据,如果你需要帮助,可以给我留言

低入侵

你可以选择copy源码或者打jar包的方式来使用,只需要在Excel实体对象上标注注解即可

约定 如果数据集返回的是List,代表是单独的、无依赖的列如果数据集返回的是Map,key是依赖列的名称,value是key所对应的数据 小试牛刀

我的项目结构 excel-example是例子,准备的一些数据 excel-extend是具体的实现 ![在这里插入图片描述](https://img-blog.csdnimg.cn/0ecd87d063cb4fe8b16fac7e253776cd.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAeGltdV9zdW5ueQ==,size_20,color_FFFFFF,t_70,g_se,x_16

首先我在com.excel.service.ExcelExampleService准备了数据列表 queryProvinceList,获取省列表 queryMunicipalityList,获取市列表,Map的Key是所属的省 queryDistrictList,获取区列表,Map的Key是所属的市

public class ExcelExampleService { public static List queryProvinceList() { return Arrays.asList("浙江省", "湖南省", "贵州省"); } public static Map queryMunicipalityList() { Map map = new HashMap(4); map.put("浙江省", Arrays.asList("杭州市", "温州市", "宁波市")); map.put("湖南省", Arrays.asList("长沙市", "邵阳市", "常德市")); map.put("贵州省", Arrays.asList("贵阳市", "遵义市", "安顺市")); return map; } public static Map queryDistrictList() { Map map = new HashMap(8); map.put("杭州市", Arrays.asList("上城区", "下城区", "萧山区")); map.put("温州市", Arrays.asList("鹿城区", "龙湾区", "瓯海区", "洞头区")); map.put("长沙市", Arrays.asList("芙蓉区", "天心区", "岳麓区")); map.put("邵阳市", Arrays.asList("双清区", "大祥区", "北塔区")); map.put("贵阳市", Arrays.asList("南明区", "云岩区", "花溪区")); return map; } }

然后配置DTO

import com.excel.annotation.ExcelFile; import com.excel.annotation.ExcelValidation; import lombok.Data; /** * @author ximu * @date 2022/4/6 * @description */ @ExcelFile(fileHeadTemplate = "province|municipality|district", fileMappingTemplate = "province=所属省|municipality=所属市|district=所属区", datasheetHidden = false, enableDataValidation = true) @Data public class ExcelExportDTO { /** * 所属省 */ @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryProvinceList") private String province; /** * 所属市 */ @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryMunicipalityList", beforeFieldName = "province") private String municipality; /** * 所属区 */ @ExcelValidation(datasourceMethod = "com.excel.service.ExcelExampleService.queryDistrictList", beforeFieldName = "municipality") private String district; }

启动项目,创建Excel

public class T_Main { public static void main(String[] args) throws IOException { ExcelExportDTO excelExportDTO = new ExcelExportDTO(); String excel = ExcelUtil.createExcel(Arrays.asList(excelExportDTO)); System.out.println(excel); } }

创建成功后,sheet1可以正常的选择下拉菜单 你会发现还有一页数据页,可以隐藏起来 使用 ExcelFile 注解属性 datasheetHidden 配置 默认是 true 隐藏的 在这里插入图片描述 如果用户随意输入值的话,是可以强制校验的 在这里插入图片描述 通过 ExcelFile 注解 showErrorBox 属性控制

实现代码 注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author ximu * @date 2021/8/29 * @description 标记类为一个Excel文件 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) public @interface ExcelFile { /** * 文件全名 */ String fileName() default "excel.xlsx"; /** * 页名称 */ String sheetName() default "sheet1"; /** * excel使用的属性排列格式模板 格式: 字段名1{fileTemplateSplit}字段名2{fileTemplateSplit}字段名3 *

* 示例:id|name|age *

* 说明表头index0=id,index1=name */ String fileHeadTemplate() default ""; /** * excel属性与表头映射模版 格式: 字段1{fileMappingSplit}映射名称1{fileTemplateSplit}字段1{fileMappingSplit}映射名称1 *

* 示例:id=学号|name=学生姓名 *

* 说明id属性映射表头为学号,name属性映射表头为学生姓名 */ String fileMappingTemplate() default ""; /** * 模版字段分隔符,默认无需调整 */ String fileTemplateSplit() default "\\|"; /** * 属性名称映射分隔符,默认无需调整 */ String fileMappingSplit() default "\\="; /** * 启用数据校验,只有当值为true时,数据页才会创建 */ boolean enableDataValidation() default false; /** * 数据页名称 */ String dataSheetName() default "dataSheet"; /** * 隐藏数据页 */ boolean datasheetHidden() default true; /** * 校验用户输入是否合法 */ boolean showErrorBox() default true; } import org.apache.poi.ss.usermodel.DataValidationConstraint; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @author ximu * @date 2022/3/26 * @description excel校验器 */ @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface ExcelValidation { /** * 数据源方法全名 */ String datasourceMethod(); /** * 前列字段 *

* 当为空字符串时,认定无前列依赖 */ String beforeFieldName() default ""; /** * 开始行 */ int firstRow() default 1; /** * 结束行 */ int lastRow() default 2000; /** * 校验类型 * * @see org.apache.poi.ss.usermodel.DataValidationConstraint.ValidationType */ int validationType() default DataValidationConstraint.ValidationType.LIST; } 工具类 import com.excel.annotation.ExcelFile; import com.excel.annotation.ExcelValidation; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.util.CellRangeAddressList; import org.apache.poi.xssf.usermodel.XSSFDataValidationConstraint; import org.apache.poi.xssf.usermodel.XSSFDataValidationHelper; import org.apache.poi.xssf.usermodel.XSSFSheet; import java.io.FileOutputStream; import java.io.IOException; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.*; import java.util.stream.Collectors; /** * @author ximu * @date 2022/4/6 * @description Excel工具类 */ public class ExcelUtil { /** * 构建excel文件 * * @param collection 数据集合 * @return excel文件路径 */ public static String createExcel(Collection collection) throws IOException { if (CollectionUtils.isEmpty(collection)) { throw new RuntimeException("excel数据不能为空!"); } Object object = collection.stream().findFirst().get(); Class clazz = object.getClass(); boolean annotationPresent = clazz.isAnnotationPresent(ExcelFile.class); if (!annotationPresent) { throw new RuntimeException("该对象不存在ExcelFile注解,不能生成Excel!"); } ExcelFile annotation = clazz.getAnnotation(ExcelFile.class); // 取到模板 String headTemplate = annotation.fileHeadTemplate(); // 取到模板上的所有对象属性 String[] split = headTemplate.split(annotation.fileTemplateSplit()); // 取到模板属性与名称的映射关系 String fileTemplateSplit = annotation.fileMappingTemplate(); String[] mappingSplit = fileTemplateSplit.split(annotation.fileTemplateSplit()); Map nameMappingMap = Arrays.stream(mappingSplit).map(x -> x.split(annotation.fileMappingSplit())).collect(Collectors.toMap(x -> x[0], x -> x[1])); // 创建excel Workbook workbook = WorkbookFactory.create(true); // 创建excel页 Sheet sheet = workbook.createSheet(annotation.sheetName()); // 创建数据页 Sheet dataSheet = null; if (annotation.enableDataValidation()) { // 创建数据页 dataSheet = workbook.createSheet(annotation.dataSheetName()); // 设置隐藏属性 workbook.setSheetHidden(workbook.getSheetIndex(dataSheet), annotation.datasheetHidden()); } Map fieldMap = ReflectionUtil.getFieldMap(object); int rowIndex = 0, colIndex = 0; // 填充表头 for (String fieldName : split) { if (annotation.enableDataValidation()) { createColumnValidation(split, fieldMap.get(fieldName), workbook, sheet, dataSheet, colIndex, annotation.showErrorBox()); } createCell(sheet, rowIndex, colIndex++, nameMappingMap.get(fieldName)); } ++rowIndex; colIndex = 0; for (Object data : collection) { for (String fieldName : split) { Field field = fieldMap.get(fieldName); field.setAccessible(true); try { Object val = field.get(data); // 创建完之后列需要忘后移动 所以需要加一 createCell(sheet, rowIndex, colIndex++, val); } catch (IllegalAccessException e) { e.printStackTrace(); } } ++rowIndex; colIndex = 0; } FileOutputStream out = new FileOutputStream(annotation.fileName()); workbook.write(out); out.close(); return annotation.fileName(); } private static void createColumnValidation(String[] split, Field field, Workbook workbook, Sheet sheet, Sheet dataSheet, int colIndex, boolean showErrorBox) { if (field == null || dataSheet == null) { return; } field.setAccessible(true); ExcelValidation excelValidation = field.getAnnotation(ExcelValidation.class); if (excelValidation == null) { return; } String datasourceMethod = excelValidation.datasourceMethod(); Method method = ReflectionUtil.getMethod(datasourceMethod); Object invoke; try { invoke = method.invoke(null); } catch (IllegalAccessException | InvocationTargetException e) { e.printStackTrace(); return; } String formulaIndirectFormat = "=INDIRECT(%s!$%s$%s)"; // 判断是否有前置字段 if (StringUtils.isBlank(excelValidation.beforeFieldName())) { if (!(invoke instanceof Collection)) { return; } Collection collection = (Collection) invoke; createNameManage(workbook, dataSheet, field.getName(), collection, colIndex); String formulaIndirect = String.format(formulaIndirectFormat, dataSheet.getSheetName(), getCellColumnFlag(1), colIndex + 1); createDataValidate(sheet, formulaIndirect, excelValidation.validationType(), excelValidation.firstRow(), excelValidation.lastRow(), colIndex, colIndex, showErrorBox); } else { if (!(invoke instanceof Map)) { return; } Map map = (Map) invoke; map.forEach((k, v) -> createNameManage(workbook, dataSheet, k, v, colIndex)); int beforeColIndex = 0; for (int i = 0; i beforeColIndex = i; } } for (int rowIndex = excelValidation.firstRow(); rowIndex CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol); XSSFDataValidationHelper xssfDataValidationHelper = new XSSFDataValidationHelper((XSSFSheet) sheet); XSSFDataValidationConstraint xssfDataValidationConstraint = new XSSFDataValidationConstraint(validationType, formula); DataValidation validation = xssfDataValidationHelper.createValidation(xssfDataValidationConstraint, cellRangeAddressList); validation.createErrorBox("输入有误!", "请选择下拉菜单里面的选项!"); validation.setEmptyCellAllowed(false); validation.setShowErrorBox(showErrorBox); sheet.addValidationData(validation); } private static void createNameManage(Workbook workbook, Sheet sheet, String nameString, Collection data, final int rowIndex) { final int size = workbook.getAllNames().size(); int columnIndex = 0; String format = "%s!$%s$%s:$%s$%s"; // 创建名称管理器 Name name = workbook.createName(); name.setNameName(nameString); String cellColumnFlag = getCellColumnFlag(columnIndex + 2); int nameManageRegan = CollectionUtils.isEmpty(data) ? 1 : data.size() + 1; String nameManageScope = String.format(format, sheet.getSheetName(), cellColumnFlag, size + 1, getCellColumnFlag(nameManageRegan), size + 1); name.setRefersToFormula(nameManageScope); createCell(sheet, size, columnIndex, nameString); if (CollectionUtils.isNotEmpty(data)) { for (Object val : data) { createCell(sheet, size, ++columnIndex, String.valueOf(val)); } } } private static String getCellColumnFlag(int num) { String colFiled = ""; int chuNum = 0; int yuNum = 0; if (num >= 1 && num chuNum = num / 26; yuNum = num % 26; yuNum = yuNum == 0 ? 1 : yuNum; colFiled += doHandle(chuNum); colFiled += doHandle(yuNum); } return colFiled; } private static String doHandle(int num) { return String.valueOf((char) (num + 64)); } /** * 创建单元格 * * @param sheet 页 * @param rowIndex 行号,从0开始 * @param colIndex 列号,从0开始 * @param val 单元格的值 */ private static void createCell(Sheet sheet, int rowIndex, int colIndex, Object val) { Row row = sheet.getRow(rowIndex); if (row == null) { row = sheet.createRow(rowIndex); } Cell cell = row.getCell(colIndex); if (cell == null) { cell = row.createCell(colIndex); cell.setCellType(CellType.STRING); } cell.setCellValue(val == null ? "" : val.toString()); } } import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * @author ximu * @date 2021/8/29 * @description 反射工具 */ public class ReflectionUtil { public static Map getFieldMap(Object object) { Map fieldMap = new ConcurrentHashMap(); refReflectionField(object, fieldMap); return fieldMap; } private static void refReflectionField(Object object, Map fieldMap) { Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { fieldMap.put(field.getName(), field); } Class superclass = object.getClass().getSuperclass(); if (superclass != null && !"java.lang.Object".equals(superclass.getName())) { refReflectionField(superclass, fieldMap); } } public static Method getMethod(String methodFullName) { int lastIndex = methodFullName.lastIndexOf('.'); String className = methodFullName.substring(0, lastIndex); String methodName = methodFullName.substring(lastIndex + 1); Method method = null; try { Class clazz = Class.forName(className); Method[] methods = clazz.getMethods(); for (Method methodObject : methods) { if (methodObject.getName().equals(methodName)) { method = methodObject; break; } } } catch (ClassNotFoundException e) { e.printStackTrace(); } return method; } } POM依赖 org.apache.commons commons-lang3 3.9 org.projectlombok lombok RELEASE compile org.apache.poi poi 4.1.2 org.apache.poi poi-ooxml 4.1.2 org.apache.poi poi-ooxml-schemas 4.1.2

这篇文章就写到这里了,虽然还有很多不足,主要是把文件上传服务器和兼容Spring没做,如果有同学需要的话,我可以改造一下。

欢迎给我留言,收到后会第一时间回复~



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3