当前位置: 首页 > news >正文

C#工具库-NPOI

一、简介

           NPOI是一个基于c#语言的,开源的,能够在不安装Microsoft Office组件的条件下读写Microsoft Office 的库。前身是Java的POI库,有“先贤”将其翻译成了c#语言的库,而这种由java到c#库的演变并非个例,比如DotNetty之于Netty,NetTopologySuite 之于TopologySuite

           在我的有限认知里面它算是c#里面读写excel最好的库(主观)。开源协议宽松和读写速度快。

          C#编程中引用NPOI类库的方法也很简单,通过Nuget进行包管理。项目中需要引用第三方库时,强烈建议优先使用nuget方式进行引用。

二、使用NPOI操作Excel文件

1、概述

        类库中主要的对象逻辑层级是按Book->Sheet->Row->Cell这种方式展开的,其他类比如样式,公式等都是对这个骨架的扩充。其实类似的表格都可以组织成这样的数据结构,如果我们自己有类似的显示需求,也可以按照此种方式展开设计。接下来两节2、读流程,3、写流程会以一些代码形式去串讲简单的续写逻辑。为什么会在文中放置写代码,甚至代码占了相当的篇幅,为了使读者在读的过程中在大脑中去过一遍这个库的常用方法的使用,增加对这个库的数据度,虽然这个熟悉度还是很浅的级别,但还是有其作用的,我尽量让NPOI在读者大脑中留下点儿东西。

       

2、读流程

         读excel要比写excel容易一些,这是由【读】的场景决定的,读excel的读更多的应用场景是读自己的配置文件、读程序间交换的数据。在这样的大前提下,我们不需要对excel的样式字体进行太多的关注,数据组织方式也可以是最简单的形式,类似数据库的那种关系表格。搞程序,一定要在工程和技术上做好权衡,保持足够简单,足够和简单要慢慢读,不要连起来,要细品。没必要的事尽量少做,代码多,出bug的可能就会大,但要适当保持代码的灵活性。

  2.1 初始化IWorkbook对象

         IWorkbook的子类有两个XSSFWorkbook(对应Excel2007以上版本,.xlsx)、HSSFWorkbook(对应Excel2003版本,xls)。现在估计没人再使用2003版本了,所以我们在封装自己的工具类库时,也没有什么必要再去兼容HSSFWorkbook了。

        一定要提供通过【流】方式初始化IWorkbook的方法,因为在有些用户的电脑上,办公文件是自动加密的,如果你的某些配置文件是以Excel的格式发布的,那么大概率程序就不能正常运行了。所为我建议,当读程序自身excel格式的配置文件时,处理策略是,将excel文件以嵌入资源的形式编译到dll中,使用的时候直接通过资源流的形式初始化IWorkbook。

public static IWorkbook GetWorkbook(string filePath)
{if (!File.Exists(filePath))return null;var file = new FileStream(filePath, FileMode.Open, FileAccess.Read);if (filePath.IndexOf(".xlsx") > 0){//2007版本return new XSSFWorkbook(file);}return null;
}public static IWorkbook GetWorkbook(Stream stream){try{return new XSSFWorkbook(stream);      }catch (Exception ex){Debug.Write(ex.Message);}return null;}
2.2 获取ISheet

    获取sheet可以根据名称获取,也可以根据索引获取,索引是基于0的;

ISheet IWorkbook.GetSheet(string name);
ISheet  IWorkbook.GetSheetAt(int index);

如果需要取出所有所有sheet,可以结合IWorkbook.NumberOfSheets属性进行遍历

for (int sheetIndex = 0; sheetIndex < workbook.NumberOfSheets; sheetIndex++)
{try{var sheet = workbook.GetSheetAt(sheetIndex);}catch (Exception ex){Debug.WriteLine(ex.Message);}
}

获取Sheet中最大使用行数,sheet.LastRowNum是基于0的

rowCount=sheet.LastRowNum + 1

获取Sheet中最大使用列数,最大使用列数没有直接提供,需要便利所有IRow的Cell数去求最大值:

/// <summary>/// 获取最后可用的列数/// </summary>/// <param name="sheet"></param>/// <returns></returns>public static int LastColumnNum(this ISheet sheet){int cellCount = 0;for (int i = 0; i <= sheet.LastRowNum; i++){IRow row = sheet.GetRow(i);if (row != null && cellCount < row.LastCellNum){cellCount = row.LastCellNum;}}return cellCount;}
2.3 获取IRow

  sheet通过行索引获取指定的行信息,索引基于0,如果行不存在则返回null

IRow ISheet.GetRow(int rownum)
2.4 获取ICell

   row通过列索引获取指定的单元格信息,索引基于0,如果行不存在则返回null

ICell IRow.GetCell(int cellnum)

   sheet直接通过行索引和列索引去取cell

/// <summary>
/// 获取指定索引的Cell(如果索引无效则返回null)
/// </summary>
/// <param name="sheet"></param>
/// <param name="rowIndex"></param>
/// <param name="columnIndex"></param>
/// <returns></returns>
public static ICell GetCell(this ISheet sheet, int rowIndex, int columnIndex)
{if (rowIndex >= 0 && sheet.LastRowNum > rowIndex){IRow row = sheet.GetRow(rowIndex);if (row == null)return null;if (columnIndex >= 0 && row.LastCellNum > columnIndex){ICell cell = row.GetCell(columnIndex);return cell;}}return null; ;
}

  ICell有可能是合并的单元格,合并的单元格的有效显示值实际上是整个合并区(CellRangeAddress)的左上角索引单元格的值

public static CellRangeAddress GetCell(this ISheet sheet, int rowIndex, int columnIndex)
{for (int i = 0; i < sheet.NumMergedRegions; i++){CellRangeAddress range = sheet.GetMergedRegion(i);if(!sheet.IsMergedRegion(range)continue;if (range.InRange(rowIndex, columnIndex)){return range;}}return null; ;
}

ICell取值,需要按照ICell的数据类型去处理,粗略参考:

public static object GetCellValue(this ICell cell){if (cell == null)return null;if (cell.IsMergedCell){var useCell = 【通过合并区域获取左上角显示单元格值】;cell = useCell ?? cell;}switch (cell.CellType){case CellType.Blank: //BLANK:  return null;case CellType.Boolean: //BOOLEAN:  return cell.BooleanCellValue;case CellType.Numeric: //NUMERIC:  return cell.NumericCellValue;case CellType.String: //STRING:  return cell.StringCellValue;case CellType.Error: //ERROR:  return cell.ErrorCellValue;case CellType.Formula:cell.SetCellType(CellType.String);return cell.StringCellValue;default:return "=" + cell.CellFormula;}}

         至此,通过NPOI读取excel数据流程涉及的到的基本环节算是介绍完了,在实际项目使用过程中还需要多加尝试,封装自己顺手的方法。

3、写流程

     写流程与上述的读流程大体类似,毕竟拿到ICell才能对他进行写入嘛。写操作的难点在于去按需求去设置各种配置,如字号,字体,列宽,行高,公式等,这些就不展开说了,随需随查知道有这么格事儿就行了

3.1构造IWorkbook

直接安普通类方式初始化就行了。

 IWorkbook workBook = new XSSFWorkbook();
3.2 创建ISheet

先获取sheet,看看是否存在,如果不存在就创建一个。后面涉及到的IRow,和ICell都是这样的操作

/// <summary>
/// 获取sheet,存在返回,没有创建
/// </summary>
/// <param name="book"></param>
/// <param name="sheetName"></param>
/// <returns></returns>
public static ISheet GetSheet2(this IWorkbook book, string sheetName)
{return book.GetSheet(sheetName) ?? book.CreateSheet(sheetName);
}
3.3创建IRow

 通用创建Row的方法

 var useRow = sheet.GetRow(rowIndex);if (useRow == null){useRow = sheet.CreateRow(rowIndex);}
3.4创建ICell

通用创建Cell的方法

var useCell = useRow.GetCell(columnIndex);if (useCell == null){useCell = useRow.CreateCell(columnIndex);}

给ICell赋值的通用方式

/// <summary>/// 设置Excel的值信息/// </summary>/// <param name="cell"></param>/// <param name="value"></param>/// <param name="valueType">0:真实Value值,1:公式表达式;其他不合法值,按0处理</param>public static void SetCellValue(ICell cell,object value,int valueType){if (cell == null)return;if (valueType == 1){cell.SetCellFormula(value?.ToString());}else{if (value == null){cell.SetBlank();}else if (value is string strValue){cell.SetCellValue(strValue);}else if (value is double dValue){cell.SetCellValue(dValue);}else if (value is bool bValue){cell.SetCellValue(bValue);}else if (value is DateTime dtValue){cell.SetCellValue(dtValue);}else{cell.SetCellValue(value.ToString());}}}

如果需要给ICell设置特殊样式的话,要通过IWorkbok创建,这个应该是为了方便管理和复用

var headCellStyle = workBook.CreateCellStyle();var f = workBook.CreateFont();f.IsBold = true;f.FontName = "等线";headCellStyle.SetFont(f);

三、注意事项

  • NPOI导出的excel文件,打开提示需要修复的问题。

     两个可能注意的点

     1、当使用stream转换成数组时,使用ToArray()方法。

     2文件保存前,如果文件存在,先将旧文件删除,再创建新文件

/// <summary>
/// 保存excel信息
/// </summary>
/// <param name="book"></param>
/// <param name="filePath"></param>
/// <param name="closeBook">保存前,是否关闭流。如果使用文件被当前excel打开,必须关闭才能写入</param>
/// <returns></returns>
public static bool Save(this IWorkbook book, string filePath, bool closeBook)
{if (string.IsNullOrWhiteSpace(filePath))throw new ArgumentNullException(nameof(filePath));using (MemoryStream ms = new MemoryStream()){book.Write(ms);if (closeBook){book.Close();}var directory = Directory.GetParent(filePath);if (!directory.Exists){directory.Create();}//不要使用OpenCreate形式,如果原始文件存在,可能出现需要修复的错误using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)){byte[] data = ms.ToArray();fs.Write(data, 0, data.Length);fs.Flush();}return true;}
}
  • 创建单元格时,直接设置Cell的公式,然后随即取值可能不生效,需要手动触发计算

   

HSSFFormulaEvaluator e = new HSSFFormulaEvaluator(iworkbook);var icell = e.EvaluateInCell(icell);
  • 行高和列宽问题

          1.行高

                 Height 属性后面的值的单位是:1/20个点,所以要想得到一个点的话,需要乘以20。

                 HeightInPoints后面的单位是点,可以不用乘。

                 sheet.DefaultRowHeight = 23*20;

         2.列宽

               SetColumnWidth方法里的第二个参数要乘以256,因为这个参数的单位是1/256个字符宽          度,所以要乘以256才是一整个字符宽度。

                 sheet.SetColumnWidth(0, 15*256);

                但是这个计算出来的值并不是excel实际的类款有偏差,这个没有再深入的了解,涉及的          列宽单位应该也是PT形式的,按字符计算可能从本质上将就是不对的。

                pt:英文中的磅值,自号中的那个数字也是这个意思。

                参考资料: px,pt,em换算表 | 菜鸟教程 (runoob.com)

  • 列索引转成列名形式(A,B,C形式)这个可能在编辑公式时用的到
/// <summary>/// 列索引转列名,索引从0开始计算/// </summary>/// <param name="columnIndex"></param>/// <returns></returns>public static string GetColumnName(int columnIndex){columnIndex = Math.Max(0, columnIndex);int calc = columnIndex + 1;int system = 26;StringBuilder sb = new StringBuilder();do{calc--;int mod = calc % system;int div = calc / system;sb.Insert(0, (char)(mod + 'A'));calc = div;} while (calc > 0);return sb.ToString();}
  • ICSharpCode.SharpZipLib报错:

          NPOI引用ICSharpCode.SharpZipLib对文件进行压缩处理,该压缩库做为很多第三方库的基础组件,所需要需要特别注意版本的一致性

          当文件进行了加密处理时,同样会报与ICSharpCode.SharpZipLib相关的错误,下图是一个原始错误形式:这个错误困扰了我很久

Wrong Local header signature: 0x65231462:   在 ICSharpCode.SharpZipLib.Zip.ZipInputStream.GetNextEntry()

           后来发现是我研发环境下的电脑会自动加密文件导致文件读取识别不了的,同样,读有损坏的文件应该也会报这个错。

  • 替代产品

        由于NPOI类库使用较广,在进行二次开发的项目时如果和其他插件引用的NPOI库冲突了,是一个很麻烦的事情,尤其是当自身开发的软件产品地位不如对方时,就需要咱们做出妥协。要么和别人保持一致的版本,要么替换一个不太常见的库。EPPlus是一个选择,当然还有其它选择,这里只是把这类问题抛砖引玉。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 案例分享—优秀国外界面设计配色舒适的原因
  • Kubernetes--深入Pod
  • MySQL索引失效的场景
  • Linux~系统基础学习
  • 深入探讨SD NAND的SD模式与SPI模式初始化
  • [数据集][目标检测]agvs仓储机器人检测数据集VOC+YOLO格式967张3类别
  • 数组去重的12重方法
  • 运维大规模K8S集群注意事项
  • java 函数接口Consumer简介与示例【函数式编程】【Stream】
  • 大学生实用工具!分享5款靠谱AI一键生成毕业论文的网站
  • 5个免费在线 AI 绘画网站推荐,附100+提示词!
  • 什么是上网行为管理呢?【上网行为管理系统功能介绍 】
  • 【C++ 面试 - 面向对象】每日 3 题(六)
  • LeetCode17 电话号码的字母组合
  • STM32——I2C通信外设
  • .pyc 想到的一些问题
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 【跃迁之路】【585天】程序员高效学习方法论探索系列(实验阶段342-2018.09.13)...
  • angular学习第一篇-----环境搭建
  • axios 和 cookie 的那些事
  • Docker 笔记(1):介绍、镜像、容器及其基本操作
  • Docker下部署自己的LNMP工作环境
  • express.js的介绍及使用
  • Linux后台研发超实用命令总结
  • Mac 鼠须管 Rime 输入法 安装五笔输入法 教程
  • MySQL数据库运维之数据恢复
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • Spring Cloud Feign的两种使用姿势
  • TCP拥塞控制
  • 从零开始学习部署
  • 诡异!React stopPropagation失灵
  • 互联网大裁员:Java程序员失工作,焉知不能进ali?
  • 排序算法学习笔记
  • 判断客户端类型,Android,iOS,PC
  • 前端路由实现-history
  • 如何用vue打造一个移动端音乐播放器
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 使用iElevator.js模拟segmentfault的文章标题导航
  • 双管齐下,VMware的容器新战略
  • 我的zsh配置, 2019最新方案
  • 小程序开发中的那些坑
  • 一个项目push到多个远程Git仓库
  • 原创:新手布局福音!微信小程序使用flex的一些基础样式属性(一)
  • 在weex里面使用chart图表
  • # Pytorch 中可以直接调用的Loss Functions总结:
  • # 数仓建模:如何构建主题宽表模型?
  • (173)FPGA约束:单周期时序分析或默认时序分析
  • (C#)获取字符编码的类
  • (Java数据结构)ArrayList
  • (rabbitmq的高级特性)消息可靠性
  • (ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_READ_ONLY)讲解
  • (ZT)北大教授朱青生给学生的一封信:大学,更是一个科学的保证
  • (动手学习深度学习)第13章 计算机视觉---微调
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (附源码)基于SpringBoot和Vue的厨到家服务平台的设计与实现 毕业设计 063133