目录
一、NPOI 核心原理(通俗版)
1. 什么是 NPOI?
2. NPOI 核心对象模型(类比理解)
3. 核心逻辑:流操作
二、环境准备(初学者第一步)
三、高频用法(带完整示例,极简版)
用法 1:创建空白 Excel 并写入数据(最基础)
用法 2:读取已有 Excel 数据
用法 3:单元格类型处理(初学者避坑重点)
用法 4:设置列宽 / 行高(优化显示)
用法 5:批量写入数据(实战常用)
四、初学者避坑指南(高频错误)
五、完整工具类(初学者可直接复用)
总结
NPOI 是 .NET 平台下免费、开源的 Excel 操作库,无需安装 Microsoft Office,支持.xls(Excel 97-2003)和.xlsx(Excel 2007+)格式,是 C# 处理 Excel 最常用的工具。本文从核心原理入手,结合极简示例讲解高频用法,适配初学者理解节奏。
一、NPOI 核心原理(通俗版)
1. 什么是 NPOI?
NPOI 是 Apache POI(Java 版 Excel 操作库)的 C# 移植版本,核心能力:
- 无需 Office 环境,跨 Windows/Linux(.NET Core/.NET Framework 均支持);
- 支持 Excel 读写、样式设置、公式计算(基础);
- 区分两种格式:
HSSF对应.xls,XSSF对应.xlsx(初学者重点记这两个类前缀)。
2. NPOI 核心对象模型(类比理解)
Excel 文件的层级结构,对应 NPOI 的核心对象,初学者可类比 “书籍结构” 理解:
| Excel 结构 | NPOI 核心对象 | 类比书籍 | 作用说明 |
|---|---|---|---|
| 整个 Excel 文件 | IWorkbook(接口) | 整本书 | 代表一个 Excel 文件,是入口 |
| 工作表(Sheet) | ISheet | 书籍的章节 | 一个 Workbook 可包含多个 Sheet |
| 行 | IRow | 章节里的行 | 一个 Sheet 包含多行,索引从 0 开始 |
| 单元格 | ICell | 行里的文字格 | 一个 Row 包含多个单元格,索引从 0 开始 |
3. 核心逻辑:流操作
NPOI 操作 Excel 本质是文件流(FileStream)处理:
- 读取 Excel:从文件流加载到
IWorkbook,解析为 Sheet/Row/Cell; - 写入 Excel:将
IWorkbook内容写入文件流,生成物理文件。
二、环境准备(初学者第一步)
通过 NuGet 安装 NPOI(Visual Studio 操作):
- 右键项目 → 管理 NuGet 程序包;
- 搜索
NPOI,安装最新稳定版(建议 2.5+); - 核心命名空间(必加):
// 基础核心(所有操作都需要) using NPOI.SS.UserModel; // 处理 .xls(HSSF) using NPOI.HSSF.UserModel; // 处理 .xlsx(XSSF) using NPOI.XSSF.UserModel; // 文件流处理 using System.IO;三、高频用法(带完整示例,极简版)
用法 1:创建空白 Excel 并写入数据(最基础)
需求:创建一个测试.xlsx,写入表头 + 1 行数据(姓名、年龄、是否学生)。
// 步骤1:创建 Workbook(根据格式选 XSSF/HSSF) IWorkbook workbook = new XSSFWorkbook(); // .xlsx 用 XSSF,.xls 用 HSSF // 步骤2:创建 Sheet(指定名称) ISheet sheet = workbook.CreateSheet("学生信息"); // 步骤3:创建行(表头行,索引 0) IRow headerRow = sheet.CreateRow(0); // 步骤4:创建单元格并赋值(表头) headerRow.CreateCell(0).SetCellValue("姓名"); headerRow.CreateCell(1).SetCellValue("年龄"); headerRow.CreateCell(2).SetCellValue("是否学生"); // 步骤5:创建数据行(索引 1) IRow dataRow = sheet.CreateRow(1); dataRow.CreateCell(0).SetCellValue("张三"); dataRow.CreateCell(1).SetCellValue(20); // 数字类型 dataRow.CreateCell(2).SetCellValue(true); // 布尔类型 // 步骤6:写入文件流(核心:using 自动释放流,避免文件占用) string filePath = "测试.xlsx"; using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); // 将 Workbook 写入流 } Console.WriteLine("Excel 创建成功!");用法 2:读取已有 Excel 数据
需求:读取上面创建的测试.xlsx,输出所有单元格内容。
string filePath = "测试.xlsx"; // 步骤1:打开文件流(读模式) using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { // 步骤2:根据文件格式加载 Workbook(初学者可简化:判断后缀) IWorkbook workbook = filePath.EndsWith(".xlsx") ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs); // 步骤3:获取第一个 Sheet(索引 0) ISheet sheet = workbook.GetSheetAt(0); // 步骤4:遍历所有行(从 0 到最后一行索引) for (int i = 0; i <= sheet.LastRowNum; i++) { IRow row = sheet.GetRow(i); // 获取第 i 行 if (row == null) continue; // 跳过空行(初学者必加,避免空引用) // 步骤5:遍历当前行的所有单元格 for (int j = 0; j < row.LastCellNum; j++) { ICell cell = row.GetCell(j); // 获取第 j 列单元格 if (cell == null) { Console.Write("空值\t"); continue; } // 步骤6:根据单元格类型读取值(初学者核心:避免类型转换错) string cellValue = cell.CellType switch { CellType.String => cell.StringCellValue, // 字符串 CellType.Numeric => cell.NumericCellValue.ToString(), // 数字/日期 CellType.Boolean => cell.BooleanCellValue.ToString(), // 布尔 _ => "未知类型" // 其他类型(公式、空等) }; Console.Write(cellValue + "\t"); } Console.WriteLine(); // 换行 } } Console.WriteLine("Excel 读取完成!");用法 3:单元格类型处理(初学者避坑重点)
Excel 单元格有多种类型(字符串、数字、日期、布尔、公式),直接强转容易报错,推荐安全读取方法:
/// <summary> /// 安全读取单元格值(适配所有类型,返回字符串) /// </summary> /// <param name="cell">单元格</param> /// <param name="defaultValue">默认值(单元格为空/异常时返回)</param> private static string GetCellValue(ICell cell, string defaultValue = "") { if (cell == null) return defaultValue; return cell.CellType switch { CellType.String => cell.StringCellValue.Trim(), // 字符串去空格 CellType.Numeric => // 区分数字和日期(NPOI 中日期也属于 Numeric 类型) DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString("yyyy-MM-dd") : cell.NumericCellValue.ToString(), CellType.Boolean => cell.BooleanCellValue ? "是" : "否", // 转中文更友好 CellType.Formula => // 公式类型:读取计算结果 cell.CachedFormulaResultType == CellType.Numeric ? cell.NumericCellValue.ToString() : cell.StringCellValue, _ => defaultValue }; }使用示例:
// 读取年龄(转int) int age = int.TryParse(GetCellValue(row.GetCell(1)), out int res) ? res : 0; // 读取是否学生(转bool) bool isStudent = GetCellValue(row.GetCell(2)) == "是";用法 4:设置列宽 / 行高(优化显示)
NPOI 列宽单位是「256 × 字符宽度」,行高单位是「1/20 点」,初学者直接传数字会显示异常:
ISheet sheet = workbook.CreateSheet("学生信息"); // 设置列宽:第0列(姓名)宽度为 10 个字符 sheet.SetColumnWidth(0, 10 * 256); // 第1列(年龄)宽度为 8 个字符 sheet.SetColumnWidth(1, 8 * 256); // 设置行高:表头行高 30px(30×20) IRow headerRow = sheet.CreateRow(0); headerRow.Height = 30 * 20;用法 5:批量写入数据(实战常用)
需求:将列表数据批量写入 Excel:
// 模拟数据 List<Student> studentList = new List<Student>() { new Student { Name = "张三", Age = 20, IsStudent = true }, new Student { Name = "李四", Age = 22, IsStudent = false }, new Student { Name = "王五", Age = 19, IsStudent = true } }; // 创建Workbook和Sheet IWorkbook workbook = new XSSFWorkbook(); ISheet sheet = workbook.CreateSheet("批量数据"); // 写入表头 IRow headerRow = sheet.CreateRow(0); headerRow.CreateCell(0).SetCellValue("姓名"); headerRow.CreateCell(1).SetCellValue("年龄"); headerRow.CreateCell(2).SetCellValue("是否学生"); // 批量写入数据行 for (int i = 0; i < studentList.Count; i++) { IRow row = sheet.CreateRow(i + 1); // 数据行从索引1开始 var student = studentList[i]; row.CreateCell(0).SetCellValue(student.Name); row.CreateCell(1).SetCellValue(student.Age); row.CreateCell(2).SetCellValue(student.IsStudent ? "是" : "否"); } // 保存文件 using (FileStream fs = new FileStream("批量数据.xlsx", FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } // 定义学生类 public class Student { public string Name { get; set; } public int Age { get; set; } public bool IsStudent { get; set; } }四、初学者避坑指南(高频错误)
- Sheet 索引越界:
GetSheetAt(0)报错 → 检查 Excel 是否有 Sheet,或创建 Workbook 后是否手动创建了 Sheet(空 Workbook 无默认 Sheet); - 文件占用:报错 “进程无法访问文件” → 确保所有
FileStream用using包裹(自动释放句柄),或关闭 Excel 后再运行程序; - 类型转换错:读取数字时报 “无法将 double 转 int” → 用
GetCellValue先转字符串,再用int.TryParse; - 空引用异常:
row.GetCell(j)报错 → 先判断row != null和cell != null; - 格式不匹配:用
HSSFWorkbook处理.xlsx→ 严格区分:.xls用HSSF,.xlsx用XSSF。
五、完整工具类(初学者可直接复用)
封装极简版 Excel 读写工具类,覆盖 80% 基础场景:
public static class SimpleExcelHelper { /// <summary> /// 写入数据到Excel /// </summary> /// <param name="filePath">保存路径(.xls/.xlsx)</param> /// <param name="sheetName">工作表名称</param> /// <param name="headers">表头列表</param> /// <param name="dataList">数据列表(每行是一个字符串数组)</param> public static void WriteExcel(string filePath, string sheetName, List<string> headers, List<string[]> dataList) { // 创建Workbook IWorkbook workbook = filePath.EndsWith(".xlsx") ? new XSSFWorkbook() : new HSSFWorkbook(); ISheet sheet = workbook.CreateSheet(sheetName); // 写入表头 IRow headerRow = sheet.CreateRow(0); for (int i = 0; i < headers.Count; i++) { headerRow.CreateCell(i).SetCellValue(headers[i]); sheet.SetColumnWidth(i, 12 * 256); // 统一列宽 } // 写入数据 for (int i = 0; i < dataList.Count; i++) { IRow row = sheet.CreateRow(i + 1); string[] rowData = dataList[i]; for (int j = 0; j < rowData.Length; j++) { row.CreateCell(j).SetCellValue(rowData[j]); } } // 保存文件 using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) { workbook.Write(fs); } } /// <summary> /// 读取Excel数据 /// </summary> /// <param name="filePath">文件路径</param> /// <returns>表头+所有数据(第一行是表头)</returns> public static List<string[]> ReadExcel(string filePath) { var result = new List<string[]>(); using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read)) { IWorkbook workbook = filePath.EndsWith(".xlsx") ? new XSSFWorkbook(fs) : new HSSFWorkbook(fs); ISheet sheet = workbook.GetSheetAt(0); for (int i = 0; i <= sheet.LastRowNum; i++) { IRow row = sheet.GetRow(i); if (row == null) continue; string[] rowData = new string[row.LastCellNum]; for (int j = 0; j < row.LastCellNum; j++) { rowData[j] = GetCellValue(row.GetCell(j)); } result.Add(rowData); } } return result; } /// <summary> /// 安全读取单元格值 /// </summary> private static string GetCellValue(ICell cell) { if (cell == null) return ""; return cell.CellType switch { CellType.String => cell.StringCellValue.Trim(), CellType.Numeric => DateUtil.IsCellDateFormatted(cell) ? cell.DateCellValue.ToString("yyyy-MM-dd") : cell.NumericCellValue.ToString(), CellType.Boolean => cell.BooleanCellValue ? "是" : "否", _ => "" }; } }使用示例:
// 写入示例 var headers = new List<string> { "姓名", "年龄", "城市" }; var data = new List<string[]>() { new[] { "张三", "20", "北京" }, new[] { "李四", "22", "上海" } }; SimpleExcelHelper.WriteExcel("工具类测试.xlsx", "测试数据", headers, data); // 读取示例 var readData = SimpleExcelHelper.ReadExcel("工具类测试.xlsx"); foreach (var row in readData) { Console.WriteLine(string.Join("\t", row)); }总结
初学者掌握 NPOI 核心:记住 Workbook→Sheet→Row→Cell 的层级,用 using 管理文件流,按单元格类型读取值,就能覆盖绝大多数基础场景。先从 “创建 / 读取空白 Excel” 入手,再逐步扩展样式、批量数据等进阶用法,避免一开始陷入复杂逻辑。