场景
原有的Excel 某一个 sheet 页中某些列需要添加指定的字典下拉,而这些字典的值又是确认的。
有两种思路:
一、如果给定的下拉字典值是确定的而且关联原有列的位置也不会变,那么这些数据可以固定写死在代码中,也是最简单的一种场景
二、如果给定的字典值数量以及关联原有列的位置这些都是会改变的,那么就是第二种比较复杂的场景,变量增多计算增多,但这样的代码也有更强的兼容性,但理解需要一定时间。
这里我以第二种更加兼容的方式来讲解也是实战中我个人的遇到的场景,理解可能会花费一些时间,但一旦理解看起来就没有那么复杂了。
1. 首先需要理解 Excel 中人工添加下拉的动作是什么
例如我的第一个sheet 和第二个sheet 分别如下
如果我要在 sheet1 中的第二和第四列标题下分别添加 sheet2 中的两个字典,那么实际操作时其实是这样的
- 先选中原有的单元格
- 然后点击添加数据验证
- 在校验中指定为序列,并指定来源为Sheet2的三个框,来源的公式可以不自己手输,点击来源右侧的图标后可以通过光标拖拽指定需要的单元格,完成后点击确定
- 效果如下
另一个字典也是如上操作;
在代码中的实现步骤其实与实际操作的顺序是类似的,即:
- 首先要先将字典在自己指定的Sheet 中创建出来
- 然后每一个字典的公式的值需要我们自己计算出来,并通过自定义的集合将字典和公式记录下来
- 然后通过数据校验将公式的值关联到第一个 Sheet 的指定单元格列上即可
实际代码中操作示例
前提:需要先理解基本的Excel 操作代码,包括sheet 和 row 以及 cell 最基本操作。
-
这里我先设置自己的第一个用于存放所有数据的sheet 页名称为 sheet1
(setDefaultColumnWidth 为设置默认的列宽度,不需要可以不设置) -
设置第二个sheet 作为字典sheet 我命名为templateDic
(setDefaultColumnWidth 为设置默认的列宽度,不需要可以不设置) -
然后我们需要将自己需要的字典整理出来(根据自己的业务自行组装所有字典的数据集合)。
-
然后写入到这个字典sheet 中。由于向单元格(cell)中写入数据时需要先指定行,但行需要先创建出来才能继续,所以这里我的思路是取出字典值最多的一项,并在字典值最长的长度上加 3 (这里加3只是为了兼容第一行的标题行和额外两行作为容错行)做为要创建的行数。
这里我们可以将所有的字典值转为 List< String> 的集合,然后依次写入
我的代码大致如下:
/**
* 获取最大的字典长度,并在sheet中创建对应长度的 row
* @param templateDicSheet 字典sheet
* @param list 所有字典集合
*/
private void getCreateRowsByDicList(Sheet templateDicSheet, List<MyExcelDicModel> list) {
Integer maxLength = 1;
// MyExcelDicModel 为我自己定义的excel字典类,里面包含了
for (MyExcelDicModel myExcelDicModel : list) {
// SourceList 为当前字典的所有字典值 List<String>
List<String> sourceList = myExcelDicModel.getSourceList();
if (CollectionUtils.isNotEmpty(sourceList) && sourceList.size() >= maxLength) {
maxLength = sourceList.size();
}
}
// 额外加3 容错
maxLength += 3;
for (Integer index = 0; index < maxLength; index++) {
templateDicSheet.createRow(index);
}
}
MyExcelDicModel 定义大致可以如下参考:
/**
* 字典所属属性中文名
*/
private String chinese;
/**
* 字典所属属性英文名
*/
private String english;
/**
* 所有下拉值
*/
private List<String> sourceList;
/**
* 在Excel 中的列名
*/
private String excelColumnName;
/**
* 字典在 Excel 中的范围起始行
*/
private Integer startExcelNum;
/**
* 字典在 Excel 中的范围结束行
*/
private Integer endExcelNum;
/**
* 回填数据的sheet页 字典所在列索引
*/
private Integer metaSheetColumnIndex;
为了获取指定字典范围的公式中的值
其中 startExcelNum 由于在Excel 中的第一行我固定用于放置表头,所有从第二行开始,第二行在Excel 中的认为 2 所以固定给与 2 即可。
对应的 endExcelNum 为 1 + 字典的所有值 sourceList 的长度。
- 接下来将准备把字典的表头以及数据写入字典的sheet 中,同时上面的公式中 指定的Excel 列名是字母 A-Z 然后AA-AZ 以此类推向后加入的,这个列名同样需要我们计算出来并赋值给定义好的 MyExcelDicModel 中的 excelColumnName,即通过列索引 0 - 某一个数值计算出表头的英文字符是多少,用于最终公式的组装
// 表头写入 allDicList 即为我所有的字典集合
for (int i = 0; i < allDicList.size(); i++) {
MyExcelDicModel myExcelDicModel = allDicList.get(i);
// 表头名称
String chinese = myExcelDicModel.getChinese();
// 当前列在excel 中的列名 getExcelColIndex方法见下方方法
governanceExcelDicModel.setExcelColumnName(this.getExcelColIndex(i));
// 获取上面已经创建好的行
Row row = templateDicSheet.getRow(0);
// 在当前行的指定列索引 即指定单元格内写入
row.createCell(i).setCellValue(chinese);
}
通过索引计算英文列名
/**
* 将index转为excel的索引,如 A AA BA
* 列名开始时从A-Z 再次出现时为AA AB - AZ 继续重复时为BA - BZ 以此类推
* @param index 当前列的值
* @return 当前列在Excel 中的列名
*/
private String getExcelColIndex(int index) {
String result = "";
if (index / 26 != 0) {
char x = (char) ('A' + (index / 26 - 1));
result += x;
}
char l = (char) ('A' + (index % 26));
result += l;
return result;
}
表头完成后开始写入所有字典的值,并同时通过 Excel POI 提供的名称管理器组装该字典对应的公式
// 字典值写入单元格中
for (int dicIndex = 0; dicIndex < allDicList.size(); dicIndex++) {
// 当前字典
MyExcelDicModel myExcelDicModel = allDicList.get(dicIndex);
// 当前这一字典所有值
List<String> sourceList = myExcelDicModel.getSourceList();
for (int labelIndex = 0; labelIndex < sourceList.size(); labelIndex++) {
Row row = templateDicSheet.getRow(labelIndex + 1);
// 给到当前行当前列单元格的值
row.createCell(dicIndex).setCellValue(sourceList.get(labelIndex));
}
// 给与字典多个命名空间范围
Name name = templateDicSheet.getWorkbook().createName();
name.setNameName(myExcelDicModel.getEnglish());
// excel 中公式需要的范围值
String excelColumnName = myExcelDicModel.getExcelColumnName();
Integer startExcelNum = myExcelDicModel.getStartExcelNum();
Integer endExcelNum = myExcelDicModel.getEndExcelNum();
// 字典下拉值在Excel 中的表达式 =sheet名称!$A$1:$A$10 前面英文字母标识在Excel 中的列名,后面为excel中的行号 两段加起来表示一个范围,最前方为sheet的名字 !$ 为固定写法
name.setRefersToFormula("templateDic!$"+excelColumnName+"$"+startExcelNum+":"+"$"+excelColumnName+"$"+endExcelNum);
}
// 到此给Name 赋值完成即可 没有其他操作 后面即可直接引用,需要注意的是一定要在正确的sheet 对象下创建Name
(如果你的字典只有一列 那么就不用循环了 一遍写完 一个Name即可)
Name name = templateDicSheet.getWorkbook().createName();
这里的 Name 为excel的名称(命名)管理器 这里通过将字典的英文名赋值给这个命名对象,并将这个这个字典的公式通过 setRefersToFormula (指代公式)将公式挂在这个命名对象上,当我们后面再使用这个命名对象时 通过字典英文名称就可以自己关联到这个公式上。
到此字典的 Sheet 就完成了!
但我们定义的excel 中还需要补充一个属性,我们人工在操作Excel 的下拉时(见最上方人工操作)就是在两边的 sheet 创建完成后,先选中了需要给与下拉的所有单元格范围
这里我们一般的范围是某一列都需要,在每一行都是这一列,如果你的列是固定的那么直接指定固定的列所在索引即可,在我定义的类 MyExcelDicModel 中,指定这一列索引的值属性名为 metaSheetColumnIndex
如果你的列不是固定在这里的,那么需要再从第一个sheet 中计算得出列的索引并赋值。
- 将字典的公式关联到指定单元格的方式
先使用要关联的 sheet 创建数据验证助手对象 XSSFDataValidationHelper,即类似于页面上如下步骤:
创建数据校验助手对象,通过主要填写数据的 sheet
XSSFDataValidationHelper dvHelper = new XSSFDataValidationHelper((XSSFSheet) sheet);
通过遍历我们之前自定义的
for (MyExcelDicModel myExcelDicModel : dicSheetCreateList) {
String english = myExcelDicModel.getEnglish();
// 通过之前指定的字典英文名 关联之前定义的公式
StringBuilder formula = new StringBuilder("=INDIRECT(\"" + english + "\"");
formula.append(")");
// 数据校验约束对象
XSSFDataValidationConstraint dvConstraint =
new XSSFDataValidationConstraint(3, formula.toString());
// 单元格范围指定
Integer metaSheetColumnIndex = myExcelDicModel.getMetaSheetColumnIndex();
// 四个入参分别为开始行,结束行,开始列,结束列 指定单元格范围 startRow,endRow 均为正整数表示开始行与结束行
CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(startRow, endRow, metaSheetColumnIndex,
metaSheetColumnIndex);
// 创建校验 将校验的单元格与约束绑定到一起创建对象
DataValidation validation = dvHelper.createValidation(dvConstraint, cellRangeAddressList);
// 将创建好的数据校验添加到主sheet 中即完成了校验和单元格的绑定,指定单元格此时将获得字典下拉
sheet.addValidationData(validation);
}
相当于这个过程并确定绑定
到此绑定字典下拉完成。
剩下的就是在主sheet 中写入要写入的数据了,均为正常的在单元格写入操作,不做多余描述 与写表头类似,在row 中 createCell 并 setCellValue 即可。