前言
在工作中总是会列表信息导出至Excel文件的需求,
此时我们就需要使用Java来生成Excel并且构建表的结构然后填充数据.
那么我们该如何做呢?
正文
引入pom依赖
<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>
首先解释以下这三个依赖都是做什么的?
Apache POI是Apache软件基金会的开源项目,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。
poi-ooxml是poi包中用于操作Excel文件的库
poi-ooxml-schemas是poi-ooxml包的依赖
所以不难看出这三个依赖缺一不可
代码实现
首先poi提供三种写入Excel文件的类,这三个类各有所长,至于具体使用哪个类来完成, 就取决于我们的需求.
HSSFWorkbook、XSSFWorkbook和SXSSFWorkbook的不同之处
HSSFWorkbook是专门用来对.xls
文件进行读写操作的实现类. .xls
文件是03版本Excel的规范, 它不支持超过65535行的大型表格.
XSSFWorkbook是专门用来对.xlsx
文件进行读写操作的实现类. .xlsx
文件是07版本的Excel的规范, 它可以向下兼容.xls
格式,相较于.xls
格式更加先进安全,也支持超大型表格.
那么SXSSFWorkbook是什么呢?
XSSFWorkbook类在读取和写入的时候会将表格(读取时)或数据(写入时)全部统统读到内存中, 然后再进行操作, 当表格或者数据过大时, 就会有一个致命的风险--堆栈溢出.
此时我们就需要一个新的类来进行大型表格或数据的写入或导出SXSSFWorkbook
便横空出世.
SXSSFWorkbook提供了一个类似于Flink的处理大型数据的逻辑--窗口.
它会将数据切片, 一点一点的进行处理, 每处理一片数据就会生成一个临时文件, 最后在将所有的临时文件合成一个最终的结果文件. 这就用硬盘的存储空间代替了捉襟见肘的内存空间.
创建工作簿
创建一个简单的工作簿,并且新建一个Sheet页,在第一个格子中填写一句话.
public static void main(String[] args){
//创建工作簿
Workbook wb = new HSSFWorkbook();
//创建Sheet
Sheet sheet = wb.createSheet("TestSheet");
//创建行
Row row = sheet.createRow(0);
//创建单元格
Cell cell = row.createCell(0);
//填充数据
cell.setCellValue("我是一个单元格!");
}
此时一个最最最简单的工作簿就写好了,这时我们创建一个文件输出流然后使用Workbook
类的write()
方法写入输出流, 就可以拿到我们写好的Excel文件了
使用IO流记得使用try-catch
方法包裹, 并且在完成操作之后关闭IO流以释放资源.
public static void main(String[] args){
Workbook wb;
FileOutputStream out = null;
try {
//读写xls格式
//创建工作簿
wb = new HSSFWorkbook();
//创建Sheet
Sheet sheet = wb.createSheet("TestSheet");
//创建行
Row row = sheet.createRow(0);
//创建单元格
Cell cell = row.createCell(0);
//填充数据
cell.setCellValue("我是一个单元格!");
out = new FileOutputStream("C:\\excelText\\firstExcel.xls")
wb.write(out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (out != null) {
//关闭文件输出流
out.close();
}
}
}
此时打开生成好的Excel文件, 我们发现字的太长超出单元格的大小, 溢出格子了.
这时我们需要设置单元格大小以适应内容长度
设置单元格宽度
先写一个计算字符串长度的方法, 为了更好的适配中文字符, 我们使用正则表达式
/**
* 计算中文字符的数量
* @param str 需要计算的字符串
* @return 中文字符的数量
*/
public static int calculateChineseNumber(String str) {
int count = 0;
//正则表达式,匹配中文字符
String regEx = "[\\u4e00-\\u9fa5]";
//编译正则表达式
Pattern pattern = Pattern.compile(regEx);
//匹配字符串
Matcher matcher = pattern.matcher(str);
//循环匹配
while (matcher.find()) {
//循环匹配到的字符
for (int i = 0; i <= matcher.groupCount(); i++) {
//数量加1
count = count + 1;
}
}
//返回数量
return count;
}
//获取字符串长度
int stringLength = calculateChineseNumber(text);
//计算像素长度
int strLen = (text.length() - stringLength) * 256 + (stringLength + 1) * 512;
//设置单元格宽度
sheet.setColumnWidth(0, strLen);
这里发现我们多进行了一部分换算,这时为什么呢?
具体来说,(text.length() - stringLength) * 256
表示非中文字符的像素长度,(stringLength + 1) * 512
表示中文字符的像素长度。+1
是因为在计算中文字符的像素长度时,需要加上一个额外的字节,以便在Excel中正确显示中文字符。
然后使用setColumnWidth( )
方法应用到对应的单元格中, 两个参数, 第一个为index(int)
, Sheet页中的第几列,从0开始. 第二个为多宽Width(int)
设置好宽度之后, 老板又对单元格中内容的样式提出了需求...
设置字体样式
在创建工作表时, 我们可以创建多个样式类型, 里面可以定义包括但不限于字体颜色,字体,字体大小,加粗,斜体等
//创建普通单元格样式
CellStyle cellStyle = wb.createCellStyle();
Font font = wb.createFont();
//设置字体颜色
font.setColor(Font.COLOR_RED);
//是否加粗
font.setBold(true);
//设置字体大小
font.setFontHeightInPoints((short) 24);
//设置斜体
font.setItalic(true);
//设置字体
font.setFontName("华文彩云");
//设置删除线
font.setStrikeout(true);
//装载
cellStyle.setFont(font);
...
Font类中有很多的方法, 篇幅优先这里就不一一列举了, 需要时可以直接查看源码, 方法名顾名思义即可..
此时, 老板又说, 我们要把内容改成单元格, 点一下就可以跳转老板娘的QQ空间公司官网!
设置单元格超链接
//创建超链接对象
CreationHelper ch = wb.getCreationHelper();
//声明超链接类型
Hyperlink hyperlink = ch.createHyperlink(HyperlinkType.URL);
//设置链接目标
hyperlink.setAddress("http://www.yangnanblog.top");
//创建一个超链接单元格,并装载超链接
Cell linkCell = row.createCell(1);
linkCell.setCellValue("我是超链接,点我!");
linkCell.setHyperlink(hyperlink);
使用Hyperlink
类来声明超链接, 接收的参数是HyperlinkType
的枚举类, 在这个枚举类中拥有下面的集中类型:
类型 | 作用 | 备注 |
---|---|---|
URL | 网络链接, 允许链接可访问的互联网地址 | 必须是http(s)协议的绝对路径 |
DOCUMENT | 文档, 允许在当前工作簿中的任何单元格 包括其他Sheet的单元格 | 链接格式需要遵守Excel的规范编写 例如: |
Email地址 | 链接格式例如:mailto:1234@gmail.com | |
FILE | 文件路径 | 这个没有特别的格式要求, 只要求绝对路径并且路径可到达 |
老板, 完成了....老板说不行我还需要冻结首行...行吧...
冻结单元格
如果想要冻结单元格, 我们可以使用这个方法createFreezePane( )
, 这个方法是Sheet类下的.
它接收两个参数,一个是从第几行开始, 另一个冻结几行
//冻结
sheet.createFreezePane(0, 10);
以上的示例代表着 首行冻结, 冻结十行, 这样子...
冻结首行之后, 我们想到那如果需要合并单元格之后在冻结呢?
合并单元格
我们现在有了合并单元格的需求
//创建一个需要合并单元格范围的对象
CellRangeAddress region = new CellRangeAddress(1, 1, 1, 4);
构造方法接收四个参数
参数 | 含义 |
---|---|
firstRow | 从哪一行开始 |
lastRow | 从哪一行结束 |
firstCol | 从哪一列开始 |
lastCol | 从哪一列结束 |
纵纵横横就圈出了需要合并的单元格范围, 然而这只是范围, 还没有装载
使用Sheet类下的addMergedRegion()
方法来装载合并范围对象
//装载合并范围
sheet.addMergedRegion(region);
此时就完成了单元格合并
效果如下:
创建文档摘要
wb.createInformationProperties();
DocumentSummaryInformation dsi = wb.getDocumentSummaryInformation();
dsi.setCategory("Excel文件");//类别
dsi.setManager("黑色閃光w");//管理者
dsi.setCompany("魏鹏文化有限公司");//公司
SummaryInformation si = wb.getSummaryInformation();
si.setAuthor("黑色閃光w");//作者
si.setComments("无");//备注
si.setTitle("POIDemo");//标题
si.setSubject("POI操作Excel");//主题
si.setKeywords("Java2Excel");//标记
si.setCreateDateTime(new Date());//创建时间
效果就像下面这样, 信息不是存在表格中, 而是存在文件属性的详细信息里.
创建批注
Drawing<?> patr = sheet.createDrawingPatriarch();
ClientAnchor anchor = patr.createAnchor(0, 0, 0, 0, 7, 1, 9, 3);//创建批注位置
Comment comment = patr.createCellComment(anchor);//创建批注
comment.setString(new HSSFRichTextString("这是一个批注段落!"));//设置批注内容
comment.setAuthor("黑色閃光");//设置批注作者
comment.setVisible(true);//设置批注默认显示
Cell cell3 = sheet.createRow(3).createCell(1);
cell3.setCellValue("被批注的单元格");
cell3.setCellComment(comment);//把批注赋值给单元格
创建注释的主要方法createAnchor()
一共有八个参数
参数 | 含义 |
---|---|
dx1 | 第1个单元格中x轴的偏移量 |
dy1 | 第1个单元格中y轴的偏移量 |
dx2 | 第2个单元格中x轴的偏移量 |
dy2 | 第2个单元格中y轴的偏移量 |
col1 | 第1个单元格的列号 |
row1 | 第1个单元格的行号 |
col1 | 第2个单元格的列号 |
col2 | 第2个单元格的行号 |
创建页眉和页脚
//创建页眉和页脚
Header header = sheet.getHeader();//得到页眉
header.setLeft("页眉左边");
header.setRight("页眉右边");
header.setCenter("页眉中间");
Footer footer = sheet.getFooter();//得到页脚
footer.setLeft("页脚左边");
footer.setRight("页脚右边");
footer.setCenter("页脚中间");
创建好页眉和页脚为啥我的表格中看不见捏? 我也不知道,但是它确实存在...
设置单元格格式
//设置单元格格式
Row row1 = sheet.createRow(2);
//设置日期格式--使用Excel内嵌的格式
Cell cell1 = row1.createCell(0);
cell1.setCellValue(new Date());
HSSFCellStyle style = wb.createCellStyle();
style.setDataFormat(HSSFDataFormat.getBuiltinFormat("m-d-yy h:mm"));
cell1.setCellStyle(style);
//设置保留2位小数--使用Excel内嵌的格式
cell1 = row1.createCell(1);
cell1.setCellValue(12.3456789);
style = wb.createCellStyle();
style.setDataFormat(HSSFDataFormat.getBuiltinFormat("0.00"));
cell1.setCellStyle(style);
//设置货币格式--使用自定义的格式
cell1 = row1.createCell(2);
cell1.setCellValue(12345.6789);
style = wb.createCellStyle();
style.setDataFormat(wb.createDataFormat().getFormat("¥#,##0"));
cell1.setCellStyle(style);
//设置百分比格式--使用自定义的格式
cell1 = row1.createCell(3);
cell1.setCellValue(0.123456789);
style = wb.createCellStyle();
style.setDataFormat(wb.createDataFormat().getFormat("0.00%"));
cell1.setCellStyle(style);
//设置中文大写格式--使用自定义的格式
cell1 = row1.createCell(4);
cell1.setCellValue(12345);
style = wb.createCellStyle();
style.setDataFormat(wb.createDataFormat().getFormat("[DbNum2][$-804]0"));
cell1.setCellStyle(style);
//设置科学计数法格式--使用自定义的格式
cell1 = row1.createCell(5);
cell1.setCellValue(12345);
style = wb.createCellStyle();
style.setDataFormat(wb.createDataFormat().getFormat("0.00E+00"));
cell1.setCellStyle(style);
这一部分呢, 没什么好说的, 就是单纯的设置单元格格式而已..
设置单元格内容的对其方式
//设置单元格对其方式
Row row3 = sheet.createRow(5);
row3.setHeight((short) (6 * 256));
Cell cell4 = row3.createCell(0);
cell4.setCellValue("单元格对齐");
CellStyle style1=wb.createCellStyle();
style1.setAlignment(HorizontalAlignment.CENTER);//水平居中
style1.setVerticalAlignment(VerticalAlignment.CENTER);//垂直居中
style1.setWrapText(true);//自动换行
style1.setIndention((short)5);//缩进
style1.setRotation((short)60);//文本旋转,这里的取值是从-90到90,而不是0-180度。
cell4.setCellStyle(style1);
效果就像是这样
设置单元格公式
//设置单元格公式
HSSFSheet sheet2 = wb.createSheet("FormulaTest");// 创建工作表(Sheet)
Row row2_1 = sheet2.createRow(0);
row2_1.createCell(0).setCellValue(1);
row2_1.createCell(1).setCellValue(2);
row2_1.createCell(2).setCellValue(3);
row2_1.createCell(3).setCellValue(4);
row2_1.createCell(4).setCellValue(5);
Row row2_2 = sheet2.createRow(1);
row2_2.createCell(0).setCellFormula("sum(A1,C1)");//等价于"A1+C1"
row2_2.createCell(1).setCellFormula("sum(B1:D1)");//等价于"B1+C1+D1"
设置单元格保护
//设置单元格密码
Row row4 = sheet.createRow(8);
Cell cell5 = row4.createCell(0);
cell5.setCellValue("密码保护");
HSSFCellStyle lock = wb.createCellStyle();
lock.setLocked(true);//锁定
sheet.protectSheet("123123");//设置保护密码
可以锁定部分的单元格
但是保护密码是保护整个Sheet页
生成下拉式菜单
//生成下拉式菜单
HSSFSheet sheet3 = wb.createSheet("ExplicitListTest");
HSSFRow row5 = sheet3.createRow(0);
row5.createCell(0).setCellValue("下拉框");
CellRangeAddressList regions = new CellRangeAddressList(1, 65535,0, 0);
DVConstraint constraint =DVConstraint.createExplicitListConstraint(new String[] { "魏鹏","鹏魏", "委鬼月月鸟" });
HSSFDataValidation dataValidate = new HSSFDataValidation(regions,constraint);
sheet3.addValidationData(dataValidate);
下拉式菜单可以有效的解决用户在Excel表中乱填的bug
解决用户在酒吧点蛋炒饭的问题
校验数据有效性
//校验数据有效性
row5.createCell(1).setCellValue("日期列");
CellRangeAddressList regions1 = new CellRangeAddressList(1, 65535,1, 1);//选定一个区域
DVConstraint constraint1 = DVConstraint.createDateConstraint(DVConstraint . OperatorType.BETWEEN , "1993-01-01" ,"2014-12-31" , "yyyy-MM-dd" );
HSSFDataValidation dataValidate1 = new HSSFDataValidation(regions1,constraint1);
dataValidate.createErrorBox("错误", "你必须输入一个时间!");
sheet3.addValidationData(dataValidate1);
看, 在酒吧中点蛋炒饭不要饭的问题也解决了..
SXSSFWorkbook的特殊操作
因为它会将任务切片, 所以会有一些特别的操作值得注意
try {
//创建工作簿
wb = new SXSSFWorkbook();
wb.setCompressTempFiles(false); //是否压缩临时文件,否则写入速度更快,但更占磁盘,但程序最后是会将临时文件删掉的
wb.write(out);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (wb != null) {
wb.dispose();// 删除临时文件,很重要,否则磁盘可能会被写满
}
if (out != null) {
out.close();
}
}
结语
目前一个对Excel表格的基础操作就到此结束了.
什么? 为什么不讲XSSFWorkbook?
实际上在用法和操作上着三个实现类大差不差, 只有SXSSFWorkbook类会有一些对临时文件的配置.
参考
Apache POI 4.1.x JavaDoc --> https://poi.apache.org/apidocs/4.1/
知乎 JAVA-POI https://zhuanlan.zhihu.com/p/357790289
评论区