简介:这份资源是面向C# WinForm初学者与.NET桌面开发者的ItextSharp PDF导出实战示例,重点解决在WinForm应用中把文本框数据生成PDF文件的常见需求。压缩包共53个文件,约4.17MB,以cs源码、exe可执行程序、dll类库、config配置、resources资源及resx设计文件为主,另含sln解决方案与csproj工程文件,可直接在Visual Studio中打开运行调试。示例围绕Document、PdfWriter、Paragraph、Chunk等核心API展开,演示了引入ItextSharp、绑定按钮Click事件、调用ExportToPdf方法写入文本并保存PDF的完整流程,同时指出PdfImportedPage与PdfCopy误用问题并给出修正思路。已有1226人学习下载,适合想快速掌握C#导出PDF基础用法、理解ItextSharp文档对象模型与常见排错方式的读者参考实践。
1. 从一份“导出 PDF 就崩”的工单说起
做过 WinForm 桌面项目的人,大概率都接过这种需求:列表里一堆数据,用户点一下“导出”,要一份能直接打印、能发邮件、排版还不能乱的 PDF。很多人第一反应是调 Office 组件或者用系统自带的打印转 PDF,结果到了客户那台没装 Office 的机器上直接翻车。这时候 iText 这类纯托管库就成了稳妥选择——它不依赖外部软件,一个 DLL 丢进项目就能跑。
这篇笔记就围绕“C# WinForm 用 iText 实现 PDF 导出”这个最小可用场景展开。我会把选型理由、环境准备、最小可跑 Demo、中文字体这个经典坑、表格排版参数,以及导出后怎么验证,按能照着复现的顺序讲一遍。适合两类人:刚接到导出需求、想半天内跑通的新手;以及已经用过 iText、但被中文乱码和字体授权绊过的熟手。全程只讲能落地的代码和参数,不绕概念。
2. 选型与最小环境:为什么是 iText,而不是打印转 PDF
2.1 三种常见导出路线的取舍
在 WinForm 里做 PDF 导出,常见做法无非三条路。第一条是调用 Office Interop,把数据写进 Word 或 Excel 再另存为 PDF。这条路排版最省心,但要求目标机器装了对应 Office,版本还得对得上,部署时经常因为 COM 组件注册失败而报错。第二条是走系统打印驱动,把画面“打印”成 PDF 文件,本质是模拟打印,分页和边距很难精确控制,批量导出时性能也一般。
第三条就是引入 iText 这类 PDF 库,直接在代码里构建文档对象。它的优势是纯托管、无外部依赖、分页和坐标完全可控,缺点是排版要自己算。对于“导出报表、发票、对账单”这类结构固定的场景,第三条路的可控性明显更高。我一般会先问一句:这份 PDF 的版式是固定的还是随内容流动的?固定版式优先 iText,流动长文再考虑别的。
提示:iText 分两个大版本线,早期版本是 LGPL/MPL 授权,较新的 7.x 系列是 AGPL 商业双授权。内部工具和自用 Demo 一般不受影响,但要做商业闭源分发,务必先确认授权条款,这是血泪经验,别等上线了才被法务叫停。
2.2 建工程与引入依赖
新建一个 WinForm 项目,目标框架选 .NET Framework 4.7.2 或更高都行,.NET 6/8 的 WinForms 同样可用。依赖引入有两种方式:老项目习惯直接引用 iTextSharp.dll,新项目更推荐用 NuGet 管理。下面用 PackageReference 的方式演示,版本号按你实际拉到的稳定版填。
<!-- 在 .csproj 里加入包引用,版本以 NuGet 上实际稳定版为准 --> <ItemGroup> <PackageReference Include="itext7" Version="8.0.0" /> <!-- 中文字体支持通常还需要这个扩展包 --> <PackageReference Include="itext7.font-asian" Version="8.0.0" /> </ItemGroup>逻辑说明:itext7是核心库,负责文档、段落、表格、字体等对象模型;itext7.font-asian提供对中日韩字体的额外支持,很多中文乱码问题就是缺了它或者字体没嵌进去导致的。参数说明:版本号不要盲目追最新,先确认它和你项目的目标框架兼容;如果公司内网拉不到 NuGet,就把 DLL 放到libs目录手动引用,但要注意同时补齐依赖的BouncyCastle等间接包,否则运行时会抛找不到程序集的异常。
2.3 一个能跑起来的最小骨架
先别急着写业务逻辑,把“能生成一个带中文的 PDF”跑通,再往里塞数据。下面这段是窗体里一个按钮的点击事件,核心就四步:建文档、拿输出流、写内容、关闭。
using iText.Kernel.Pdf; using iText.Kernel.Font; using iText.Layout; using iText.Layout.Element; using iText.IO.Font; using iText.IO.Font.Constants; private void btnExport_Click(object sender, EventArgs e) { // 1. 选择保存路径 using var sfd = new SaveFileDialog { Filter = "PDF 文件|*.pdf", FileName = "导出结果.pdf" }; if (sfd.ShowDialog() != DialogResult.OK) return; // 2. 准备字体:用系统里的中文字体,避免默认字体不含汉字 string fontPath = @"C:\Windows\Fonts\msyh.ttc"; // 微软雅黑,按机器实际路径调整 PdfFont font = PdfFontFactory.CreateFont(fontPath, PdfEncodings.IDENTITY_H); // 3. 构建文档 using var writer = new PdfWriter(sfd.FileName); using var pdfDoc = new PdfDocument(writer); using var doc = new Document(pdfDoc); // 4. 写入内容 doc.Add(new Paragraph("导出测试标题").SetFont(font).SetFontSize(18)); doc.Add(new Paragraph("这是一段中文正文,用来验证字体是否正常嵌入。").SetFont(font).SetFontSize(12)); doc.Close(); // 关闭文档才会把内容刷进文件 MessageBox.Show("导出完成"); }逻辑说明:PdfWriter负责把字节写到磁盘,PdfDocument是 PDF 的底层对象,Document是面向排版的高层封装,三层职责别搞混。参数说明:PdfEncodings.IDENTITY_H是关键,它告诉 iText 用 Unicode 方式处理字形,中文、日文这类多字节字符必须用它,否则就是一堆问号。SetFont要作用到每个段落上,或者用doc.SetFont(font)设默认字体,后者更省事但容易被后续样式覆盖,新手建议逐段显式设置,出问题好定位。
3. 把数据排进 PDF:段落、表格与分页控制
3.1 用 Table 承载列表数据
真实需求里,导出的主体是一张表。iText 的Table对象支持列宽、对齐、边框、单元格合并,够覆盖绝大多数报表。下面把一组模拟数据写进表格,注意列宽用的是相对比例,不是像素。
// 假设有一组模拟订单数据 var rows = new List<(string No, string Name, decimal Amount)> { ("A001", "办公用品", 128.50m), ("A002", "打印耗材", 342.00m), ("A003", "电子配件", 89.90m), }; // 3 列的表格,列宽比例 2:3:2 var table = new Table(UnitValue.CreatePercentArray(new float[] { 2, 3, 2 })) .UseAllAvailableWidth(); // 表头 table.AddHeaderCell(new Cell().Add(new Paragraph("编号").SetFont(font))); table.AddHeaderCell(new Cell().Add(new Paragraph("名称").SetFont(font))); table.AddHeaderCell(new Cell().Add(new Paragraph("金额").SetFont(font))); // 数据行 foreach (var r in rows) { table.AddCell(new Cell().Add(new Paragraph(r.No).SetFont(font))); table.AddCell(new Cell().Add(new Paragraph(r.Name).SetFont(font))); table.AddCell(new Cell().Add(new Paragraph(r.Amount.ToString("F2")).SetFont(font))); } doc.Add(table);逻辑说明:CreatePercentArray让列宽按比例分配,UseAllAvailableWidth让表格撑满可用宽度,这两句组合起来比写死像素更适应不同纸张。参数说明:AddHeaderCell添加的表头在跨页时会自动重复,这是 iText 的默认行为,做长报表时很实用;如果不想重复,就改用普通AddCell。金额用ToString("F2")保留两位小数,别直接拼字符串,否则128.5会显示成128.5而不是128.50,对账场景会被挑刺。
3.2 分页、边距与页眉页脚
内容一多就会跨页,默认分页往往不满足要求。常见做法是给文档设页边距,再用事件处理器画页眉页脚。下面这段设置 A4 纸张和四周边距。
using iText.Kernel.Geom; // 在 new Document 之前设置页面尺寸 pdfDoc.SetDefaultPageSize(PageSize.A4); doc.SetMargins(36, 36, 54, 36); // 上 右 下 左,单位是点,1 点约 1/72 英寸逻辑说明:SetDefaultPageSize决定纸张,SetMargins决定内容区。参数说明:36 点约等于 1.27 厘米,54 点约 1.9 厘米,这是比较通用的报表边距。页眉页脚需要实现IEventHandler接口,在PdfDocument上注册PdfDocumentEvent.END_PAGE事件,在回调里用PdfCanvas画文字或线条。新手可以先不做页眉页脚,等主体跑通再加,避免一开始就被坐标计算劝退。
3.3 中文字体到底怎么选才不出乱码
中文乱码是 iText 最高频的翻车点,没有之一。根因通常有三个:字体文件本身不含汉字、编码方式没用IDENTITY_H、字体没被嵌入导致换台机器就显示异常。选字体时优先用系统自带的msyh.ttc(微软雅黑)或simsun.ttc(宋体),它们覆盖常用汉字。如果要做跨平台分发,最好把字体文件随程序打包,用相对路径加载,而不是写死C:\Windows\Fonts。
// 随程序打包字体时的加载方式 string fontPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "fonts", "SourceHanSansCN-Regular.otf"); PdfFont font = PdfFontFactory.CreateFont(fontPath, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);逻辑说明:PREFER_EMBEDDED表示优先把字体子集嵌入 PDF,这样目标机器没装这个字体也能正常显示。参数说明:嵌入字体会增大文件体积,一个中文字体子集通常增加几百 KB 到一两 MB,对普通报表可以接受;如果对体积敏感,可以只嵌入用到的字符子集,iText 默认就是子集嵌入,不用额外配置。
4. 避坑与排查:导出 PDF 最常见的五个问题
4.1 中文显示成方框或问号
现象:PDF 打开后中文全是方块或问号。原因:字体不含汉字,或者编码没用IDENTITY_H。解决:换成msyh.ttc这类中文字体,创建PdfFont时显式传PdfEncodings.IDENTITY_H,并确认每个段落都SetFont了。如果还不行,检查字体路径是否真的存在,CreateFont传错路径有时不报错,只是回退到默认字体。
4.2 文件被占用,第二次导出失败
现象:第一次导出正常,第二次点导出报“文件正被另一进程使用”。原因:PdfWriter、PdfDocument、Document没有及时释放,或者用户还开着上一次的 PDF。解决:用using包住这三层对象,确保Close被调用;导出前先判断目标文件是否被占用,被占用就提示用户关闭或换个文件名。别小看这个,批量导出时它是头号故障。
4.3 表格跨页后表头丢失或错位
现象:长表格翻到第二页,表头没了或者列宽变了。原因:用了普通AddCell而不是AddHeaderCell,或者每页重新建了表格。解决:表头统一用AddHeaderCell,让 iText 自动重复;如果一页放不下要手动分表,记得每张表都补表头,并保持列宽比例一致。
4.4 导出大文件时界面卡死
现象:数据量上千行时,点导出后窗体无响应。原因:PDF 构建在 UI 线程同步执行。解决:把导出逻辑放到Task.Run里,完成后用Invoke回 UI 线程提示。注意 iText 对象不是线程安全的,别在多个线程里共享同一个Document,每个导出任务独立建一套对象。
4.5 目标机器没装字体导致排版错乱
现象:开发机正常,客户机打开 PDF 字体变了、行距乱了。原因:字体没嵌入,PDF 阅读器回退到替代字体。解决:创建PdfFont时用PREFER_EMBEDDED,或者干脆把字体文件随程序发布。验证方法是把生成的 PDF 拷到一台没装该字体的机器上打开,看显示是否一致。
5. 进阶技巧:让导出结果可验证、可复用
5.1 用一段校验代码确认 PDF 真的写对了
导出完别只看“有没有生成文件”,要确认页数和内容。iText 可以反向读取 PDF,用几行代码把页数和第一页文本打出来,作为自动化校验的雏形。
using iText.Kernel.Pdf; using iText.Kernel.Pdf.Canvas.Parser; // 读取刚生成的 PDF,输出页数和首页文本片段 using var reader = new PdfReader("导出结果.pdf"); using var pdfDoc = new PdfDocument(reader); int pages = pdfDoc.GetNumberOfPages(); var strategy = new SimpleTextExtractionStrategy(); string firstPageText = PdfTextExtractor.GetTextFromPage(pdfDoc.GetPage(1), strategy); Console.WriteLine($"页数:{pages}"); Console.WriteLine($"首页文本:{firstPageText.Substring(0, Math.Min(50, firstPageText.Length))}");逻辑说明:PdfReader打开已有文件,GetNumberOfPages拿页数,PdfTextExtractor配合提取策略拿文本。参数说明:SimpleTextExtractionStrategy适合结构简单的文档,复杂排版可能顺序错乱,这时换LocationTextExtractionStrategy按坐标排序。把这段放进单元测试,每次改完导出逻辑跑一遍,比肉眼检查靠谱得多。
5.2 把导出逻辑抽成可复用服务
Demo 跑通后,别把代码堆在按钮事件里。抽一个PdfExportService类,入参是数据集合和输出路径,出参是导出结果。这样 WinForm、控制台、后台任务都能复用同一套逻辑。我一般会定义一个小结果对象,包含是否成功、页数、耗时,方便上层做提示和日志。
| 关注点 | Demo 写法 | 可复用写法 |
|---|---|---|
| 字体加载 | 按钮里临时创建 | 服务构造时加载一次并缓存 |
| 输出路径 | 硬编码或对话框 | 由调用方传入 |
| 异常处理 | 直接抛到 UI | 捕获后返回结果对象 |
| 分页配置 | 写死在方法里 | 通过配置对象传入 |
5.3 一个我踩过的字体授权坑
最后说个教训。早期做内部工具时,我图省事直接用了某款商用中文字体打包进程序,Demo 阶段没人管,后来要对外分发,才发现字体授权不允许随程序嵌入。返工换字体、重新验证排版,折腾了好几天。现在的习惯是:项目一开始就确认字体授权,优先用系统自带或明确允许嵌入的开源字体,把字体文件路径做成配置项,换字体不用改代码。这个习惯帮我省掉了至少两次返工。希望帮到你。
本文还有配套的精品资源,点击获取