首页 / 资讯中心 / 文章详情

C# WinForm导出PDF:DataGridView转PdfPTable实现

C# WinForm导出PDF:DataGridView转PdfPTable实现 ★ FEATURED ARTICLE
简介面向C#初学者的WinForm PDF导出示例代码包演示如何借助ItextSharp在桌面应用中快速创建并导出PDF。资源共53个文件压缩包仅4.17MB其中包含10个C#源码文件、5个DLL动态库含itextsharp核心库、6个可执行程序以及若干配置、资源和工程文件并附完整Visual Studio解决方案可直接打开运行、逐行调试。目前已有1226人学习下载是入门.NET环境下PDF生成的不错选择。该示例从窗体按钮触发开始完整呈现了通过PdfWriter创建Document、设置字体、使用Paragraph添加文本段落并输出到文件的关键流程同时特别指出将PdfImportedPage误用于新建PDF的典型错误及修正方法有助于避开常见坑。项目结构清晰源码中区分窗体界面、程序入口与PDF辅助封装类便于理解模块划分。掌握这一基础后还能继续探索图片、表格、页眉页脚、超链接等扩展功能满足实际业务中的多样化PDF需求。1. 一个WinForm导出PDF的demo把DataGridView变成PdfPTable半小时能跑通如果你在WinForms项目里被“导出报表”这个需求卡住过大概都经历过这样的纠结先试网页打印布局漂了再试把DataGridView转成Excel客户又说“我要的是PDF”。这个demo解决的就是这个具体问题——用C# iText方案在WinForms窗体里把表格数据、中文字体、PDF页脚一次搞定最终生成一个可分发、可打印的PDF文件。它没有什么花哨的界面胜在链路完整从点按钮弹出保存对话框到PDF真正落到磁盘中间每一步都有代码对应适合刚接触iText但不想看一堆英文文档的开发者。想二次开发的人也能直接拿它当骨架把硬编码的数据源替换成业务查询结果改一改就能接进现有系统。2. 环境准备与项目结构iTextSharp包怎么选Demo源码骨架怎么摆2.1 为什么选iTextSharp 5.x而不是iText 7版本和授权要分清NuGet上搜“iText”会出来好几个包最常见的是iTextSharp5.x和iText77.x如果不认识版本差异很容易装错。从标题和实际场景看这个demo用的是老牌的iTextSharp对应传统WinForms开发里最常见的写法。它和iText 7的API差别很大比如iText 7用PdfWriter配合PdfDocument而iTextSharp沿用Document加PdfWriter的经典组合网上大量老代码也是这个风格照着搜资料不会踩版本坑。授权方面iTextSharp 5.x是AGPL许可iText 7分商业版和AGPL版。如果项目是内部工具或者不涉及对外分发用AGPL通常问题不大但真要商用还是要让法务看一眼。我一般写小工具默认装iTextSharp 5.5.13.3这是5.x系列的最后一个稳定版本API成熟资料也好找。对比项iTextSharp 5.5.13.3iText 7.x包名iTextSharpitext7API风格Document PdfWriterPdfDocument PdfWriter资料数量多老博客基本都基于5.x官方文档为主适合场景WinForms快速落地跨平台、新项目2.2 从空白窗体到第一个PDF最小运行闭环新建一个WinForms项目目标框架建议选.NET Framework 4.7.2或4.8。这不是随便选的因为iTextSharp 5.x针对.NET Framework编译如果在.NET 6/8的WinForms项目里引用虽然能装上但运行时偶尔会出现类型加载异常。如果非要用新框架就得切换iText 7的itext7.LGPLv2.Core这类包API又对不上所以在demo阶段老老实实用.NET Framework最省事。打开包管理器控制台执行Install-Package iTextSharp -Version 5.5.13.3这条命令会安装iTextSharp主包它依赖System.Drawing和System.IOWinForms项目默认都有。装完后在代码里引用using iTextSharp.text; using iTextSharp.text.pdf;如果控制台提示找不到包源检查NuGet源是否设置了nuget.org如果提示依赖版本冲突看看项目目标框架是不是低于4.5。这两类问题是新手装包时最常遇到的。2.3 Demo的目录结构与文件职责一个小demo不需要复杂分层但这个项目的文件划分值得借鉴——它把“UI触发”和“PDF生成”拆开了。目录大概长这样PdfExportDemo/ ├── PdfExportDemo.sln ├── PdfExportDemo/ │ ├── Program.cs // 程序入口标准WinForms启动 │ ├── FormMain.cs // 主窗体放DataGridView和导出按钮 │ ├── FormMain.Designer.cs │ ├── PdfExportHelper.cs // 核心封装PDF生成逻辑 │ └── packages.config // NuGet依赖锁定重点说FormMain.cs和PdfExportHelper.cs的分工。前者负责在窗体上放一个DataGridView、一个按钮按钮点击后读取表格数据再调用Helper后者不关心UI只接收数据、字体路径、输出路径返回成功或异常。这样拆的最大好处是以后想从WinForms换成控制台程序或者批量导出任务PdfExportHelper.cs可以原封不动拿走。FormMain.cs里有一段初始化演示数据的代码一般会用DataTable构造一些假数据填进表格目的就是让PDF生成时有东西可画。实际接入业务时把这段替换成自己的查询结果就行导出按钮的点击事件不用动。2.4 装好包先跑一个最小验证别急着写业务逻辑我接到这类需求时有个习惯装完包先写一个10行的最小文档验证环境而不是直接堆业务代码。最小验证有很多细节问题能提前暴露。比如在按钮点击事件里先写using (FileStream fs new FileStream(D:\test.pdf, FileMode.Create)) { Document doc new Document(); PdfWriter.GetInstance(doc, fs); doc.Open(); doc.Add(new Paragraph(hello iTextSharp)); doc.Close(); }如果能生成PDF并且用阅读器正常打开说明iTextSharp的DLL加载、字体渲染、文件流这三级链路都是通的后面再往文档里加表格、加中文就只剩下逻辑问题了。这一步跑不通的话先查包版本和框架版本不要急着写几百行再排查那样定位问题会非常痛苦。这段代码里还有一个容易忽略的点Document和FileStream的生命周期。doc.Close()必须执行否则PDF会缺尾部标记文件打不开FileStream用using包裹避免文件被占用。这个习惯在后面完整导出逻辑里会反复用到。3. 核心代码拆解从点击按钮到PDF落盘的完整链路3.1 最外层三段式Document、PdfWriter、FileStream的关系iTextSharp生成PDF的思路和画图不一样它更像是往一个“文档流”里写元素最后由PdfWriter把元素序列化成PDF格式。很多新手写代码时搞不清Document和PdfWriter谁先谁后其实顺序很固定先有输出流再创建Document然后让PdfWriter监听这个Document最后Open()往里加内容。导出按钮的核心入口代码private void btnExport_Click(object sender, EventArgs e) { SaveFileDialog dialog new SaveFileDialog(); dialog.Filter PDF文件|*.pdf; dialog.FileName 导出报表.pdf; if (dialog.ShowDialog() ! DialogResult.OK) return; string outputPath dialog.FileName; PdfExportHelper.ExportDataGridViewToPdf(dataGridView1, outputPath); }这一段说明两点。第一SaveFileDialog的Filter要写清楚否则用户保存出来的文件没有.pdf后缀FileName给一个默认名字避免用户留空导致保存异常。第二所有PDF生成逻辑都被封装在ExportDataGridViewToPdf里按钮事件只传递三个东西表格控件、目标路径。这样做日志和异常处理都很干净。SaveFileDialog选完路径后outputPath可能带中文比如“导出报表.pdf”这在iTextSharp里没问题因为最终写入用的是FileStream文件系统对中文路径支持良好。真正要注意的是别在using块外提前释放流否则写入到一半文件就被锁定了。3.2 中文字体不配字体就是方块配字体要看TTF和TTCiTextSharp默认字体是Helvetica它不支持中文直接往Paragraph里塞中文内容生成的PDF里全是一个个小方块。这个问题十个新手有九个会遇到解决方法是手动注册一个支持中文的字体。常见写法是拿系统自带的宋体或黑体来用宋体文件在Windows目录下是simsun.ttc黑体是simhei.ttf。注意.ttc是三四个字体打包在一个文件里要用逗号加下标指定取哪一个比如simsun.ttc,0表示取TTF集合里的第一个字体。BaseFont.CreateFont的三个参数分别是字体路径、编码、是否内嵌。public static iTextSharp.text.Font GetChineseFont(float size) { string fontPath C:\Windows\Fonts\simsun.ttc,0; BaseFont baseFont BaseFont.CreateFont( fontPath, BaseFont.IDENTITY_H, BaseFont.EMBEDDED ); return new iTextSharp.text.Font(baseFont, size); }参数说明BaseFont.IDENTITY_H表示使用Unicode编码这是正确处理中文的关键如果写CP1252之类的编码中文还是会乱BaseFont.EMBEDDED表示把字体嵌入PDF文件这样换一台没装宋体的电脑打开PDF也能正常显示代价是文件体积稍大。如果字体路径写错CreateFont会抛DocumentException建议在代码里先判断File.Exists(fontPath)找不到就回退到simhei.ttf。用Font对象创建段落时加载过一次的BaseFont可以缓存成静态字段避免每次导出都重新读取字体文件。在高频导出场景下这一步能省掉不少IO时间。3.3 表格映射DataGridView的列宽和行数据怎么变成PdfPTablePDF里的表格用PdfPTable表示它的工作方式很像HTML里的table——先定义列数再往里面逐个添加单元格单元格按从左到右、从上到下的顺序自动排布。把DataGridView内容填充进去思路就是两层循环外层遍历行内层遍历列。PdfPTable table new PdfPTable(dataGridView.Columns.Count); float[] columnWidths new float[dataGridView.Columns.Count]; for (int i 0; i dataGridView.Columns.Count; i) { columnWidths[i] dataGridView.Columns[i].Width; } table.SetWidths(columnWidths); table.WidthPercentage 100; // 表头 iTextSharp.text.Font headerFont GetChineseFont(10f); foreach (DataGridViewColumn col in dataGridView.Columns) { PdfPCell cell new PdfPCell(new Phrase(col.HeaderText, headerFont)); cell.BackgroundColor new BaseColor(230, 230, 230); cell.HorizontalAlignment Element.ALIGN_CENTER; table.AddCell(cell); } // 数据行 iTextSharp.text.Font contentFont GetChineseFont(9f); foreach (DataGridViewRow row in dataGridView.Rows) { if (row.IsNewRow) continue; foreach (DataGridViewCell cell in row.Cells) { string text cell.Value?.ToString() ?? ; PdfPCell pdfCell new PdfPCell(new Phrase(text, contentFont)); pdfCell.HorizontalAlignment Element.ALIGN_LEFT; table.AddCell(pdfCell); } }这段代码有三个关键点。第一SetWidths接收的float[]是相对比例数组比如全部传1就等宽分列传5和5和2就是按5:5:2分配WidthPercentage 100的意思是表格占满页面可用宽度。第二DataGridViewRow.IsNewRow一定要跳过否则表格底部会多出一行空白数据。第三new Phrase(text, font)里的text要先判空cell.Value为null时直接调ToString()会抛异常用?? 兜底是常见的写法。PdfPTable一个非常实用的特性是自动分页——内容超过一页高度时它会自动断开继续在下一页绘制不需要我们手动计算页数。不过它默认不会自动重复表头如果数据量跨了两页第二页开头直接是数据行读起来不太友好。解决办法是用table.HeaderRows 1设置前一行作为表头这样分页后表头会自动带过去。3.4 页脚页码用PdfPageEventHelper在每页底部画页码页码是导出PDF时经常被遗漏的需求但有页码和没页码的观感差距很大尤其是多页报表。iTextSharp提供了一个事件机制PdfPageEventHelper允许我们在每一页开始或结束的时候插入自定义绘制逻辑页码就在OnEndPage里写。public class PageNumberEventHandler : PdfPageEventHelper { public override void OnEndPage(PdfWriter writer, Document document) { PdfContentByte canvas writer.DirectContent; iTextSharp.text.Font font GetChineseFont(8f); Phrase pageText new Phrase( $第 {writer.PageNumber} 页, font ); float x (document.LeftMargin document.PageSize.Width - document.RightMargin) / 2; float y document.BottomMargin - 10; PdfContentByte cb writer.DirectContent; cb.BeginText(); cb.SetTextMatrix(x, y); cb.SetColorFill(BaseColor.GRAY); foreach (Chunk chunk in pageText.Chunks) { cb.ShowText(chunk.ToString()); } cb.EndText(); } }PdfPageEventHelper的妙处在于它不侵入内容流事件在页面结束时触发正好用来画那些“不属于表格”的装饰元素。writer.PageNumber从1开始计数不需要自己维护计数器。document.LeftMargin和RightMargin能用来计算水平居中位置BottomMargin - 10把文字放在页脚附近。需要注意的是BeginText()和EndText()必须成对出现漏掉任何一头都会导致PDF内容流受损。另外ShowText只接受纯文本别往里面传Phrase对象要先通过Chunk拿字符串。这套事件机制还能扩展出很多玩法比如在OnStartPage里画公司Logo、在OnEndPage里画机密水印改改坐标就行。3.5 组装与收尾把表格加进Document再关闭顺序错了文件就废了有了字体、表格、页脚处理器最后一步就是组装和输出。完整代码的关键是务必按照“先注册页脚处理器再Open()然后加表格最后Close()”的顺序执行Close()一旦执行就不能再往Document里加内容了。public static void ExportDataGridViewToPdf(DataGridView grid, string outputPath) { Document document new Document(PageSize.A4, 30f, 30f, 30f, 30f); using (FileStream fs new FileStream(outputPath, FileMode.Create)) { PdfWriter writer PdfWriter.GetInstance(document, fs); writer.PageEvent new PageNumberEventHandler(); document.Open(); iTextSharp.text.Font titleFont GetChineseFont(14f); document.Add(new Paragraph(数据导出报表, titleFont)); document.Add(new Paragraph( )); PdfPTable table BuildTableFromDataGridView(grid); document.Add(table); document.Close(); } }构造函数里PageSize.A4指定页面大小后面的四个30f是左、右、上、下四边距单位是像素点。FileMode.Create表示目标文件存在就覆盖不存在就新建这是导出场景常用的模式。document.Close()执行后iTextSharp会在文件末尾写入%%EOF标记PDF阅读器靠这个标记识别文件完整性。如果代码在某个分支里提前return导致没走Close()生成的文件用阅读器打开会提示“文件已损坏”调试的时候可以优先检查这一步。4. 避坑清单字体方块、文件被占用、PDF打不开一次讲全4.1 中文字体全是方块但代码没报错现象生成的PDF能打开表格和英文都正常中文全部显示为“□”占位符。原因Paragraph用的是iTextSharp默认字体Helvetica它不包含中文字形渲染时只能画方框。这个坑最麻烦的地方在于不抛异常代码看起来“成功”了结果直到交付才发现内容不可读。解决GetChineseFont方法必须生效且要确认段落和单元格全部用了这个字体。我曾见过一个项目表头用了自定义字体数据行却漏了设置结果只有数据是方块。另一个隐蔽原因是simsun.ttc的路径写错CreateFont抛异常反而能暴露问题怕就怕路径写对但没加,0下标取到的是空的子字体。建议用一个Font静态缓存然后所有Paragraph、Phrase、PdfPCell都走同一个获取方法能从根上避免漏设置。4.2 生成的文件提示“PDF header not found”或EOF异常现象调用导出方法时抛IOException提示内容包含PDF header not found或Unexpected end of file。原因Document和PdfWriter的创建顺序写反了或者文件流在doc.Open()之前就被写入了其他数据。更常见的原因是PdfWriter.GetInstance(doc, fs)在执行时fs已经指向一个非空文件比如先写了日志再打开PDF流。解决严格按FileStream - Document - PdfWriter.GetInstance - document.Open()的顺序来不要在PDF写入前往同一个FileStream里写任何字节。如果项目里同时开了日志文件务必用不同的文件名。另外检查一下document.Close()是否在所有return分支之前都能执行到用try/finally或using包裹是最稳的写法。4.3 保存到桌面后文件被占用第二次导出抛IOException现象第一次导出成功第二次点击按钮生成同名文件时抛IOException: 文件正在被另一个进程使用。原因上一个FileStream没有被释放常见是写了using但Document.Close()放在using块外或者SaveFileDialog路径和上次相同导致文件被PDF阅读器进程锁住。严格说这个锅不一定在iTextSharp身上但导出代码最容易被怀疑。解决确保FileStream和Document共用一个using作用域谁先创建谁后释放。如果确认代码没问题那就是本机PDF阅读器开了预览锁定了文件把阅读器关掉或者换一个新文件名导出。对用户交互来说SaveFileDialog已经有覆盖确认保持默认行为即可。4.4 表格列宽设置无效所有列一样宽现象SetWidths传了自定义比例数组但生成的PDF里列宽完全没有变化。原因float[]的元素数量和PdfPTable的列数不一致iTextSharp会跳过设置并回退到等宽列而这个异常通常不会抛出。另一个常见原因是列宽数组里传了0或负数导致计算布局时概率性失败。解决先从dataGridView.Columns.Count动态创建数组别写死数字。然后在SetWidths前加断言如果数组长度不等于列数直接抛出ArgumentException让问题在开发阶段暴露而不是在交付后发现。还要注意SetWidths是相对比例不是像素值dataGridView.Columns[i].Width本身是像素直接用没问题但如果想精确控制PDF中的绝对宽度得配合SetTotalWidth和LockedWidth一起用这属于进阶用法简单场景用百分比即可。5. 二次开发方向从固定Demo到通用导出工具还要改哪里5.1 把硬编码内容改成“表头字典 数据列表”demo里数据源是DataGridView但真实业务里数据往往来自数据库查询结果或List实体。如果每次导出都要构造一个临时的DataGridView再取数据绕了一圈很傻。我一般会做一个重载方法接收两个参数一个是列名映射字典Dictionarystring, string表示“实体属性名 → PDF表头文字”另一个是数据列表IEnumerableobject然后用反射读取属性值填充单元格。这样业务层查询完数据直接传给导出方法无需经过UI控件。public static void ExportToPdfT( string outputPath, Dictionarystring, string columnMapping, IEnumerableT dataList) { // 遍历 dataList用反射读取 columnMapping 中每个键对应的属性值 // 构建 PdfPTable 的逻辑和之前一致 }需要调整的两处字体路径从绝对路径改成配置文件或者自动探测Windows Fonts目录页脚事件里的页码格式改成“第 X 页共 Y 页”这需要OnCloseDocument记录总页数再回填多一个字段的事。5.2 验证PDF的三个土办法比看代码更快改完代码后我会打开生成的PDF做三件事用阅读器放大到400%看中文边缘是否平滑检查有没有轻微的乱码打印预览看表格是否跨页断行断行处表头有没有重复再用文字选择工具框选PDF里的文字如果能选中说明文字是真正可复制的文本而不是图片。如果框选出来是空白说明字体没有真正嵌入检查BaseFont.EMBEDDED参数是否生效。这三个方法都不需要额外工具但基本能覆盖日常改动可能引入的问题。回头看这个demo我最大的教训是当时把字体加载写在了方法内部每次导出都重新读一次字体文件批量导出时性能慢了将近一倍。从那以后我每次做PDF导出都会强制把字体缓存成静态字段并且先在代码里检查一遍BaseFont的创建顺序养成这个习惯后很少再遇到方块字和文件损坏的问题希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站