
简介在.NET桌面开发中Excel文件的导入导出与报表生成是高频需求EPPlus作为一款轻量级开源组件可帮助C#开发者以面向对象方式操作xlsx数据表格。这份实例源码包恰好覆盖上述应用场景内容由浅入深适合初中级程序员系统练习。压缩包共包含41个文件整体大小约为2.55MB以源代码文件和动态链接库为主体搭配解决方案、配置文件、资源文件、调试符号以及一个示例工作簿全部文件按标准项目结构组织解压后使用Visual Studio打开即可编译运行。源码完整演示了从创建包实例、添加工作表、设置单元格数值到批量装载列表数据的写入过程同时实现读取指定单元格、遍历整行数据、修改后保存的完整流程。此外还展示字体加粗、背景填充、求和公式与柱状图等高级功能并说明资源释放与异常处理要点有助理解核心接口调用顺序与常见排错方法。已有3721人学习下载既可运行观察实际效果也可作为项目模板直接复用配合清晰注释学习曲线平缓能快速融入日常开发。 做C#开发这几年凡是和数据打交道的项目基本都绕不开Excel。早年间我用NPOI后来换成了EPPlus再后来发现很多人还在用COM组件操作Excel踩了无数坑。今天就把我用EPPlus实现Excel读、写、改的完整思路和代码整理出来照着抄就行。日常开发里最常见的三类需求生成报表给客户或领导看、把数据库数据批量导出成Excel、读取客户上传的Excel表格做数据入库。再加上工业上位机场景里需要把设备采集的数据定时写入Excel或者从配置表读取参数。这些操作EPPlus全都覆盖而且不依赖Office环境服务器上不需要装Excel也能跑这一点在部署的时候特别省心。这篇文章适合谁看刚接触C#的初学者可以照着代码顺利跑通第一个Excel操作程序有经验的开发者可以重点关注性能优化和踩坑部分比如大文件写入、公式计算、数据类型判断这些细节。我会把每一步的代码、参数含义、为什么这么写都讲清楚。1. 为什么是EPPlusExcel操作库的选型解析1.1 主流方案对比EPPlus、NPOI、COM组件先看一张对比表这是我在实际项目里反复权衡后得出的结论也代表了社区里的主流共识。对比项EPPlusNPOICOM组件环境依赖无需安装Office无需安装Office必须安装Office性能快内存占用中等较快处理xls更稳慢频繁创建进程功能丰富度极强图表、透视表、样式、公式基础功能齐全功能全但调用繁琐开源许可4.x免费5.x起商业收费Apache 2.0免费需正版Office授权上手难度简单中等较复杂如果你只是简单读写NPOI和EPPlus都能干。但EPPlus在样式控制、条件格式化、图表生成、数据透视表这些高级功能上做得很细致API设计也更符合C#开发者的使用习惯链式调用写起来非常顺手。1.2 版本选择和License问题EPPlus从5.0版本开始改为商业许可公司项目使用必须购买授权。个人学习、练手可以用5.x版本有30天评估期也可以使用老版本4.5.3.3这个版本完全免费功能也足够稳定。网上大量教程基于4.x的写法比如Worksheet.Cells[A1]这种方式在5.x版本里依然兼容但要注意命名空间从OfficeOpenXml变成了OfficeOpenXml实际没变变化的是内部API。我的建议是个人学习用4.5.3.3代码跑通、理解机制最重要商业项目如果预算允许直接用最新的5.x比如5.8.xAPI面更完整遇到问题时社区资料多。提示判断一个NuGet包是否被项目正确引用最简单的方法是编译后在输出目录里找到 EPPlus.dll如果找不到说明引用没生效。2. 环境准备与Excel写入操作2.1 通过NuGet安装EPPlus创建一个.NET 6.0或.NET 8.0的控制台程序老项目用.NET Framework 4.6.1以上也可以然后在包管理器控制台执行Install-Package EPPlus -Version 5.8.19或者用新版Visual Studio的NuGet包管理器界面搜索“EPPlus”直接安装。如果你的项目用了老版本EPPlus升级到5.x后需要改一行代码// 4.x时代不需要设置LicenseContext // 5.x需要在程序入口处配置 ExcelPackage.LicenseContext LicenseContext.NonCommercial;这个设置是为了声明非商业用途如果在商业环境中被审计到漏了这行有可能收到律师函。个人项目写NonCommercial就好了。2.2 基础写入从创建Workbook到填充数据EPPlus的核心对象模型是ExcelPackage整个Excel文件 → ExcelWorkbook工作簿 → ExcelWorksheet工作表 → Cells单元格。最基础的操作是创建一个全新的Excel文件往里填数据然后保存using OfficeOpenXml; using OfficeOpenXml.Style; using System; using System.IO; class Program { static void Main(string[] args) { // 5.x必须设置4.x可以省略 ExcelPackage.LicenseContext LicenseContext.NonCommercial; // 1. 创建一个ExcelPackage实例可以传一个文件对象 string filePath D:\demo\设备数据报表.xlsx; using (var package new ExcelPackage(new FileInfo(filePath))) { // 2. 添加工作表 ExcelWorksheet sheet package.Workbook.Worksheets.Add(设备采集数据); // 3. 写入表头 sheet.Cells[A1].Value 设备编号; sheet.Cells[B1].Value 采集时间; sheet.Cells[C1].Value 温度; sheet.Cells[D1].Value 湿度; // 4. 写入数据行 sheet.Cells[A2].Value DEV-001; sheet.Cells[B2].Value DateTime.Now; sheet.Cells[C2].Value 36.5; sheet.Cells[D2].Value 58.2; // 5. 保存文件 package.Save(); Console.WriteLine($文件已保存{filePath}); } } }这里有几个细节值得展开说说第一用using包裹ExcelPackage是必须的因为Excel文件的写出操作是在Dispose()时完成的。如果你不写using也不调用package.Dispose()数据可能根本没写进文件而且会占用文件句柄。第二sheet.Cells[A1].Value可以直接接收object类型所以传入字符串、DateTime、double都不需要手动转换框架会自动处理类型映射。这在从DataTable导数据时特别方便直接遍历DataRow往里塞就行。第三你可能会注意到这个例子中FileInfo的对象并没有判断文件是否存在。EPPlus的设计是如果文件不存在会创建一个空文件如果存在会打开并加载内容。所以“写入”和“修改”其实用的是同一个入口区别在于你传的文件路径是否存在。2.3 批量写入从DataTable导出数据实际项目中数据往往来自数据库查询结果而DataTable是C#里最常见的数据载体。我封装了一个通用的导出方法/// summary /// 将DataTable导出到Excel /// /summary public static void ExportDataTableToExcel(DataTable dt, string filePath, string sheetName Sheet1) { ExcelPackage.LicenseContext LicenseContext.NonCommercial; using (var package new ExcelPackage()) { var sheet package.Workbook.Worksheets.Add(sheetName); // 写表头 for (int col 0; col dt.Columns.Count; col) { sheet.Cells[1, col 1].Value dt.Columns[col].ColumnName; } // 写数据 for (int row 0; row dt.Rows.Count; row) { for (int col 0; col dt.Columns.Count; col) { sheet.Cells[row 2, col 1].Value dt.Rows[row][col]; } } // 自动调整列宽这个功能在导出报表时很好用 sheet.Cells.AutoFitColumns(); package.SaveAs(new FileInfo(filePath)); } }注意我在这里用了Cells[row 2, col 1]这种行列索引的方式。Excel的行列从1开始我第一行写了表头所以数据行从第2行开始第1列对应A所以列号从1开始。AutoFitColumns()会根据单元格内容的长度自动调整列宽避免导出后数字变成了“###”或者文字挤在一起。不过如果数据量很大比如上万行全表自适应宽度会比较耗时可以选择对指定的列做sheet.Column(1).AutoFit();2.4 样式设置合并单元格、边框、字体、背景色导出报表肯定要一些基本样式不然一坨数据丢给客户体验太差。EPPlus的样式API设计得挺人性化// 设置表头样式 using (var range sheet.Cells[A1:D1]) { range.Style.Font.Bold true; range.Style.Font.Size 12; range.Style.Font.Color.SetColor(Color.White); range.Style.Fill.PatternType ExcelFillStyle.Solid; range.Style.Fill.BackgroundColor.SetColor(Color.FromArgb(79, 129, 189)); range.Style.HorizontalAlignment ExcelHorizontalAlignment.Center; // 添加边框 range.Style.Border.Top.Style ExcelBorderStyle.Thin; range.Style.Border.Bottom.Style ExcelBorderStyle.Thin; range.Style.Border.Left.Style ExcelBorderStyle.Thin; range.Style.Border.Right.Style ExcelBorderStyle.Thin; } // 合并单元格A1和B1合并 sheet.Cells[A1:B1].Merge true; // 设置行高和列宽 sheet.Cells[1, 1].RowHeight 20; sheet.Column(1).Width 15;这里有个常见的误区range.Style.Fill.PatternType必须设置为ExcelFillStyle.Solid纯色填充否则背景色不生效这是EPPlus的一个特性很多新手都栽在这。顺序也很重要——先设置PatternType再设置BackgroundColor否则颜色值可能被覆盖。2.5 写入公式Excel的精髓在于公式。我在上位机项目里经常需要把采集到的原始数据写入Excel然后让Excel自动计算均值和极差。// 假设A2:A100是一列温度数据 sheet.Cells[C1].Formula AVERAGE(A2:A100); sheet.Cells[C2].Formula MAX(A2:A100) - MIN(A2:A100); // 如果希望文件打开时就强制重算一次公式 package.Workbook.CalcMode ExcelCalcMode.Manual;老版本EPPlus默认不计算公式结果除非你调用sheet.Calculate()。如果你在代码里读取“带公式单元格的值”读到的可能是null。所以上面的例子中如果需要立即获取结果应该写成sheet.Calculate(); var result sheet.Cells[C1].Value; // 计算后就拿得到值了Calculate()是EPPlus的一个重要方法它可以批量计算工作簿中所有公式。实测大文件时比较耗时但比打开Excel再按F9要快得多。3. 读取Excel数据3.1 基础读取遍历单元格内容读Excel的时候通常分两种情况一种是格式很规整的表格按行遍历就行另一种是模板比较复杂需要定位单元格。先看最简单的按行读取public static void ReadExcel(string filePath) { ExcelPackage.LicenseContext LicenseContext.NonCommercial; using (var package new ExcelPackage(new FileInfo(filePath))) { var sheet package.Workbook.Worksheets[0]; // 读取第一个工作表 // 获取有数据的最大行列号 int rowCount sheet.Dimension?.Rows ?? 0; int colCount sheet.Dimension?.Columns ?? 0; Console.WriteLine($共 {rowCount} 行{colCount} 列); for (int row 1; row rowCount; row) { for (int col 1; col colCount; col) { var cellValue sheet.Cells[row, col].Value; if (cellValue ! null) { Console.Write(cellValue.ToString() \t); } } Console.WriteLine(); } } }sheet.Dimension返回一个包含实际数据范围的ExcelAddressBase对象如果整个工作表为空它的值是null。所以要加?.的空判定这是我在线上环境被空Event日志坑过之后加上的防护。读取的时候要注意sheet.Cells[row, col].Value返回的是object类型真实类型可能是string、double、DateTime或者bool。如果直接ToString()日期可能会变成一串数字比如46452.654。原因在于Excel内部把日期存储为自1899年12月30日以来的天数。遇到这种情况建议用Text属性代替Valuestring cellText sheet.Cells[row, col].Text; // 返回单元格显示文本格式已转换Text属性读取的是单元格格式化后的文本和Excel界面上看到的一致省去了类型转换的麻烦。但注意Text对未格式化单元格返回空字符串所以两种方式结合使用。3.2 进阶读取转换为DataTable和对象列表在数据导入场景里最终目的多半是把Excel数据装进DataTable再去入库。直接遍历单元格然后组装DataTablepublic static DataTable ExcelToDataTable(string filePath, int sheetIndex 0) { var dt new DataTable(); ExcelPackage.LicenseContext LicenseContext.NonCommercial; using (var package new ExcelPackage(new FileInfo(filePath))) { var sheet package.Workbook.Worksheets[sheetIndex]; if (sheet.Dimension null) return dt; int rowCount sheet.Dimension.Rows; int colCount sheet.Dimension.Columns; // 用第一行作为列名 for (int col 1; col colCount; col) { string colName sheet.Cells[1, col].Text; dt.Columns.Add(string.IsNullOrEmpty(colName) ? $Column{col} : colName); } // 从第二行开始填充数据 for (int row 2; row rowCount; row) { var newRow dt.NewRow(); for (int col 1; col colCount; col) { newRow[col - 1] sheet.Cells[row, col].Value; } dt.Rows.Add(newRow); } } return dt; }这个方法是做Excel导入数据库的桥梁。比如上位机软件里用户上传一份产品参数表后台调用这个方法拿到DataTable再拿DataTable直接SqlBulkCopy到数据库一整条链路就通了。如果要转成实体对象列表只要把DataTable做一层映射即可注意处理DBNull和EmptyString的情况建议封装一个扩展方法GetValueT(this DataRow row, string columnName)这样能省去一堆if (row[xxx] ! DBNull.Value)的判断。3.3 判断单元格类型和处理边界值Excel单元格类型判断是个细节活。最常见的坑一个数字列里某一格是空字符串读取后类型变成string而其他格子是double导致后续强转失败。实测下来没发现其他方式更靠谱还是得自己写类型判断public static T GetCellValueT(ExcelWorksheet sheet, int row, int col) { var cell sheet.Cells[row, col]; if (cell.Value null || cell.Value DBNull.Value) return default(T); Type targetType Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T); // 处理空字符串转数值的情况 if (targetType typeof(double) cell.Value is string s string.IsNullOrWhiteSpace(s)) return default(T); // 日期类型直接用Text转换避免出现double日期 if (targetType typeof(DateTime)) { string txt cell.Text; if (DateTime.TryParse(txt, out DateTime dt)) return (T)(object)dt; return default(T); } try { return (T)Convert.ChangeType(cell.Value, targetType); } catch { return default(T); } }这段代码的核心思路是“能映射就映射映射不了就返回默认值”。在实际项目里比直接抛异常要舒服得多——毕竟Excel表格永远不可控谁也不知道用户会在格子里塞什么奇怪的东西。4. 修改Excel文档更新数据与样式调整4.1 打开已有文件并修改指定单元格修改Excel使用的是同一个ExcelPackage入口传一个已经存在的文件路径然后定位单元格改值public static void UpdateExcelCell(string filePath, string cellAddress, object newValue) { ExcelPackage.LicenseContext LicenseContext.NonCommercial; using (var package new ExcelPackage(new FileInfo(filePath))) { var sheet package.Workbook.Worksheets[0]; sheet.Cells[cellAddress].Value newValue; package.Save(); // 覆盖原文件 } }这段代码看起来简单但有一个关键点package.Save()是直接覆盖原文件所以传入的cellAddress如果写错了很容易把别人辛苦维护的数据搞坏。建议在修改前先备份或者用SaveAs另存为新文件给用户多一层保护。对于批量更新比如从数据库同步一批数据到Excel中固定的行可以先用字典把要更新的行号、列号、值都收集起来再一次性写入并保存避免频繁打开关闭文件。4.2 修改公式与清除内容我遇到比较多的情况是模板表格里已经预先填充了公式但公式引用的范围是固定的需要动态改成新的。比如模板里C1公式原来是SUM(A1:A10)现在数据变成了100行需要改成SUM(A1:A100)。sheet.Cells[C1].Formula SUM(A1:A rowCount );这个操作本质就是给Formula属性重新赋值。EPPlus对公式的处理是当字符串存储你赋什么它就存什么。清除内容可以用sheet.Cells[C1:C100].Clear();这个方法可以清除值、公式和注释但保留样式。如果你连样式一起清需要用Delete或重新设置Style。4.3 删除和插入行上位机日志场景里如果数据超过预计行数我经常在写入前先删掉旧数据// 删除从第2行到第100行的数据保留表头 sheet.DeleteRow(2, 99); // 在指定位置插入一行在第5行上方插入一行 sheet.InsertRow(5, 1);DeleteRow(第几行开始, 删除几行)和InsertRow(从哪行开始, 插入几行)都有两个参数。注意InsertRow会复制被插入行的格式到你新插入的行如果不想复制可以用InsertRow(row, cnt, true)的第三个参数控制。这个细节我以前没注意结果日志文件里越插越乱排查了半天才发现是行格式串了。4.4 写入图片把设备和现场照片嵌入Excel还有一个高频场景生成质检报告时要把现场照片或设备照片贴进Excel。EPPlus对图片的支持很成熟string imagePath D:\photos\device_001.png; using (var img System.Drawing.Image.FromFile(imagePath)) { var picture sheet.Drawings.AddPicture(DevicePhoto, img); picture.SetPosition(2, 0, 5, 0); // 行2行偏移0列5列偏移0 picture.SetSize(300, 200); // 宽度和高度像素 }这里有两个注意点AddPicture的第一个参数要求全工作簿唯一重复会报错SetPosition的第一、三个参数是从0开始的下标0就是第1行、第1列和Cells的从1开始不同很容易混淆。5. 常见问题、性能优化与实战心得5.1 常见异常与解决方案我把自己和身边同事踩过的坑汇总成了一张表几乎囊括了EPPlus最常见的异常情况。异常信息原因解决方案The specified value is out of range行号或列号超出Excel界限最大1048576行、16384列检查行列号是否从1开始是否超过限制The package is not open / Cannot access a disposed objectExcelPackage已被释放但代码还在访问Worksheet对象检查using作用域不要把Worksheet引用暴露到using外Data error: cyclic reference / formula error公式引用自身或引用了错误单元格在代码里先用 Calculate() 测试公式确认没有循环引用File is corrupt / workbook cannot be loaded文件被占用或已经损坏确认文件没有在其他进程中被打开将读取改为 FileShare.ReadWriteCannot save a package that is disposed在package释放后又调用Save()检查代码运行顺序让Save在using内部执行最后一个“文件被占用”的问题最常见的原因是杀毒软件或Excel程序锁定了文件。在工业上位机项目里数据采集软件可能每5分钟写一次Excel同时在另一个进程里打开报表供人工查看两个进程抢一个文件是家常便饭。我的做法是升级到“生成临时文件 替换原文件”的模式而不是直接往原文件里写。5.2 性能优化大数据量写入的正确姿势当数据量超过5万行时使用sheet.Cells[row, col].Value ...逐格写入已经明显变慢10万行可能要几分钟。原因是每个单元格赋值都会触发一次内部的变更通知和结构校验。正确的做法是利用二维数组整块写入int rowCount 100000; int colCount 10; object[,] data new object[rowCount, colCount]; // 填充数据 for (int i 0; i rowCount; i) { for (int j 0; j colCount; j) { data[i, j] $row{i}col{j}; } } // 一次性写入从A2开始的区域 var startCell sheet.Cells[A2]; var endCell sheet.Cells[rowCount 1, colCount]; string rangeAddress ${startCell.Address}:{endCell.Address}; sheet.Cells[rangeAddress].LoadFromArrays(data);实测同样10万行10列的数据逐格写入耗时40秒左右用LoadFromArrays整块写入只需要3秒性能提升了10倍以上。还有一个细节写入前设置sheet.Workbook.CalcMode ExcelCalcMode.Manual避免写入过程中反复重算公式也能提升一部分性能。5.3 实战心得文件并发处理的稳健策略在C#上位机软件中数据采集线程和Excel写线程经常是并行工作的。我的建议是不要在线程里直接共享同一个ExcelPackage实例EPPlus的API不是线程安全的。稳妥的做法是每次都打开独立的ExcelPackage对象用完立即释放。如果数据量不大几千行每次采集完成就打开-追加数据-保存-释放。如果数据量大可以把待写入数据缓存到内存里每10分钟或者每累积到5000行一次性写入这样既减少了IO频率也降低了文件锁冲突概率。5.4 读取与写入时的编码和格式注意事项日期格式和区域设置问题同一Excel文件在中文系统上显示日期可能是“2024/8/15”在英文系统上可能是“8/15/2024”。在代码里读写日期时强烈建议统一使用DateTime类型然后通过NumberFormat设置显示格式sheet.Cells[B2].Value DateTime.Now; sheet.Cells[B2].Style.Numberformat.Format yyyy-MM-dd HH:mm:ss;字符串超过255个字符EPPlus本身没问题但旧版Excel打开时会警告。如果字符串超过255字符考虑用sheet.Cells[row, col].IsRichText true或者干脆把单元格设置为“自动换行”WrapText true。首列/首行冻结做报表时数据很多用户滚动就忘记表头了一行代码解决sheet.View.FreezePanes(2, 1); // 冻结第一行表头FreezePanes(2,1)的含义是在第二行、第一列的位置冻结所以第一行固定不动。这在给项目组写周报模板的时候特别实用。5.5 保存为CSV或转换格式有时候客户要求数据同时输出Excel和CSV。EPPlus可以直接把工作表内容保存为CSVsheet.SaveToCsv(new FileInfo(D:\output\data.csv));这个方法在4.x版本里有5.x版本可以通过LoadFromText配合TextFormat实现或者直接把DataTable用CsvHelper导出反正都很方便。顺带一提如果Excel文件是.xls老格式EPPlus是打不开的需要在源头上做好规避让上传模块只接受.xlsx。6. 结束前的一些经验分享做Excel操作类功能最容易翻车的往往不是语法和API而是对数据格式的假设。我在项目里遇到最多次的意外包括用户上传的Excel文件明明看着是数字读到程序里却变成了科学计数法的字符串单元格明明有值用Text读取时返回的却是空串日期列在不同Excel版本里显示的格式不一样导致排序错乱。针对这些不确定性我的建议是统一做一个“数据清洗层”在进入业务逻辑之前先把Excel里的值转成期望的规范格式比如所有数字统一为double、所有日期统一为yyyy-MM-dd HH:mm:ss、所有空字符串统一转换为 null。这一步在数据量不大时不会带来性能问题但它能把后面的逻辑代码写得异常清爽bug率直线下降。EPPlus这个库整体学习成本很低只要抓住ExcelPackage → Workbook → Worksheet → Cells这条主线基本功能一天就能上手。但真正用好还是要靠项目里反复打磨和踩坑。希望这篇实例总结能给你省下一些试错时间。本文还有配套的精品资源点击获取