若要在 Delphi 中將 TDataSet 或報表匯出為 PDF,losLab PDF Library 提供了兩條路徑。PDFlibTableExport 可將任何 TDataSet(例如 FireDAC 查詢、ClientDataSet、記憶體資料表)轉換為分頁的 PDF 表格,而 Addons 底下的三個橋接器則可將準備好的 FastReport、QuickReport 或 ReportBuilder 報表傳遞給同一個 PDF 寫入器。兩者都能產生真正的 PDF,無需印表機驅動程式,也無需顯示任何視窗
這兩個問題看似相似,實則不然。DBGrid 或原始查詢結果本身沒有配置,因此匯出它意味著要重新設計一個:包含欄位、頁首和分頁。而 FastReport 或 ReportBuilder 文件已經包含設計好的配置,因此匯出它意味著要忠實地在 PDF 空間中重現他人的繪圖指令。losLab PDF Library 將這些功能保留在不同的單元中,正是因為它們的失敗模式不同。本文的其餘部分將逐步介紹每種方法以及它們在何處會失去可靠性
如何在 Delphi 中將 TDataSet 匯出為 PDF?
PDFlibTableExport 只要一次呼叫就能將資料集轉換為表格。匯出器會遍歷欄位列表一次,自動跳過二進位 BLOB 欄位(ftBlob、ftGraphic、ftBytes 及其相關類型沒有實用的儲存格文字)、將數值欄靠右對齊,並在設定好樣式的頁首列上繪製選用的斑馬紋。在底層,它使用 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 個欄位在 32 公釐下剛好容納於 A4 頁面
Options.RepeatHeader := True; // 每頁重複繪製頁首
PDFlibExportDataSet(PDF, MemTable, 'customers.pdf', Options);
finally
PDF.Free;
MemTable.Free;
end;
end;
自動跳過 BLOB 是預設行為,而非限制。當您需要針對每個欄位進行控制時(例如自訂欄位標籤、更窄的寬度,或捨棄非 BLOB 的內部鍵值欄位),可以直接建構 TPDFlibTableExporter 並掛載其 OnFieldFilter 事件,該事件會針對每個欄位觸發一次,並向您傳遞可修改的規格。將 Include 設定為 False 以捨棄該欄位,或設定 ColumnWidth 和 DisplayLabel 來覆寫預設值。該勾點(hook)也是您排除不希望使頁面過度膨脹的寬備忘錄(memo)欄位的地方
分頁:在頁面之間傳遞繪製狀態
DrawTableRows 承載了分頁狀態,理解其運作機制是關鍵所在。您在呼叫它時需指定起始列 and 結束列;傳入小於 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 具有類似的參數開關——AllowPrintToFile 和 ShowPrintDialog——而透過 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;
精確度的極限
在將工作流程提交給這些橋接器之前,請先瞭解其限制。資料集匯出器會默默捨棄二進位欄位,因此必須顯示內嵌影像的報表需要與直接表格不同的方法。QuickReport 路徑因為經過 EMF,會將文字轉換為向量字形——頁面看起來正確,但不包含可選取或可搜尋的文字,也沒有用於輔助說明的標記 PDF 結構。FastReport 和 ReportBuilder 保留了真實文字,但類型化處理常式僅涵蓋常見的檢視(memo、圖片、形狀、線條),對於特殊的檢視則會退回到邊界框或直接跳過,因此依賴 Rich Text、條碼或漸層填滿的報表將會遺失細節。這並非缺陷,而是轉換層的真实邊界
有一個操作上的注意事項比其他事項更重要。這三個引擎均未隨附於 losLab PDF Library 內,且橋接器被刻意排除在主建置之外——它們只能在搜尋路徑中已包含 FastReport VCL 6.x、QuickReport 8 或 ReportBuilder 20 的專案中進行編譯。它們所針對的關係圖和 DrawCommand 介面在多個主要引擎版本中都保持穩定,但您需要自行確保版本相容。阻版本正確,這兩條路徑就能涵蓋所有應用,從臨時的 DBGrid 傾印到設計好的發票。表格匯出器和報表橋接器均隨附於適用於 Delphi 和 C++Builder 的 losLab PDF Library