该图片由birgl在Pixabay上发布
你好,我是看山。
前文 聊了 EasyExcel 的内容导出,本文主要说一下导出文件的格式化,格式化包括工作表/单元格样式和内容格式化。毕竟,有时候还是要看脸。
内容比较多,文内只会列出关键代码,想要完整源码,可以关注公号「看山的小屋」回复“easyexcel”获取。
注解格式
通过注解定义格式是 EasyExcel 封装的高级功能,可以让我们很方便的定义格式。
格式化内容
先定义一个使用注解格式化内容的实体类:
@Data public class FormatContentItem { @ExcelProperty(value = "字符串标题", converter = TitleFormatConverter.class) private String string; @DateTimeFormat("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒") @ExcelProperty(value = "日期标题") private Date date; @NumberFormat("0.000%") @ExcelProperty("数字标题") private Double doubleData; }
其中DateTimeFormat和NumberFormat两个注解都是自带的注解,用于格式化时间和数字。
DateTimeFormat注解有两个属性,一个属性是value,用来定义时间格式,可以参考java.text.SimpleDateFormat;另一个属性是use1904windowing,表示使用时间使用 1904 时间系统还是 1900 时间系统,默认是否。
NumberFormat注解有两个属性,一个属性是value,用来定义数字格式,可以参考java.text.DecimalFormat;另一个属性是roundingMode,用来定义保留小数的方式,使用的是java.math.RoundingMode枚举。
想要格式化字符串,可以借助ExcelProperty的 converter 属性,这个属性传入实现Converter的类。比如示例中的TitleFormatConverter,代码如下:
public class TitleFormatConverter implements Converter<String> { @Override public Class<?> supportJavaTypeKey() { return String.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.STRING; } @Override public WriteCellData<?> convertToExcelData(String value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) { return new WriteCellData<>(String.format("标题:%s(自定义)", value)); } }
结果为:
定义行高、列宽
使用注解定义行高的话,可以使用HeadRowHeight定义表头高度,使用ContentRowHeight定义表体高度,这个注解定义之后,所有表体高度都是相同的。列宽可以使用ColumnWidth注解定义,这个注解可以定义在类上,表示整个表格的列都一样宽,也可以定义的属性上,表示指定列的宽度。
@Data @HeadRowHeight(20) @ContentRowHeight(10) @ColumnWidth(25) public class FormatCellItem { @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ColumnWidth(50) @ExcelProperty("数字标题") private Double doubleData; }
结果为:
单元格定义样式
控制单元格样式有四个注解:HeadStyle、HeadFontStyle、ContentStyle、ContentFontStyle,这四个注解可以定义在类上作为全局表格的样式,也可以定义在字段上,作为当前列的样式。下面分别说一下这几个注解中比较常用的配置。
*Style:分为HeadStyle和ContentStyle,分别定义表头和表体样式
dataFormat:表头格式化,short 格式,是org.apache.poi.ss.usermodel.BuiltinFormats类中已定义格式的小标
border*:分别是 borderLeft、borderRight、borderTop、borderBottom 四个属性,类型是com.alibaba.excel.enums.poi.BorderStyleEnum枚举,用来定义表头单元格边框样式。边框的颜色也可以定义,使用、*BorderColor 定义即可。
fillPatternType:填充类型,类型是com.alibaba.excel.enums.poi.FillPatternTypeEnum枚举,如果想要填充背景色,这个属性需要设置为SOLID_FOREGROUND。
fillForegroundColor:前景色,类型是 short,值却是使用的org.apache.poi.ss.usermodel.IndexedColors枚举的 idx 值,只不过,两个类型不一致,一个是 short,一个是 int,没有办法直接引用。可见 java 中的依赖之间,还是有很多坑的。
fillBackgroundColor:背景色,同fillForegroundColor。
rotation:内容旋转角度
*FontStyle:有HeadFontStyle和ContentFontStyle,分别定义表头和表体的字体样式。
fontName:定义字体名称,类型字符串
fontHeightInPoints:字号大小,类型是 short
italic:是否斜体,类型是com.alibaba.excel.enums.BooleanEnum
bold:是否加粗,类型是com.alibaba.excel.enums.BooleanEnum
strikeout:是否使用删除线(这个词本意是三振出局的意思,应该是与棒球有关)
color:文本颜色,值使用的是org.apache.poi.ss.usermodel.IndexedColors,依然有类型不一致的情况
underline:下划线,类型是 byte,可以直接使用Font.U_NONE、Font.U_SINGLE、Font.U_DOUBLE、Font.U_SINGLE_ACCOUNTING、Font.U_DOUBLE_ACCOUNTING。
我们可以这么定义:
@Data // 头背景设置成红色 IndexedColors.RED.getIndex() @HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 10) // 头字体设置成 20 @HeadFontStyle(fontHeightInPoints = 20) // 内容的背景设置成绿色 IndexedColors.GREEN.getIndex() @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 17) // 内容字体设置成 20 @ContentFontStyle(fontHeightInPoints = 20) public class FormatStyleCellItem { // 字符串的头背景设置成粉红 IndexedColors.PINK.getIndex() @HeadStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 14) // 字符串的头字体设置成 20 @HeadFontStyle(fontHeightInPoints = 30) // 字符串的内容的背景设置成天蓝 IndexedColors.SKY_BLUE.getIndex() @ContentStyle(fillPatternType = FillPatternTypeEnum.SOLID_FOREGROUND, fillForegroundColor = 40) // 字符串的内容字体设置成 20 @ContentFontStyle(fontHeightInPoints = 30) @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
结果为:
类对象定义格式
这种方式可以说是纯手工组装数据了,使用的是com.alibaba.excel.metadata.data.WriteCellData类,这个类相当于是单元格的定义,通过设置com.alibaba.excel.enums.CellDataTypeEnum枚举类型的 type 属性,可以指明当前单元格格式。
守恒定律一直存在。这种方式灵活度很高,可以精细到具体的单元格格式,但是繁琐程度也增加了。
超链接
超链接使用的是com.alibaba.excel.metadata.data.HyperlinkData类,需要设置地址、超链类型(com.alibaba.excel.metadata.data.HyperlinkData.HyperlinkType枚举),然后将值写入到WriteCellData对象的hyperlinkData属性即可。
// 设置超链接 HyperlinkData hyperlinkData = new HyperlinkData(); hyperlinkData.setAddress("https://www.howardliu.cn"); hyperlinkData.setHyperlinkType(HyperlinkType.URL); WriteCellData<String> hyperlink = new WriteCellData<>("网站"); hyperlink.setHyperlinkData(hyperlinkData);
备注
备注使用的是com.alibaba.excel.metadata.data.CommentData类,需要设置作者、备注内容(com.alibaba.excel.metadata.data.RichTextStringData类型),因为备注的默认大小是单元格大小,如果感觉太小,还可以设置相对高度和宽度。
// 设置备注 CommentData commentData = new CommentData(); commentData.setAuthor("Howard Liu"); commentData.setRichTextStringData(new RichTextStringData("这是一个备注")); // 备注的默认大小是按照单元格的大小 这里想调整到 4 个单元格那么大 所以向后 向下 各额外占用了一个单元格 commentData.setRelativeLastColumnIndex(1); commentData.setRelativeLastRowIndex(1); WriteCellData<String> comment = new WriteCellData<>("备注的单元格信息"); comment.setCommentData(commentData);
公式
公式使用的是com.alibaba.excel.metadata.data.FormulaData类,可以直接设置formulaValue公式,不过官方不太推荐使用公式。
// 设置公式 FormulaData formulaData = new FormulaData(); // 将 123456789 中的第一个数字替换成 2 // 这里只是例子 如果真的涉及到公式 能内存算好尽量内存算好 公式能不用尽量不用 formulaData.setFormulaValue("REPLACE(123456789,1,1,2)"); WriteCellData<String> formula = new WriteCellData<>(); formula.setFormulaData(formulaData);
单元格格式
通过类定义单元格格式,与通过注解定义本质是一样的。所以与注解HeadStyle、HeadFontStyle、ContentStyle、ContentFontStyle对应,设置单元格格式的类是WriteCellStyle,设置字体的类是WriteFont。其中这些类的属性与注解的也是类似,不再赘述太多,直接上例子。(其实我觉得使用类定义格式的场景不多,真的碰到了,看看类定义就明白了)
// 设置单个单元格的样式 当然样式 很多的话 也可以用注解等方式。 WriteCellStyle writeCellStyleData = new WriteCellStyle(); // 这里需要指定 FillPatternType 为 FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色。 writeCellStyleData.setFillPatternType(FillPatternType.SOLID_FOREGROUND); // 背景绿色 writeCellStyleData.setFillForegroundColor(IndexedColors.GREEN.getIndex()); WriteCellData<String> writeCellStyle = new WriteCellData<>("单元格样式"); writeCellStyle.setWriteCellStyle(writeCellStyleData); writeCellStyle.setType(CellDataTypeEnum.STRING); // 设置单个单元格多种样式 RichTextStringData richTextStringData = new RichTextStringData(); richTextStringData.setTextString("红色绿色默认"); // 前 2 个字红色 WriteFont writeFont = new WriteFont(); writeFont.setColor(IndexedColors.RED.getIndex()); richTextStringData.applyFont(0, 2, writeFont); // 接下来 2 个字绿色 writeFont = new WriteFont(); writeFont.setColor(IndexedColors.GREEN.getIndex()); richTextStringData.applyFont(2, 4, writeFont); WriteCellData<String> richTest = new WriteCellData<>(); richTest.setType(CellDataTypeEnum.RICH_TEXT_STRING); richTest.setRichTextStringDataValue(richTextStringData);
结果为:
拦截器定义格式
除了直接使用类定义格式,我们还可以借助拦截器实现。(这里在名称上会有一些歧义,所用的类对象命名都是 xxxStrategy,翻译过来就是 xxx 策略,但是官方对其命名为拦截器)
已有拦截器
前面示例中使用WriteCellStyle、WriteFont可以实现单元格的样式,如果想要实现整行数据都是相同的格式,可以借助com.alibaba.excel.write.style.HorizontalCellStyleStrategy拦截器。
/** * 使用已有策略实现自定义样式 * * <ul> * <li>HorizontalCellStyleStrategy 每一行的样式都一样 或者隔行一样</li> * <li>AbstractVerticalCellStyleStrategy 每一列的样式都一样 需要自己回调每一页</li> * </ul> */ private static void writeByCellStyleStrategy() { String fileName = defaultFileName("writeByCellStyleStrategy"); // 表头策略 WriteCellStyle headWriteCellStyle = new WriteCellStyle(); // 背景设置为红色 headWriteCellStyle.setFillForegroundColor(IndexedColors.RED.getIndex()); WriteFont headWriteFont = new WriteFont(); headWriteFont.setFontHeightInPoints((short) 40); headWriteCellStyle.setWriteFont(headWriteFont); // 表体策略 WriteCellStyle contentWriteCellStyle = new WriteCellStyle(); // 这里需要指定 FillPatternType 为 FillPatternType.SOLID_FOREGROUND 不然无法显示背景颜色。表头默认了 FillPatternType 所以可以不指定 contentWriteCellStyle.setFillPatternType(FillPatternType.SOLID_FOREGROUND); // 背景绿色 contentWriteCellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex()); WriteFont contentWriteFont = new WriteFont(); // 字体大小 contentWriteFont.setFontHeightInPoints((short) 20); contentWriteCellStyle.setWriteFont(contentWriteFont); // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现 HorizontalCellStyleStrategy horizontalCellStyleStrategy = new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle); // 这里 需要指定写用哪个 class 去写,然后写到第一个 sheet,名字为模板 然后文件流会自动关闭 EasyExcelFactory.write(fileName) .head(Item.class) .registerWriteHandler(horizontalCellStyleStrategy) .sheet() .doWrite(sampleItems()); }
结果为:
正如上面的结果,如果我们某个单元格数据比较长,可能会有遮挡,这个时候我们可以使用com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy实现自动列宽调整。不过这个不太精确,但聊胜于无。
private static void writeUseLongestMatchColumnWidthStyleStrategy() { String fileName = defaultFileName("writeUseLongestMatchColumnWidthStyleStrategy"); EasyExcelFactory.write(fileName) .head(Item.class) .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) .sheet() .doWrite(sampleItems()); }
结果为:
可以看到,确实不够精确。
自定义拦截器
上面展示的拦截器,都是实现了com.alibaba.excel.write.handler.WriteHandler接口,然后使用com.alibaba.excel.write.builder.AbstractExcelWriterParameterBuilder.registerWriteHandler方法注册到写函数中。所以,我们也可能根据需要,自己定义需要的拦截器。
这种自定义拦截器属于低级功能,需要了解很多底层设计和 API,鉴于篇幅,本文没有办法覆盖,这里只给出例子。如果有需要,可以留言沟通。
比如,我们需要某些单元格设置数据验证,展现形式就是下拉菜单,我们可以这样写:
public class ColumnValidationWriteHandler implements SheetWriteHandler { @Override public void afterSheetCreate(SheetWriteHandlerContext context) { // 区间设置 第一列第一行和第二行的数据。由于第一行是头,所以第一、二行的数据实际上是第二三行 CellRangeAddressList cellRangeAddressList = new CellRangeAddressList(1, 2, 0, 0); DataValidationHelper helper = context.getWriteSheetHolder().getSheet().getDataValidationHelper(); DataValidationConstraint constraint = helper.createExplicitListConstraint(new String[] {"测试 1", "测试 2"}); DataValidation dataValidation = helper.createValidation(constraint, cellRangeAddressList); context.getWriteSheetHolder().getSheet().addValidationData(dataValidation); } }
如果我们需要将某个单元格的格式设置为超链,也可以使用拦截器:
public class CellStyleWriteHandler implements CellWriteHandler { @Override public void afterCellDispose(CellWriteHandlerContext context) { Cell cell = context.getCell(); // 这里可以对 cell 进行任何操作 if (BooleanUtils.isTrue(context.getHead()) && cell.getColumnIndex() == 0) { CreationHelper createHelper = context.getWriteSheetHolder().getSheet().getWorkbook().getCreationHelper(); Hyperlink hyperlink = createHelper.createHyperlink(HyperlinkType.URL); hyperlink.setAddress("https://www.howardliu.cn"); cell.setHyperlink(hyperlink); } } }
结果为:
合并单元格
EasyExcel 提供的合并单元格功能比较简单,有两种方式:基于注解的合并、基于拦截器的合并。
注解
基于注解的合并单元格提供了两个注解:
OnceAbsoluteMerge注解实现指定位置的合并
ContentLoopMerge这个是内容的循环合并,指定某一列每几行合并。
// 将第 6-7 行的 2-3 列合并成一个单元格 @OnceAbsoluteMerge(firstRowIndex = 5, lastRowIndex = 6, firstColumnIndex = 1, lastColumnIndex = 2) @Data public class MergeCellItem { @ContentLoopMerge(eachRow = 2) @ExcelProperty("字符串标题") private String string; @ExcelProperty("日期标题") private Date date; @ExcelProperty("数字标题") private Double doubleData; }
结果为:
拦截器
拦截器合并也是有两种,对应着注解:
OnceAbsoluteMergeStrategy,相对位置合并
LoopMergeStrategy循环合并
private static void writeMergeCellCustom() { String fileName = defaultFileName("writeMergeCellCustom"); // 每隔 2 行会合并 // 把 eachColumn 设置成 3 也就是我们数据的长度,所以就第一列会合并。当然其他合并策略也可以自己写 LoopMergeStrategy loopMergeStrategy = new LoopMergeStrategy(2, 0); EasyExcelFactory.write(fileName) .head(Item.class) .registerWriteHandler(loopMergeStrategy) .sheet() .doWrite(sampleItems()); }
结果为:
文末总结
本文从实战角度说了一下 EasyExcel 如果实现写出好看的表格,EasyExcel中提供了很多用于格式化的注解、拦截器,可以时薪通用的格式化输出,如果还有更加个性化的格式要求,也可以自定义拦截器实现。接下来聊一下如何填充模板。
推荐阅读
阿里开源的这个库,让 Excel 导出不再复杂(简简单单的写)
阿里开源的这个库,让 Excel 导出不再复杂(既要能写,还要写的好看)