为何要写这个工具类
- EasyExcel 或者 EasyPOI这两种开源工具类也支持Excel模板的导出,并且功能和处理大数据量导出时有明显优势,但是这两个工具类对多个list占位的替换实现不太友好
- 有时候处理完模板占位之后可能还需要一些自定义的操作,EasyExcel支持自定义处理,但是不完全自定义,处理复杂,而EasyPOI不支持自定处理
实现目标
- 同时支持单个占位替换和list多行占位替换,并且list可以有多个
- 支持完全自定义后续Excel处理
实现步骤
一个Excel模板
参考附件
占位符说明
单个常量占位符用 {key}
,如图中的 {taskNo}
;
list占位符用 #[key]
,如图中的 #[list1]
,注意同一批list占位只需要一个就行,如果有多批list需要多个占位,比如 #[mainList]
和 #[list1]
就是两批list要替换,而单独的每一批中都有多行数据,每一行里边又是多个单元格
引入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>
模板类ExcelTemplate
package com.hugo.poi.eladmin;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
/**
* @author zhangmy
* @date 2023/11/8 15:47
* @description
*/
@Slf4j
public class ExcelTemplate {
/**
* 工作簿
*/
private Workbook workbook;
/**
* sheet表单
*/
private Sheet sheet;
public Workbook getWorkbook() {
return workbook;
}
public Sheet getSheet() {
return sheet;
}
/**
* 构造函数 -- 在建造器中的build方法中调用此构造函数
* @param workbook
* @param sheet
*/
ExcelTemplate(Workbook workbook, Sheet sheet) {
this.workbook = workbook;
this.sheet = sheet;
}
public void closeWorkbook() {
if (workbook != null) {
try {
workbook.close();
} catch (IOException e) {
log.error("关闭workbook失败:", e);
}
}
}
/**
* 获取ExcelTemplateBuilder
* @return
*/
public static ExcelTemplate.ExcelTemplateBuilder builder() {
return new ExcelTemplateBuilder();
}
/**
* 内部构造器类
*/
public static class ExcelTemplateBuilder {
/**
* 内部builder类也必须持有外部相同的属性
*/
private Workbook workbook;
/**
* 内部builder类也必须持有外部相同的属性
*/
private Sheet sheet;
/**
* 无参构造函数,在外部builder方法中会调用
*/
ExcelTemplateBuilder() {}
/**
* 模板文件路径
* @param templatePath
* @return
*/
public ExcelTemplate.ExcelTemplateBuilder workbook(String templatePath) {
try (InputStream inputStream = Files.newInputStream(new File(templatePath).toPath())) {
this.workbook = new XSSFWorkbook(inputStream);
} catch (Exception e) {
log.error("读取Excel模板错误", e);
}
return this;
}
public ExcelTemplate.ExcelTemplateBuilder workbook(InputStream inputStream) {
try {
this.workbook = new XSSFWorkbook(inputStream);
} catch (Exception e) {
log.error("读取Excel模板错误", e);
}
return this;
}
/**
* 模板所在工作簿的sheet,默认第一个
* @return
*/
public ExcelTemplate.ExcelTemplateBuilder sheet() {
return sheet(0);
}
/**
* 模板所在工作簿的sheet,指定index
* @param sheetIndex
* @return
*/
public ExcelTemplate.ExcelTemplateBuilder sheet(Integer sheetIndex) {
this.sheet = this.workbook.getSheetAt(sheetIndex);
return this;
}
/**
* 最后调用此方法返回ExcelTemplate
* 调用示例: ExcelTemplate.builder().templatePath(path).sheet().build()
* @return
*/
public ExcelTemplate build() {
return new ExcelTemplate(this.workbook, this.sheet);
}
}
}
这个工具类有几处注意的地方:
- 必须持有Workbook对象和Sheet,因为后续都是基于sheet操作,而sheet来源于workbook;
- 工具类采用了建造者模式的思想,将ExcelTemplate的创建改造为有内部的ExcelTemplateBuilder类创建,目标是实现链式创建对象
ExcelTemplate template = ExcelTemplate.builder().workbook(templatePath).sheet().build();
;
- 别忘记关闭workbook文件流,参考方法closeWorkbook
自定义处理器WorkbookHandler
package com.hugo.poi.eladmin;
import org.apache.poi.ss.usermodel.Workbook;
/**
* @author zhangmy
* @date 2023/11/10 10:52
* @description 自定义workbook处理接口
*/
@FunctionalInterface
public interface WorkbookHandler {
/**
* 自定义处理workbook
* @param workbook workbook
*/
void handle(Workbook workbook);
}
自定义处理器就一个接口handle,用户在自定义处理的时候可自行实现handle方法,@FunctionalInterface
表示功能性接口,到时候我们就可以用箭头函数
工具类ExcelTemplateUtil
package com.hugo.poi.eladmin;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author zhangmy
* @date 2023/11/13 15:40
* @description
*/
public class ExcelTemplateUtil {
/**
* workbook对象
*/
private Workbook workbook;
/**
* sheet对象
*/
private Sheet sheet;
/**
* 待替换数据()
*/
private final Map<String, Object> map;
/**
* 合并区域map
* key为区域index
* value为合并的区域对象
*/
private Map<Integer, CellRangeAddress> cellRangeAddressMap;
/**
* 最大列
*/
private int maxColumn = 0;
/**
* 初始化工具类
* @param template 模板对象
* @param map 待填充数据
*/
public ExcelTemplateUtil(ExcelTemplate template, Map<String, Object> map) {
this.map = map;
this.workbook = template.getWorkbook();
if (this.workbook != null) {
this.sheet = template.getSheet();
}
}
/**
* 替换并导出
* @param savePath 保存路径
* @param handler 自定义处理
*/
public void replaceAndExport(String savePath, WorkbookHandler handler) {
try {
// 替换模板数据
replaceModel();
// 允许用户自定义处理Workbook
if (handler != null) {
handler.handle(workbook);
}
export(savePath);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
closeWorkbook();
}
}
/**
* 替换并导出
* @param savePath 保存路径
*/
public void replaceAndExport(String savePath) {
try {
// 替换模板数据
replaceModel();
export(savePath);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
closeWorkbook();
}
}
public void replaceModel(WorkbookHandler handler) {
replaceModel();
// 允许用户自定义处理Workbook
if (handler != null) {
handler.handle(workbook);
}
}
/**
* 替换数据
*/
public void replaceModel() {
// list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据
Map<Integer[], List<List<String>>> cellListDataMap = new LinkedHashMap<>();
// 处理list占位行
dealListRow(cellListDataMap);
// 最后填充数据
fillData(cellListDataMap);
}
/**
* 填充常量和list
* @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据(外层list为多行,里层list为单行多个单元格的值)
*/
private void fillData(Map<Integer[], List<List<String>>> cellListDataMap) {
// 填充数据之前重新读取表格单元格合并情况
readCellRangeAddress();
int lastRowNum = sheet.getLastRowNum();
// 遍历每一行,查找占位符
for (int i = 0; i < lastRowNum; i++) {
// 当前行
Row row = sheet.getRow(i);
if (row == null) continue;
if (row.getLastCellNum() > maxColumn) {
maxColumn = row.getLastCellNum();
}
// 首个单元格
int firstCellNum = row.getFirstCellNum();
// 最后一个单元格
int lastCellNum = row.getLastCellNum();
// 遍历每一个单元格,查找list占位符
for (int j = firstCellNum; j < lastCellNum; j++) {
// 当前单元格
Cell cell = row.getCell(j);
if (cell == null) continue;
// 只读取cellType为String的
if (cell.getCellType() != CellType.STRING) continue;
// 占位符字符
String placeHolderStr = cell.getStringCellValue().trim();
// 常量替换
if (placeHolderStr.startsWith("{") && placeHolderStr.endsWith("}")) {
// 真正的占位符
String placeHolder = placeHolderStr.substring(1, placeHolderStr.length() - 1);
if (map.containsKey(placeHolder)) {
// 替换为值
setCellValue(cell, map.get(placeHolder));
// 自动设置高度
// autoSetCellHeightByText(cell, map.get(placeHolder).toString());
}
// list替换
} else if (placeHolderStr.startsWith("#[") && placeHolderStr.endsWith("]")) {
// 真正的占位符
String placeHolder = placeHolderStr.substring(2, placeHolderStr.length() - 1);
if (map.containsKey(placeHolder)) {
// 清空值
cell.setCellValue("");
}
}
}
}
readCellRangeAddress();
fillListData(cellListDataMap);
}
/**
* 处理list占位符
* 1.查找list占位符标签
* 2.复制出需要的行数
* @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据(外层list为多行,里层list为单行多个单元格的值)
*/
private void dealListRow(Map<Integer[], List<List<String>>> cellListDataMap) {
// 读取单元格合并区域情况
this.readCellRangeAddress();
int lastRowNum = sheet.getLastRowNum();
// 遍历每一行,查找占位符
for (int i = 0; i < lastRowNum; i++) {
// 当前行
Row row = sheet.getRow(i);
if (row == null) continue;
if (row.getLastCellNum() > maxColumn) {
maxColumn = row.getLastCellNum();
}
// 首个单元格
int firstCellNum = row.getFirstCellNum();
// 最后一个单元格
int lastCellNum = row.getLastCellNum();
// 遍历每一个单元格,查找list占位符
for (int j = firstCellNum; j < lastCellNum; j++) {
// 当前单元格
Cell cell = row.getCell(j);
if (cell == null) continue;
// 只读取cellType为String的
if (cell.getCellType() != CellType.STRING) continue;
// 占位符字符
String placeHolderStr = cell.getStringCellValue().trim();
if (placeHolderStr.startsWith("#[") && placeHolderStr.endsWith("]")) {
// 真正的占位符
String placeHolder = placeHolderStr.substring(2, placeHolderStr.length() - 1);
// 占位符待替换的数据值
@SuppressWarnings("unchecked")
List<List<String>> cellDataList = (List<List<String>>)map.get(placeHolder);
// 记录此单元格待替换数据
cellListDataMap.put(new Integer[]{j, i}, cellDataList);
// 新行的行号
int r = i;
if (cellDataList != null && !cellDataList.isEmpty()) {
// 往下移动待插入数据总数-1行,占位符行默认要使用
sheet.shiftRows(r + 1, sheet.getLastRowNum(), cellDataList.size() -1, true, false);
// 向下移动之后重置最后一行行号
lastRowNum = sheet.getLastRowNum();
// 跳过遍历新增的行
i = i + cellDataList.size() - 1;
}
if (cellDataList != null && !cellDataList.isEmpty()) {
// 待插入行数据处理
for (int n = 0; n < cellDataList.size() - 1; n++) {
r++;
Row newRow = sheet.createRow(r);
// 复制行 -- 包含样式的复制
copyRow(row, newRow);
}
}
if (cellDataList != null && !cellDataList.isEmpty()) {
readCellRangeAddress();
}
}
}
}
}
/**
* 导出
* @param savePath 保存文件路径
*/
public void export(String savePath) {
try (FileOutputStream out = new FileOutputStream(savePath)){
workbook.write(out);
out.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
closeWorkbook();
}
}
/**
* 关闭工作簿(很重要别忘记了)
*/
public void closeWorkbook() {
if (workbook != null) {
try {
workbook.close();
} catch (IOException e) {
System.out.println("关闭workbook失败:" + e);
}
}
}
/**
* 填充list占位符数据
* @param cellListDataMap list占位符所在单元格待替换数据,key为单元格的行列坐标,value为待替换的数据
*/
private void fillListData(Map<Integer[], List<List<String>>> cellListDataMap) {
if (cellListDataMap != null && !cellListDataMap.isEmpty()) {
// 遍历每一个list占位符
for (Map.Entry<Integer[], List<List<String>>> entry : cellListDataMap.entrySet()) {
// 单元格坐标 [列,行]
Integer[] colRow = entry.getKey();
// 行index
int rowIndex = colRow[1];
// 数据
List<List<String>> dataList = entry.getValue();
if (dataList != null && !dataList.isEmpty()) {
// 遍历多行数据
for (List<String> list : dataList) {
// dataList中第n个数据就是rowIndex + n所在行的数据
Row row = sheet.getRow(rowIndex);
if (row == null) break;
// 列index
int c = colRow[0];
// 遍历第rowIndex + n行数据,填充每一个单元格
for (String s : list) {
Cell cell = row.getCell(c);
if (cell == null) break;
// 设置单元格的值
String cellValue = s != null ? s : "";
cell.setCellValue(cellValue);
// 查询单元格是否合并
CellRangeAddress mergedRegion = getMergedRegion(cell);
// 遍历下一个单元格
c++;
if (mergedRegion != null) {
// 合并了单元格,则跳过合并单元格的遍历
c += mergedRegion.getLastColumn() - mergedRegion.getFirstColumn();
}
}
rowIndex++;
}
}
}
}
}
/**
* 设置单元格的值
* @param cell 单元格对象
* @param value 值
*/
private void setCellValue(Cell cell, Object value) {
// 根据值的类型来设置不同类型的单元格值
if (value == null) {
cell.setCellValue("");
}
if (value instanceof String) {
cell.setCellValue((String) value);
} else if (value instanceof Integer) {
cell.setCellValue((Integer) value);
} else if (value instanceof Double) {
cell.setCellValue((Double) value);
} else if (value instanceof Date) {
cell.setCellValue((Date) value);
} else if (value != null) {
// 对于其他所有类型,调用toString()
cell.setCellValue(value.toString());
}
}
/**
* 根据文本值自动设置单元格的高度
* @param cell 单元格对象
* @param text 文本值
*/
private void autoSetCellHeightByText(Cell cell, String text) {
// 获取合并区域
CellRangeAddress mergedRegion = getMergedRegion(cell);
if (mergedRegion == null) {
// 获取宽度(像素)
float columnWidthInPixels = sheet.getColumnWidthInPixels(cell.getColumnIndex());
// 将文本以换\n分割
String[] texts = text.split("\n");
int rows = 0;
for (String content : texts) {
// 获取文本像素
float realPx = getLengthByContent(content);
int row = (int) Math.ceil(realPx / columnWidthInPixels);
rows += row;
}
} else {
int firstRow = mergedRegion.getFirstRow();
int lastRow = mergedRegion.getLastRow();
int firstColumn = mergedRegion.getFirstColumn();
int lastColumn = mergedRegion.getLastColumn();
float columnWidthInPixels = 0f;
for (int i = firstColumn; i <= lastColumn; i++) {
columnWidthInPixels += sheet.getColumnWidthInPixels(i);
}
String[] texts = text.split("\r\n");
short rows = 0;
for (String content : texts) {
float realPx = getLengthByContent(content);
int row = (int) Math.ceil(realPx / columnWidthInPixels);
rows += row;
}
if (firstRow == lastRow) {
// 同一行
float heightInPoints = 18.65f;
if (rows > 1) {
cell.getCellStyle().setWrapText(true);
}
cell.getRow().setHeightInPoints((int) (heightInPoints * rows - (rows - 1) * 0.15 * heightInPoints));
}
}
}
/**
* 获取文本像素
* 汉字13.72像素
* 大写英文 9.80像素
* 小写 5.69像素
* @param content 文本内容
* @return 像素
*/
private float getLengthByContent(String content) {
float length=0;
for(int i=0;i<content.length();i++){
char c=content.charAt(i);
if(c>='A' && c<='Z'){
length+=5.69;
}else if(c>='\0' && c<='\255'){
length+=9.80;
}else{
length+=13.72;
}
}
return length;
}
/**
* 内部方法 -- 获取单元格的值
* @param cell
* @return
*/
private Object getCellValue(Cell cell) {
if (cell != null) {
switch (cell.getCellType()) {
case NUMERIC:
return cell.getNumericCellValue();
case STRING:
return cell.getStringCellValue();
case BOOLEAN:
return cell.getBooleanCellValue();
case ERROR:
return cell.getErrorCellValue();
}
}
return null;
}
/**
* 内部方法 -- 行复制功能(包含数据和样式)
* @param row 原行
* @param newRow 新行
*/
private void copyRow(Row row, Row newRow) {
if (row == null || newRow == null) return;
// 遍历行的每一个单元格
for (int i = row.getFirstCellNum(); i < row.getLastCellNum(); i++) {
// 读取要复制的单元格
Cell cell = row.getCell(i);
if (cell == null) break;
Cell newCell;
// 读取待复制单元格的单元格合并情况
CellRangeAddress mergedRegion = getMergedRegion(cell);
if (mergedRegion == null) {
newCell = newRow.createCell(i);
// 复制样式
copyCellStyle(cell.getCellStyle(), newCell.getCellStyle());
} else {
// 看合并了几个-就复制几个
if (mergedRegion.getFirstRow() == mergedRegion.getLastRow()) {
// 确定合并了同一行-合并了n个格
int n = mergedRegion.getLastColumn() - mergedRegion.getFirstColumn();
for (int j = mergedRegion.getFirstColumn(); j <= mergedRegion.getLastColumn(); j++) {
newCell = newRow.createCell(i);
if (j == mergedRegion.getFirstColumn()) {
// 复制样式
copyCellStyle(cell.getCellStyle(), newCell.getCellStyle());
}
}
// System.out.println("插入新行--合并单元格:第" + (newRow.getRowNum() + 1) + "行,第" + (i + 1) + "~" + (i + n + 1) + "列");
// 合并单元格
mergedRegion(newRow.getRowNum(), i, i + n);
// 已合并的单元格不再遍历
i = i + n;
}
}
}
}
/**
* 进行合并单元格 合并同行的区间格
* @param firstRow 合并开始行,这里只有一行的合并
* @param firstCol 合并开始列
* @param lastCol 合并结束列
*/
private void mergedRegion(int firstRow, int firstCol,int lastCol) {
CellRangeAddress region = new CellRangeAddress(firstRow, firstRow, firstCol, lastCol);
sheet.addMergedRegion(region);
}
/**
* 内部方法 -- 样式复制
* @param cellStyle 原单元格样式
* @param newCellStyle 新单元格样式
*/
private void copyCellStyle(CellStyle cellStyle, CellStyle newCellStyle) {
if (cellStyle == null || newCellStyle == null) return;
newCellStyle.cloneStyleFrom(cellStyle);
}
/**
* 获取单元格的合并情况
* @param cell
* @return
*/
private CellRangeAddress getMergedRegion(Cell cell) {
return cellRangeAddressMap.get(cell.getRowIndex() * 100000 + cell.getColumnIndex());
}
/**
* 读取单元格合并区域情况
* 注意在不断替换占位符的过程中,合并区域会不断变化
*/
private void readCellRangeAddress() {
if (cellRangeAddressMap == null) {
cellRangeAddressMap = new LinkedHashMap<>();
} else {
cellRangeAddressMap.clear();
}
// 遍历合并的区域
for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
// 当前合并区域
CellRangeAddress cellRangeAddress = sheet.getMergedRegion(i);
// 合并区域-第一行
int firstRow = cellRangeAddress.getFirstRow();
// 合并区域-最后一行
int lastRow = cellRangeAddress.getLastRow();
// 合并区域-第一列
int firstColumn = cellRangeAddress.getFirstColumn();
// 合并区域-最后一列
int lastColumn = cellRangeAddress.getLastColumn();
// System.out.println("当前的合并区域为:第" + (firstRow + 1) + "~" + (lastRow + 1) + "行,第" + (firstColumn + 1) + "~" + (lastColumn + 1) + "列");
// 组装区域合并情况map
for (int r = firstRow; r <= lastRow; r++) {
for (int c = firstColumn; c <= lastColumn; c++) {
cellRangeAddressMap.put(r * 100000 + c, cellRangeAddress);
}
}
}
}
/**
* 设置合并单元格外边框
*/
public void setMergedCellBoarder() {
for (int rowNum = 0; rowNum <= sheet.getLastRowNum(); rowNum++) {
Row row = sheet.getRow(rowNum);
if (row != null) {
for (int colNum = 0; colNum < row.getLastCellNum(); colNum++) {
Cell cell = row.getCell(colNum);
if (cell != null) {
// 读取原单元格样式
CellStyle originalStyle = cell.getCellStyle();
// 判断当前单元格是否是合并单元格
CellRangeAddress cellRangeAddress = isMergedRegion(rowNum, colNum);
if (cellRangeAddress != null) {
RegionUtil.setBorderTop(BorderStyle.THIN, cellRangeAddress, sheet);
RegionUtil.setBorderBottom(BorderStyle.THIN, cellRangeAddress, sheet);
RegionUtil.setBorderLeft(BorderStyle.THIN, cellRangeAddress, sheet);
RegionUtil.setBorderRight(BorderStyle.THIN, cellRangeAddress, sheet);
}
// 设置原格式
cell.setCellStyle(originalStyle);
}
}
}
}
}
/**
* 判断是否是合并单元格并返回合并单元格区域
* @param row 行index
* @param column 列index
* @return 合并区域
*/
private CellRangeAddress isMergedRegion(int row, int column) {
for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) {
if (mergedRegion.isInRange(row, column)) {
return mergedRegion;
}
}
return null;
}
}
工具类说明:
- 核心处理逻辑在
replaceModel()
方法里边; - 有一个
readCellRangeAddress()
方法,是用来读取合并区域情况的,这个方法调用的比较频繁,开始的时候要调用一次,中间插入了新行或者手动合并了单元格都需要重新读取,最后在填充数据之前也需要重新读取一次; - 对于单个list占位,首先根据要填充数据list的size插入指定行数,这个插入新行对应在POI中是需要先
sheet.shiftRows(r + 1, sheet.getLastRowNum(), cellDataList.size() -1, true, false);
,然后sheet.createRow(r)
; - 插入行之后为了保证下移过程中的数据和样式保持不变,需要对插入的行的样式复制成倍复制对象的样式,参考
copyRow(row, newRow);
方法,++这一点很重要(★★★★★)++,否则的话会出现数据和样式的丢失 - 填充list数据的时候注意跳过合并单元格的数据填充;
- 自定义的处理handler可以根据需要调整处理的位置,我这里是在最后导出excel之前进行的处理,参考
replaceAndExport
方法; - 在导出表格的方法里记得要关闭workbook,参考
export
方法。
测试
package com.hugo.poi.eladmin;
import com.alibaba.excel.util.MapUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFCellStyle;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @author zhangmy
* @date 2023/11/13 18:02
* @description
*/
public class ExcelTemplateUtilTest {
public static void main(String[] args) {
// 模板文件名
String templatePath = "C:\\Users\\Administrator\\Desktop\\easyExcel测试\\v2\\Pooling任务单-是v2.xlsx";
// 导出文件名
String saveFileName = "C:\\Users\\Administrator\\Desktop\\easyExcel测试\\v2\\Pooling任务单-是v2" + System.currentTimeMillis() + ".xlsx";
ExcelTemplate template = ExcelTemplate.builder().workbook(templatePath).sheet().build();
// 测试数据
Map<String, Object> dataMap = buildData();
// 替换模板、自定义处理并导出
ExcelTemplateUtil excelTemplateUtil = new ExcelTemplateUtil(template, dataMap);
excelTemplateUtil.replaceAndExport(saveFileName, workbook -> {
// 获取第一个Sheet
Sheet sheet = template.getSheet();
// 第2~最后一行行高设置为18
int startRow = 2;
int lastRow = sheet.getLastRowNum();
for (int i = startRow; i <= lastRow; i++) {
Row row = sheet.getRow(i);
if (row == null) {
row = sheet.createRow(i); // 如果行不存在,则创建
}
row.setHeightInPoints(18); // 设置行高为18点
}
// 设置合并单元格外边框
excelTemplateUtil.setMergedCellBoarder();
});
}
/**
* 组装测试数据
* @return
*/
private static Map<String, Object> buildData() {
// 组装填充数据 -- 简单填充(占位符用{xxx})
Map<String, Object> map = MapUtils.newHashMap();
map.put("taskNo", "pooling-第28批-nextseq 550AR(2)-TPNB500543AR");
map.put("createTime", "2023-03-17 09:51:59");
map.put("phixK", "Phix");
map.put("libMer", "1.7pm");
map.put("rsb", 25.3);
map.put("changeLib", 476.0);
map.put("rsb2", 990);
map.put("mainPoolingV", 37.3471);
map.put("mainPoolingMer", 3.53);
map.put("mainPoolingData", 1.2);
map.put("mainPoolingTheoryAbout", "0.8ng/μl");
map.put("mainPoolingTheoryRange", "0.64~0.96ng/μl");
map.put("pooling1V", 405.82);
map.put("pooling1Mer", 2.67);
map.put("pooling1Data", 0.9);
map.put("pooling1TheoryAbout", "0.61ng/μl");
map.put("pooling1TheoryRange", "0.49~0.73ng/μl");
map.put("pooling2V", 2.93);
map.put("pooling2Mer", 122.92);
map.put("pooling2Data",0.3);
map.put("pooling2TheoryAbout", "27.96ng/μl");
map.put("pooling2TheoryRange", "22.37~33.56ng/μl");
// 组装填充数据 -- 列表填充(占位符用#{}) -- 主Pooling
List<List<String>> mainPoolingList = new ArrayList<>();
mainPoolingList.add(new ArrayList<String>() {
{
add(1 + "");
add("pooling1");
add("37.08");
}
});
mainPoolingList.add(new ArrayList<String>() {
{
add(2 + "");
add("pooling2");
add("0.27");
}
});
mainPoolingList.add(new ArrayList<String>() {
{
add(3 + "");
add("pooling3");
add("18");
}
});
map.put("mainList", mainPoolingList);
// 组装填充数据 -- 列表填充(占位符用{}) -- Pooling1
List<List<String>> pooling1List = new ArrayList<>();
pooling1List.add(new ArrayList<String>() {
{
add("B1-06A");
add("BY220095");
add("测试");
add("42.76");
}
});
pooling1List.add(new ArrayList<String>() {
{
add("B1-06B");
add("BY220095");
add("测试");
add("7.24");
}
});
pooling1List.add(new ArrayList<String>() {
{
add("B1-06c");
add("BY2200956");
add("测试");
add("15.55");
}
});
map.put("list1", pooling1List);
return map;
}
}
总结
- 大的步骤分为三步:
- 读取list占位符
- 根据待填充的数据容量大小插入指定数量的空白行
- 填充常量占位和list占位的值
- 还有两个核心的点:
- 要保证在每一轮修改excel单元格样式之前要先读取合并区域的情况,比如插入空白行之前(多个list占位就会有多次插入空白行,每次都需要读取之前的合并区域情况),在填充数据之前也需要,因为list占位填充的的单元格也可能有合并单元格,参考readCellRangeAddress(方法)
- 逻辑不要搞错了,我之前尝试想读取每一行每一个单元格,查找到占位符就直接替换或者插入新行再替换,这样出来的效果是不对的,一定是要先读取整个表格的布局,然后找到对应的占位符,需要上下移动就上下移动,然后把空白行创建好,最后再填充数据
- 最后一个需要注意的地方:
不要直接在变量行或者单元格对象的时候修改行或者单元格,那样会并行修改异常,应该用int i,j的方式,根据i,j去获取行、列。
评论区