questpdf 用 c# 声明式构建 pdf,document 不可变,必须调用 generatepdf() 导出;需手动注册中文字体(如 noto sans cjk sc)并指定 fontfamily;表格跨页须用 pageablerow 或 column 模拟;大数据导出应流式构造避免 oom。

QuestPDF 生成 PDF 不需要模板引擎或 HTML 渲染层,直接用 C# 对象描述布局——但如果你照着旧版 iTextSharp 或 PdfSharp 的思路去写,大概率会卡在文档结构和生命周期上。
为什么 Document.Create() 必须配合 .GeneratePdf() 而不能只调用 .Show()?
QuestPDF 的 Document 是不可变构建器,Create() 返回的是一个待渲染的声明式描述,不是可执行对象。它不持有流、不打开文件句柄,必须显式触发生成动作。
-
.Show()只适用于 WPF 预览(需引用QuestPDF.Fluent+QuestPDF.Views),且仅限开发调试,不能用于服务端导出 - 生产环境必须用
.GeneratePdf()或.GeneratePdfAsync(),它们接受Stream参数,把字节写入目标位置 - 常见错误:调用
doc.Show()后没看到 PDF 文件——因为没生成,也没抛异常,只是静默失败
如何正确设置中文支持(微软雅黑/思源黑体)?
QuestPDF 默认不嵌入中文字体,遇到中文会显示方块或空白。它不自动读取系统字体,必须手动注册并指定使用。
- 先用
FontCollection.Default.Register()注册 TTF 文件路径,例如:FontCollection.Default.Register("C:/Windows/Fonts/msyh.ttc") - 注册后必须在文档中显式调用
.FontFamily("Microsoft YaHei")(注意名称要和字体内部 PostScript 名一致,不是文件名) - 推荐用思源黑体(
NotoSansCJKsc-Regular.otf),开源免费且兼容性好;注册后用.FontFamily("Noto Sans CJK SC") - 如果部署到 Linux 服务器,不能依赖
C:/Windows/Fonts,要把字体文件随程序发布,并用相对路径或Assembly.GetExecutingAssembly().GetManifestResourceStream()加载
表格内容超长被截断?用 PageableRow 还是 Table?
QuestPDF 的 Table 默认不支持跨页,整行会强制挤在同一页面上。一旦某列文本过长或行高突增,就导致内容消失或报 LayoutException: Unable to measure element。
- 短表格(确定不超过一页)用
.Table(table => { ... })简洁清晰 - 含动态数据、可能跨页的表格,必须改用
.PageableRow()+ 手动分页逻辑,或者更稳妥地用Column().Item().Element()模拟表格行为 - 关键技巧:给每行加
.MinHeight(20)和.PaddingVertical(4),避免因空行或换行导致高度塌陷 - 不要对
Table套.OverflowY(OverflowBehavior.Wrap)——这个设置只对单个Text有效,对表格无效
如何避免内存溢出(OOM)导出万级数据?
QuestPDF 在生成大文档时会缓存所有元素的布局状态,若一次性构建整个文档对象树,很容易触发 GC 压力甚至 OutOfMemoryException。
- 禁用:不要写
Document.Create(() => RootElementWithManyItems()),尤其当WithManyItems()是 foreach 循环生成上千个Item() - 启用流式构造:用
IEnumerable<icontainer></icontainer>分批 yield 数据项,在.Children()中传入迭代器 - 示例:用
.Children(data.Select(x => Container().Text(x.Name)))替代.Children(data.Select(x => new Text(x.Name)).ToList()),避免中间 List分配 - 导出前调用
GC.Collect()并等待完成(仅限后台服务场景),能缓解但不能根治——根本解法还是分页导出或限制单次导出条数
QuestPDF 的“声明式”本质意味着你写的不是指令序列,而是布局契约。最容易被忽略的点是:它没有“重绘”“刷新”或“重排版”概念,一旦 Document 构建完成,所有尺寸、换行、分页都已静态决定——所以调试时别盯着运行时看效果,要检查构建逻辑本身是否产生了合法的嵌套结构。










