C# 操作 Excel 单元格格式:从 NPOI 样式设置到模板复用
简介C#操作Excel单元格格式的参考文档面向需要在.NET程序中自动化处理Excel报表、批量生成表格的开发者。压缩包内只有一个doc格式的帮助文档文件数量少但内容集中整体大小约50KB适合快速查阅与直接参考。文档按十五个实操小节组织完整覆盖了设置单元格文本格式、获取多单元格区域、合并单元格、单元格赋值、设置字体大小与下划线、调整水平垂直对齐方式、设置列宽、填充背景色、绘制边框、自动调整列宽、文本自动换行、设置填充颜色和字体颜色等常用操作每一项均配合对应的属性或方法调用示例帮助读者理解如何用代码实现表格样式。即使是初次接触Office Interop的开发者也能按照文档逐步完成常见格式代码的编写并学会在保存时关闭系统提示以免中断流程。目前该资源已有三百余人学习使用适合具备基础C#语法、希望提升Excel二次开发效率的开发人员作为参考备忘录。1. C# 操作 Excel 单元格格式为什么每个导出方案都要补这一课同样一组数据有人导出的 Excel 打开就是一张成品报表标题有底色、加粗、边框、合适的列宽有人导出来的只有白底黑字的值打印不通看也没层次。差别不在数据恰恰在“C# 操作 Excel 单元格格式”这一层上。C# 里往 Excel 写值很简单CreateRow、CreateCell、SetCellValue 三件套即可但格式如果停在这一步交付的东西基本没法直接用。字体、填充色、边框、对齐、合并、数字格式哪一项没安排好都可能让文件在业务方手里变成返工件。这篇文章按实际做导出功能的顺序来讲库怎么选、常见格式怎么写、经常遇见的坑怎么排最后用模板文件把整套样式固化下来。正在做报表导出、单据生成或数据导入模板的顺着这条线读最合适。2. 库选型与最小环境NPOI 为什么更适合操作单元格格式2.1 几个常见的 C# Excel 操作方案怎么选不翻车C# 读写 Excel 的路子常见有四条NPOI、EPPlus、ClosedXML还有直接用 Microsoft.Office.Interop.Excel。见过某公司的报表模块为了省事在服务端装了 Office 跑 Interop结果并发一高Excel 进程互相打架导出文件时好时坏。后来全组改成纯托管方案才消停。所以在选型上第一条原则就是尽力选不依赖本机 Office 的库这样才能避免“本机正常、容器里翻车”的被动局面。方案支持格式是否依赖本机 Office格式控制粒度适合场景NPOI.xls / .xlsx否细单元格级样式可控服务端批量报表、模板填充EPPlus.xlsx 为主否细API 较现代报表、图表、公式较多的场景ClosedXML.xlsx否中等覆盖常用格式轻量导入导出、模板替换Interop.xls / .xlsx是与 Excel 完全一致客户端本机自动化流程大多数导出需求我默认选 NPOI。它能同时处理老版 .xls 与新版 .xlsx字体、边框、合并、数字格式这类单元格操作都有直接 API开源免费商用没有授权上的顾虑。EPPlus 也很好但在需要同时兼容 .xls 的老项目里不如 NPOI 方便ClosedXML 适合格式不复杂的文件真要是做大量单元格级定制还是 NPOI 更顺手。2.2 最小可运行环境从控制台生成一个带格式的 xlsx先把环境搭起来。新建一个 .NET 控制台项目然后拉取 NPOI 包。dotnet new console -n FormatDemo cd FormatDemo dotnet add package NPOIdotnet new console负责生成项目骨架dotnet add package NPOI会从 NuGet 获取类库。不熟命令的用 Visual Studio 里的“管理 NuGet 程序包”搜索 NPOI 安装也行效果一样。接着在 Program.cs 里写第一段可运行代码using NPOI.SS.UserModel; using NPOI.XSSF.UserModel; var workbook new XSSFWorkbook(); var sheet workbook.CreateSheet(格式示例); var row sheet.CreateRow(0); var cell row.CreateCell(0); cell.SetCellValue(表格标题); var font workbook.CreateFont(); font.FontHeightInPoints 14; font.Boldweight (short)FontBoldWeight.Bold; var style workbook.CreateCellStyle(); style.SetFont(font); cell.CellStyle style; using (var fs File.Create(D:\demo.xlsx)) { workbook.Write(fs); }这段代码做了一次完整的“建簿-建表-建行-建格-设样式-落盘”流程。XSSFWorkbook 代表整个工作簿sheet 代表工作表row 和 cell 通过从 0 开始的索引定位。字体对象由 workbook.CreateFont() 创建样式对象由 workbook.CreateCellStyle() 创建样式通过 SetFont(font) 与字体绑定最终再挂到 cell.CellStyle 上。相互之间的关系就是Cell 持有 CellStyleCellStyle 持有 Font。有两个地方容易在接手代码时困惑。一个是行和列索引从 0 开始CreateRow(0) 对应 Excel 的第 1 行。另一个是 FontHeightInPoints 的单位是磅写 14 就是 Excel 里的 14 号字。文件流为什么用 File.Create 而不是 File.WriteAllBytes因为 XSSFWorkbook.Write 需要往可写的流里写File.Create 正好提供这个流。这段跑通后打开生成的 demo.xlsx 会看到一个 14 号加粗的“表格标题”。这就完成了第一个有效的格式验证字体被成功写进了文件。2.3 三个工作簿类的关系别把 .xls 当 .xlsx 写NPOI 里工作簿类不止一个创建时选错很容易导致文件打不开。常用三个HSSFWorkbook对应 .xls 老格式单表最多 65536 行XSSFWorkbook对应 .xlsx单表上限 1048576 行SXSSFWorkbookXSSF 的流式版本适合一次性写入超大文件数据会走临时磁盘缓冲。新项目我基本只推 XSSFWorkbook。客户有历史 .xls 文件需要原地修改时才用 HSSFWorkbook。SXSSF 在大数据场景有优势但它把部分数据临时放到磁盘上而且样式与内存的使用方式不同第一次做格式化导出就上 SXSSF 容易踩到“样式没写完整”的雷。两个命名空间建议先记牢NPOI.SS.UserModel 提供 ICell、IFont、ICellStyle、CellRangeAddress 这些接口NPOI.XSSF.UserModel 提供 XSSFWorkbook 以及 xlsx 专用的丰富实现。写代码时多半是这两个 using 一起用接口负责统一抽象实现类负责细节逻辑。3. 单元格格式的核心落地字体、填充、边框、对齐、合并、数字格式3.1 表头样式字体加粗、底色填充颜色参数怎么给报表里最常用的是表头样式加粗、底色、居中。把颜色和字体都放在同一个 CellStyle 上然后让整个表头行复用。这里直接给一套能抄的表头写法var headerStyle workbook.CreateCellStyle(); var headerFont workbook.CreateFont(); headerFont.FontName 微软雅黑; headerFont.FontHeightInPoints 11; headerFont.Boldweight (short)FontBoldWeight.Bold; headerStyle.SetFont(headerFont); headerStyle.FillForegroundColor IndexedColors.LightBlue.Index; headerStyle.FillPattern FillPattern.SolidForeground; headerStyle.BorderTop BorderStyle.Thin; headerStyle.BorderBottom BorderStyle.Thin; headerStyle.BorderLeft BorderStyle.Thin; headerStyle.BorderRight BorderStyle.Thin; headerStyle.Alignment HorizontalAlignment.Center; headerStyle.VerticalAlignment VerticalAlignment.Center;逻辑说明很直接先建字体再把字体交给样式样式负责填充色和边框及对齐。这里有一个老手都踩过的坑FillPattern 必须写在 FillForegroundColor 后面并赋值否则颜色会不生效。NPOI 的填充逻辑是先指定颜色再指定填充方式只有 FillPattern 为 SolidForeground 时前景色才会真正显示成实心底色。属性细节上FontName 填服务器上真实存在的字体名。如果部署环境是 Linux 容器微软雅黑可能不存在Excel 会自己找替代字体如果在意最终效果可以服务端装字体或者直接选宋体这种兼容性好的字体。FillForegroundColor 用的是内置调色板IndexedColors 枚举里的值对应 Excel 传统 56 色之一。如果想精确到任意 RGB 色就需要用 XSSFWorkbook 专属的 XSSFColorusing NPOI.XSSF.UserModel; var rgbColor new XSSFColor(new byte[] { 0xF2, 0xF2, 0xF2 }); headerStyle.FillForegroundColor rgbColor.Indexed; headerStyle.FillPattern FillPattern.SolidForeground;new byte[] 里依次填 R、G、B范围 0 到 255。这个写法只适用于 XSSFWorkbookHSSFWorkbook 场景下建议退回 IndexedColors否则颜色可能失真。3.2 数据区边框与对齐让整张表有线可看数据区域没有边框的话打开表格就像一份纯文本清单打印出来尤其明显。通常我会做一个共享的 dataStyle在填充数据行时循环复用var dataStyle workbook.CreateCellStyle(); dataStyle.BorderTop BorderStyle.Thin; dataStyle.BorderBottom BorderStyle.Thin; dataStyle.BorderLeft BorderStyle.Thin; dataStyle.BorderRight BorderStyle.Thin; dataStyle.Alignment HorizontalAlignment.Left; dataStyle.VerticalAlignment VerticalAlignment.Center; dataStyle.WrapText true; // 超长文本自动换行 for (int r 1; r 100; r) { var dataRow sheet.CreateRow(r); for (int c 0; c 6; c) { var dataCell dataRow.CreateCell(c); dataCell.CellStyle dataStyle; } }这里最需要强调的一点是循环里没有新建任何 CellStyle。同一个 dataStyle 被一百行、六百个单元格复用文件里只有一份样式定义生成快文件也小。如果改成每次循环都 workbook.CreateCellStyle()结果一样但文件会以肉眼可见的速度膨胀。带哪个参数、不带哪个参数主要看业务。BorderStyle 枚举有 Thin、Medium、Dashed、Dotted 等Thin 是最常用的细实线HorizontalAlignment 控制水平方向VerticalAlignment 控制垂直方向两者别混。WrapText 配合数据列的 SetColumnWidth 一起用才不会出现内容被截断或者表格高度参差不齐。3.3 合并单元格与行列宽让表格结构和显示一致有些表头需要跨列比如“商品说明”下面再分割为“名称、规格、备注”。合并区域在 NPOI 里用 CellRangeAddress 描述// 合并第 0 行、第 0 列到第 3 列 sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, 3));CellRangeAddress 四个参数依次是起始行、结束行、起始列、结束列全部是 0 基索引。合并后值必须写在左上角单元格也就是第 0 行第 0 列否则数据不知道跑到哪里去。列宽和行高的单位也容易看懵sheet.SetColumnWidth(0, 30 * 256); // 字符宽度的 1/256 为单位 sheet.SetColumnWidth(1, 12 * 256); row.Height 24 * 20; // 磅的 1/20 为单位SetColumnWidth 的单位是字符宽度的 1/256所以 30 * 256 表示这一列大约容纳 30 个英文字符。行高单位是“磅”的 1/2024 * 20 就是 24 磅。这段代码的意图是给首列留出足够宽度第二列收紧到常规长度行高加高让内容不拥挤。合并操作有个顺序问题建议先给区域内每个单元格指定样式再执行 AddMergedRegion。原因是边框由各单元格自身提供如果只给左上角设了边框合并后四周会有缺线。走一段通用写法var region new CellRangeAddress(0, 0, 0, 3); for (int c 0; c 3; c) { var cell sheet.GetRow(0).GetCell(c); if (cell ! null) { cell.CellStyle headerStyle; } } sheet.AddMergedRegion(region);循环里对每个目标列都赋同一套样式再合并边框才不会断。追求简洁的话把上面这段封装成方法遇到跨列标题直接调用。3.4 日期与数字的显示格式让数据单元格呈现正确外观日期写入 Excel 后显示成数字这是新手最常问的情况。原因是 Excel 内部把日期存储为序列值显示成什么样由单元格的 DataFormat 决定。用下面这段把日期格式化var dateStyle workbook.CreateCellStyle(); dateStyle.DataFormat workbook.CreateDataFormat().GetFormat(yyyy/mm/dd hh:mm); var dateCell row.CreateCell(5); dateCell.SetCellValue(DateTime.Now); dateCell.CellStyle dateStyle;GetFormat 接收的字符串是 Excel 原生格式代码yyyy 是年mm 是月或分钟dd 是日hh 是小时。这里第一次容易犯迷糊同一行里的两个 mm前面是月后面是分钟。拿格式字符串不全对时打开 Excel 看到一串数字而不是日期先从格式代码里找原因。数字格式同样靠 DataFormat 控制常用几组可以直接抄需求DataFormat 代码三位分隔的整数#,##0保留两位小数#,##0.00百分比0.0%人民币价格¥#,##0.00日期yyyy-mm-dd日期时间yyyy-mm-dd hh:mm提示不要把日期先转成字符串再 SetCellValue。字符串形式的日期虽然在 Excel里“看起来”是日期但排序、筛选、透视表都不会把它当日期处理。要导出日期列就传入 DateTime 类型再用 DataFormat 控制显示。这条规则适用于所有导出场景尤其是对账单、订单列表这类下游还要继续做数据分析的文件。4. 单元格格式的常见问题排查与避坑样式不生效的典型原因4.1 设置了填充颜色单元格依然白底现象FillForegroundColor 明明写了打开 Excel 后单元格还是白底。原因少了 FillPattern。NPOI 遵循 Excel 的填充体系颜色需要有具体的填充方式才能落到格子上。只给颜色不给图案背景色没有承载。解决把 FillPattern 补上headerStyle.FillForegroundColor IndexedColors.LightBlue.Index; headerStyle.FillPattern FillPattern.SolidForeground;这个坑经常和字体问题一起出现。字体设置了却没有加粗通常是没把 font 交给 style字体和样式是两套对象只有 style.SetFont(font) 之后才算绑定。排查时顺着这条链看create font → create cellstyle → setFont(font) → cell.CellStyle style四步少一步都白写。4.2 合并单元格后边框缺失四周断线现象合并跨列表头后只看到左上角有框线右边和下边缺边框。原因边框是每个单元格自己的属性合并之后 Excel 只把左上角的样式作为主要显示但边框渲染依赖周围单元格的样式。先合并后赋值只有左上角有边框其余位置自然缺线。解决反着来先让区域内每个单元格都拥有同一套样式再执行 AddMergedRegionvar region new CellRangeAddress(0, 0, 0, 3); for (int c 0; c 3; c) { var cell sheet.GetRow(0).GetCell(c) ?? sheet.GetRow(0).CreateCell(c); cell.CellStyle headerStyle; } sheet.AddMergedRegion(region);这样合并后的视觉边框是完整的。注意 GetCell 可能返回 null尤其是单元格还没创建的时候所以代码里补了 CreateCell 兜底。4.3 日期或数字列显示成现象单元格里没有内容只有一排 #或者长数字变成科学计数法。原因多半是列宽不够Excel 显示不下。也有部分是 DataFormat 没设置日期被显示成底层序列数字。解决先解决列宽再解决格式。列宽设置用 SetColumnWidth比如日期列放 20 个字符就写 20 * 256。然后给单元格指定 DataFormat。如果列宽已经很大仍显示 #######检查是不是单元格里塞了意外的大数值或者格式代码写反了。这类问题在报表里最常见也最容易被误判成“数据写错”。实际上数据是好的只是显示层没对齐。4.4 大批量导出时格式操作变慢文件体积膨胀现象几千行数据带上边框、底色导出要几分钟生成文件很大。原因循环里每个单元格都新建了 CellStyle。Excel 工作簿的样式保存在全局样式列表里重复新建会堆积大量重复定义文件体积变大打开也慢。解决把样式缓存起来按特征签名复用var styleCache new Dictionarystring, ICellStyle(); string key center|thin-border|font-11; if (!styleCache.TryGetValue(key, out var cachedStyle)) { cachedStyle workbook.CreateCellStyle(); cachedStyle.Alignment HorizontalAlignment.Center; cachedStyle.VerticalAlignment VerticalAlignment.Center; cachedStyle.BorderTop BorderStyle.Thin; cachedStyle.BorderBottom BorderStyle.Thin; cachedStyle.BorderLeft BorderStyle.Thin; cachedStyle.BorderRight BorderStyle.Thin; styleCache[key] cachedStyle; } cell.CellStyle cachedStyle;key 就是样式签名同样的 key 拿同一个对象。数据量大时这种写法能把样式数量从几万个压到十几个。老版 .xls 格式 HSSFWorkbook 对样式总数有限制全新建样式的循环最终会直接把文件写坏缓存样式是必须而不是优化。5. 用模板文件复用整套单元格格式给代码一个不容易返工的落点单元格格式写到后面真正省力的做法是维护一个模板文件而不是在代码里硬编码几百行样式。业务方改颜色、调列宽、加 Logo都只改 xlsx 模板代码不用重新部署。这个收益在报表类项目里非常明显。最常见的模板填充方式是打开一个已经做好格式的 xlsx从已有单元格里取样式复用using (var templateStream File.OpenRead(D:\报表模板.xlsx)) { var templateWorkbook new XSSFWorkbook(templateStream); var sheet templateWorkbook.GetSheetAt(0); var headerRow sheet.GetRow(1); var dataStyle headerRow.GetCell(0).CellStyle; for (int r 2; r 30; r) { var dataRow sheet.GetRow(r) ?? sheet.CreateRow(r); for (int c 0; c 6; c) { var cell dataRow.GetCell(c) ?? dataRow.CreateCell(c); cell.CellStyle dataStyle; } } using (var fs File.Create(D:\填充结果.xlsx)) { templateWorkbook.Write(fs); } }模板第 1 行放样例数据它的样式就是整套数据行的标准样式。代码从第 2 行开始填充每行每列都复用同一个 dataStyle模板里的表头、列宽、边框、配色全部原样保留。后续换新模板只需要替换文件不需要重新编译。我这里有个定了很久的约定模板第 0 行是表头第 1 行是样式样本第 2 行起才是正式数据。模板制作人员改文件时要保证第 1 行始终保留不能删除。数据写入和样式复用两个职责分开填值方法只关心行列索引样式不重复创建全部从模板捞。这套做法救了不少次赶进度的导出需求。如果哪天模板打不开了排查顺序是先确认模板文件本身能用 Excel 打开再判断是 NPOI 版本不支持还是模板里存在特殊对象。多数情况下把模板换成最普通的单元格内容就能解决。前几章那些样式写法不是白学的它们是读懂模板行为的钥匙。我现在的习惯是新接一个导出需求先问同事有没有现成的 xlsx 模板没有再做一套最小模板代码里能少写一个样式就少写一个。希望帮到你。本文还有配套的精品资源点击获取