简介:NPOI v2.2.1 是面向 .NET 开发者的开源 Office 文档处理库二进制包,适用于 C#、VB.NET 及 WPF 应用场景,可帮助开发者在程序中创建、读取和修改 Word(.docx)与 Excel(.xlsx)文档,解决数据导出、报表生成、模板填充与邮件合并等实际需求。压缩包共 19 个文件,以 10 个 dll 程序集为核心,另含 jpg、png 图片、txt 说明文档、xml 配置及 license 许可文件,整体约 3.52MB,体积轻便,便于直接引用到项目。该资源已有 2505 人学习下载,说明其在 .NET 办公文档处理领域具有较高的参考与复用价值。借助包内程序集与说明文件,读者可快速完成库的引用与版本适配,掌握工作表、行列单元格操作、数据格式化、公式计算及段落样式处理等能力,并了解版本兼容、内存流优化与异常处理等实践要点,从而提升文档自动化开发效率。
1. 老项目里那份 NPOI v2.2.1:不装 Office 也能读写 Excel 的底牌
接手一个 .NET 老系统时,最怕看到导出按钮背后写着Microsoft.Office.Interop.Excel。服务器上没装 Office,一跑就报 COM 组件找不到;装了 Office 又怕进程残留、权限打架,导出个报表能把整台机器拖慢。NPOI v2.2.1 就是绕开这套玄学的方案:纯托管库,不依赖 Office,直接对 xls/xlsx 做读写,WPF 和 WinForm 里都能用。它适合谁?维护 .NET Framework 老项目、需要在服务端批量生成报表、又不想引入重型组件的 C# 开发者。这份包的价值不在新,而在稳——老框架、老运行时下能直接落地。
2. 拆开 NPOI v2.2.1:程序集构成与选型理由
2.1 包里到底有什么
NPOI 的命名空间划分很清晰,v2.2.1 这个版本的核心程序集大致是这几块,理解它们的分工比背 API 更重要:
| 程序集/命名空间 | 职责 | 典型用途 |
|---|---|---|
NPOI.HSSF | 读写 Excel 97-2003 的 .xls | 老系统导出、兼容旧模板 |
NPOI.XSSF | 读写 Excel 2007+ 的 .xlsx | 新报表、大数据量导出 |
NPOI.SS | 公共抽象层 | 统一操作接口、公式求值 |
NPOI.POIFS | OLE2 复合文档底层 | 处理 .xls 的存储结构 |
NPOI.Util | 工具类 | 字节流、编码处理 |
实际项目里最常打交道的入口是HSSFWorkbook和XSSFWorkbook,两者都实现了IWorkbook接口。选哪个不是看心情:.xls 单表上限 65536 行、256 列,.xlsx 理论上百万行,但内存占用也高一个量级。老系统如果模板是 .xls,硬转 .xlsx 会踩格式丢失的坑,这点后面细说。
2.2 为什么是它,而不是 Interop 或 OpenXML SDK
选型时我一般按三个维度比:部署依赖、内存模型、API 熟悉度。Interop 依赖本机 Office,服务端基本判死刑;OpenXML SDK 只认 .xlsx,且 API 偏底层,写个单元格要绕好几层;NPOI 的 API 风格接近 Java 的 POI,CreateRow、CreateCell、SetCellValue一路写下来很顺,而且 .xls/.xlsx 一套思路通吃。v2.2.1 虽然版本不算新,但对 .NET Framework 4.0/4.5 的兼容性好,很多老项目升级运行时成本高,锁在这个版本反而省事。
提示:如果项目已经上了 .NET Core/.NET 5+,优先考虑更新的 NPOI 分支;v2.2.1 主要面向 .NET Framework 场景,别硬套。
2.3 引入方式与第一个可跑的例子
把下载包里的 DLL 加进引用,或者走 NuGet 装对应版本。下面这段是控制台/WPF 后台都能直接跑的导出骨架,先跑通再谈优化:
using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System.IO; // 创建 xlsx 工作簿,xls 场景换成 HSSFWorkbook IWorkbook workbook = new XSSFWorkbook(); ISheet sheet = workbook.CreateSheet("报表"); // 表头 IRow header = sheet.CreateRow(0); header.CreateCell(0).SetCellValue("订单号"); header.CreateCell(1).SetCellValue("金额"); header.CreateCell(2).SetCellValue("日期"); // 数据行 for (int i = 1; i <= 100; i++) { IRow row = sheet.CreateRow(i); row.CreateCell(0).SetCellValue("NO" + i); row.CreateCell(1).SetCellValue(i * 12.5); // 数值型,别传字符串 row.CreateCell(2).SetCellValue(System.DateTime.Now); // 日期型,靠单元格样式显示 } // 写文件,务必用 using 包住流 using (FileStream fs = new FileStream("report.xlsx", FileMode.Create, FileAccess.Write)) { workbook.Write(fs); }逻辑说明:CreateSheet建表,CreateRow按索引建行(索引从 0 开始,跳行会留空),CreateCell建单元格后立刻SetCellValue。参数上要注意SetCellValue有重载,传 double 和传 string 生成的单元格类型不同,金额传字符串会导致 Excel 里无法求和——这是新手最常见的翻车点。日期同理,直接塞DateTime后还得配CellStyle的DataFormat,否则显示成一串数字。
3. 读写实战:从模板填充到大数据量导出
3.1 读现有文件并遍历单元格
读比写更容易踩坑,尤其是单元格类型判断。下面这段把一份已有 xlsx 读进来,按类型取值:
using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System.IO; using (FileStream fs = new FileStream("input.xlsx", FileMode.Open, FileAccess.Read)) { IWorkbook workbook = new XSSFWorkbook(fs); ISheet sheet = workbook.GetSheetAt(0); // 按索引取第一个 sheet for (int r = 0; r <= sheet.LastRowNum; r++) { IRow row = sheet.GetRow(r); if (row == null) continue; // 空行必须判空 for (int c = 0; c < row.LastCellNum; c++) { ICell cell = row.GetCell(c); if (cell == null) continue; string value; switch (cell.CellType) { case CellType.Numeric: // 日期在 NPOI 里也是 Numeric,要额外判断 value = DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString("yyyy-MM-dd") : cell.NumericCellValue.ToString(); break; case CellType.String: value = cell.StringCellValue; break; case CellType.Boolean: value = cell.BooleanCellValue.ToString(); break; case CellType.Formula: value = cell.CellFormula; // 取公式文本,求值见 3.3 break; default: value = ""; break; } } } }关键点:LastRowNum和LastCellNum都可能因格式残留而偏大,遍历时GetRow/GetCell返回 null 是常态,不判空必崩。CellType.Numeric同时覆盖数字和日期,必须用DateUtil.IsCellDateFormatted二次判断,否则日期会读成 40000 多这样的序列号。公式单元格默认拿不到计算结果,只能拿公式文本,这是 NPOI 的已知边界。
3.2 模板填充:保留样式的最省事做法
业务报表往往有固定格式,重新画样式费时费力。常见做法是读模板、定位占位符、替换值,样式自动保留:
using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; using System.IO; using (FileStream fs = new FileStream("template.xlsx", FileMode.Open, FileAccess.Read)) { IWorkbook workbook = new XSSFWorkbook(fs); ISheet sheet = workbook.GetSheetAt(0); // 遍历找占位符 {{name}} 并替换 for (int r = 0; r <= sheet.LastRowNum; r++) { IRow row = sheet.GetRow(r); if (row == null) continue; for (int c = 0; c < row.LastCellNum; c++) { ICell cell = row.GetCell(c); if (cell == null || cell.CellType != CellType.String) continue; string text = cell.StringCellValue; if (text.Contains("{{name}}")) { // 用 SetCellValue 会丢样式,改用 RichText 或直接改值后重设样式 cell.SetCellValue(text.Replace("{{name}}", "张三")); } } } using (FileStream outFs = new FileStream("output.xlsx", FileMode.Create, FileAccess.Write)) { workbook.Write(outFs); } }这里有个细节:SetCellValue对字符串单元格通常保留原样式,但如果原单元格是数值格式,替换成字符串后格式可能错乱。稳妥做法是替换前记录cell.CellStyle,替换后重新赋回去。模板方案的最大好处是合并单元格、边框、字体全不用管,代价是模板本身要维护好,占位符命名别和正文撞车。
3.3 公式求值与大数据量的内存取舍
公式单元格想拿计算结果,得用IFormulaEvaluator:
using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; IWorkbook workbook = new XSSFWorkbook(fs); IFormulaEvaluator evaluator = workbook.GetCreationHelper().CreateFormulaEvaluator(); ICell formulaCell = workbook.GetSheetAt(0).GetRow(1).GetCell(2); CellValue result = evaluator.Evaluate(formulaCell); // result.NumberValue / result.StringValue 按类型取求值器会递归计算依赖单元格,跨表引用也能处理,但遇到 NPOI 不支持的函数会抛异常,这时只能退回读缓存值或让上游先算好。大数据量导出则是另一个战场:XSSFWorkbook全量驻留内存,十万行以上容易 OOM。v2.2.1 里可用的缓解手段是SXSSFWorkbook(流式写,只保留窗口内的行在内存),代价是不能回头改已 flush 的行,公式和样式也要提前定好。我一般按数据量分档:万行以内XSSFWorkbook够用,十万行级别上SXSSFWorkbook,再大就考虑分文件导出。
4. 避坑排查:NPOI v2.2.1 的五个血泪现场
4.1 现象:导出报「未能加载文件或程序集」→ 原因:依赖 DLL 没跟齐 → 解决:核对引用链
NPOI 不是单文件库,NPOI.dll之外还依赖NPOI.OOXML.dll、NPOI.OpenXml4Net.dll、NPOI.OpenXmlFormats.dll等。只拷一个 DLL 到输出目录,运行时就报程序集加载失败。解决是把包内所有相关 DLL 一起引用,并确认「复制到本地」为 True;发布时检查 bin 目录是否齐全。
4.2 现象:日期读出来是 43210 这种数字 → 原因:Numeric 类型没做日期判断 → 解决:加 IsCellDateFormatted
前面提过,日期在底层是序列号。只按CellType.Numeric取NumericCellValue就会拿到数字。解决是用DateUtil.IsCellDateFormatted(cell)判断后再取DateCellValue,并注意时区和 1900/1904 日期系统差异,跨系统导入的模板尤其容易偏一天。
4.3 现象:大文件导出内存飙升甚至 OOM → 原因:XSSFWorkbook 全量驻留 → 解决:换 SXSSFWorkbook 并控制窗口
XSSFWorkbook把所有行对象留在内存,行数一多就顶不住。换成SXSSFWorkbook并设置new SXSSFWorkbook(100)这样的行窗口,超出窗口的行自动刷到磁盘。注意刷出去的行不能再GetRow修改,样式和列宽要在写数据前设好。
4.4 现象:中文写入后乱码或显示问号 → 原因:流编码或字体问题 → 解决:用二进制流写、显式设字体
NPOI 写文件必须用FileStream二进制写,别套StreamWriter,否则字节被二次编码。显示层面如果中文变问号,多半是单元格字体没指定中文字体,创建IFont时设FontName = "宋体"并挂到CellStyle上。
4.5 现象:读 .xls 正常、读 .xlsx 报错 → 原因:Workbook 类型用错 → 解决:按扩展名分派
HSSFWorkbook只认 .xls,XSSFWorkbook只认 .xlsx,拿错就抛OfficeXmlFileException或OldExcelFormatException。常见做法是封装一个工厂方法,按文件头或扩展名决定实例化哪个,别在业务代码里硬编码。
5. 进阶技巧:用统一封装把 xls/xlsx 的差异挡在业务之外
写到后面会发现,业务代码里到处if (isXlsx)很脏。我的习惯是抽一层轻封装:工厂负责按扩展名建 workbook,导出方法只依赖IWorkbook接口,样式和公式求值各留一个工具类。这样换版本、换格式时改动集中在一处。验证是否封装到位,有个简单标准:把一份 .xls 模板和一份 .xlsx 模板喂给同一段导出逻辑,输出都不报错、格式都对,才算过关。
再补一个实用技巧:批量导出时给文件名加时间戳和业务键,避免并发写同一文件导致「文件被占用」。WPF 里导出完记得在 UI 线程外做 IO,别把大文件写盘卡在界面线程上。还有个小习惯——每次升级或替换 NPOI 版本前,先拿一份含合并单元格、公式、日期的「刁钻模板」跑一遍回归,比看更新日志管用。从那以后我每次接老项目导出需求,都强制先跑这份模板验证,省下的排查时间远超准备它的成本。希望帮到你。
本文还有配套的精品资源,点击获取