技术文章

在 Delphi 中将 TDataSet 和报表导出为 PDF

要在 Delphi 中将 TDataSet 或报表导出为 PDF,losLab PDF Library 提供了两条路径。PDFlibTableExport 可以将任何 TDataSet(例如 FireDAC 查询、ClientDataSet 或内存表)转换为分页 of PDF 表格;而位于 Addons 下的三个桥接组件则可以将准备好的 FastReport、QuickReport 或 ReportBuilder 报表传递给同一个 PDF 写入器。两者都可以在没有打印机驱动程序和可见窗口的情况下生成真正的 PDF

这两个问题看起来相似,但实际上并不同。DBGrid 或原始查询结果本身没有布局,因此导出它意味着要重新设计一个布局:列、表头、分页。而 FastReport 或 ReportBuilder 文档已经包含了设计好的布局,因此导出它意味着将别人的绘制命令忠实地重放到 PDF 空间中。losLab PDF Library 正是因为它们的失败模式不同,才将这些功能放在不同的单元中,本文的其余部分将详细介绍每种方法以及它们不再可靠的边界情况

如何在 Delphi 中将 TDataSet 导出为 PDF?

PDFlibTableExport 只需一次调用即可将数据集转换为表格。导出器会遍历一次字段列表,自动跳过二进制 Blob 字段(ftBlobftGraphicftBytes 及其同类字段没有有用的单元格文本),将数值列右对齐,并在设置了样式的表头栏上绘制可选的斑马纹。在底层,它使用 CreateTable 构建网格,使用 SetTableCellContent 填充单元格,并使用 DrawTableRows 进行渲染,这与您手动操作的公共 Table API 相同。便捷包装器 PDFlibExportDataSet 会为您配置毫米单位和左上角原点,因此调用者只需提供一个页面即可

uses
  Data.DB, FireDAC.Comp.Client, FireDAC.Stan.StorageBin,
  PDFlibrary, PDFlibTableExport;

var
  MemTable: TFDMemTable;
  PDF: TPDFlib;
  Options: TPDFlibTableExportOptions;
begin
  MemTable := TFDMemTable.Create(nil);
  PDF := TPDFlib.Create;
  try
    MemTable.LoadFromFile('customer.FDS');   // 任何 TDataSet 都可以在这里使用

    PDF.SetOrigin(1);
    PDF.SetMeasurementUnits(1);              // 毫米
    PDF.SetPageSize('A4');
    PDF.AddStandardFont(4);

    Options := DefaultTableExportOptions;
    Options.Title := 'Customers';
    Options.ColumnWidth := 32;               // 11 fields fit on A4 at 32 mm
    Options.RepeatHeader := True;            // 在每个页面上重新绘制表头

    PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
  finally
    PDF.Free;
    MemTable.Free;
  end;
end;

自动跳过 Blob 是默认行为,但并非无法更改。当您需要对每个字段进行控制时(例如自定义列标签、更窄的宽度或丢弃不是 Blob 的内部键列),可以直接构建 TPDFlibTableExporter 并挂钩其 OnFieldFilter 事件,该事件在每个字段上触发一次并向您传递一个可变规范。将 Include 设置为 False 以丢弃该字段,或设置 ColumnWidthDisplayLabel 来覆盖默认值。该钩子也是您排除不希望导致页面膨胀的宽 Memo 列的地方

分页:在页面之间传递绘制状态

DrawTableRows 承载了分页状态,理解其约定是关键所在。您在调用它时需要提供第一行和最后一行;传入小于 1 的最后一行表示绘制到表格末尾,并受您指定的宽度/高度限制。该函数返回实际绘制的高度,并且 GetTableLastDrawnRow 会报告适合的最后一行。这对参数是您从一个页面传递到下一个页面的状态:当最后绘制的行未达到总数时,您将打开一个新页面并从其后的一行继续。无需重新测量——后续绘制将精确地从前一次调用结束的地方开始

// 导出器如何跨页面继续绘制长表格
PageHeight := PDF.PageHeight;
Y := PageHeight - Options.Top;
Row := 1;
while Row <= TotalRows do
begin
  DrawHeight := Y - Options.BottomMargin;
  // 最后一行 = 0 表示“绘制到末尾”,受 DrawHeight 限制
  PDF.DrawTableRows(TableID, Options.Left, Y, DrawHeight, Row, 0);
  LastDrawn := PDF.GetTableLastDrawnRow(TableID);
  if LastDrawn >= TotalRows then
    Break;                       // 整个表格都适合该页面
  if LastDrawn < Row then
    Break;                       // 安全起见:无正向进展,退出
  PDF.NewPage;
  Row := LastDrawn + 1;          // 从第一行未绘制的行继续
  Y := PageHeight - Options.Top;
end;

该循环也是重复表头所在的地方。在每个后续页面上,导出器可以选择在数据行之前再次绘制第 1 行(即表头),并使用第一次调用 DrawTableRows 返回的高度来偏移其下方的正文。这就是为什么 DrawTableRows 返回的是高度而不是下一行坐标的原因:返回值允许您在不硬编码任何一个大小的情况下,堆叠重新绘制的表头和继续绘制的正文

如何将 FastReport 或 ReportBuilder 报表导出为 PDF?

报表桥接组件采用相反的方法:它们从不设计布局,而是重放布局。每个桥接组件都连接到对应引擎的本机导出接口。PDFlibFRExport 继承自 TfrxCustomExportFilter,并接收 FastReport 的 Memo、图片、形状和线条对象,将每个对象转换为 PDFlibPas 图元。PDFlibRBDevice 继承自 TppFileDevice 并遍历 ReportBuilder 的 DrawCommand 列表。PDFlibQRExport 则完全采用第三种路线,下一节将对此进行介绍。这三者都以无头模式运行,这正是在服务器上使用它们的原因

无头运行并非没有代价,FastReport 就是一个警示案例。TfrxReport.Export 会路由过预览页面,这些页面会查询过滤器的 ShowDialog 属性,而该属性默认继承为 True。在没有交互式桌面的机器上,模式对话框会返回取消结果,导致导出悄然失败。在调用 Export 之前将 ShowDialog 设置为 False,报表即可静默渲染。ReportBuilder 具有类似的开关(AllowPrintToFileShowPrintDialog),而通过 Prepare 驱动一切的 QuickReport 则完全不需要抑制对话框

var
  Exporter: TPDFlibFRExport;
begin
  Report.PrepareReport;                  // 先构建页面
  Exporter := TPDFlibFRExport.Create(nil);
  try
    Exporter.FileName := 'invoice.pdf';
    Exporter.ShowDialog := False;        // 无头模式:跳过模式对话框,不取消
    Report.Export(Exporter);
  finally
    Exporter.Free;
  end;
end;

三个引擎,三个坐标系

每个桥接组件都执行自己的坐标运算,因为每个引擎对世界的测量方式都不同。PDFlibFRExport 将 FastReport 对象位置视为 96 PPI 下的像素,并乘以 96/25.4 来转换为毫米。PDFlibRBDevice 读取千分之一毫米单位下的 ReportBuilder 绘制命令并除以 1000,并且它通过 ppRegisterDevice 注册自身,因此将 ppReport.DeviceType 设置为 'PDFlibPas' 并调用 Print 就足以将输出路由过它。PDFlibQRExport 根本不进行坐标计算:准备好的 QuickReport 页面已经是 EMF 图元文件,因此该桥接组件将每个页面保存到流中并将其送入 ImportEMFFromStream,让该库的 设备上下文和图元文件渲染路径 放置每个字形和线条。相比之下,FastReport 和 ReportBuilder 桥接组件会发射 原生矢量绘制图元 和原生文本

var
  PDF: TPDFlib;
begin
  PDF := TPDFlib.Create;
  try
    PDF.SetOrigin(1);
    // 一次调用即可准备报表并追加每个页面;桥接组件
    // 通过 ImportEMFFromStream 将每个页面的 EMF 转换为 PDF
    PDFlibQRExportReport(PDF, QuickRep1, 'ledger.pdf');
  finally
    PDF.Free;
  end;
end;

保真度的终点

在将工作流程提交给这些桥接组件之前,请先了解其边界。数据集导出器会静默丢弃二进制列,因此必须显示嵌入图像 the 报表需要与纯表格不同的方法。由于 QuickReport 路径通过 EMF,它会将文本烘焙为矢量字形——页面看起来正确,但不包含可选择或可搜索的文本,也没有用于可访问性的 标记 PDF 结构。FastReport 和 ReportBuilder 保留了真实的文本,但类型化处理程序仅覆盖了常见视图(Memo、图片、形状、线条),而对于其他特殊的视图则会退回到边界框或直接跳过,因此依赖富文本、条形码或渐变填充的报表会丢失细节。这并非缺陷,而是转换层客观存在的局限性

有一个操作上的警告尤为重要。这三个引擎都不在 losLab PDF Library 内部提供,且桥接组件被特意排除在主构建之外——它们仅在搜索路径中已包含 FastReport VCL 6.x、QuickReport 8 或 ReportBuilder 20 的项目中才能编译。它们所针对的图元文件和 DrawCommand 接口在几个主要引擎版本中都保持稳定,但您需要自行保证版本的匹配。处理好这一点,这两条路径就能覆盖所有情况,从即席 DBGrid 转储到精心设计的发票。表格导出器和报表桥接组件都随适用于 Delphi 和 C++Builder 的 losLab PDF Library 一起提供