C#实战:iTextSharp生成PDF从环境搭建到中文表格图片全攻略

发布时间:2026/9/8 11:14:28
C#实战:iTextSharp生成PDF从环境搭建到中文表格图片全攻略 简介面向C# WinForm初学者的iTextSharp PDF导出简易Demo演示如何将文本框内容快速生成PDF文档帮助理解Document、PdfWriter、Paragraph等核心对象的配合用法从界面设计到文件保存覆盖一个完整的基础开发流程非常适合刚接触桌面端PDF生成需求的开发者。压缩包共53个文件大小4.17MB包含C#源码、iTextSharp核心类库、项目配置文件、WinForm界面资源及可执行程序目录结构清晰方便直接加载解决方案进行编译运行与断点调试。示例覆盖从NuGet安装iTextSharp、创建WinForm界面、绑定按钮事件到实现ExportToPdf方法的完整流程并专门指出初学者容易误用PdfImportedPage复制页面的错误改为用Paragraph添加文本段落的正确做法让代码逻辑更清晰避免新手踩坑同时还可进一步扩展图片、表格、页眉页脚等元素。目前已有1224人学习下载适合快速上手将基础能力迁移到报表导出、数据呈现等实际业务场景也为后续使用iTextSharp丰富API、构建复杂PDF应用打下基础同时可作为课程设计或小型工具的开发起点。1. 为什么我从Html转PDF方案转向iTextSharp1.1 一个上位机项目的真实需求先说下我接手这个需求的场景。公司做工业检测设备的上位机是C# WinForm现场要给客户输出检测报告。一开始用的是前端同事留下的方案本地起一个浏览器控件把数据塞进HTML模板再调打印。这套方案有个让人头疼的问题——客户现场机器不一定装了对应版本的浏览器而且页面样式在不同分辨率下缩放经常把表格挤得歪七扭八。每次客户说报告格式不对我得远程连上去看半天最后往往是要么调CSS、要么重新截屏效率极低。后来客户提了个硬性要求报告必须是PDF文件能存档、能邮件发、能直接微信传给其他部门。我调研了一圈发现核心需求其实很清晰纯后台生成PDF文件不需要用户干预中英文混排表格要规整文件大小别太夸张最好能直接嵌入数据曲线截图。当时我第一个想到的就是iTextSharp。市面上.NET生态里能生成PDF的库不少但论成熟度和社区资料丰富度iText系列绝对排得上号。1.2 PDF生成的主流方案对比先把我调研时对比的几个方案拉出来说省得你们自己再去踩坑。我当时列了一个简单的表格方案学习成本中文支持复杂表格依赖环境授权模式iTextSharp5.x中等需注册字体支持良好强无外部依赖LGPL/商业双授权PdfSharp低支持较好中等无外部依赖MITAspose.PDF低开箱即用强无外部依赖商业付费Html转PDF浏览器控件低好强依赖浏览器组件组件各有授权原生PrintDocument绘制高好弱无外部依赖免费Aspose虽然好用但价格不菲项目预算有限PdfSharp做简单票据够用但遇到复杂布局、页眉页脚、图片混排要写很多底层绘制代码PrintDocument那套做过报表的人都知道坐标算到怀疑人生。综合下来iTextSharp在免费方案里是功能最全的而且网上教程多——遇到问题一搜基本都有答案这对被工期追着跑的人来说太重要了。选型还有个关键考量iTextSharp 5.x版本在NuGet上就能直接拉原生支持C#API设计也符合.NET开发者的习惯不需要像Java的iText那样再包一层适配。所以最终定了iTextSharp项目里实测跑通后一直用到现在。2. 环境准备iTextSharp版本选择和第一个Demo跑通2.1 用NuGet安装时的版本坑这里必须提一个很多人栽过的坑。你先打开Visual Studio的NuGet包管理器搜索iTextSharp会发现列表里有iTextSharp和iText7包名是itext7两个主要的。iText7是iText集团后来推的.NET版本API设计和5.x完全不一样很多老教程里的PdfWriter、Document用法在7.x里变了。如果照着老教程敲代码却装成了7.x第一步就卡住。我做的是简单Demo而且要最大程度复用网上的中文教程所以选5.x的最后一个稳定版——iTextSharp 5.5.13.3。这个版本在NuGet上直接安装就行命令也很简单Install-Package iTextSharp -Version 5.5.13.3或者直接在包管理器界面搜iTextSharp看到版本号5.5.13.3那个就是了。有几个旧版本还挂在列表里像5.5.12、5.5.9不建议用那些可能会有已知Bug。顺带说下iTextSharp在5.x时代是LGPL授权的免费使用但如果你要打包成商业闭源软件对外分发得仔细看一下授权条款。我们公司是自用内部系统问题不大。如果是做商业软件卖给别人我建议还是老老实实买商业授权或者选MIT协议的PdfSharp别在授权上留隐患。2.2 10行代码生成第一个PDF文件装好之后找个WinForm项目加一个按钮双击进去写代码。我先给你看一个最小可运行的版本using System; using System.Windows.Forms; using iTextSharp.text; using iTextSharp.text.pdf; public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void btnExport_Click(object sender, EventArgs e) { // 指定输出文件路径 string filePath D:\test\simple.pdf; // 创建文档对象A4纸大小左右边距40上下边距40 Document document new Document(PageSize.A4, 40f, 40f, 40f, 40f); try { // 创建PdfWriter实例把文档对象和文件流关联起来 PdfWriter.GetInstance(document, new FileStream(filePath, FileMode.Create)); // 打开文档 document.Open(); // 添加一行文字 document.Add(new Paragraph(Hello iTextSharp!)); // 关闭文档 document.Close(); MessageBox.Show(导出成功文件位置 filePath); } catch (Exception ex) { MessageBox.Show(导出失败 ex.Message); } } }这段代码的核心逻辑就四步创建Document对象、创建PdfWriter、打开文档、往里加内容再关闭。我把文件路径写死了你实际用的时候最好用SaveFileDialog让客户自己选保存位置这个后面会讲到。这里解释一下为什么需要PdfWriterDocument对象本身不负责生成PDF文件它只是内容的容器真正把文字、表格、图片写进PDF文件的是PdfWriter。你可以把Document想象成一块画布PdfWriter是拿笔记录的人。这个设计在整个iTextSharp体系里到处都是——内容对象和输出对象分离理解了这个后面看API就不会晕。跑通这个Demo之后你会发现一个很现实的问题Paragraph对象默认用的是Helvetica字体不支持中文。如果你直接写document.Add(new Paragraph(中文测试))生成的PDF里中文全是乱码。这就引出了第三部分要说的中文字体问题。3. 实战需求中文、表格、图片一个不能少3.1 中文字体注册根治乱码做过iTextSharp的人基本都被中文乱码折磨过。乱码的根源在于PDF默认字体只覆盖拉丁字符集要显示中文必须把系统里的中文字体手动注册进PDF文档。最常用的方案是用系统自带的宋体或微软雅黑。注册代码是这样// 指定系统中文字体文件路径 string fontPath Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), simsun.ttc); // 创建BaseFont对象注册中文字体 BaseFont baseFont BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); // 用BaseFont创建iTextSharp的Font对象 Font chineseFont new Font(baseFont, 12f, Font.NORMAL);注意几个关键参数BaseFont.IDENTITY_H表示用Unicode编码映射这是中英文混排能正确显示的核心。如果你用其他编码很可能还是乱码。BaseFont.NOT_EMBEDDED表示不把字体文件嵌入PDF。这样生成的PDF体积小很多但换到没有这个字体的电脑上可能显示异常。如果是内部存档问题不大如果要发给客户或者打印建议用BaseFont.EMBEDDED代价是PDF文件会大几百KB。simsun.ttc的ttc后缀是Windows字体集合格式iTextSharp支持这种格式。如果你偏好微软雅黑路径换成msyh.ttc用法一样。每次导PDF都要写一遍字体注册代码很啰嗦我的习惯是在项目里做一个静态类统一管理字体public static class PdfFontHelper { private static Font _normalFont; private static Font _boldFont; public static Font NormalFont { get { if (_normalFont null) { _normalFont CreateFont(12f, Font.NORMAL); } return _normalFont; } } public static Font BoldFont { get { if (_boldFont null) { _boldFont CreateFont(12f, Font.BOLD); } return _boldFont; } } private static Font CreateFont(float size, int style) { string fontPath Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Fonts), simsun.ttc); BaseFont bf BaseFont.CreateFont(fontPath, BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED); return new Font(bf, size, style); } }这样在业务代码里直接new Paragraph(设备名称, PdfFontHelper.BoldFont)就可以了代码干净也不会每次都去拿字体文件。3.2 表格构建PdfPTable和单元格样式PDF报告里最常见的需求就是表格。iTextSharp的表格API我个人觉得设计得挺顺手核心就两个类PdfPTable和PdfPCell。先看一个稍微复杂点的表格示例我用的是设备检测报告的场景// 创建一个5列的表格列宽按比例分配 PdfPTable table new PdfPTable(5); table.WidthPercentage 100f; // 表格占页面宽度100% table.SetWidths(new float[] { 1f, 2f, 1.5f, 1.5f, 2f }); // 设置表头行 string[] headers { 序号, 检测项目, 标准值, 实测值, 判定结果 }; foreach (string header in headers) { PdfPCell cell new PdfPCell(new Phrase(header, PdfFontHelper.BoldFont)); cell.BackgroundColor BaseColor.LIGHT_GRAY; cell.HorizontalAlignment Element.ALIGN_CENTER; cell.VerticalAlignment Element.ALIGN_MIDDLE; cell.Padding 5f; table.AddCell(cell); } // 添加数据行 for (int i 0; i dataList.Count; i) { table.AddCell(new Phrase((i 1).ToString(), PdfFontHelper.NormalFont)); table.AddCell(new Phrase(dataList[i].ItemName, PdfFontHelper.NormalFont)); table.AddCell(new Phrase(dataList[i].StandardValue, PdfFontHelper.NormalFont)); table.AddCell(new Phrase(dataList[i].ActualValue, PdfFontHelper.NormalFont)); PdfPCell resultCell new PdfPCell(new Phrase(dataList[i].Result, PdfFontHelper.NormalFont)); // 根据判定结果设置不同的文字颜色 if (dataList[i].Result 合格) { resultCell.BackgroundColor new BaseColor(0, 200, 0); } else { resultCell.BackgroundColor new BaseColor(255, 100, 100); } resultCell.HorizontalAlignment Element.ALIGN_CENTER; table.AddCell(resultCell); } document.Add(table);这里有几个容易忽略的细节SetWidths方法的参数数组长度必须和表格列数一致否则会抛异常AddCell是按行顺序填充的5列表格添加了5个表头单元格之后第6个单元格自动换行到第二行单元格对齐方式有水平对齐HorizontalAlignment和垂直对齐VerticalAlignment两个属性都要设不然默认左上角对齐看起来会很别扭。还有个小技巧如果表格内容太多可以在PdfPCell构造时传入跨列数例如new PdfPCell(phrase) { Colspan 2 }这在做合并单元格时特别有用。3.3 插入数据曲线截图图片混排上位机报告里通常要贴数据曲线图我的做法是把Chart控件或第三方曲线控件绘制的结果直接保存成图片再塞进PDF。用iTextSharp的Image对象非常方便// 截取控件图像保存为图片 Bitmap btn new Bitmap(chartControl.Width, chartControl.Height); chartControl.DrawToBitmap(btn, new Rectangle(0, 0, chartControl.Width, chartControl.Height)); btn.Save(D:\chart.png, System.Drawing.Imaging.ImageFormat.Png); // 将图片插入PDF Image chartImage Image.GetInstance(D:\chart.png); chartImage.ScaleToFit(520f, 280f); // 等比缩放到最大宽度520最大高度280 chartImage.Alignment Element.ALIGN_CENTER; document.Add(chartImage);ScaleToFit这个方法的内部逻辑是等比缩放不会把图拉伸变形。需要注意因为PDF页面默认单位是点point1英寸等于72点A4纸宽度大约是595点所以图片最大宽度我设置520点留出左右边距后正好合适。如果你用510以下的宽度视觉上会显得图片偏小。如果不想生成临时图片文件也可以用MemoryStream的方式using (MemoryStream ms new MemoryStream()) { btn.Save(ms, System.Drawing.Imaging.ImageFormat.Png); Image pdfImage Image.GetInstance(ms.ToArray()); pdfImage.ScaleToFit(520f, 280f); document.Add(pdfImage); }这样可以避免临时文件残留在反复导出测试的时候特别省心——不然临时文件夹里全是chart_1.png、chart_2.png这种垃圾文件。4. iTextSharp导出PDF必踩的坑4.1 字体文件缺失与字体路径写死的问题我第一版代码是直接把字体路径写死的C:\\Windows\\Fonts\\simsun.ttc。在自己机器上跑得好好的结果部署到客户现场的工控一体机上导出PDF全是方块乱码折腾了半天才发现那台机器系统瘦身过宋体文件被精简掉了。后来我换成用Environment.GetFolderPath(Environment.SpecialFolder.Fonts)动态获取系统字体目录再用File.Exists判断文件是否存在不存在就回退到微软雅黑再不行就弹提示让客户装字体这个问题才算彻底解决。还有个要注意的点如果你决定用EMBEDDED模式把字体嵌进去生成的PDF文件体积会显著变大。有一种更优雅的方案是只嵌入用到的字符子集但iTextSharp 5.x对中文子集嵌入的支持比较弱实测下来反而容易出问题。我的建议是内部系统用NOT_EMBEDDED就好PDF文件才几十KB发给客户如果对方显示不正常再考虑转成图片版PDF。4.2 文件被占用和流未关闭的问题初学的时候很容易这么写FileStream fs new FileStream(filePath, FileMode.Create); PdfWriter.GetInstance(document, fs); document.Open(); // ... 添加内容 document.Close(); fs.Close();如果中间某个环节抛异常document.Close()和fs.Close()都不会执行文件就一直被进程占用。下次再点导出按钮就会报文件正在被另一个进程使用。正确写法是包在using或者try-catch-finally里Document document null; try { document new Document(PageSize.A4, 40f, 40f, 40f, 40f); using (FileStream fs new FileStream(filePath, FileMode.Create)) { PdfWriter.GetInstance(document, fs); document.Open(); // ... 添加内容 } } catch (Exception ex) { MessageBox.Show(导出失败 ex.Message); } finally { if (document ! null document.IsOpen) { document.Close(); } }注意using块结束时fs会ReleaseComObject并关闭文件句柄document在finally里保证必定关闭。这样即使中间抛异常文件流也能被释放下次再导出不会被锁住。另外一个隐藏问题是Document对象关闭之后不能再重新打开往里面加内容。如果需求是一份报告多个模块分别生成后再合并要用PdfCopy或PdfImportedPage的方式去处理这块本文不展开但你先有个概念——Document是单向的开-写-关闭环。4.3 表格跨页时表头不重复的尴尬表格数据一多自动分页是好事但默认情况下iTextSharp把PdfPTable当作一个整体内容来排版跨页之后第二页不会自动重复表头。客户拿到一份5页的报告从第2页开始就不知道每列代表什么了非常不专业。解决方式很直接设置表头的重复行数table.HeaderRows 1; // 前1行作为表头跨页时自动重复这个属性可以在创建表之后立刻设置。如果你有合并表头的需求比如第一行是大分类、第二行是字段名就设置HeaderRows 2。不过要注意的是设置HeaderRows之后表示这些行只会出现在每一页开头不会出现在表格末尾如果你的第一行有大合并单元格跨列某些情况下跨页显示会有点怪异这时候建议你测试一下实际效果。还有个分页时的排序问题在遍历大量数据行添加单元格时我习惯每满20行就手动调用document.Add(table)然后新建PdfPTable继续填这样控制每页的数据行数避免表格行被拦腰截断。这个技巧在导出几百条检测记录的时候特别关键效果比自动分页整齐得多。5. 从Demo到能用的进阶思路5.1 用事件给每页加页眉页脚和页码基础Demo跑通后下一个需求大概率是帮我在每页底部加上页码页眉放公司logo和报告编号。iTextSharp里实现这个功能是通过PdfPageEventHelper这个事件类它不像Html里有现成的header footer标签需要自己写public class PageNumberEventHandler : PdfPageEventHelper { private Font _footerFont; public PageNumberEventHandler(Font footerFont) { _footerFont footerFont; } public override void OnEndPage(PdfWriter writer, Document document) { PdfContentByte cb writer.DirectContent; string text 第 writer.PageNumber 页; float textWidth _footerFont.GetCalculatedBaseFont().GetWidthPoint(text, _footerFont.Size); // 在页面底部居中显示页码 cb.BeginText(); cb.SetFontAndSize(_footerFont.GetCalculatedBaseFont(), _footerFont.Size); cb.SetTextMatrix((document.PageSize.Width - textWidth) / 2, 20f); cb.ShowText(text); cb.EndText(); } }用的时候注册到PdfWriter上一句代码搞定PdfWriter writer PdfWriter.GetInstance(document, fs); writer.PageEvent new PageNumberEventHandler(PdfFontHelper.NormalFont);这样每一页结束的时候都会触发OnEndPage方法往页面底部写页码。你也可以在同一个事件类里重写OnStartPage方法画页眉原理一样。唯一要注意的是GetWidthPoint算文字宽度这个细节——中文字符宽度用默认的12磅宋体是能算准的但如果字体没注册成功这里会返回一个错误值导致页码位置偏移所以务必保证字体注册成功后再用这个方法。5.2 SaveFileDialog代替写死路径和异步导出Demo里写死文件路径没问题实际项目里一定要用SaveFileDialog让用户选位置同时把耗时操作丢到异步线程里避免UI卡死。我最终的工具方法长这样private async void btnExport_Click(object sender, EventArgs e) { using (SaveFileDialog sfd new SaveFileDialog()) { sfd.Filter PDF文件|*.pdf; sfd.FileName $检测报告_{DateTime.Now:yyyyMMdd_HHmmss}.pdf; if (sfd.ShowDialog() DialogResult.OK) { btnExport.Enabled false; btnExport.Text 正在导出...; try { await Task.Run(() GeneratePdf(sfd.FileName)); MessageBox.Show($导出成功\n文件位置{sfd.FileName}, 提示); } catch (Exception ex) { MessageBox.Show(导出失败 ex.Message, 错误); } finally { btnExport.Enabled true; btnExport.Text 导出PDF报告; } } } } private void GeneratePdf(string filePath) { // 上述所有构建文档内容逻辑放在这里 }异步导出这块有个容易被忽略的坑如果在后台线程里访问了UI控件比如读取DataGridView里的数据会抛跨线程访问异常。正确做法是在进入Task.Run之前先把数据集合快照出来传入方法或者用Invoke回调UI线程取值。我习惯封装一个数据模型集合把界面数据和PDF生成彻底解耦这样导出方法本身也更容易写单元测试。5.3 内存流方式输出到别处除了生成文件iTextSharp还可以直接输出到MemoryStream这就给导出到指定位置以外的场景留了空间。比如你把PDF输出成字节数组然后直接上传到服务器或者用Process.Start打开PDF阅读器预览MemoryStream ms new MemoryStream(); Document doc new Document(PageSize.A4); PdfWriter.GetInstance(doc, ms); doc.Open(); // ... 添加内容 doc.Close(); // 把PDF字节数组保存为临时文件并调用系统默认PDF阅读器打开预览 byte[] pdfBytes ms.ToArray(); string tempFile Path.Combine(Path.GetTempPath(), preview.pdf); File.WriteAllBytes(tempFile, pdfBytes); Process.Start(tempFile);这个方案我实测在客户现场也非常好用——有些客户电脑上PDF阅读器没关联好双击文件没反应用Process.Start直接调用默认程序大概率能弹起来。而且因为内容都在内存里也没有文件占用、临时文件残留之类的问题。我后来把导出模块做成了一个独立类库所有业务系统共用一套PDF生成逻辑字体注册、页眉页脚、页边距、图片缩放这些统统封装好。新项目里只需要拼数据、调方法基本上半天就能搞定一个带完整格式的PDF报告导出功能。这就是从Demo到工程化的过程——先把一个简单功能跑通再逐步把会变的部分抽成参数、把重复的部分收敛成工具类最后你会发现PDF导出不再是什么让人发怵的活而是一个很标准的模块而已。本文还有配套的精品资源点击获取