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实体对象上标注注解即可

约定

  1. 如果数据集返回的是List,代表是单独的、无依赖的列
  2. 如果数据集返回的是Map,key是依赖列的名称,value是key所对应的数据

小试牛刀

我的项目结构
excel-example是例子,准备的一些数据
excel-extend是具体的实现

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

public class ExcelExampleService {public static List<String> queryProvinceList() {return Arrays.asList("浙江省", "湖南省", "贵州省");}public static Map<String, List<String>> queryMunicipalityList() {Map<String, List<String>> map = new HashMap<>(4);map.put("浙江省", Arrays.asList("杭州市", "温州市", "宁波市"));map.put("湖南省", Arrays.asList("长沙市", "邵阳市", "常德市"));map.put("贵州省", Arrays.asList("贵阳市", "遵义市", "安顺市"));return map;}public static Map<String, List<String>> queryDistrictList() {Map<String, List<String>> 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* <p>* 示例:id|name|age* <p>* 说明表头index0=id,index1=name*/String fileHeadTemplate() default "";/*** excel属性与表头映射模版 格式: 字段1{fileMappingSplit}映射名称1{fileTemplateSplit}字段1{fileMappingSplit}映射名称1* <p>* 示例:id=学号|name=学生姓名* <p>* 说明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();/*** 前列字段* <p>* 当为空字符串时,认定无前列依赖*/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<String, String> nameMappingMap = Arrays.stream(mappingSplit).map(x -> x.split(annotation.fileMappingSplit())).collect(Collectors.toMap(x -> x[0], x -> x[1]));// 创建excelWorkbook 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<String, Field> 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<String, Collection> map = (Map<String, Collection>) invoke;map.forEach((k, v) -> createNameManage(workbook, dataSheet, k, v, colIndex));int beforeColIndex = 0;for (int i = 0; i < split.length; i++) {if (split[i].equals(excelValidation.beforeFieldName())) {beforeColIndex = i;}}for (int rowIndex = excelValidation.firstRow(); rowIndex <= excelValidation.lastRow(); rowIndex++) {String formulaIndirect = String.format(formulaIndirectFormat, sheet.getSheetName(), getCellColumnFlag(beforeColIndex + 1), rowIndex + 1);createDataValidate(sheet, formulaIndirect, excelValidation.validationType(), rowIndex, rowIndex, colIndex, colIndex, showErrorBox);}}}private static void createDataValidate(Sheet sheet, String formula, int validationType, int firstRow, int lastRow, int firstCol, int lastCol, boolean showErrorBox) {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 <= 26) {colFiled = doHandle(num);} else {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<String, Field> getFieldMap(Object object) {Map<String, Field> fieldMap = new ConcurrentHashMap<>();refReflectionField(object, fieldMap);return fieldMap;}private static void refReflectionField(Object object, Map<String, Field> 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依赖

<dependencies><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.9</version></dependency><!--  lombok  --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>RELEASE</version><scope>compile</scope></dependency><!-- poi 相关 --><dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>4.1.2</version></dependency><dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml-schemas</artifactId><version>4.1.2</version></dependency></dependencies>

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

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

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

  1. mysql vue 菜谱_vue+ java 实现多级菜单递归效果

    效果如图: 大概思路:树形视图使用的是vue官方事例代码,java负责封装数据,按照vue官方事例的数据结构封装数据即可.有两个需要关注的点: 1.官方事例的数据结构是一个对象里面包含着集合,而不是一 ...

  2. java递归实现多级菜单栏_vue+ java 实现多级菜单递归效果

    效果如图: 大概思路:树形视图使用的是vue官方事例代码,java负责封装数据,按照vue官方事例的数据结构封装数据即可.有两个需要关注的点: 1.官方事例的数据结构是一个对象里面包含着集合,而不是一 ...

  3. Android 多级菜单联动操作

    今天是分享一个android实现三级菜单联动效果,到了第三级菜单有点复杂,下面带着大家看下代码. 项目结构: 为了让大家更有的耐心的阅读, 我先从简单的开始说起,我们先看下demo实现的效果吧!这样可 ...

  4. Java 实现 多级菜单

    一:前言 最近老师布置了给多级菜单的作业,感觉蛮有意思的,可以提升自己的逻辑!下面我写个简易版的多级菜单,本人还是菜鸟,欢迎各位给予宝贵的建议! 二:正文 由于是给各位演示,所有我把涉及的其他条件全省 ...

  5. java 递归查询多级菜单

    类目表是多级目录表,数据如下: 想获取所有数据的多级目录,代码如下: /*** 获取树形接口的 类目** @return*/@Overridepublic List<ExamCategory&g ...

  6. java实现多级菜单(java递归)方法二

    @Autowiredprivate TreeBuilder treeBuilder; /*** 获取树状结构数据*/@RequestMapping("menu/queryMenuTree&q ...

  7. java实现多级菜单(java递归)方法一

    查询菜单树 public List<Map<String, Object>> queryCategoryInfo() {List<Map<String, Objec ...

  8. 12864多级菜单实现方法

    前言        一般来说使用12864进行显示,配合按键作为人机界面交互,如果显示的内容较多的话,往往不能在一个页面显示完全,这里就需要用到多级菜单来进行管理,根据显示内容的不同,将其划分成不同的 ...

  9. 几张表格怎么联动_如何实现多张excel表格数据联动-Excel 如何实现多级下拉菜单的联动...

    Excel 如何实现多级下拉菜单的联动 excel中份表格实现数据同步的步骤如下: 首先打开计算机,在计算机桌面找到excel软件标左键双击excel的快捷方式以打开软件.然后打开需要进行数据同步的表 ...

最新文章

  1. 第四章第五章 环境搭建和24个命令总结
  2. 对学校公开课信息网站一次渗透测试
  3. Phoenix二级索引(Secondary Indexing)的使用(转:https://www.cnblogs.com/MOBIN/p/5467284.html)
  4. Linux进行设置环境变量
  5. javafx中的tree_JavaFX中的塔防(5)
  6. Head.First.Object-Oriented.Design.and.Analysis《深入浅出面向对象的分析与设计》读书笔记(七)...
  7. ActiveX: 如何用.inf和.ocx文件生成cab文件
  8. 程序员上班都在做什么?
  9. linux高级的脚本,【2018.07.23学习笔记】【linux高级知识 Shell脚本编程练习】
  10. 一万块是存入支付宝里的余额宝好还是存在微信的零钱通里好?
  11. 三菱a系列motion软体_三菱系列 PLC常见问题解答
  12. (高级)Matlab绘制中国地图超全教程详解
  13. win7系统服务器无法局域网访问,Win7局域网无法访问如何解决?
  14. Dzz任务板初版完成笔记-仿trello可私有部署的一款轻量团队任务协作工具。
  15. 电脑蓝屏怎么解决0x0000007b,解决电脑蓝屏问题
  16. npm --save-dev --save 的区别
  17. 如果不懂 numpy,请别说自己是 python 程序员
  18. 纵深与动感同在 体会线条构图的魅力
  19. 数字化的下一个目标,就是产业链|数字思考者50人
  20. 性能测试监控零散知识点

热门文章

  1. 最“全”新零售运维保障解决方案——阿里巴巴GOC技术实践经验独家曝光
  2. JavaScript的数据类型
  3. android 华为推送sd卡,华为推送通道集成指南
  4. mysql数据库要定期清除吗_数据库mysql定时清除数据
  5. 基于微信小程序的校园体育设施管理系统的设计与实现计算机毕业设计源码70715
  6. fastboot刷机——理论
  7. 带你领略极致简便的报表生成工具——阿里的easyExcel
  8. launcher3-布局load与bind
  9. 用例建模 - 绘制用例图
  10. Unity Text字间距和行间距调整