简介:这份资源是面向C# WinForm初学者与.NET桌面开发者的ItextSharp PDF导出实战示例,针对“如何在WinForm应用中把文本数据生成PDF文件”这一常见需求,提供可直接运行的完整工程。压缩包共53个文件,约4.17MB,以cs源码、exe可执行程序、dll类库、config配置、resources资源及resx窗体文件为主,另含sln解决方案、csproj项目文件与一份示例pdf,结构完整,便于对照源码逐层理解。示例围绕Document、PdfWriter、Paragraph与Chunk等核心API展开,演示了引入ItextSharp、绑定按钮Click事件、调用导出方法并输出PDF的完整链路,同时点出PdfImportedPage误用等易错细节,帮助读者少走弯路。目前已有1226人学习下载,适合想快速掌握C#导出PDF基本流程、并在此基础上扩展图片、表格、页眉页脚等复杂排版功能的开发者参考。
1. 用 C# WinForm 和 iText 把 PDF 导出做进业务系统:一个能直接抄的简单 demo
很多做 WinForm 业务系统的开发者都遇到过这个需求:用户点一下按钮,把表格里的数据、或者一张排版好的单据,导出成 PDF 存到本地。第一反应往往是调用 Office 组件或者用系统自带的打印功能,结果要么依赖环境、要么排版失控。我一般会直接上 iText,在 WinForm 里跑一个最小闭环:一个按钮、一个 SaveFileDialog、一段排版代码,几十行就能把 PDF 导出跑通。这篇笔记就围绕这个简单 demo 展开,把 iText 的选型理由、环境准备、最小可运行代码、中文字体这个最大的坑,以及参数怎么调,一层层讲清楚。适合刚接触 PDF 导出、或者被 Office 组件坑过的 WinForm 开发者照着复现。
2. iText 在 WinForm 里到底怎么选版本、怎么装
2.1 为什么是 iText,而不是 Office 组件或系统打印
先说选型。WinForm 里导出 PDF,常见做法无非几种:调用 Office 的 COM 组件、用系统自带的 Microsoft Print to PDF 虚拟打印机、或者用第三方 PDF 库。前两种的翻车概率很高——COM 组件要求目标机器装了 Office,版本还对不上;虚拟打印机依赖系统打印驱动,排版基本靠打印预览,程序里很难精确控制。iText 是纯托管库,不依赖外部软件,排版由代码完全掌控,生成的是标准 PDF 文件,部署时只要带上对应的 DLL 就行。
iText 本身分两条线:一条是老的 iTextSharp(对应 iText 5),命名空间是iTextSharp.text;另一条是 iText 7,包名变成了itext7,API 做了较大重构,命名空间是iText.Kernel.Pdf、iText.Layout这些。对于「简单 demo」这个定位,我一般建议直接用 iText 7,因为它是当前维护的主线,NuGet 上装起来干净,文档也相对统一。iTextSharp 虽然网上老例子多,但很多写法在新版本里已经对不上,新手照着抄容易踩坑。
需要提前说清楚的是授权问题:iText 是 AGPL 协议,商业闭源项目里直接用需要购买商业授权,这一点在选型阶段就要评估,别等上线了才发现。如果只是内部工具、或者项目本身开源,那按 AGPL 用没问题。这个边界心里要有数。
2.2 用 NuGet 装 iText 7 的最小依赖
在 Visual Studio 里新建一个 WinForm 项目(.NET Framework 4.6.1 以上,或者 .NET 6/8 的 Windows Forms 都行),然后通过 NuGet 装包。打开「工具」→「NuGet 包管理器」→「程序包管理器控制台」,执行:
# 安装 iText 7 核心包,PDF 读写的基础 Install-Package itext7 # 中文字体支持需要额外的字体包,或者自己加载 ttf 文件 # 这里用 itext7 自带的字体模块即可,实际中文字体走文件加载 Install-Package itext7.font-asian如果你用的是 .NET CLI,等价命令是:
dotnet add package itext7 dotnet add package itext7.font-asian装完之后,项目引用里会出现itext7相关的 DLL。这里有个细节:itext7.font-asian这个包主要是给 iText 提供亚洲字体的识别能力,但真正要显示中文,最稳的做法还是自己指定一个系统里的 ttf/ttc 字体文件,比如宋体、黑体。原因后面避坑章节会细讲。
2.3 一个 WinForm 窗体的最小骨架
在动手写导出逻辑前,先把界面搭起来。拖一个 Button 到窗体上,命名btnExport,双击生成点击事件。整个 demo 的结构就是:按钮点击 → 弹出保存对话框 → 拿到路径 → 调 iText 写 PDF。窗体代码文件顶部需要引入的命名空间:
using System; using System.IO; using System.Windows.Forms; using iText.Kernel.Pdf; using iText.Layout; using iText.Layout.Element; using iText.Layout.Properties; using iText.Kernel.Font; using iText.IO.Font;这些命名空间分别对应:PDF 文档对象、布局元素、段落表格、字体加载。少引一个就会在编译时报「找不到类型」,新手常见的就是漏了iText.Layout.Properties,导致设置对齐方式时找不到TextAlignment。
3. 把导出逻辑写进按钮事件:从 SaveFileDialog 到 PDF 落盘
3.1 用 SaveFileDialog 拿到用户指定的保存路径
导出第一步是让用户选保存位置,这是 WinForm 的标准做法,避免程序自己拼路径导致权限问题。代码:
private void btnExport_Click(object sender, EventArgs e) { // 创建保存文件对话框,过滤条件限定为 PDF using (SaveFileDialog sfd = new SaveFileDialog()) { sfd.Filter = "PDF 文件 (*.pdf)|*.pdf"; sfd.FileName = "导出结果.pdf"; // 默认文件名 sfd.Title = "选择 PDF 保存位置"; // 用户点了取消就直接返回,不做任何事 if (sfd.ShowDialog() != DialogResult.OK) return; string filePath = sfd.FileName; ExportToPdf(filePath); // 真正的导出逻辑单独抽方法 } }这里把导出逻辑抽成ExportToPdf方法,是为了让按钮事件保持干净,也方便后面单独测试。SaveFileDialog的Filter属性写法要注意,|前面是显示给用户看的描述,后面是实际扩展名,写错了会导致文件类型筛选失效。FileName给个默认值,用户体验会好很多,不然对话框打开是空的。
3.2 用 iText 7 写一个带标题和表格的 PDF
下面是核心导出方法。这个 demo 生成一份带标题、一段说明文字、一个三列表格的 PDF,覆盖最常见的单据/报表场景:
private void ExportToPdf(string filePath) { // PdfWriter 负责把文档写到指定路径,PdfDocument 是文档对象 using (PdfWriter writer = new PdfWriter(filePath)) using (PdfDocument pdf = new PdfDocument(writer)) using (Document document = new Document(pdf)) { // 加载中文字体,这里用系统自带的宋体,路径按实际系统调整 string fontPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "simsun.ttc,0"); // ttc 需要指定索引,0 表示第一个字体 PdfFont font = PdfFontFactory.CreateFont( fontPath, PdfEncodings.IDENTITY_H, // 关键:用 IDENTITY_H 支持中文 PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); // 标题:字号 18,居中,加粗 Paragraph title = new Paragraph("销售数据导出报表") .SetFont(font) .SetFontSize(18) .SetTextAlignment(TextAlignment.CENTER) .SetBold(); document.Add(title); // 说明段落:字号 10.5,左对齐 Paragraph desc = new Paragraph("以下数据由系统自动生成,仅供内部核对使用。") .SetFont(font) .SetFontSize(10.5f) .SetTextAlignment(TextAlignment.LEFT); document.Add(desc); // 创建三列表格,宽度占满页面 Table table = new Table(UnitValue.CreatePercentArray(new float[] { 2, 3, 2 })) .UseAllAvailableWidth(); // 表头 string[] headers = { "编号", "产品名称", "金额" }; foreach (string h in headers) { Cell cell = new Cell() .Add(new Paragraph(h).SetFont(font).SetFontSize(11).SetBold()) .SetTextAlignment(TextAlignment.CENTER) .SetBackgroundColor(iText.Kernel.Colors.ColorConstants.LIGHT_GRAY); table.AddHeaderCell(cell); } // 数据行,实际项目里这里换成从数据库或 List 里循环 string[][] rows = { new[] { "001", "办公桌", "1200.00" }, new[] { "002", "人体工学椅", "899.00" }, new[] { "003", "显示器支架", "259.00" } }; foreach (var row in rows) { foreach (string val in row) { Cell cell = new Cell() .Add(new Paragraph(val).SetFont(font).SetFontSize(10.5f)) .SetTextAlignment(TextAlignment.CENTER); table.AddCell(cell); } } document.Add(table); document.Close(); // 显式关闭,确保内容全部刷入文件 } }这段代码有几个关键点要说明。第一,PdfFontFactory.CreateFont的第二个参数PdfEncodings.IDENTITY_H是中文能正常显示的核心,用默认编码会变成一堆方块或问号。第三,EmbeddingStrategy.PREFER_EMBEDDED表示把字体嵌入 PDF,这样换台机器打开也不会因为缺字体而乱码,代价是文件体积会大一些。第三,Table的列宽用UnitValue.CreatePercentArray按比例分配,比固定像素更适应不同页面尺寸。第四,document.Close()不能省,虽然using块结束也会释放,但显式关闭能保证内容完整写入,避免偶发的文件损坏。
3.3 参数怎么调:字号、边距、页面尺寸
demo 跑通后,实际项目里最常调的就是这几个参数。页面尺寸默认是 A4,如果要改成横向或者自定义,在Document构造时传PageSize:
// 横向 A4 Document document = new Document(pdf, PageSize.A4.Rotate()); // 自定义边距:上 36、右 36、下 36、左 36(单位是点,1 点约 1/72 英寸) document.SetMargins(36, 36, 36, 36);字号方面,正文一般 10.5 到 11,标题 16 到 18,表头比正文大半号到一号。边距默认是 36 点,如果内容多、想省纸,可以调到 20 到 28。表格列宽的比例数组,元素个数必须和列数一致,否则运行时会抛异常,这是新手很容易忽略的细节。金额这类数字列,建议右对齐而不是居中,视觉上更整齐,改SetTextAlignment(TextAlignment.RIGHT)即可。
4. 中文乱码、字体嵌入和文件占用的排查清单
4.1 中文显示成方块或问号
现象:PDF 生成成功,但中文全是方块、问号或者空白。原因基本只有一个——字体没配对。iText 默认用的标准字体不含中文字形,必须显式加载一个中文字体文件。解决:确认fontPath指向的 ttf/ttc 文件真实存在,并且用PdfEncodings.IDENTITY_H编码。如果是 ttc 集合文件,路径后面要带,0这样的索引,索引错了会加载到别的字体甚至报错。我一般会先File.Exists(fontPath)判断一下,不存在就抛个明确异常,比让用户看到乱码强。
4.2 换台机器打开就乱码
现象:本机生成正常,发给别人打开中文变样。原因是字体没嵌入,对方机器没有同款字体。解决:CreateFont时用EmbeddingStrategy.PREFER_EMBEDDED,强制把字体子集嵌进 PDF。注意有些字体授权不允许嵌入,商用场景要确认字体许可。嵌入后文件会变大,一份几页的报表通常也就几百 KB,可以接受。
4.3 导出时报「文件正被占用」
现象:第二次导出到同一个路径时抛异常,提示文件被占用。原因是上一次的PdfDocument或Document没释放干净,文件句柄还开着。解决:严格用using包裹PdfWriter、PdfDocument、Document三层,确保异常路径下也能释放。如果导出前用户可能已经用阅读器打开了同名文件,那属于外部占用,程序里捕获异常后提示用户关闭文件即可,不要硬覆盖。
4.4 表格内容太多被截断
现象:数据行多了以后,表格超出页面,后面的内容不见了。原因是 iText 默认不会自动分页处理超大表格,需要确认Document的布局是否允许跨页。iText 7 的Table在内容超过一页时会自动分页,但如果单元格里塞了超长不换行的文本,就会溢出。解决:给单元格里的Paragraph设置合适的宽度,或者对长文本做换行处理;同时确认没有把表格放进固定高度的容器里。
4.5 路径里有中文或空格导致失败
现象:保存到带中文的目录时抛异常。原因是PdfWriter对路径的处理在某些环境下对非 ASCII 字符不友好。解决:优先用SaveFileDialog返回的完整路径,它已经是系统认可的格式;如果必须自己拼路径,用Path.Combine而不是字符串拼接,避免分隔符问题。实在不行,先写到临时目录再移动,绕开路径编码的玄学问题。
5. 把 demo 变成能复用的导出工具:抽方法、传数据、加页眉页脚
demo 跑通只是起点,真正在项目里用,得把它变成可复用的东西。我的习惯是抽一个PdfExporter静态类,把字体加载、页面设置、表格生成拆成独立方法,业务代码只负责传数据进来。这样换个报表不用重写排版逻辑。
public static class PdfExporter { // 字体只加载一次,避免每次导出都读文件 private static readonly PdfFont ChineseFont = LoadFont(); private static PdfFont LoadFont() { string path = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "simsun.ttc,0"); return PdfFontFactory.CreateFont( path, PdfEncodings.IDENTITY_H, PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED); } // 通用导出:标题 + 表头 + 数据行 public static void Export(string filePath, string title, string[] headers, List<string[]> dataRows) { using (PdfWriter writer = new PdfWriter(filePath)) using (PdfDocument pdf = new PdfDocument(writer)) using (Document doc = new Document(pdf)) { doc.Add(new Paragraph(title).SetFont(ChineseFont) .SetFontSize(18).SetTextAlignment(TextAlignment.CENTER)); Table table = new Table( UnitValue.CreatePercentArray( Enumerable.Repeat(1f, headers.Length).ToArray())) .UseAllAvailableWidth(); foreach (string h in headers) table.AddHeaderCell(new Cell() .Add(new Paragraph(h).SetFont(ChineseFont).SetBold())); foreach (var row in dataRows) foreach (string v in row) table.AddCell(new Cell() .Add(new Paragraph(v).SetFont(ChineseFont))); doc.Add(table); doc.Close(); } } }调用方就一行:PdfExporter.Export(path, "报表", headers, rows);。字体做成静态字段只加载一次,是因为CreateFont每次都要读磁盘文件,数据量大时反复调用会明显拖慢导出速度,这个优化在批量导出场景里很值。
再进一步是页眉页脚和页码。iText 7 用事件处理器实现,给PdfDocument挂一个IEventHandler,在HandleEvent里判断当前是START_PAGE还是END_PAGE,然后用PdfCanvas在页面固定位置画文字。页码可以用pdf.GetPageNumber()拿当前页。这块代码稍长,但套路固定,网上例子多,核心就是别在事件里做重活,只画文字和线条。
验证导出结果是否正确的习惯:我一般会写个小的控制台入口,脱离 WinForm 直接调PdfExporter.Export,生成后用阅读器打开检查中文、分页、表格边框。这样排查问题时能排除 UI 层的干扰,定位到底是排版逻辑还是界面传参的问题。踩过最深的坑就是一开始把字体加载写在按钮事件里,每次点击都读一遍字体文件,数据一多就卡,后来挪成静态字段才顺。希望帮到你。
本文还有配套的精品资源,点击获取