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。

cs 复制代码
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的;

cs 复制代码
ISheet IWorkbook.GetSheet(string name);
ISheet  IWorkbook.GetSheetAt(int index);

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

cs 复制代码
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的

cs 复制代码
rowCount=sheet.LastRowNum + 1

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

cs 复制代码
/// <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

cs 复制代码
IRow ISheet.GetRow(int rownum)
2.4 获取ICell

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

cs 复制代码
ICell IRow.GetCell(int cellnum)

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

cs 复制代码
/// <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)的左上角索引单元格的值

cs 复制代码
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的数据类型去处理,粗略参考:

cs 复制代码
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

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

cs 复制代码
 IWorkbook workBook = new XSSFWorkbook();
3.2 创建ISheet

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

cs 复制代码
/// <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的方法

cs 复制代码
 var useRow = sheet.GetRow(rowIndex);
 if (useRow == null)
 {
     useRow = sheet.CreateRow(rowIndex);
 }
3.4创建ICell

通用创建Cell的方法

cs 复制代码
var useCell = useRow.GetCell(columnIndex);
 if (useCell == null)
 {
     useCell = useRow.CreateCell(columnIndex);
 }

给ICell赋值的通用方式

cs 复制代码
/// <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创建,这个应该是为了方便管理和复用

cs 复制代码
var headCellStyle = workBook.CreateCellStyle();
  var f = workBook.CreateFont();
  f.IsBold = true;
  f.FontName = "等线";
  headCellStyle.SetFont(f);

三、注意事项

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

两个可能注意的点

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

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

cs 复制代码
/// <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的公式,然后随即取值可能不生效,需要手动触发计算
cs 复制代码
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形式)这个可能在编辑公式时用的到
cs 复制代码
/// <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是一个选择,当然还有其它选择,这里只是把这类问题抛砖引玉。

相关推荐
friklogff1 小时前
【无标题】云端之C#:全面解析6种云服务提供商的SDK
开发语言·flask·c#
elina80132 小时前
安卓实现导入Excel文件
android·excel
Eiceblue2 小时前
Python 复制Excel 中的行、列、单元格
开发语言·python·excel
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel
c#上位机2 小时前
C#事件的用法
java·javascript·c#
chnyi6_ya2 小时前
一些写leetcode的笔记
笔记·leetcode·c#
IT规划师3 小时前
C#|.net core 基础 - 扩展数组添加删除性能最好的方法
c#·.netcore·数组
时光追逐者3 小时前
分享6个.NET开源的AI和LLM相关项目框架
人工智能·microsoft·ai·c#·.net·.netcore
friklogff4 小时前
【C#生态园】提升C#开发效率:深入了解自然语言处理库与工具
开发语言·c#·区块链
__water12 小时前
『功能项目』回调函数处理死亡【54】
c#·回调函数·unity引擎