POI创建Excel记录

1、背景

需求中有需要用户自定义Excel表格表头,然后生成Excel文件,使用EasyExcel更适合生成固定表头的Excel文档,所以此处采用POI原生方式进行开发。文档如下:
在这里插入图片描述

2、创建表格

主要的代码逻辑如下,非主要方法可以在完整代码中找到。

2.1 定义表头对象

根据需求,表头需要制定2级表头,我们先定义一个Excel表头对象。

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ExcelModelDto {
/*** 名称 */
private String fieldName;
/*** 提示语 */
private String comment;
/*** 类型 */
private Integer type;
/*** 背景色 */
private short backgroundColor;
/*** 子标题 */
private List<Child> children;
  @Data
  @NoArgsConstructor
  @AllArgsConstructor
  public static class Child {
  /*** 字段编码 */
  private String fieldCode;
  /*** 字段名称 */
  private String fieldName;
  /*** 提示语 */
  private String comment;
  /*** 类型 */
  private Integer type;
  /*** 下拉框选项 */
  private String[] items;
  }
  }

2.2 Excel生成器

创建一个Excel文件生成对象,包含多个属性,其中包括:文件路径、文件名称、是否需要下拉框、文件后缀名、最大文本行数等。

@Slf4j
public class ExcelGenerator {
private final String localPath;
private final String sheetName;
private final String fileName;
private final String file;
private final Boolean needItems;
private final List<ExcelModelDto> data;
  /*** 字段编码集合,从data中解析 */
  private final List<String> fieldCodeList;
    public static final Integer FIRST_ROW = 2;
    public static final Integer LAST_ROW = 65535;
    public static final String FILE_SUFFIX = ".xlsx";
    public static final String PATH_SUFFIX = "/";
    public static final String ITEM_SHEET_NAME = "itemSheet";
    public static final String END_FLAG = "*";
    public static final Integer MAX_CONTENT_ROW_NUMBER = 1002;
    /**
    * 扩展字段sheet页行数记录key值
    */
    public static final String EXTEND_PAGE_ROW_NUMBER_KEY = "extend";
    public ExcelGenerator(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) {
      this(localPath, fileName, sheetName, true, data);
      }
      public ExcelGenerator(String localPath, String fileName, String sheetName, Boolean needItems, List<ExcelModelDto> data) {
        this.localPath = localPath;
        this.fileName = fileName;
        this.sheetName = sheetName;
        this.file = localPath + fileName;
        this.needItems = needItems;
        this.data = data;
        fieldCodeList = this.parseField(data);
        }
        }

2.3 创建模板

/**
* 生成模板
*
* @throws IOException 异常
*/
public void createTemplate() throws IOException {
this.doCreateSheet(Paths.get(file), sheetName, data);
}
/**
* 向Excel文件新增一个新的工作表,并处理表头。
*
* @param pathForFile 新工作表将要保存的文件路径。
* @throws IOException 如果读写文件时发生异常。
*/
private void doCreateSheet(Path pathForFile, String sheetName, List<ExcelModelDto> data)
  throws IOException {
  long startTime = System.currentTimeMillis();
  Workbook workbook = new XSSFWorkbook();
  Sheet sheet = this.getSheetByName(workbook, sheetName, false);
  // 处理Excel表头
  this.dealExcelHeadingCell(workbook, sheet, data);
  // 处理Excel内容单元格,默认都是有二级标题
  this.dealExcelContentCell(workbook, sheet, data);
  // 将inputStream转换为outputStream,并重新写入文件
  try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
  workbook.write(outputStream);
  } finally {
  long endTime = System.currentTimeMillis();
  log.info("创建Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
  }
  }

2.4 处理Excel表头

/**
* 处理 Excel 表头数据,包括第一行和第二行的标题单元格样式设置、数据填充和合并单元格。
*
* @param workbook 工作簿对象
* @param sheet    主表的工作表对象
* @param data     表头数据
*/
private void dealExcelHeadingCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
  // 创建第一行和第二行表头数据,并设置行高
  Row row1 = this.getRow(sheet, 0);
  Row row2 = this.getRow(sheet, 1);
  row1.setHeightInPoints(20);
  row2.setHeightInPoints(20);
  // 已经存在的列号
  int lastCellNum = this.getLastCellNum(sheet, 1);
  int currentCellNum = lastCellNum;
  int startCellNum = lastCellNum;
  int endCellNum;
  for (ExcelModelDto excelModelDto : data) {
  // 一级标题名称
  String firstTitleName = excelModelDto.getFieldName();
  // 一级标题单元格样式
  CellStyle firstTitleCellStyle = this.buildFirstTitleCellStyle(workbook, excelModelDto);
  // 二级标题的单元格样式
  CellStyle secondTitleCellStyle = this.getCellStyle(workbook, IndexedColors.WHITE.getIndex());
  List<ExcelModelDto.Child> children = excelModelDto.getChildren();
    if (children == null || children.size() == 0) {
    continue;
    }
    for (ExcelModelDto.Child child : children) {
    // 处理表头单元格
    this.dealTitleCell(workbook, sheet, child, firstTitleName, firstTitleCellStyle, secondTitleCellStyle, currentCellNum);
    // 处理完后列号加一
    currentCellNum++;
    }
    endCellNum = currentCellNum - 1;
    // POI 版本升级后,合并单元格需要大于一个单元格
    if (startCellNum != endCellNum) {
    CellRangeAddress region = new CellRangeAddress(0, 0, startCellNum, endCellNum);
    sheet.addMergedRegion(region);
    }
    startCellNum = endCellNum + 1;
    }
    }

2.5 处理Excel内容单元格样式

/**
* 格式化内容单元格。
*
* @param sheet    工作表对象。
* @param workbook 工作簿对象。
*/
private void dealExcelContentCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
  // 获取统一的单元格样式,不用每个单元格获取一个对象,防止对象过多
  CellStyle childCellStyle = this.getContentCellStyle(workbook);
  // 只格式化内容单元格,且有上限
  int maxContentRowNumber = MAX_CONTENT_ROW_NUMBER;
  // 跳过表头,从文本行开始
  for (int rowNumber = 2; rowNumber < maxContentRowNumber; rowNumber++) {
  Row row = sheet.createRow(rowNumber);
  // 列号从0开始
  int cellNumber = 0;
  for (ExcelModelDto excelModelDto : data) {
  List<ExcelModelDto.Child> children = excelModelDto.getChildren();
    for (ExcelModelDto.Child child : children) {
    String[] items = child.getItems();
    if (Objects.isNull(items) || items.length == 0) {
    Cell cell = row.createCell(cellNumber);
    cell.setCellStyle(childCellStyle);
    }
    // 每处理完一个单元格,列号加1
    cellNumber++;
    }
    }
    }
    }

2.6 处理单个表头

在处理表头过程中,如果items 不为空,则说明此列需要下拉框,数组为供用户选择的下拉内容,防止下拉框内容过大,所以将下拉内容单独生成到一个隐藏的sheet页中,并且使用表达式来表达下拉框内容,设定到单元格中。

/**
* 处理Excel表格的标题单元格。
*
* @param workbook             工作簿对象
* @param sheet                工作表对象
* @param child                ExcelModelDto.Child 对象,包含字段名、注释和下拉框选项等信息
* @param firstTitleName       一级标题名称
* @param firstTitleCellStyle  一级标题单元格样式
* @param secondTitleCellStyle 二级标题单元格样式
* @param index                当前处理的列索引
*/
private void dealTitleCell(Workbook workbook, Sheet sheet,
ExcelModelDto.Child child, String firstTitleName,
CellStyle firstTitleCellStyle, CellStyle secondTitleCellStyle,
int index) {
Row row1 = this.getRow(sheet, 0);
Row row2 = this.getRow(sheet, 1);
String secondFieldName = child.getFieldName();
String comment = child.getComment();
String[] items = child.getItems();
// 一级表头
Cell cell1 = row1.createCell(index);
cell1.setCellValue(firstTitleName);
cell1.setCellStyle(firstTitleCellStyle);
// 二级表头,标题如果以* 号结尾,则* 置为红色
Cell cell2 = row2.createCell(index);
RichTextString textString = this.parseCellValue(workbook, Font.COLOR_NORMAL, true, secondFieldName);
cell2.setCellValue(textString);
cell2.setCellStyle(secondTitleCellStyle);
// 设置下拉框
if (items != null && items.length > 0 && needItems) {
this.appendItems(workbook, sheet, secondTitleCellStyle, secondFieldName, items, index);
}
// 设置表头备注
if (!org.apache.commons.lang.StringUtils.isEmpty(comment)) {
this.setComment(sheet, cell2, comment);
}
// 根据字段长度自动调整列的宽度
sheet.setColumnWidth(index, 100 * 50);
}
/**
* 在指定的工作簿和工作表中追加枚举类型的项,并设置公式引用。
*
* @param workbook        工作簿对象
* @param sheet           工作表对象
* @param childCellStyle  子单元格样式
* @param secondTitleName 第二级标题名称
* @param items           枚举类型的项数组
* @param index           当前项在总体中的索引位置
*/
private void appendItems(Workbook workbook, Sheet sheet, CellStyle childCellStyle, String secondTitleName, String[] items, int index) {
// 如果有序列单元格,则创建一个sheet页,来保存所有的枚举类型,同时隐藏该sheet页
Sheet itemsSheet = this.getSheetByName(workbook, ITEM_SHEET_NAME, true);
// 追加sheet的时候,需要看隐藏sheet的列已经到哪一列了,避免追加时将原有隐藏列覆盖掉
int existItemCell = this.getLastCellNum(itemsSheet, 0);
// 将枚举数组写入到独立的sheet页中,同时设置表头格式
String formula = this.writeItems(itemsSheet, childCellStyle, secondTitleName, existItemCell, items);
// 设置公式到模板的sheet页中,格式化后的最终公式为
// =itemSheet!$B$1:$B$88
// 表明该单元格引用的是 itemSheet sheet页中 B1~B88的数据
formula = String.format("=%s!%s", ITEM_SHEET_NAME, formula);
this.setItems(sheet, formula, FIRST_ROW, LAST_ROW, index, index);
}

3、追加sheet

有些需要在已有的Excel文档中追加新的sheet表格内容,效果如下:

在这里插入图片描述

/**
* 在指定的 Excel 文件中添加一个新的工作表,并填充数据。
*
* @param sheetName 新工作表的名称
* @param data      要填充的数据列表
* @throws IOException 如果在操作文件时发生了 I/O 错误
*/
public void appendSheet(String sheetName, List<ExcelModelDto> data) throws IOException {
  long startTime = System.currentTimeMillis();
  // 路径不存在则创建,保证路径是存在的
  Path pathForLocalPath = Paths.get(localPath);
  boolean existPath = Files.exists(pathForLocalPath);
  if (!existPath) {
  Files.createDirectories(pathForLocalPath);
  }
  // 如果文件不存在,则走创建sheet逻辑
  Path pathForFile = Paths.get(file);
  if (!Files.exists(pathForFile)) {
  this.doCreateSheet(pathForFile, sheetName, data);
  return;
  }
  // 如果文件存在则走追加sheet逻辑
  try (InputStream inputStream = Files.newInputStream(pathForFile)) {
  this.doAppendSheet(inputStream, pathForFile, sheetName, data);
  long endTime = System.currentTimeMillis();
  log.info("追加Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
  } catch (Exception e) {
  log.error("追加Excel模板文件失败!", e);
  throw new BizException(e);
  }
  }
  /**
  * 向Excel文件追加一个新的工作表,并处理表头。
  *
  * @param inputStream Excel文件的输入流。
  * @param pathForFile 新工作表将要保存的文件路径。
  * @throws IOException 如果读写文件时发生异常。
  */
  private void doAppendSheet(InputStream inputStream, Path pathForFile, String sheetName, List<ExcelModelDto> data)
    throws IOException {
    Workbook workbook = new XSSFWorkbook(inputStream);
    Sheet sheet = this.getSheetByName(workbook, sheetName, false);
    // 处理Excel表头
    this.dealExcelHeadingCell(workbook, sheet, data);
    // 处理Excel内容单元格,默认都是有二级标题
    this.dealExcelContentCell(workbook, sheet, data);
    // 将inputStream转换为outputStream,并重新写入文件
    try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
    IOUtils.copy(inputStream, outputStream);
    workbook.write(outputStream);
    }
    }

4、静态工具

每次使用都需要new一个对象来创建Excel文件,所以创建一个静态工具类,来通过静态方法实现文档的创建与追加。

public class ExcelGeneratorExecutors {
/**
* 创建 Excel 模板文件。
*
* @param localPath 本地路径
* @param fileName  文件名
* @param sheetName 工作表名称
* @param data      数据列表
* @throws IOException 如果创建模板文件失败
*/
public static void createTemplate(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) throws IOException {
  ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
  excelGenerator.createTemplate();
  }
  /**
  * 在指定路径的Excel文件中追加一个新的工作表,并填充数据。
  *
  * @param localPath Excel文件的本地路径。
  * @param fileName  Excel文件的名称。
  * @param sheetName 新增工作表的名称。
  * @param data      填充到新增工作表的数据。
  * @throws IOException 如果在追加工作表或填充数据时发生I/O错误。
  */
  public static void appendSheet(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) throws IOException {
    ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
    excelGenerator.appendSheet(sheetName, data);
    }
    }

5、单元测试

@Test
public void testGenerate() {
String localPath = "D:\\mytmp\\template\\";
String dateTime = DateUtils.format(new Date(), DateUtils.DATE_FORMAT_COMMENT_2);
String fileName = String.format("生成模板-%s.xlsx", dateTime);
String sheetName = "测试";
List<ExcelModelDto> data = this.buildExcelModelDtoList();
  ExcelGenerator excelGenerator = new ExcelGenerator(localPath, fileName, sheetName, data);
  try {
  excelGenerator.createTemplate();
  List<ExcelModelDto> data2 = this.buildExcelModelDtoList2();
    excelGenerator.appendSheet("自定义sheet", data);
    excelGenerator.appendSheet("自定义sheet2", data2);
    excelGenerator.appendSheet("自定义sheet3", data2);
    log.info("模板文件生成,名称为:{}", fileName);
    } catch (IOException e) {
    e.printStackTrace();
    }
    }
    @Test
    public void testGenerate2() {
    String localPath = "D:\\mytmp\\template\\";
    String dateTime = DateUtils.format(new Date(), DateUtils.DATE_FORMAT_COMMENT_2);
    String fileName = String.format("生成模板-%s.xlsx", dateTime);
    String sheetName = "测试";
    List<ExcelModelDto> data = this.buildExcelModelDtoList();
      try {
      ExcelGeneratorExecutors.createTemplate(localPath, fileName, sheetName, data);
      ExcelGeneratorExecutors.appendSheet(localPath, fileName, sheetName, data);
      ExcelGeneratorExecutors.appendSheet(localPath, fileName, "自定义sheet3", data);
      log.info("模板文件生成,名称为:{}", fileName);
      } catch (IOException e) {
      e.printStackTrace();
      }
      }
      public List<ExcelModelDto> buildExcelModelDtoList() {
        List<ExcelModelDto> data = new ArrayList<>();
          ExcelModelDto excelModelDto = new ExcelModelDto();
          excelModelDto.setFieldName("电器");
          excelModelDto.setComment("song");
          excelModelDto.setType(2);
          excelModelDto.setBackgroundColor((short) 2);
          List<ExcelModelDto.Child> children = new ArrayList<>();
            ExcelModelDto.Child child1 = new ExcelModelDto.Child();
            child1.setComment("类目1");
            child1.setFieldCode("category");
            child1.setFieldName("类目1");
            List<String> list1 = Lists.newArrayList("冰箱", "洗衣机", "空调");
              child1.setItems(list1.toArray(new String[0]));
              ExcelModelDto.Child child2 = new ExcelModelDto.Child();
              child2.setComment("数量1");
              child2.setFieldCode("qty");
              child2.setFieldName("数量1");
              List<String> list2 = Lists.newArrayList("1", "2", "3");
                child2.setItems(list2.toArray(new String[0]));
                ExcelModelDto.Child child3 = new ExcelModelDto.Child();
                child3.setComment("文本内容");
                child3.setFieldCode("textValue");
                child3.setFieldName("文本内容");
                children.add(child1);
                children.add(child2);
                children.add(child3);
                excelModelDto.setChildren(children);
                data.add(excelModelDto);
                return data;
                }
                public List<ExcelModelDto> buildExcelModelDtoList2() {
                  List<ExcelModelDto> data = new ArrayList<>();
                    ExcelModelDto excelModelDto0 = new ExcelModelDto();
                    excelModelDto0.setFieldName("商家运单号");
                    excelModelDto0.setComment("商家运单号");
                    excelModelDto0.setType((int) IndexedColors.TURQUOISE1.getIndex());
                    excelModelDto0.setBackgroundColor(IndexedColors.TURQUOISE1.getIndex());
                    ExcelModelDto.Child child0 = new ExcelModelDto.Child();
                    child0.setComment("关联第一个sheet页的商家运单号");
                    child0.setFieldCode("orderNo");
                    child0.setFieldName("商家运单号*");
                    List<ExcelModelDto.Child> children0 = new ArrayList<>();
                      children0.add(child0);
                      excelModelDto0.setChildren(children0);
                      ExcelModelDto excelModelDto = new ExcelModelDto();
                      excelModelDto.setFieldName("购买电器");
                      excelModelDto.setComment("song");
                      excelModelDto.setType((int) IndexedColors.TURQUOISE1.getIndex());
                      excelModelDto.setBackgroundColor(IndexedColors.TURQUOISE1.getIndex());
                      ExcelModelDto.Child child1 = new ExcelModelDto.Child();
                      child1.setComment("类目");
                      child1.setFieldCode("category");
                      child1.setFieldName("类目");
                      List<String> list1 = Lists.newArrayList("冰箱", "洗衣机", "空调");
                        child1.setItems(list1.toArray(new String[0]));
                        ExcelModelDto.Child child2 = new ExcelModelDto.Child();
                        child2.setComment("数量");
                        child2.setFieldCode("qty");
                        child2.setFieldName("数量");
                        //List<String> list2 = Lists.newArrayList("1", "2", "3");
                          //child2.setItems(list2.toArray(new String[0]));
                          List<ExcelModelDto.Child> children = new ArrayList<>();
                            children.add(child1);
                            children.add(child2);
                            excelModelDto.setChildren(children);
                            data.add(excelModelDto0);
                            data.add(excelModelDto);
                            return data;
                            }

6、完整代码示例

@Slf4j
public class ExcelGenerator {
private final String localPath;
private final String sheetName;
private final String fileName;
private final String file;
private final Boolean needItems;
private final List<ExcelModelDto> data;
  /*** 字段编码集合,从data中解析 */
  private final List<String> fieldCodeList;
    public static final Integer FIRST_ROW = 2;
    public static final Integer LAST_ROW = 65535;
    public static final String FILE_SUFFIX = ".xlsx";
    public static final String PATH_SUFFIX = "/";
    public static final String ITEM_SHEET_NAME = "itemSheet";
    public static final String END_FLAG = "*";
    public static final Integer MAX_CONTENT_ROW_NUMBER = 1002;
    /**
    * 扩展字段sheet页行数记录key值
    */
    public static final String EXTEND_PAGE_ROW_NUMBER_KEY = "extend";
    public ExcelGenerator(String localPath, String fileName, String sheetName, List<ExcelModelDto> data) {
      this(localPath, fileName, sheetName, true, data);
      }
      public ExcelGenerator(String localPath, String fileName, String sheetName, Boolean needItems, List<ExcelModelDto> data) {
        this.localPath = localPath;
        this.fileName = fileName;
        this.sheetName = sheetName;
        this.file = localPath + fileName;
        this.needItems = needItems;
        this.data = data;
        fieldCodeList = this.parseField(data);
        }
        /**
        * 创建对象时,将ExcelModel中的字段按顺序排好,保存到List中
        *
        * @param data 入参
        * @return 返回值
        */
        public List<String> parseField(List<ExcelModelDto> data) {
          List<String> fieldCodeList = new ArrayList<>();
            for (ExcelModelDto modelDto : data) {
            List<ExcelModelDto.Child> children = modelDto.getChildren();
              for (ExcelModelDto.Child child : children) {
              String fieldCode = child.getFieldCode();
              fieldCodeList.add(fieldCode);
              }
              }
              return fieldCodeList;
              }
              /**
              * 生成模板
              *
              * @throws IOException 异常
              */
              public void createTemplate() throws IOException {
              this.doCreateSheet(Paths.get(file), sheetName, data);
              }
              /**
              * 在指定的 Excel 文件中添加一个新的工作表,并填充数据。
              *
              * @param sheetName 新工作表的名称
              * @param data      要填充的数据列表
              * @throws IOException 如果在操作文件时发生了 I/O 错误
              */
              public void appendSheet(String sheetName, List<ExcelModelDto> data) throws IOException {
                long startTime = System.currentTimeMillis();
                // 路径不存在则创建,保证路径是存在的
                Path pathForLocalPath = Paths.get(localPath);
                boolean existPath = Files.exists(pathForLocalPath);
                if (!existPath) {
                Files.createDirectories(pathForLocalPath);
                }
                // 如果文件不存在,则走创建sheet逻辑
                Path pathForFile = Paths.get(file);
                if (!Files.exists(pathForFile)) {
                this.doCreateSheet(pathForFile, sheetName, data);
                return;
                }
                // 如果文件存在则走追加sheet逻辑
                try (InputStream inputStream = Files.newInputStream(pathForFile)) {
                this.doAppendSheet(inputStream, pathForFile, sheetName, data);
                long endTime = System.currentTimeMillis();
                log.info("追加Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
                } catch (Exception e) {
                log.error("追加Excel模板文件失败!", e);
                throw new BizException(e);
                }
                }
                /**
                * 向Excel文件新增一个新的工作表,并处理表头。
                *
                * @param pathForFile 新工作表将要保存的文件路径。
                * @throws IOException 如果读写文件时发生异常。
                */
                private void doCreateSheet(Path pathForFile, String sheetName, List<ExcelModelDto> data)
                  throws IOException {
                  long startTime = System.currentTimeMillis();
                  Workbook workbook = new XSSFWorkbook();
                  Sheet sheet = this.getSheetByName(workbook, sheetName, false);
                  // 处理Excel表头
                  this.dealExcelHeadingCell(workbook, sheet, data);
                  // 处理Excel内容单元格,默认都是有二级标题
                  this.dealExcelContentCell(workbook, sheet, data);
                  // 将inputStream转换为outputStream,并重新写入文件
                  try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
                  workbook.write(outputStream);
                  } finally {
                  long endTime = System.currentTimeMillis();
                  log.info("创建Excel模板文件共耗时:{}秒。", (endTime - startTime) / 1000);
                  }
                  }
                  /**
                  * 向Excel文件追加一个新的工作表,并处理表头。
                  *
                  * @param inputStream Excel文件的输入流。
                  * @param pathForFile 新工作表将要保存的文件路径。
                  * @throws IOException 如果读写文件时发生异常。
                  */
                  private void doAppendSheet(InputStream inputStream, Path pathForFile, String sheetName, List<ExcelModelDto> data)
                    throws IOException {
                    Workbook workbook = new XSSFWorkbook(inputStream);
                    Sheet sheet = this.getSheetByName(workbook, sheetName, false);
                    // 处理Excel表头
                    this.dealExcelHeadingCell(workbook, sheet, data);
                    // 处理Excel内容单元格,默认都是有二级标题
                    this.dealExcelContentCell(workbook, sheet, data);
                    // 将inputStream转换为outputStream,并重新写入文件
                    try (OutputStream outputStream = Files.newOutputStream(pathForFile)) {
                    IOUtils.copy(inputStream, outputStream);
                    workbook.write(outputStream);
                    }
                    }
                    /**
                    * 处理 Excel 表头数据,包括第一行和第二行的标题单元格样式设置、数据填充和合并单元格。
                    *
                    * @param workbook 工作簿对象
                    * @param sheet    主表的工作表对象
                    * @param data     表头数据
                    */
                    private void dealExcelHeadingCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
                      // 创建第一行和第二行表头数据,并设置行高
                      Row row1 = this.getRow(sheet, 0);
                      Row row2 = this.getRow(sheet, 1);
                      row1.setHeightInPoints(20);
                      row2.setHeightInPoints(20);
                      // 已经存在的列号
                      int lastCellNum = this.getLastCellNum(sheet, 1);
                      int currentCellNum = lastCellNum;
                      int startCellNum = lastCellNum;
                      int endCellNum;
                      for (ExcelModelDto excelModelDto : data) {
                      // 一级标题名称
                      String firstTitleName = excelModelDto.getFieldName();
                      // 一级标题单元格样式
                      CellStyle firstTitleCellStyle = this.buildFirstTitleCellStyle(workbook, excelModelDto);
                      // 二级标题的单元格样式
                      CellStyle secondTitleCellStyle = this.getCellStyle(workbook, IndexedColors.WHITE.getIndex());
                      List<ExcelModelDto.Child> children = excelModelDto.getChildren();
                        if (children == null || children.size() == 0) {
                        continue;
                        }
                        for (ExcelModelDto.Child child : children) {
                        // 处理表头单元格
                        this.dealTitleCell(workbook, sheet, child, firstTitleName, firstTitleCellStyle, secondTitleCellStyle, currentCellNum);
                        // 处理完后列号加一
                        currentCellNum++;
                        }
                        endCellNum = currentCellNum - 1;
                        // POI 版本升级后,合并单元格需要大于一个单元格
                        if (startCellNum != endCellNum) {
                        CellRangeAddress region = new CellRangeAddress(0, 0, startCellNum, endCellNum);
                        sheet.addMergedRegion(region);
                        }
                        startCellNum = endCellNum + 1;
                        }
                        }
                        /**
                        * 格式化内容单元格。
                        *
                        * @param sheet    工作表对象。
                        * @param workbook 工作簿对象。
                        */
                        private void dealExcelContentCell(Workbook workbook, Sheet sheet, List<ExcelModelDto> data) {
                          // 获取统一的单元格样式,不用每个单元格获取一个对象,防止对象过多
                          CellStyle childCellStyle = this.getContentCellStyle(workbook);
                          // 只格式化内容单元格,且有上限
                          int maxContentRowNumber = MAX_CONTENT_ROW_NUMBER;
                          // 跳过表头,从文本行开始
                          for (int rowNumber = 2; rowNumber < maxContentRowNumber; rowNumber++) {
                          Row row = sheet.createRow(rowNumber);
                          // 列号从0开始
                          int cellNumber = 0;
                          for (ExcelModelDto excelModelDto : data) {
                          List<ExcelModelDto.Child> children = excelModelDto.getChildren();
                            for (ExcelModelDto.Child child : children) {
                            String[] items = child.getItems();
                            if (Objects.isNull(items) || items.length == 0) {
                            Cell cell = row.createCell(cellNumber);
                            cell.setCellStyle(childCellStyle);
                            }
                            // 每处理完一个单元格,列号加1
                            cellNumber++;
                            }
                            }
                            }
                            }
                            /**
                            * 处理Excel表格的标题单元格。
                            *
                            * @param workbook             工作簿对象
                            * @param sheet                工作表对象
                            * @param child                ExcelModelDto.Child 对象,包含字段名、注释和下拉框选项等信息
                            * @param firstTitleName       一级标题名称
                            * @param firstTitleCellStyle  一级标题单元格样式
                            * @param secondTitleCellStyle 二级标题单元格样式
                            * @param index                当前处理的列索引
                            */
                            private void dealTitleCell(Workbook workbook, Sheet sheet,
                            ExcelModelDto.Child child, String firstTitleName,
                            CellStyle firstTitleCellStyle, CellStyle secondTitleCellStyle,
                            int index) {
                            Row row1 = this.getRow(sheet, 0);
                            Row row2 = this.getRow(sheet, 1);
                            String secondFieldName = child.getFieldName();
                            String comment = child.getComment();
                            String[] items = child.getItems();
                            // 一级表头
                            Cell cell1 = row1.createCell(index);
                            cell1.setCellValue(firstTitleName);
                            cell1.setCellStyle(firstTitleCellStyle);
                            // 二级表头,标题如果以* 号结尾,则* 置为红色
                            Cell cell2 = row2.createCell(index);
                            RichTextString textString = this.parseCellValue(workbook, Font.COLOR_NORMAL, true, secondFieldName);
                            cell2.setCellValue(textString);
                            cell2.setCellStyle(secondTitleCellStyle);
                            // 设置下拉框
                            if (items != null && items.length > 0 && needItems) {
                            this.appendItems(workbook, sheet, secondTitleCellStyle, secondFieldName, items, index);
                            }
                            // 设置表头备注
                            if (!org.apache.commons.lang.StringUtils.isEmpty(comment)) {
                            this.setComment(sheet, cell2, comment);
                            }
                            // 根据字段长度自动调整列的宽度
                            sheet.setColumnWidth(index, 100 * 50);
                            }
                            /**
                            * 设置单元格下拉框
                            * 下拉框引用单独一个sheet页中的数据
                            *
                            * @param sheet    sheet
                            * @param formula  公式
                            * @param firstRow 起始行
                            * @param lastRow  结束行
                            * @param firstCol 起始列
                            * @param lastCol  结束列
                            */
                            public void setItems(Sheet sheet, String formula, int firstRow, int lastRow, int firstCol, int lastCol) {
                            CellRangeAddressList addressList = new CellRangeAddressList(firstRow, lastRow, firstCol, lastCol);
                            DataValidationHelper helper = sheet.getDataValidationHelper();
                            DataValidationConstraint constraint = helper.createFormulaListConstraint(formula);
                            DataValidation validation = helper.createValidation(constraint, addressList);
                            validation.setShowErrorBox(true);
                            sheet.addValidationData(validation);
                            }
                            /**
                            * 设置单元格备注信息
                            *
                            * @param sheet      sheet
                            * @param cell       单元格
                            * @param textString 提示信息
                            */
                            public void setComment(Sheet sheet, Cell cell, String textString) {
                            Drawing<?> drawing = sheet.createDrawingPatriarch();
                              CreationHelper factory = sheet.getWorkbook().getCreationHelper();
                              // 设置提示框大小,默认根据 提示信息的大小来确认提示框高度
                              /// ClientAnchor anchor = factory.createClientAnchor();
                              textString = StringUtils.defaultIfBlank(textString, "");
                              int length = textString.length();
                              int row2 = length / 25 + 6;
                              // (int dx1, int dy1, int dx2, int dy2, short col1, int row1, short col2, int row2)
                              // 前四个参数是坐标点,后四个参数是编辑和显示批注时的大小.
                              ClientAnchor anchor = new XSSFClientAnchor(0, 0, 0, 0, (short) 4, 2, (short) 6, row2);
                              Comment comment = drawing.createCellComment(anchor);
                              RichTextString str = factory.createRichTextString(textString);
                              comment.setString(str);
                              comment.setAuthor("Auto+");
                              // 以上参数不设置时会有默认值,当一个被重复设置批注时会报错
                              // Multiple cell comments in one cell are not allowed
                              // 故在设置批注前检查锚点位置有无批注,有的话移除
                              if (cell.getCellComment() != null) {
                              cell.removeCellComment();
                              }
                              cell.setCellComment(comment);
                              }
                              /**
                              * 获取单元格样式对象
                              *
                              * @param workbook        工作簿
                              * @param backGroundColor 背景色
                              * @return 返回样式对象
                              */
                              public CellStyle getCellStyle(Workbook workbook, short backGroundColor) {
                              CellStyle cellStyle = workbook.createCellStyle();
                              CreationHelper createHelper = workbook.getCreationHelper();
                              cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
                              // IndexedColors.YELLOW.getIndex()
                              cellStyle.setFillForegroundColor(backGroundColor);
                              cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
                              // 水平居中
                              cellStyle.setAlignment(HorizontalAlignment.CENTER);
                              // 垂直居中
                              cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
                              // 设置边框及颜色
                              cellStyle.setBorderTop(BorderStyle.DOUBLE);
                              cellStyle.setBorderBottom(BorderStyle.DOUBLE);
                              cellStyle.setBorderLeft(BorderStyle.DOUBLE);
                              cellStyle.setBorderRight(BorderStyle.DOUBLE);
                              cellStyle.setTopBorderColor(IndexedColors.BLACK.getIndex());
                              cellStyle.setBottomBorderColor(IndexedColors.BLACK.getIndex());
                              cellStyle.setLeftBorderColor(IndexedColors.BLACK.getIndex());
                              cellStyle.setRightBorderColor(IndexedColors.BLACK.getIndex());
                              return cellStyle;
                              }
                              /**
                              * 向sheet中写入 序列内容
                              *
                              * @param sheet     sheet
                              * @param cellStyle 单元格格式,表头格式
                              * @param itemsType 序列类型
                              * @param col       列号
                              * @param items     序列数组
                              * @return 返回坐标
                              */
                              protected String writeItems(Sheet sheet, CellStyle cellStyle, String itemsType, int col, String[] items) {
                              // 第一行为表头数据
                              Row row = sheet.getRow(0);
                              if (row == null) {
                              row = sheet.createRow(0);
                              }
                              Cell cell = row.createCell(col);
                              // 获取单元格列所对应的字母,即 0=A,1=B ...
                              String columnLetter = CellReference.convertNumToColString(col);
                              cell.setCellValue(itemsType);
                              cell.setCellStyle(cellStyle);
                              int length = items.length;
                              for (int i = 0; i < length; i++) {
                              Row itemRow = sheet.getRow(i + 1);
                              if (itemRow == null) {
                              itemRow = sheet.createRow(i + 1);
                              }
                              Cell itemRowCell = itemRow.createCell(col);
                              itemRowCell.setCellValue(items[i]);
                              }
                              // 格式化后的公式坐标为  $B$1:$B$88
                              return String.format("$%s$%s:$%s$%s", columnLetter, 2, columnLetter, items.length + 1);
                              }
                              /**
                              * 格式化单元格字体样式
                              *
                              * @param workbook  工作簿
                              * @param fontColor 字体颜色
                              * @param isBold    是否加粗
                              * @param value     单元格值
                              */
                              public RichTextString parseCellValue(Workbook workbook, short fontColor, boolean isBold, String value) {
                              value = StringUtils.defaultIfBlank(value, "");
                              XSSFRichTextString textString = new XSSFRichTextString(value);
                              Font font1 = getFontStyle(workbook, fontColor, isBold);
                              if (StringUtils.isNotBlank(value)) {
                              int length = value.length();
                              // 如果内容是以 * 号结尾的,则将 * 号置为红色,默认黑色
                              if (value.endsWith(END_FLAG)) {
                              int point = length - 1;
                              textString.applyFont(0, point, font1);
                              Font font2 = getFontStyle(workbook, Font.COLOR_RED, isBold);
                              textString.applyFont(point, length, font2);
                              } else {
                              textString.applyFont(0, length, font1);
                              }
                              }
                              return textString;
                              }
                              /**
                              * 获取字体样式
                              *
                              * @param workbook  工作簿
                              * @param fontColor 字体颜色
                              * @param isBold    是否加粗
                              * @return 返回值
                              */
                              public Font getFontStyle(Workbook workbook, short fontColor, boolean isBold) {
                              Font font = workbook.createFont();
                              font.setColor(fontColor);
                              if (isBold) {
                              font.setBold(true);
                              }
                              font.setFontName("宋体");
                              // 字体大小
                              font.setFontHeightInPoints((short) 10);
                              return font;
                              }
                              /**
                              * 获取指定行在给定工作表中的最后一个单元格的索引。
                              *
                              * @param sheet  工作表对象
                              * @param rowNum 行号(从0开始计数)
                              * @return 最后一个单元格的索引,若行不存在则返回0
                              */
                              private int getLastCellNum(Sheet sheet, int rowNum) {
                              int existCell = 0;
                              // 指定sheet页不为空,则获取已经有多少列
                              Row row = sheet.getRow(rowNum);
                              if (Objects.nonNull(row)) {
                              existCell = row.getLastCellNum();
                              // 如果不存在返回的是-1,业务上从0开始计算
                              if (existCell < 0) {
                              existCell = 0;
                              }
                              }
                              return existCell;
                              }
                              /**
                              * 获取或创建指定名称的工作表并将其隐藏。
                              *
                              * @param workbook 工作簿对象
                              * @return 指定名称的工作表对象
                              */
                              private Sheet getSheetByName(Workbook workbook, String sheetName, boolean hide) {
                              Sheet itemsSheet = workbook.getSheet(sheetName);
                              // 指定sheet页为空则创建
                              if (Objects.isNull(itemsSheet)) {
                              itemsSheet = workbook.createSheet(sheetName);
                              int sheetIndex = workbook.getSheetIndex(sheetName);
                              workbook.setSheetHidden(sheetIndex, hide);
                              }
                              return itemsSheet;
                              }
                              /**
                              * 根据行号获取或创建指定Sheet中的Row对象。
                              *
                              * @param sheet  要操作的Sheet对象。
                              * @param rowNum 需要获取或创建的行号。
                              * @return 指定行号的Row对象。
                              */
                              private Row getRow(Sheet sheet, int rowNum) {
                              Row row = sheet.getRow(rowNum);
                              if (Objects.isNull(row)) {
                              row = sheet.createRow(rowNum);
                              }
                              return row;
                              }
                              /**
                              * 构建第一行标题单元格样式。
                              *
                              * @param workbook      工作簿对象。
                              * @param excelModelDto Excel模型数据传输对象。
                              * @return 第一行标题单元格样式。
                              */
                              private CellStyle buildFirstTitleCellStyle(Workbook workbook, ExcelModelDto excelModelDto) {
                              // 根据字段类型来获取背景色
                              short backGroundColor = excelModelDto.getBackgroundColor();
                              CellStyle cellStyle = this.getCellStyle(workbook, backGroundColor);
                              Font font = this.getFontStyle(workbook, Font.COLOR_NORMAL, true);
                              cellStyle.setFont(font);
                              return cellStyle;
                              }
                              /**
                              * 在指定的工作簿和工作表中追加枚举类型的项,并设置公式引用。
                              *
                              * @param workbook        工作簿对象
                              * @param sheet           工作表对象
                              * @param childCellStyle  子单元格样式
                              * @param secondTitleName 第二级标题名称
                              * @param items           枚举类型的项数组
                              * @param index           当前项在总体中的索引位置
                              */
                              private void appendItems(Workbook workbook, Sheet sheet, CellStyle childCellStyle, String secondTitleName, String[] items, int index) {
                              // 如果有序列单元格,则创建一个sheet页,来保存所有的枚举类型,同时隐藏该sheet页
                              Sheet itemsSheet = this.getSheetByName(workbook, ITEM_SHEET_NAME, true);
                              // 追加sheet的时候,需要看隐藏sheet的列已经到哪一列了,避免追加时将原有隐藏列覆盖掉
                              int existItemCell = this.getLastCellNum(itemsSheet, 0);
                              // 将枚举数组写入到独立的sheet页中,同时设置表头格式
                              String formula = this.writeItems(itemsSheet, childCellStyle, secondTitleName, existItemCell, items);
                              // 设置公式到模板的sheet页中,格式化后的最终公式为
                              // =itemSheet!$B$1:$B$88
                              // 表明该单元格引用的是 itemSheet sheet页中 B1~B88的数据
                              formula = String.format("=%s!%s", ITEM_SHEET_NAME, formula);
                              this.setItems(sheet, formula, FIRST_ROW, LAST_ROW, index, index);
                              }
                              /**
                              * 获取单元格样式对象
                              *
                              * @param workbook 工作簿
                              * @return 返回样式对象
                              */
                              public CellStyle getContentCellStyle(Workbook workbook) {
                              CellStyle cellStyle = workbook.createCellStyle();
                              CreationHelper createHelper = workbook.getCreationHelper();
                              cellStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
                              // 背景色为纯色
                              cellStyle.setFillPattern(FillPatternType.NO_FILL);
                              // 设置单元格格式为文本格式
                              DataFormat format = workbook.createDataFormat();
                              cellStyle.setDataFormat(format.getFormat("@"));
                              return cellStyle;
                              }
                              }
posted @ 2025-11-20 19:41  clnchanpin  阅读(32)  评论(0)    收藏  举报