简介:C# DocX 是一套用于操作 Word 文档的 .NET 组件资源,面向需要在服务端或客户端程序中自动生成、修改 Word 内容,并完成 PDF 转换的开发者。使用它不需要安装微软办公软件,可通过编程方式控制段落、表格、图片、样式、页眉页脚等文档元素,特别适合报表生成、合同处理、在线文档编辑与数据批量导出等应用场景。整个压缩包约 4.93MB,共 244 个文件,包含 7 个核心动态库、83 个源码文件、6 个工程文件以及 86 个 Word 示例文档,还配有图片、配置和说明文档,目录结构清晰,便于按模块阅读与二次开发。随包演示程序覆盖了新建文档、内容替换、表格调整、PDF 转换等典型用法,源码注释详细,能够帮助初中级 .NET 开发者快速掌握接口调用方式;其中动态库可直接引用使用,免去额外配置 Office COM 组件的麻烦,也可作为小型自动化办公项目的基础参考。目前已有 277 人学习下载,适合希望快速上手 Word 文档编程处理的开发者。
1. DocX 源码 + Demo:C# 编辑 Word 和转 PDF 的最省心方案
很多老项目的“导出报表”需求,第一反应都是挂 Microsoft.Office.Interop.Word,但换一台没装 Office 的服务器就当场翻车。我拆的这个包里带的是 DocX(Xceed.Document.NET 与 Xceed.Words.NET 两枚 DLL),它走 OpenXML 路线,不依赖 Office,能在内存里直接建文档、插表格、配样式,再搭配 LibreOffice 或本机 Word 服务把 PDF 一并产出。源码 + demo 都齐,适合做进销存报表、合同批量生成、OA 公文导出的 C# 开发者,拿来就能跑,坑我已经替你们踩过一轮了。
2. 拆开源码包:两枚 DLL 的职责、工程结构与引用方式
解压资源包之后,第一眼很容易被一堆源码文件搞懵。别着急打开 sln,先按文件类别把家当摸清,再决定用哪种姿势引用。
2.1 源码目录长什么样:三块内容各管一摊
大多数 DocX 源码包解开后是这种结构:根目录放解决方案文件,下面有 DocX 源码工程、Demo 工程、bin 目录和文档。第一次拿到手,先看三块地方:
- bin 目录:放着两个编译好的 DLL,Xceed.Document.NET.dll 和 Xceed.Words.NET.dll。不想深究源码的直接引这两个就够了。
- 源码工程:DocX 类库的全部源代码,适合想跟踪内部实现、想改默认行为的开发者。
- Demo 工程:一个可直接运行的控制台或 WinForm 项目,里面写了从建文档到保存的全流程,用它验证环境最快。
文件清单可以用这张表概括:
| 文件/目录 | 是什么 | 使用时怎么处置 |
|---|---|---|
| Xceed.Document.NET.dll | 底层 OpenXML 文档包结构与序列化 | 必须引用 |
| Xceed.Words.NET.dll | 对外主 API,DocX/Paragraph/Table 等类型 | 必须引用 |
| DocX 源码工程 | 库的源代码 | 需要调试库内部时加入解决方案 |
| Demo 工程 | 可直接运行的示例 | 第一次跑建议直接运行它 |
| 说明文档 | 使用说明与版本记录 | 部署前先看版本约束 |
注意:包里的两个 DLL 是编译好的二进制,版本已经固定。写代码时以包内版本为准,别用 NuGet 最新版硬套,后面第 5 章会讲为什么。
2.2 两枚 DLL 的分工:谁在管磁盘,谁在管对象
这个库早期开源版叫 DocX,后来代码被整合进 Xceed.Words.NET 这条线,程序集拆成两个。两者的边界很清晰:
- Xceed.Document.NET.dll 管的是“文档在磁盘上的长相”:OpenXML 包结构、WordprocessingDocument、序列化和解压缩,相当于把内存对象翻译成 .docx 文件的翻译器。
- Xceed.Words.NET.dll 管的是“你在代码里摸到的对象”:DocX、Paragraph、Table、Image、Footer,相当于模具。
写代码时大多数情况会这样开头:
using Xceed.Words.NET; using Xceed.Document.NET;两个 using 都写上不是手误,因为部分类型与枚举分散在两个命名空间里。只写一个也能编过一多半,但一用到 TableDesign、Alignment 这些枚举,另一个找不到又会报错。源码 Demo 里的写法就是两个一起 using,照抄最省事。
这里有个常见误用:有人嫌麻烦只引 Xceed.Words.NET.dll,结果 InsertParagraph 能过,InsertTable 就报类型不存在。原因是 Table 类型的完整定义牵扯到 Xceed.Document.NET 程序集,单引一个 DLL 无法解析全部依赖。所以不管是直接引 DLL 还是走 NuGet,两个程序集都必须能在输出目录找到。
提示:如果编译出现“无法解析类型 Xceed.Document.NET.Table”这类错误,第一时间看 bin 目录下两个 DLL 是否都在,优先排查引用丢失,而不是去改代码。
2.3 把 DLL 放进项目的三种接法
接法一:直接引用源码包里的 DLL。在 VS 里右键项目 → 添加 → 引用 → 浏览,选中 bin 下两个 DLL。随后马上检查引用属性里的“复制本地”:
<ItemGroup> <Reference Include="Xceed.Document.NET"> <HintPath>..\lib\Xceed.Document.NET.dll</HintPath> <Private>true</Private> </Reference> <Reference Include="Xceed.Words.NET"> <HintPath>..\lib\Xceed.Words.NET.dll</HintPath> <Private>true</Private> </Reference> </ItemGroup>这段是 csproj 里的引用配置。HintPath 是 DLL 的路径,Private=true 表示“复制本地”,部署时 DLL 会跟着 exe 走。如果不设 Private,运行时大概率抛 FileNotFoundException,那种“编译过了但一跑就崩”的怪问题多半来自这里。
接法二:用 NuGet 安装。包管理器里搜 Xceed.Words.NET 直接装:
Install-Package Xceed.Words.NET包管理器会自动把 Xceed.Document.NET 依赖一起带上。但注意:NuGet 拉下来的是新版,API 和源码包里的老版本可能有出入,尤其是枚举名和方法重载。想稳定复现这个资源包的行为,优先用包内自带的 DLL。
接法三:把源码工程加入当前解决方案。右键解决方案 → 添加 → 现有项目,选 DocX 工程,再把项目引用从“程序集引用”改成“工程引用”。好处是调试时能 F11 跳进库内部,看它到底怎么解析段落;代价是库源码也要参与编译,目标框架必须对齐,否则编译报一堆杂七杂八的类型错误。
2.4 一张空文档验证环境:三行代码决生死
引用配好后,我习惯先建一个最小控制台项目验证环境通不通:
using System; using Xceed.Words.NET; class Program { static void Main(string[] args) { using (DocX doc = DocX.Create("env_check.docx")) { doc.InsertParagraph("环境正常"); doc.Save(); } Console.WriteLine("生成完成"); } }逻辑说明:DocX.Create 在内存里新建空白文档,不落盘;InsertParagraph 插入一段文本;Save 才写入工作目录下的 env_check.docx。using 保证对象及时释放,这个库持有文件句柄,忘了释放,后面删文件会报“正由另一进程使用”。
参数说明:Create 的参数是输出文件路径,相对路径会落到程序运行目录,WinForm 调试时通常在 bin\Debug 下。如果这段能编译能运行,说明两个 DLL 引用和 .NET 框架版本都没问题。
2.5 源码包与 NuGet 版本不一致时的处理原则
这个包里面有现成的 DLL,而 NuGet 上又有新版本,两者 API 不完全一致,这是个经常被问到的点。我的建议是:
- 如果只是做报表导出、合同生成这些常规操作,用包里带的老版本 DLL 完全够,没必要追新。
- 如果要用新版才有的功能,先把 Demo 工程切到新包跑一遍回归,确认原来能用的大功能没变化再升级。
- 记录一下 DLL 的文件版本和大小,出问题排查时可以对照,避免“我明明改的是这套代码,跑起来怎么行为不一样”这种黑匣子问题。
把版本差异当成固定变量处理,能省掉很多排查时间。
3. 跑通 Demo:从空文档到一份带样式、表格、图片的完整 Word
环境通了之后,把最小示例扩展成能交付的报表骨架。这一章按“段落 → 表格 → 图片 → 页脚分页”的顺序推进,每一步都可直接抄。
3.1 最小创建流程:Create → Insert → Save
把第 2 章的 env_check 扩展成正式点的报表开头:
using System; using System.Drawing; using Xceed.Words.NET; class Program { static void Main(string[] args) { using (DocX doc = DocX.Create("report.docx")) { Paragraph title = doc.InsertParagraph("月度销售报表", false) .FontSize(20).Bold().Color(Color.DarkBlue); title.Alignment = Alignment.center; doc.InsertParagraph("统计时间:2024-05-01 至 2024-05-31") .FontSize(11) .SpacingAfter(8); doc.Save(); } Console.WriteLine("报表骨架已生成"); } }逻辑说明:InsertParagraph(string text, bool trackChanges) 的第二个参数是“是否开启修订跟踪”,业务生成一般传 false。FontSize、Bold、Color 都返回 Paragraph 本身,所以能一路链式调用。Alignment 是枚举属性,不能放进链式调用里,只能单独赋值。
参数说明:FontSize 单位是磅(pt),正文常用 10.5(五号)或 12(小四),标题可以到 20 左右。SpacingAfter 是段后间距,单位也是磅,控制段与段之间的留白。Color 用的是 System.Drawing.Color,可以写 Color.DarkBlue 这种预设色,也能用 Color.FromArgb(51, 102, 204) 自定义。
这个骨架能适配任意业务:把标题替换成“合同编号:xxx”,就是合同导出的雏形。
3.2 表格:从二维数据生成带边框的表格
报表离不开表格,DocX 的 Table 用法比较直接:
using Xceed.Document.NET; // 生成 4 行 4 列 Table table = doc.AddTable(4, 4); table.Design = TableDesign.TableGrid; table.AutoFit = AutoFit.Window; string[,] data = new string[,] { { "商品", "数量", "单价", "金额" }, { "键盘", "20", "299", "5980" }, { "鼠标", "35", "89", "3115" }, { "显示器", "8", "1299", "10392" } }; for (int r = 0; r < table.Rows.Count; r++) { for (int c = 0; c < table.Columns.Count; c++) { table.Rows[r].Cells[c].Paragraphs[0].InsertText(data[r, c]); } } doc.InsertTable(table);逻辑说明:AddTable 先建空表壳,Design 决定整套边框和底纹风格,AutoFit.Window 让表格宽度铺满页面。两层循环把数据填进每个单元格里的 Paragraph,最后 InsertTable 把表格挂到文档正文尾部。
参数说明:TableDesign 有很多预设,TableDesign.TableGrid 是最接近普通报表的“田字格”,细黑边框;LightListAccent1 是带浅色底纹的列表风格。AutoFit 有两个常用值:Window 是按页面宽度铺满,Contents 是按内容收缩。建议固定写 TableGrid + Window,最稳。
提示:很多新手漏写 Design,默认样式下表格线可能很淡甚至完全看不到。这里先记住“建表必设 Design”,第 5 章会单独展开这个坑。
3.3 插入图片:从文件到内存,再挂到段落
带 logo、带签章的公文导出,需要往文档里插图片。DocX 的操作分两步:
Image logo = doc.AddImage("logo.png"); Picture pic = logo.CreatePicture(); pic.Width = 120; // 像素 pic.Height = 40; // 像素 Paragraph logoPara = doc.InsertParagraph(string.Empty); logoPara.Alignment = Alignment.center; logoPara.InsertPicture(pic);逻辑说明:AddImage 把图片文件读进内存并建立映射,不立即显示;CreatePicture 创建图片对象,Width/Height 控制显示尺寸;InsertPicture 把图片挂到某个段落里。图片本身要占一行,所以先建一个空段落,顺手把对齐方式设成居中。
参数说明:Width/Height 的单位是像素,原图很大时不设尺寸就会按原始像素显示,直接超出页面宽度。建议固定宽高同时保持宽高比,不然会变形。AddImage 的参数还能接字节数组或 MemoryStream,从数据库读图片时可以先转成 MemoryStream 再传入,不需要落盘临时文件。
3.4 页脚与分页:让文档更像正式交付物
// 在页脚插入固定文本 Footer footer = doc.Footers.Odd; footer.InsertParagraph("内部资料,注意保密").Alignment = Alignment.center; // 在正文末尾分页,续写第二页 doc.InsertParagraph("本页为签署页").InsertPageBreakAfterSelf();逻辑说明:Footers.Odd 是“奇数页页脚”,整篇默认用同一个页脚时直接操作它。InsertPageBreakAfterSelf 会在当前段落后面插入分页符,适合把签署栏单独放到新的一页。
参数说明:如果文档设置了“首页不同”,首页页脚要单独写 doc.Footers.first。没有特殊需求时,用 Odd 就能把整篇统一搞定。这里能跑通,说明你已经具备生成一份完整 Word 交付物的能力了。
4. 把 Word 转成 PDF:两条路线、参数与并发边界
DocX 本身的重心是 docx 的读写,PDF 渲染不是它的核心职责。所以转 PDF 的常见做法是二选一:本机装了 Office 就走 Word COM 精确还原;服务器不想装 Office,就用 LibreOffice 的 headless 模式批量转换。
4.1 路线一:本机装了 Word,用 COM 转出“像素级”PDF
这个方式适合内网、桌面端、Office 环境本来就存在的场景。先生成 docx,再用互操作打开另存为 PDF:
using System; using System.Runtime.InteropServices; using Word = Microsoft.Office.Interop.Word; public static void ConvertWithWord(string docxPath, string pdfPath) { Word.Application app = new Word.Application { Visible = false }; Word.Document doc = null; try { doc = app.Documents.Open(docxPath); doc.ExportAsFixedFormat(pdfPath, Word.WdExportFormat.wdExportFormatPDF); } finally { if (doc != null) doc.Close(false); app.Quit(); Marshal.ReleaseComObject(doc); Marshal.ReleaseComObject(app); } }逻辑说明:先启动 Word 应用,Open 打开文件,ExportAsFixedFormat 是 Word“另存为 PDF”的底层接口。finally 里 Close 文档、Quit 应用、再用 Marshal.ReleaseComObject 把 COM 引用计数降下去,三步缺一步都会让 WINWORD.EXE 进程留在后台。
参数说明:wdExportFormatPDF 指定 PDF 导出格式;Close(false) 表示不保存对 docx 的改动,保持源文件干净。app.Visible = false 让转换在后台跑,不弹窗口。如果转换过程中抛“拒绝访问”,去看一下 Office 是否允许服务账户以交互方式运行。
这条路排版还原度最高,Word 渲染 PDF 天然兼容 Word 自身的字体和分页规则。代价是两个硬约束:服务器必须装 Office,授权方式得允许服务调用;并发转换要控制数量,几十个线程同时启动 Word 进程,内存会先撑不住。
4.2 路线二:不装 Office,用 LibreOffice headless 批量转
Linux 服务器、Docker 容器、不想碰 Office 授权时,改用 LibreOffice 更稳。它打开 docx 后另存为 PDF,渲染引擎独立,不占 Office 许可。
先确认 soffice 可执行文件的位置,Windows 典型路径是:
C:\Program Files\LibreOffice\program\soffice.exe然后写一个 C# 包装方法:
using System.Diagnostics; public static int ConvertWithLibreOffice(string sofficePath, string inputDir, string outputDir) { ProcessStartInfo psi = new ProcessStartInfo { FileName = sofficePath, Arguments = $"--headless --convert-to pdf --outdir \"{outputDir}\" \"{inputDir}\"", UseShellExecute = false, CreateNoWindow = true }; using (Process p = Process.Start(psi)) { // 等待最多 60 秒 if (p.WaitForExit(60000)) { return p.ExitCode; } p.Kill(); return -1; } }逻辑说明:headless 让 LibreOffice 不带界面运行;--convert-to pdf 指定目标格式;--outdir 是输出目录,最后一个参数是待转换文件或目录。Process 对象等待退出,超时则 Kill 并返回 -1,调用方要把这个返回值当失败处理。
参数说明:--outdir 必须提前创建好,否则 LibreOffice 会静默失败返回非 0;如果传的是目录,LibreOffice 会把目录下所有可转换文件都转一遍,业务上要按单文件处理时尽量传具体文件路径。首次运行会初始化用户配置目录,可能多花几秒,之后会快很多。
这条路要注意中文字体:转换结果依赖系统安装的中文字体,容器里如果没装,PDF 里中文全是方块,具体排查在第 5 章 5.3。
4.3 验证转换结果:文件大小、页数、内容
转换完别急着上线,我一般做三步验证。第一步,确认 PDF 存在且大小不是 0;第二步,读页数看是否匹配;第三步,把 PDF 再转成文本搜关键词。
using System; using System.IO; static void ValidatePdf(string pdfPath) { FileInfo fi = new FileInfo(pdfPath); if (!fi.Exists) { throw new Exception("PDF 未生成"); } if (fi.Length < 1024) { throw new Exception("PDF 大小异常,可能为空文件"); } Console.WriteLine($"PDF 存在,大小 {fi.Length} 字节"); }逻辑说明:这是一个最小校验函数,文件存在和大小非零只能证明转换过程没失败,不能证明内容正确。要验证内容,可以把 PDF 再转成 txt,在纯文本里搜“报表编号”“合计金额”这类必然出现的关键词,搜到才算成功。
参数说明:1024 字节这个阈值需要根据实际模板调整。纯文字模板可能很小,带 logo 的可能几百 KB,建议先用最小模板实测一次再定阈值,别直接抄固定值。
两条转换路线对比如下:
| 方案 | 环境要求 | 排版还原度 | 并发能力 | 维护成本 |
|---|---|---|---|---|
| Word COM | 要装 Office + 授权 | 最高 | 差,需限制并发 | 高,补丁和授权要跟着管 |
| LibreOffice headless | 免授权 | 高,字体需对齐 | 好,多进程可控 | 低,独立于办公软件 |
5. 避坑与常见问题:五个翻车点与对应排查
源码和 demo 能跑通只是第一步,上生产的坑大部分在环境差异。这一章列五个最常见问题,每条都是“现象 → 原因 → 解决”的结构。
5.1 现象:两个 DLL 都引了,编译仍报找不到类型
某开发者把 DLL 拖进引用列表,代码里也写了 using,但编译时 Xceed.Document.NET 命名空间下的类型时有时无。
原因是工程目标框架过低。DocX 面向 .NET Framework 4.5 及以上,如果工程是 3.5,编译器加载程序集后识别不了部分元数据,表现就是“这个类型存在,但就是解析不出来”。
解决:把目标框架切到 .NET Framework 4.6.2 或更高。如果是 .NET Core / NET 5+ 项目,直接引用 Framework 编译的 DLL 有兼容风险,改用 NuGet 里对应新框架的包,或者单独建一个 .NET Framework 4.6.2 的辅助工程负责生成 docx,主程序通过文件或消息队列调用它。
5.2 现象:保存后 Word 打开提示“文档内容有问题”
代码没报错,docx 也生成了,但 Word 打开时提示文件损坏需要修复,打开后内容还可能是空的。
最常见的原因是 Save 之后又继续往 DocX 对象里写内容;或者把 using 放在 Save 前面,导致文档包结构没写完整就释放句柄。DocX 的对象模型很“记仇”,保存动作之后继续操作,状态就乱了。
解决:严格按“创建 → 编辑 → Save → 离开 using”的顺序写。Save 尽量放到 using 块的最后一步,保存后不要再用 InsertParagraph 或 AddTable。如果同时用 FileStream 读写同一个文件,也要先关 FileStream 再动文档,避免两把锁抢同一个文件。
5.3 现象:PDF 中文全部变成方块
用 LibreOffice 转出来的 PDF,英文数字正常,中文全是方框。
原因是转换环境缺少中文字体。LibreOffice 找不到字体就用默认字体兜底,默认字体不支持中文字形,渲染结果就是方框。这既不是 DocX 的问题,也不是代码能直接解决的。
解决:给服务器或容器安装中文字体,常见做法是装 fonts-noto-cjk,或者直接装系统自带的宋体、微软雅黑。装完字体后注意清理字体缓存,不然新字体不生效。容器场景里这个最玄学,建议在镜像构建时就装好字体并重建缓存,别等到运行时再补。
5.4 现象:表格没有边框线,打印出来一片空白格
表里有数据,导出后表格线看不到,打印预览更明显,整张表像隐形了一样。
原因是 AddTable 默认的表格样式本身不包含明显边框。很多人以为表格天然带框,实际上 OpenXML 里默认样式很可能就是不画线的。
解决:建表后必须显式指定 Design。前面示例中的 TableDesign.TableGrid 就是干这个的;如果用了自定义设计,逐单元格检查边框属性。这个坑坑过不少人,我一般建表三件套固定写:AddTable → Design = TableDesign.TableGrid → AutoFit = Window。
5.5 现象:大批量生成时内存只增不减
循环生成 1000 个合同,内存一路涨到 1GB,GC 回收好像也不管事。
原因是 DocX 对象和 COM 对象没有被及时释放。尤其和 Word COM 混用时,Process 或 COM 实例的引用还挂在托管对象上,.NET 垃圾回收管不了这些非托管资源,内存自然只升不降。
解决:每轮循环必须用 using 包住 DocX;COM 转换流程记得 Marshal.ReleaseComObject。LibreOffice 路径每次转换是独立进程,好一些,但还是要用信号量把并发控制在 2 到 3 个以内。机器内存吃紧时,把大循环拆成每批 100 个,处理完强制重启一次辅助进程,内存就回到基线了。不建议在循环里频繁 GC.Collect,那属于治标不治本。
6. 进阶:模板占位符批量生成合同,把格式和数据解耦
DocX 另一个高频用法是模板填充。做法是先准备一个 docx 模板,所有要替换的位置放 {姓名}、{编号}、{金额} 这样的占位符,代码里加载模板做替换。格式调整只改模板,和数据代码互不牵连,这是大批量文档生成里维护成本最低的路子:
using Xceed.Words.NET; var records = GetRecords(); // 假设从数据库读出要生成的记录 foreach (var row in records) { string outp = $@"D:\output\{row.No}.docx"; using (DocX doc = DocX.Load("contract_template.docx")) { doc.ReplaceText("{编号}", row.No); doc.ReplaceText("{姓名}", row.Name); doc.ReplaceText("{金额}", row.Amount.ToString("N2")); doc.ReplaceText("{日期}", row.Date.ToString("yyyy-MM-dd")); doc.SaveAs(outp); } }逻辑说明:DocX.Load 打开模板文件,ReplaceText 把占位符逐段替换成真实数据,SaveAs 另存到输出目录。模板本身始终不被改动,第二次循环加载的永远是干净的模板,可以反复使用。
参数说明:ReplaceText 不区分大小写,给占位符加上大括号可以避免把正文里的普通词语误替换;SaveAs 路径的扩展名必须是 .docx。如果模板里有几十个占位符,也可以用正则批量替换,但占位符越简单越好维护。输出目录要先创建,SaveAs 不会自动建目录。
配合文本校验,批量生成后我习惯再加一道保险:用 LibreOffice 把每个结果 PDF 转成文本,搜“总金额”等关键词统计命中数,和输入记录条数对比,数目对得上说明没有一条因为占位符写错而静默漏值。
我吃过一次亏:某次批量通知导出,数据里有一个人的姓名恰好和系统占位符长得很像,结果被正则替换错位,文档内容两处串了。那次之后我定了规矩:占位符统一带大括号,替换前先用正则预检模板,确保占位符一一对应齐全。从那以后,每次做文档导出都强制走一遍“模板预检 → 批量生成 → PDF 回读校验”三条流程,这个习惯救了我不少次。希望这份源码和踩坑清单帮到你。
本文还有配套的精品资源,点击获取