公司动态

C#使用NPOI在Word中插入图片和表格的完整指南

📅 2026/9/2 18:55:55
C#使用NPOI在Word中插入图片和表格的完整指南
简介面向C#与.NET开发者的NPOI实战资源围绕使用NPOI库在Word文档中动态插入表格和图片展开。资源以SQLLiteToWord示例为核心演示了从SQLite数据库读取数据、组织内容并生成带格式Word报表的完整链路适合需要自动化生成文档或导出数据的开发者参考。压缩包为7z格式共218个文件约35.08MB。包内包含NPOI及System.Data.SQLite等核心DLL、C#源代码、配置文件、XML文档以及项目说明文档doc/docx/md和少量示例图片同时附带完整的解决方案与工程文件便于直接打开调试。该资源已有6000余人学习下载。通过学习这份材料可以掌握XWPFDocument创建段落与文本、XWPFTable构建表格并设置单元格样式以及XWPFPicture插入本地图片等关键写法还能结合示例了解SQLite数据读取到Word输出的应用方式为报表生成、数据导出等场景提供可复用的代码基础。 做C#开发这些年被问得最多的办公需求大概就是怎么用代码生成一份带图片和表格的Word文档。我这阵子刚好在一个上位机配套的报表功能里要用C#和NPOI生成Word文档往里插产品照片和数据表格。整个过程上手不算难但坑是真不少光是图片空白和表格宽度不生效两个问题就折腾了两个晚上。这篇文章就把实现步骤、完整代码和踩坑记录一次性说透给正在做同类需求的同学一个能直接抄作业的参考。NPOI目前是.NET生态里最常用的免费Office操作库不需要安装Microsoft Office纯托管代码能读能写xls/xlsx/docx/pptx。用它生成Word核心就是XWPFDocument这套对象模型。这篇文章适合遇到以下情况的读者项目里不想装Office、服务器环境不允许常驻Word进程、被Interop.Word的COM权限问题折磨过的朋友。1. 为什么选NPOI而不是其他方案1.1 方案对比先说选型背景。这次开发的是一个工业设备上位机软件需要导出一份包含设备照片、参数表、检测数据的验收报告Word文档。开发环境是C# WinForm部署目标是客户现场的工控机系统环境干净得很不可能保证每台都装了Office。当时摆在面前的方案大致有四条我列个对比表看得更清楚方案是否免费是否需要装Office学习成本典型问题Microsoft.Office.Interop.Word免费但需Office授权必须安装低COM权限、Windows服务调用崩溃、内存泄漏Open XML SDK免费不需要高直接操作底层XML代码量大Aspose.Words / Spire.Doc商业收费不需要低授权费用免费版有限制NPOI开源免费不需要中复杂排版能力有限Interop.Word是最早被排除的。做服务端或工控机上的文档生成Word COM组件一旦进程异常退出会一直挂在任务管理器里内存越吃越多而且Retrieving the COM class factory这类权限报错在运维时非常头疼。Open XML SDK功能完整但偏向底层标准操作实现标题图片表格这种结构代码量明显多一截学习曲线也陡。Aspose.Words确实是好东西但商业授权意味着公司要额外走采购流程个人开发更是难顶。NPOI胜在开源免费、纯托管、不依赖OfficeAPI风格接近Apache POI社区用户量大遇到问题基本都能搜到答案。虽然复杂排版能力不如商业库但标题图片表格这种高频需求完全够用。最终结论很明确就是它了。1.2 NPOI操作Word的对象模型NPOI操作docx的核心命名空间是NPOI.XWPF.UserModel它把Word文档抽象成一棵对象树XWPFDocument对应整个docx文档XWPFParagraph对应Word里的段落XWPFRun段落里的文本片段同一个段落可以包含多个Run每个Run可以独立设置字体、字号、颜色还可以嵌入图片XWPFTable / XWPFTableRow / XWPFTableCell表格、行、单元格XWPFPictureXWPFRun里嵌入的图片对象理解这套模型的捷径是把它想成段落里面跑字符字符里面藏图片。你看到的图片和文字混排本质上就是Run里附带了一个图片数据块。后面所有代码都是在往这棵对象树上挂节点。NPOI对象对应Word元素常见操作XWPFDocument文档创建段落/表格Write保存XWPFParagraph段落设置对齐、间距创建RunXWPFRun文本/图片片段设置字体、插入图片XWPFTable表格合并单元格、设置行高列宽掌握这棵树之后后续想扩展加页眉页脚、加脚注都能在同一套模型里找到对应对象。我后面做的检测项签字栏、页脚页码都是在这个基础上加的。2. 环境准备与基础搭建2.1 用NuGet安装NPOI两步到位新建项目这一步不用多说我用的是.NET Framework 4.7.2的WinForm项目实际上NPOI 2.5对.NET Core / .NET 5同样支持得很好公司里用ASP.NET Core WebApi做导出功能也完全能跑。在Package Manager Console里执行Install-Package NPOI或者直接在NuGet包管理界面搜索NPOI装最新稳定版即可。装完后引用里会出现NPOI.dll、NPOI.OpenXml4Net.dll、NPOI.OpenXmlFormats.dll这几个核心程序集。代码里一般需要引入这几个命名空间using NPOI.XWPF.UserModel; using NPOI.OpenXmlFormats.Wordprocessing; using System.IO;2.2 创建空文档并写出第一个段落创建文档非常直接using (MemoryStream ms new MemoryStream()) { XWPFDocument doc new XWPFDocument(); XWPFParagraph paragraph doc.CreateParagraph(); XWPFRun run paragraph.CreateRun(); run.SetText(这是第一行正文); run.SetFontSize(24); // 字号参数是半磅24表示12pt run.FontFamily 微软雅黑; doc.Write(ms); File.WriteAllBytes(D:\report.docx, ms.ToArray()); }这里有几个新手容易卡住的地方CreateParagraph每次调用都会在文档末尾追加一个新段落。CreateRun会在当前段落的文本流末尾追加一个RunRun和Run之间可以有独立的字体格式。SetFontSize的单位是半磅half-point想要小四12pt就传24三号16pt传32。FontFamily在NPOI里是字符串属性直接填字体名称即可。但某些版本中设置中文字体还需要同步设置底层XML的eastAsia属性否则生成的docx打开时中文可能默认成宋体或Calibri这个坑后面专门讲。到这里一个能正常打开的空Word文档已经能从代码里出来了。接下来是重头戏往文档里塞图片和表格。3. 核心功能在Word中插入图片和表格3.1 插入图片最容易被参数坑哭的环节NPOI插入图片的标准做法是在XWPFRun上调用AddPicture方法。图片是嵌在Run里的所以要先创建段落和Run再往里加图片数据。using (FileStream fs new FileStream(D:\product.jpg, FileMode.Open, FileAccess.Read)) { XWPFParagraph picPara doc.CreateParagraph(); picPara.Alignment ParagraphAlignment.CENTER; XWPFRun picRun picPara.CreateRun(); picRun.AddPicture(fs, (int)PictureType.JPEG, product.jpg, 300, 200); }AddPicture的参数分别是图片流、图片类型、图片文件名docx内部记录的占位名称、宽度像素、高度像素。这里最容易踩坑的是图片流的位置。如果前面已经读取过fs再次传入时没有把Position重置为0NPOI会读到空数据生成的文档里图片是空白的。建议每次传入前都做fs.Position 0。图片类型枚举。NPOI里有PictureType枚举常见的有JPEG、PNG、GIF、BMP。传错类型虽然有的版本不报错但打开文档时图片可能损坏。拿到文件后先根据扩展名映射别硬编码。宽高比例失衡。AddPicture的宽高是像素值不会自动按原图比例缩放。原图是竖构图你硬塞300宽度、200高度照片会被拉扁。最好先从Image里读取原始尺寸等比计算目标尺寸。我在实际项目里写了一个小工具方法读取原图尺寸后按最大宽度限制等比缩放private static (int width, int height) CalcImageSize(string imagePath, int maxWidth, int maxHeight) { using (var img Image.FromFile(imagePath)) { double ratio Math.Min((double)maxWidth / img.Width, (double)maxHeight / img.Height); if (ratio 1) return (img.Width, img.Height); return ((int)(img.Width * ratio), (int)(img.Height * ratio)); } }提示不管图片流来自文件、网络还是数据库调用AddPicture前先把Position重置为0这个习惯能避免一大半图片异常问题。3.2 创建表格并控制单元格宽度表格创建用doc.CreateTable(rowCount, colCount)先建好行列骨架再逐格填数据和设置样式。XWPFTable table doc.CreateTable(3, 4); table.Width 5000; // 整体宽度单位是DXA for (int r 0; r 3; r) { XWPFTableRow row table.GetRow(r); for (int c 0; c 4; c) { XWPFTableCell cell row.GetCell(c); cell.SetText($第{r}行第{c}列); cell.SetWidth(1200); // 对每个单元格设置宽度 } }关于宽度单位NPOI里表格宽度常用的是DXAtwips换算关系是1厘米约等于567 twipsA4纸默认页边距下的正文宽度通常在9000 twips上下。我之前试过用百分比写法某些版本里不生效后来统一用固定twips值各环境都稳定。一个很容易忽视的细节不要只设置表头行的单元格宽度。Word的表格布局里如果某一行单元格宽度不设置它会按内容自动分配导致同一列在不同行的宽度不一致。保险做法是循环所有行对每个单元格都调用SetWidth。3.3 合并单元格与垂直居中合并单元格有两种需求横向合并、纵向合并。横向合并且简单用MergeCells方法参数是Excel风格坐标table.MergeCells(A1, B1);这句会把第一行的A列和B列合并成一个单元格。值得注意的是合并操作最好在填充数据之前进行否则坐标容易算乱。合并后单元格里的文字用GetRow(0).GetCell(0)拿合并后保留的那个单元格去写就行。纵向合并稍麻烦。NPOI没有直接提供跨行合并的方法需要操作底层XML的vMerge属性private static void MergeCellsVertically(XWPFTable table, int rowIndex, int colIndex) { XWPFTableCell cell table.GetRow(rowIndex).GetCell(colIndex); CT_Tc tc cell.GetCTTc(); CT_TcPr tcPr tc.IsSetTcPr() ? tc.tcPr : tc.AddNewTcPr(); tcPr.AddNewVMerge().val ST_Merge.restart; }实际项目中如果要做更复杂的跨页重复表头、单元格底纹、边框样式我建议直接考虑Open XML SDK或模板替换方案。NPOI强行做也能做但维护成本会明显上升。单元格垂直居中也是通过底层CT_TcPr的vAlign属性控制CT_TcPr tcPr cell.GetCTTc().IsSetTcPr() ? cell.GetCTTc().tcPr : cell.GetCTTc().AddNewTcPr(); tcPr.AddNewVAlign().val ST_VerticalJc.center;这些底层操作在NPOI里并不神秘本质就是把Word背后Open XML的标签用代码写出来多参考几个案例之后就会形成肌肉记忆。4. 完整案例生成一份产品信息报表4.1 需求拆解与文档结构设计为了把上面的知识点串起来我用项目里真实的产品信息报表场景做一个完整示例。需求是这样的报表第一行是标题产品检测报告中间插入一张产品照片照片下面是产品参数表包含4列参数名称、参数值、检测标准、检测结果。整个表格有6行数据表头合并成一整行横向合并。文档结构对应到NPOI对象上可以拆成四层标题段落一个段落、一个Run、居中、加粗、大字号图片段落一个段落、一个Run、居中、图片表格一个表格对象、表头行合并单元格、6行数据行保存写入MemoryStream再落盘4.2 核心代码实现这里给出一段可运行的核心逻辑生产环境的代码我在这个基础上做了参数化和抽方法但骨架就是下面这样public void GenerateReport(string imagePath, string outputPath) { using (MemoryStream ms new MemoryStream()) { XWPFDocument doc new XWPFDocument(); // 1. 标题 XWPFParagraph titlePara doc.CreateParagraph(); titlePara.Alignment ParagraphAlignment.CENTER; XWPFRun titleRun titlePara.CreateRun(); titleRun.SetText(产品检测报告); titleRun.SetFontSize(32); // 16pt titleRun.FontFamily 微软雅黑; titleRun.IsBold true; // 2. 插入产品图片 XWPFParagraph picPara doc.CreateParagraph(); picPara.Alignment ParagraphAlignment.CENTER; XWPFRun picRun picPara.CreateRun(); var size CalcImageSize(imagePath, 320, 240); using (FileStream fs new FileStream(imagePath, FileMode.Open, FileAccess.Read)) { picRun.AddPicture(fs, (int)PictureType.JPEG, product.jpg, size.width, size.height); } // 3. 创建参数表格7行4列1行表头 6行数据 XWPFTable table doc.CreateTable(7, 4); table.Width 9000; // 表头A1到D1合并 table.MergeCells(A1, D1); XWPFTableCell headerCell table.GetRow(0).GetCell(0); headerCell.SetText(检测项参数汇总); headerCell.SetWidth(9000); // 数据行表头 string[] headers { 参数名称, 参数值, 检测标准, 检测结果 }; for (int c 0; c 4; c) { table.GetRow(1).GetCell(c).SetText(headers[c]); } // 示例数据 string[,] data { { 输出电压, 12.0V, GB/T 1234-2021, 合格 }, { 输出电流, 2.5A, GB/T 1234-2021, 合格 }, { 工作温度, -20~60℃, GB/T 5678-2022, 合格 }, { 外观尺寸, 120×80×40mm, 企标, 合格 }, { 重量, 850g, 企标, 合格 }, { 防护等级, IP65, GB/T 4208-2017, 合格 } }; for (int r 0; r data.GetLength(0); r) { XWPFTableRow row table.GetRow(r 2); for (int c 0; c data.GetLength(1); c) { XWPFTableCell cell row.GetCell(c); cell.SetText(data[r, c]); cell.SetWidth(2250); } } // 4. 保存 doc.Write(ms); File.WriteAllBytes(outputPath, ms.ToArray()); } }4.3 运行效果与细节说明跑完上面的代码用Word打开生成的docx你会得到一份有标题、有居中产品照片、有合并表头表格的文档。表格整体宽度9000 twips4列平均每列2250 twips表头横向合并成一行检测项参数汇总。这里有两个我实际调过的细节第一MergeCells之后原D1单元格对象虽然还存在但已经处于合并隐藏状态不要再往里写文本写了也看不到。表头文本要写在合并后保留的A1单元格里。第二如果图片和表格之间想加一点空行最简单的办法是在图片段落之后插入一个空段落或者给图片段落设置SpacingAfter属性比如picPara.SpacingAfter 200。如果想让表格数据看起来更整齐可以再给单元格设置对齐。水平居中通过paragraph对齐控制垂直居中通过前面说的CT_TcPr的vAlign控制。这些控制项在报表类文档里属于刚需建议封装成工具方法复用。5. 常见问题与排查技巧实录5.1 表格宽度设置后不生效这是被问得最多的问题。现象是table.Width和cell.SetWidth都已经设置但打开文档后表格还是窄窄一列或者宽度随内容乱跳。排查后确认主要原因有两个只设置了表格整体宽度没有设置每一行的单元格宽度。Word表格默认开启了自动调整autofitNPOI生成的表格如果没显式关闭自动调整即使设置了宽度也会被自动布局覆盖。关闭自动调整需要在底层XML上加tblLayout这一行代码基本能解决大多数宽度失灵CT_Tbl cttbl table.GetCTTbl(); CT_TblPr tblPr cttbl.IsSetTblPr() ? cttbl.tblPr : cttbl.AddNewTblPr(); CT_TblLayout layout tblPr.IsSetTblLayout() ? tblPr.tblLayout : tblPr.AddNewTblLayout(); layout.type ST_TblLayoutType.fixedLayout; // 固定布局设置完这一步再把每个单元格的宽度设好表格宽度才会真正听话。5.2 图片显示为空白或文件损坏图片相关的问题九成出在流上。最常见的情况是同一个FileStream先读了图片信息比如算宽高然后没重置Position就直接传给AddPicture结果NPOI读取到的数据长度为0。解决方式很简单调AddPicture之前强制fs.Position 0或者干脆用独立的FileStream去读。另外图片类型枚举要跟实际文件格式匹配PNG图片却声明为PictureType.JPEG在部分版本的NPOI里不会立即报错但生成的文档打开后图片无法显示。我习惯写一个扩展名到PictureType的映射方法private static PictureType GetPictureType(string ext) { switch (ext.ToLower()) { case .jpg: case .jpeg: return PictureType.JPEG; case .png: return PictureType.PNG; case .gif: return PictureType.GIF; case .bmp: return PictureType.BMP; default: return PictureType.JPEG; } }5.3 中文乱码与字体无效NPOI生成的docx打开发现中文显示为方块或者字体没生效通常是字体的两个属性没设置完整。Word处理中文文本时会分别看ascii字体和eastAsia字体。NPOI里run.FontFamily设置的往往是ascii部分中文部分可能没有同步设置。稳妥做法是直接操作底层rPr的rFonts节点把eastAsia也设成相同字体CT_RPr rpr run.GetCTR().IsSetRPr() ? run.GetCTR().rPr : run.GetCTR().AddNewRPr(); CT_Fonts fonts rpr.IsSetRFonts() ? rpr.rFonts : rpr.AddNewRFonts(); fonts.eastAsia 宋体;这一步如果忽略客户打开文档说字体不对排查起来非常隐蔽。代码里明明设了FontFamily文档里看架构也对但字体渲染细节就是不对。5.4 其他几个高频坑点编辑完复杂段落内容后再往里插表格偶尔会报对象引用错误多半是段落或Run引用已经失效。解决原则是先建结构后填内容把表格、段落对象一次性创建好再逐项填充数据不要边创建边保存引用。另外NPOI对Word的复杂功能支持有限比如带图片的文字环绕、页眉页脚中多形状排版真遇到这种需求不要硬磕NPOI直接换方案复杂模板用Open XML SDK处理或者用文档模板占位符替换的方式生产环境稳定性和效率都更好。做这个功能时我有一个很深的体会生成Word文档的代码一定要封装成独立的服务类输入参数用DTO输出统一返回文件路径或字节数组。这样后续不管是WinForm调用、WebApi调用还是定时任务调用都不用动核心代码。我当时把报表生成抽成了一个ReportService后面客户说要加检测标准列、要调图片宽度都只是改数据映射和传参完全没碰NPOI底层操作。NPOI处理常规图文表格的需求能力边界我很清楚但在这个边界内它是真好用。如果你也只是需要标题 图片 表格这类结构化文档照着上面这套思路改改就能交付真没必要一上来就上重量级方案。本文还有配套的精品资源点击获取