在C#应用中生成PDF并不是只有一个标准答案。不同组件在分页控制、字体嵌入、中文支持、许可协议以及部署复杂度上差异很大,选定方案之前首先要确认这些能力是否满足业务要求。本文以一个控制台项目为例,演示用QuestPDF生成一份带页眉、页脚、动态表格和中文内容的PDF文件,并给出完整可编译的代码。示例覆盖项目创建、依赖引用、文档模型定义和输出保存,修改业务字段即可复用到回执单、发货单或周报导出等场景。

一、C#生成PDF的常用库如何选择
C#生态里可用的PDF库不少,但定位差异很明显。PdfSharp和MigraDoc提供较低层的绘图与表格能力,轻量且免费,不过要处理中文时需要额外配置字体解析,代码量会随着页面复杂而增加。iText系列功能非常完整,适合大型文档处理,但AGPL许可对闭源商业项目限制较多,引入前要确认合规风险。
QuestPDF采用声明式API,页面结构通过Lambda表达式描述,表头、内容区和页脚分离得很清楚。它基于Skia进行排版,中文支持比早期PdfSharp方案省心,而且分页、页码、自动重复表头等能力可以直接使用。对于大多数需要生成报表、回执和合同预览的业务,QuestPDF是开发效率较高的选择。
| 库 | 许可特点 | 中文支持 | 适合场景 |
|---|---|---|---|
| PdfSharp/MigraDoc | MIT,可商用 | 需手动处理字体 | 简单绘图、基础表格 |
| iText7 | AGPL或商业授权 | 较好,需引入字体 | 复杂PDF处理、大型文档 |
| QuestPDF | 社区版可商用 | 依赖系统字体,配置简单 | 报表、回执、合同预览 |
| 商业报表控件 | 通常收费 | 完善 | 企业级报表平台 |
选择QuestPDF还有一个原因:它把数据、页面和组件组合放在同一个C#文件中即可完成,不需要额外设计模板文件。相比HTML转PDF或报表模板方案,部署时少了一层外部依赖,问题排查也更直接。
二、创建项目并安装QuestPDF
先通过dotnet CLI创建控制台程序。在Windows、Linux或macOS终端执行以下命令即可完成项目初始化和依赖安装。
dotnet new console -n PdfDemo cd PdfDemo dotnet add package QuestPDF
安装完成后,需要在程序启动时设置社区许可证。如果不设置,新版QuestPDF会在生成PDF时抛出许可证异常。下面的代码写在Program.cs顶部即可。
using QuestPDF.Infrastructure; QuestPDF.Settings.License = LicenseType.Community;
许可证声明只需要执行一次。设置完成后,后续所有文档生成都会沿用这个配置。对于内部工具、后台服务和商业产品中的文档导出模块,社区许可通常可以满足基本使用要求,但如果涉及大量分发,建议阅读官方许可说明确认细节。
三、完整项目源码:生成带表格和页码的PDF
下面给出一个完整控制台项目的源码。它定义了一个简单的交付单模型,使用QuestPDF的IDocument接口组织页面头部、内容区和页脚。运行后会生成名为delivery-report.pdf的文件。入口方法准备数据,创建文档实例并调用GeneratePdf,文档类在Compose方法中描述页面结构,这是QuestPDF声明式模型的核心。
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
QuestPDF.Settings.License = LicenseType.Community;
var data = InvoiceDataSource.GetInvoiceData();
var document = new InvoiceDocument(data);
document.GeneratePdf("delivery-report.pdf");
Console.WriteLine("PDF已生成:delivery-report.pdf");
public class InvoiceDocument : IDocument
{
public InvoiceData Data { get; }
public InvoiceDocument(InvoiceData data)
{
Data = data;
}
public DocumentMetadata GetMetadata() => DocumentMetadata.Empty;
public void Compose(IDocumentContainer container)
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.Margin(2, Unit.Centimetre);
page.DefaultTextStyle(x => x.FontSize(10).FontFamily("Microsoft YaHei"));
page.Header().Column(column =>
{
column.Item().Text("项目交付报告").FontSize(20).SemiBold();
column.Item().Text($"客户:{Data.CustomerName}").FontSize(11);
column.Item().Text($"项目编号:{Data.ProjectCode}").FontSize(11);
});
page.Content().PaddingVertical(1, Unit.Centimetre).Column(column =>
{
column.Item().Text("交付明细").FontSize(14).SemiBold();
column.Item().Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.RelativeColumn(3);
columns.RelativeColumn(3);
columns.RelativeColumn(1);
});
table.Header(header =>
{
header.Cell().Background(Colors.Grey.Lighten2).Text("模块");
header.Cell().Background(Colors.Grey.Lighten2).Text("说明");
header.Cell().Background(Colors.Grey.Lighten2).Text("状态");
});
foreach (var item in Data.Items)
{
table.Cell().Text(item.Module);
table.Cell().Text(item.Description);
table.Cell().Text(item.Status);
}
});
});
page.Footer().AlignCenter().Text(text =>
{
text.DefaultTextStyle(x => x.FontSize(8));
text.CurrentPageNumber();
text.Span(" / ");
text.TotalPages();
});
});
}
}
public class InvoiceData
{
public string CustomerName { get; set; } = "";
public string ProjectCode { get; set; } = "";
public DateTime DeliveryDate { get; set; }
public List<InvoiceItem> Items { get; set; } = new();
}
public class InvoiceItem
{
public InvoiceItem(string module, string description, string status)
{
Module = module;
Description = description;
Status = status;
}
public string Module { get; set; }
public string Description { get; set; }
public string Status { get; set; }
}
public static class InvoiceDataSource
{
public static InvoiceData GetInvoiceData()
{
return new InvoiceData
{
CustomerName = "北京示例科技有限公司",
ProjectCode = "PRJ-2025-086",
DeliveryDate = new DateTime(2025, 3, 18),
Items = new List<InvoiceItem>
{
new("报名模块", "在线报名与审核", "已完成"),
new("支付模块", "订单创建与退款", "已完成"),
new("报表模块", "数据导出与汇总", "测试中")
}
};
}
}项目文件同样很简单,只需要声明.NET版本和QuestPDF包引用。下面给出完整的csproj内容,PackageReference版本在示例中固定为2024.10.0,实际开发时可以替换为当时的稳定版本。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="QuestPDF" Version="2024.10.0" />
</ItemGroup>
</Project>把上述两个文件放在同一目录后执行dotnet run,程序会在当前工作目录生成delivery-report.pdf。打开后可以看到A4页面、中文标题、三列明细表格以及底部的页码信息。表格数据来自InvoiceDataSource,可以直接替换为数据库查询结果或接口返回对象。
四、中文显示与字体处理的几个关键点
为什么示例刻意在DefaultTextStyle里设置FontFamily("Microsoft YaHei")?因为PDF不是简单把文字画上去,而是需要嵌入或引用字形数据。Windows系统自带微软雅黑,所以本地调试通常没问题;但如果把程序发布到Linux容器,系统可能没有这个字体,中文字符就会显示成方框或空白。
QuestPDF基于Skia文字排版,字体解析依赖操作系统字体。部署到Linux时,可以安装Noto CJK字体,并把默认字体改为Noto Sans CJK SC。如果目标环境不能安装字体,则应当准备一个允许分发的字体文件,在程序启动时注册字体并从项目资源中加载。这样能保证不同机器上生成效果一致。
另一个常见问题是分页。动态表格行数较多时,QuestPDF会自动处理表头重复和内容分页,但需要在Header中定义表头,并把表格放在Content区域。页脚页码使用CurrentPageNumber和TotalPages可以自动维护,不要自行维护计数器,否则翻页后容易错乱。
最后,生成PDF的目录需要具备写入权限。控制台示例相对简单,但在Web应用中不要把文件保存到应用根目录,建议写入专用临时目录或通过流直接返回给客户端。把项目中的CustomerName、Items等字段替换成实际业务实体,就能快速扩展为回执单、发货单、合同预览等文档导出功能。