C#操作Word文档:DocX库实战指南与避坑技巧 简介这份资源面向 .NET 开发者尤其是需要在无 Microsoft Office 环境下处理 Word 文档的 C# 程序员提供 DocX 开源库的源码与配套 demo。核心组件为 Xceed.Document.NET.dll 与 Xceed.Words.NET.dll可实现 Word 文档的创建、编辑、读取及转换 PDF并支持模板生成、内容替换、表格尺寸自动调整等进阶操作适用于自动化报告生成、在线文档编辑器、数据导出等场景。压缩包共 244 个文件约 4.93MB以 86 个 docx 示例文档、83 个 cs 源码文件为主另含 png、jpg 截图dll、pdb 程序集与调试符号以及 csproj、sln 工程文件和 config 配置便于直接编译运行。目前已有 276 人学习下载。示例代码注释详尽配合完整工程结构可帮助读者快速掌握 API 调用方式理解文档对象模型与转换流程减少自行摸索成本。1. 从一份 DLL 说起为什么 C# 里操作 Word 文档总绕不开 DocX如果你在 C# 项目里接过“导出 Word 报告”这类需求大概率经历过两种极端要么用 Office Interop 硬扛服务器上装 Office、进程残留、并发一高就崩要么手写 OpenXML对着几百行 XML 标签调格式改一个字体要翻三层节点。DocX 这个库就是冲着这个痛点来的——它把 Word 文档抽象成段落、表格、图片、页眉页脚这些对象用纯托管代码读写 .docx不依赖 Office 安装。标题里提到的 Xceed.Document.NET.dll 和 Xceed.Words.NET.dll 是它的两个核心程序集前者管文档对象模型后者管文档级操作。这篇笔记不讲空泛概念而是把“拿到源码和 demo 后怎么跑通、参数怎么设、哪里会翻车”一条条拆开适合正在选型文档导出方案、或者已经拿到这套 DLL 却不知道怎么下手的 C# 开发者。2. 环境准备与最小可运行工程把两个 DLL 挂进项目2.1 先搞清楚两个 DLL 的分工别一股脑全引用很多人拿到 Xceed.Document.NET.dll 和 Xceed.Words.NET.dll 之后第一反应是两个都拖进引用列表然后发现命名空间冲突或者方法找不到。实际分工是这样的Xceed.Document.NET.dll 提供的是文档内容层的类型比如 Paragraph、Table、Picture、Formatting、List你日常写段落、插表格、设字体颜色用的都是这一层。Xceed.Words.NET.dll 提供的是文档容器层的类型核心是 DocX 这个类负责创建、加载、保存文档以及页眉页脚、节、样式表这些文档级结构。常见做法是业务代码里只 using Xceed.Document.NET 和 Xceed.Words.NET 两个命名空间创建文档用 DocX.Create() 或 DocX.Load()后续所有内容操作都走返回的对象。如果你只引了 Document.NET 没引 Words.NET编译时会在 DocX 这个类型上报错反过来只引 Words.NET则 Paragraph、Table 这些类型找不到。两个必须成对出现版本也要一致混用不同版本的 DLL 是后面很多玄学问题的根源。2.2 建工程、加引用、写第一段文字下面是一个最小可运行的控制台工程搭建过程。我一般用 .NET Framework 4.6.1 以上或 .NET 6/8 的 Console 项目DocX 对这两条线都支持。新建项目后把两个 DLL 放到项目目录下的 libs 文件夹然后在 csproj 里用 Reference 或 PackageReference 引入。!-- 在 csproj 中显式引用本地 DLLHintPath 指向实际存放位置 -- ItemGroup Reference IncludeXceed.Document.NET HintPathlibs\Xceed.Document.NET.dll/HintPath /Reference Reference IncludeXceed.Words.NET HintPathlibs\Xceed.Words.NET.dll/HintPath /Reference /ItemGroup这段配置的作用是告诉编译器去哪里找这两个程序集。HintPath 必须写相对项目文件的实际路径如果 DLL 放在解决方案根目录的 packages 文件夹就改成对应的相对路径。注意不要用绝对路径否则换一台机器就编译不过。加完引用后写一段创建文档并保存的代码using Xceed.Document.NET; using Xceed.Words.NET; // 创建一个新文档false 表示不加载默认模板 using (var doc DocX.Create(report.docx)) { // 插入一段普通文字 var p doc.InsertParagraph(这是第一段由 DocX 生成的文字。); p.Font(微软雅黑).FontSize(12); // 设置字体和字号 p.Alignment Alignment.center; // 居中对齐 p.SpacingAfter(10); // 段后间距 10 磅 // 再插一段带加粗的 var p2 doc.InsertParagraph(); p2.Append(加粗部分).Bold(); p2.Append(普通部分。); doc.Save(); // 保存到磁盘 }逻辑说明DocX.Create 接收一个文件名返回 DocX 实例它实现了 IDisposable用 using 包住可以确保文件句柄及时释放。InsertParagraph 返回 Paragraph 对象链式调用 Font、FontSize、Alignment 都是在设置段落级格式。Append 是在同一段落内追加不同格式的文本片段Bold() 只作用于当前 Append 的那一段。SpacingAfter 的单位是磅不是像素设成 10 大约等于 Word 里段后 10 磅的视觉效果。参数方面FontSize 传的是磅值中文文档常用 10.5五号或 12小四。Alignment 是枚举可选 left、center、right、both两端对齐。如果你发现设置字体后中文没生效检查是否只设了 Font 没设 FontFamily某些版本需要 p.Font(new Font(微软雅黑)) 这种写法。保存路径如果包含不存在的目录Save 会抛异常需要提前 Directory.CreateDirectory。2.3 用 demo 验证读一个已有文档并改内容光会创建还不够实际项目里更多是“打开模板、替换占位符、另存”。下面这段代码演示加载已有文档、查找替换、再保存using (var doc DocX.Load(template.docx)) { // 把文档里所有 {{name}} 替换成实际值 doc.ReplaceText({{name}}, 张三); doc.ReplaceText({{date}}, DateTime.Now.ToString(yyyy-MM-dd)); // 找到第一个表格往第二行第二列写数据 if (doc.Tables.Count 0) { var table doc.Tables[0]; table.Rows[1].Cells[1].Paragraphs[0].Append(已处理); } doc.SaveAs(output.docx); // 另存不覆盖模板 }ReplaceText 是全局替换会遍历所有段落、表格、页眉页脚里的文本。注意它区分大小写且不支持正则如果需要正则替换得自己遍历 Paragraphs 用 Regex。Tables[0] 取第一个表格Rows[1] 是第二行索引从 0 开始Cells[1] 是第二列。往单元格写内容时Cells[1].Paragraphs[0] 默认已经有一个空段落直接 Append 即可如果调 InsertParagraph 会多出一个空行。这里有个容易翻车的点如果模板里的占位符被 Word 拆成了多个 Run比如 {{name}} 被拆成 “{{” “name” “}}”ReplaceText 可能匹配不到。解决办法是在模板里把占位符写在一个连续的文本块里或者用 Paragraph.Text 读出来自己判断。这个坑后面避坑章节还会展开。3. 段落、表格、图片三大件的参数怎么调3.1 段落格式行距、缩进、分页的取值逻辑段落是 Word 文档最基本的单位DocX 里 Paragraph 对象暴露了几十个格式属性但日常高频使用的就那么几个。行距用 LineSpacing单位是磅但要注意它和 Word 界面里的“1.5 倍行距”不是直接对应关系。1.5 倍行距在 Word 里约等于字号 × 1.5 × 1.2如果你要精确控制建议直接用 LineSpacing 设固定值比如 12 号字配 18 磅行距。var p doc.InsertParagraph(这是一段需要精细排版的文字。); p.LineSpacing 18; // 固定行距 18 磅 p.IndentationBefore 24; // 左缩进 24 磅约两个字符 p.IndentationFirstLine 21; // 首行缩进 21 磅约两个中文字符 p.SpacingBefore(6).SpacingAfter(6); // 段前段后各 6 磅 p.KeepTogether(); // 段中不分页 p.PageBreakBefore(); // 段前分页IndentationFirstLine 是首行缩进中文文档常用两个字符宽度12 号字对应约 21 磅。KeepTogether 防止段落被分页截断适合表格标题或代码块。PageBreakBefore 让该段从新页开始适合章节标题。这些属性都是可选的不设就用文档默认样式。3.2 表格列宽、边框、合并单元格的实操表格是报告类文档的重头戏。DocX 创建表格用 doc.AddTable(rows, cols)返回 Table 对象。列宽设置有个坑直接设 Column.Width 有时不生效因为 Word 会按内容自动调整。可靠做法是先设表格的 AutoFit 为 false再逐列设宽度。var table doc.AddTable(3, 4); // 3 行 4 列 table.Alignment Alignment.center; // 表格整体居中 table.Design TableDesign.TableGrid; // 使用内置网格样式 // 关闭自动调整否则列宽会被内容撑开 table.AutoFit AutoFit.Fixed; // 设置每列宽度单位是磅 float[] widths { 80f, 120f, 120f, 100f }; for (int i 0; i widths.Length; i) { table.SetColumnWidth(i, widths[i]); } // 合并第一行前两列 table.MergeCellsInColumn(0, 0, 1); // 参数列索引, 起始行, 结束行 // 往单元格写内容并设格式 var cell table.Rows[1].Cells[0]; cell.Paragraphs[0].Append(序号).Bold(); cell.Paragraphs[0].Alignment Alignment.center;SetColumnWidth 的第一个参数是列索引第二个是宽度磅值。MergeCellsInColumn 是按列合并如果要按行合并用 MergeCellsInRow。注意合并后单元格数量会变后续遍历 Rows[i].Cells 时索引要重新算。表格边框默认继承 Design 样式如果要自定义边框颜色和粗细需要操作 Table.Borders 属性这个在免费版里支持有限复杂边框建议用模板预置。3.3 图片尺寸、位置、环绕方式的控制插入图片用 doc.AddImage(path) 得到 Image 对象再 InsertParagraph 或 Append 到段落里。图片尺寸控制是重点不设的话会按原始像素插入可能撑破页面。var image doc.AddImage(chart.png); // 按比例缩放到宽度 400 磅高度自动 var pic image.CreatePicture(400, 300); // 或者只设宽度高度按比例算 // var pic image.CreatePicture(); // pic.Width 400; var p doc.InsertParagraph(); p.AppendPicture(pic); p.Alignment Alignment.center;CreatePicture(width, height) 两个参数都是磅值如果只传宽度高度会按原始比例自动计算。图片环绕方式在免费版里支持有限默认是嵌入型InLine如果要浮动环绕需要更高级的 API。图片路径支持相对和绝对路径但建议用绝对路径避免工作目录变化导致找不到文件。图片格式支持 png、jpg、bmp、gif透明背景的 png 能正常保留透明度。4. 避坑与排查五个真实踩过的坑4.1 坑一服务器上跑报“未找到 Office 组件”现象本地调试正常部署到服务器后抛异常提示缺少某个 Office 相关的程序集或注册表项。原因项目里混用了 Office Interop 的代码或者某个依赖间接引用了 Microsoft.Office.Interop.Word。DocX 本身不依赖 Office但如果你在同一个项目里既用了 DocX 又用了 Interop部署环境没装 Office 就会崩。解决全局搜索 Microsoft.Office.Interop把所有 Interop 引用和 using 删掉统一用 DocX 的 API。如果第三方库强依赖 Interop考虑换库或隔离到独立进程。4.2 坑二ReplaceText 替换不生效占位符明明在文档里现象模板里写了 {{title}}代码里 doc.ReplaceText({{title}}, 报告)生成的文件里占位符原样保留。原因Word 在编辑过程中会把一个占位符拆成多个 Run比如 “{{”、“title”、“}}” 三段ReplaceText 按 Run 匹配就找不到完整字符串。解决在模板里把占位符一次性输入不要中途改字体或加格式或者用代码遍历 Paragraph把 Paragraph.Text 读出来做替换后重建段落。更稳妥的做法是用 Content Controls结构化文档标签但免费版支持有限。4.3 坑三保存时文件被占用抛 IOException现象doc.Save() 或 doc.SaveAs() 时报“文件正被另一进程使用”。原因上一次打开的 DocX 实例没有 Dispose文件句柄没释放或者生成的文件正被 Word 打开着。解决所有 DocX 实例都用 using 包住确保 Dispose 被调用。生成前检查目标文件是否被占用可以用 FileStream 尝试独占打开再关闭。如果是在 Web 环境给每个请求生成唯一文件名避免并发写同一个文件。4.4 坑四中文字体设置后显示为宋体现象代码里设了 Font(微软雅黑)生成的文件在有些机器上显示为宋体。原因DocX 设置字体时只设了西文字体名中文字体需要单独设 FontFamily或者目标机器没装微软雅黑。解决用 p.Font(new Font(微软雅黑)) 这种重载同时设 EastAsia 字体。如果目标环境字体不确定用“宋体”或“黑体”这类系统必装字体。生成后可以在 Word 里全选查看实际字体名确认。4.5 坑五表格列宽设了没效果列还是被内容撑开现象调了 SetColumnWidth生成后列宽还是按内容自动调整。原因表格的 AutoFit 属性默认为 AutoWord 会忽略固定列宽。解决先设 table.AutoFit AutoFit.Fixed再设列宽。如果还不行检查是否在设列宽之后又往单元格里加了超长内容Word 可能重新计算布局。建议先设列宽再填内容或者填完内容后再设一次列宽。5. 进阶技巧用模板 数据驱动批量生成文档5.1 模板设计原则占位符、样式、节的分工批量生成文档的核心思路是“模板管样式代码管数据”。模板里把固定不变的页眉页脚、封面、章节标题样式都做好只留占位符给代码替换。占位符命名建议用 {{字段名}} 格式字段名和数据库列名或 DTO 属性名保持一致方便映射。模板里不要用复杂的域代码或宏DocX 对域的支持有限。表格模板可以预置表头和格式代码只负责往数据行填内容。如果表格行数不固定模板里放一行数据行作为样式参考代码里复制这一行的格式再插入新行。5.2 数据填充从 List 到表格的通用方法下面是一个把 List 数据填充到表格的通用方法支持动态行数和列宽public void FillTable(DocX doc, Table table, Liststring[] data, float[] colWidths) { // 先清空模板里的数据行保留表头 while (table.Rows.Count 1) { table.RemoveRow(table.Rows.Count - 1); } // 逐行插入数据 foreach (var row in data) { var newRow table.InsertRow(); // 在末尾插入新行 for (int i 0; i row.Length i newRow.Cells.Count; i) { newRow.Cells[i].Paragraphs[0].Append(row[i]); newRow.Cells[i].Paragraphs[0].FontSize(10); } } // 重新设列宽防止插入行后布局变化 table.AutoFit AutoFit.Fixed; for (int i 0; i colWidths.Length; i) { table.SetColumnWidth(i, colWidths[i]); } }逻辑说明RemoveRow 从末尾删行保留第一行表头。InsertRow 在表格末尾追加一行返回新行对象。填充完数据后重新设列宽是因为插入行可能触发 Word 的自动布局重算。参数 colWidths 要和表格列数一致否则 SetColumnWidth 会越界。5.3 批量生成时的性能与内存注意点批量生成几百份文档时不要在一个 DocX 实例里反复创建和保存。正确做法是每份文档独立创建、独立保存、独立 Dispose。如果数据量大用 Parallel.ForEach 并行生成但要注意每个线程用独立的 DocX 实例DocX 本身不是线程安全的。内存方面图片是最大的消耗项。如果每份文档都插入同一张大图考虑把图片先加载到内存再复用或者用缩略图。生成完的文档如果不再修改及时释放 DocX 实例避免句柄泄漏。我一般会在批量任务里加一个计数器每生成 50 份输出一次进度方便排查卡在哪一份。5.4 验证生成结果用代码检查关键内容生成完不能只靠肉眼打开看最好用代码做一轮自动校验。下面这段检查文档里是否包含预期关键字、表格行数是否正确using (var doc DocX.Load(output.docx)) { // 检查全文是否包含关键字 bool hasKeyword doc.Text.Contains(张三); Console.WriteLine($包含关键字: {hasKeyword}); // 检查表格行数 if (doc.Tables.Count 0) { Console.WriteLine($表格行数: {doc.Tables[0].Rows.Count}); } // 检查段落总数 Console.WriteLine($段落总数: {doc.Paragraphs.Count}); }doc.Text 返回全文纯文本适合做关键字断言。Tables[0].Rows.Count 验证数据行是否全部写入。段落总数可以用来对比模板和生成结果的差异。这套校验可以集成到单元测试里每次改模板或改填充逻辑后跑一遍比人工打开 Word 检查靠谱得多。我自己的习惯是模板改一次就跑一轮批量生成加自动校验确认没问题再交付。这套 DocX 方案在纯托管、无 Office 依赖的场景下足够稳但前提是把版本对齐、句柄释放、占位符连续性这几个点守住。希望帮到你。本文还有配套的精品资源点击获取