技術文章

在 C++Builder 中動態建立與釋放 HotPDF 元件

在設計階段將 THotPDF 拖曳到表單上,對於快速製作原型來說沒問題,但這會將元件與表單的生命週期綁在一起,而這通常不是生產環境程式碼所想要的。一個在每次點擊按鈕時執行的報表產生器、一個批次處理夜間匯出的服務執行緒、或者一個根本沒有表單的輔助類別:在這些情況下,您會希望該元件的存在時間恰好等於一個 PDF 任務的時間,然後隨即消失。這意味著必須在執行階段配置 (runtime allocation),而在撰寫第一行程式碼之前,有兩件事值得了解:誰擁有這個物件,以及當發生錯誤時清理工作是如何執行的

VCL 中的擁有者語意 (Owner semantics)

每個 VCL 元件的建構式都接受一個型別為 TComponent*Owner 參數。傳入 this(該表單)會將新物件註冊到表單的擁有元件清單中,因此如果表單在元件仍然存活時被銷毀,VCL 會自動釋放它。傳入 nullptr 則表示沒有擁有者:您將全權負責這個指標,且如果例外狀況在您明確呼叫 delete 之前就展開堆疊 (unwinds the stack),沒有任何機制會為您清理它

對於在單一函式內完成的一次性匯出任務,這兩種選擇都可行,但它們有不同的失敗模式。以 this 作為擁有者,只要表單最終被關閉,就不可能發生記憶體洩漏;使用 nullptr,則該指標必須到達 __finally 區塊。在實務上,對於短命的物件,nullptr 加上 __finally 的模式會稍微乾淨一點,因為它讓生命週期的邊界一目瞭然,並避免表單累積那些原本預期只是暫時存在的擁有物件

例外狀況安全 (Exception-safe) 結構

PDF 的產生可能會因為一些與 API 無關的原因而失敗:輸出目錄為唯讀、遺失字型檔案、串流提早刷新,或者是呼叫端提供的資料達到了長度限制。無論原因為何,都必須執行清理路徑。保證能做到這點且符合 C++Builder 慣用作法的是使用 try/__finally

#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#pragma package(smart_init)
#pragma link "HPDFDoc"
#pragma resource "*.dfm"

TForm1 *Form1;

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
    THotPDF* Pdf = new THotPDF(nullptr);
    try
    {
        Pdf->FileName = "output.pdf";
        Pdf->Compression = cmFlateDecode;
        Pdf->FontEmbedding = true;
        Pdf->BeginDoc();
        Pdf->CurrentPage->SetFont("Arial", TFontStyles(), 12);
        Pdf->CurrentPage->TextOut(72, 720, 0, L"Hello from C++Builder");
        Pdf->EndDoc();
    }
    __finally
    {
        delete Pdf;
    }
}

清單中有幾件事值得特別提出。擁有者是 nullptr,這使得生命週期非常明確。CompressionFontEmbedding 是在 BeginDoc 之前設定的:這兩個都是文件層級的選項,HotPDF 會在文件開啟時提交它們,而在開啟後才指派它們將不會有任何效果。TextOut 的座標單位是點 (points),從頁面的左下角開始測量,Y 軸向上遞增;72, 720 這組座標會將文字放在 Letter 尺寸頁面靠近左上角的位置,並帶有一英寸的左邊界。__finally 區塊中的 delete Pdf 無論在 BeginDoc、繪製或 EndDoc 是否引發例外狀況,都會執行

請避免在 delete 之後呼叫 Pdf 的任何方法。如果該指標被儲存在成員變數中,請在刪除後立即將其設為 nullptr,這樣一來,任何意外的後續存取都會產生乾淨的崩潰,而不是默默發生記憶體損毀

專案設定

C++Builder 透過 include 路徑、函式庫路徑與 pragma 指令的組合來定位 THotPDF。產生的標頭檔與 HotPDF 原始碼目錄中的 HPDFDoc.pas 放在一起;請將該目錄新增至 Project > Options > C++ Compiler > Include path#pragma link "HPDFDoc" 指令告訴連結器引入編譯後的 unit,而無需手動將其列在專案檔中。如果您使用的是執行階段套件而非靜態連結,請先安裝 HotPDF 設計階段與執行階段套件;該 pragma 依然適用

請保持 unit 名稱 HPDFDoc 不變。C++Builder 是從 Pascal unit 名稱衍生出標頭檔名稱,因此重新命名檔案或在 pragma 中使用路徑別名,都會默默破壞尋找機制

作用域與多文件任務

對於由使用者操作觸發的單次匯出,使用作用域限定在按鈕處理常式內的區域變數是正確的解答:它在一個呼叫框架 (call frame) 內被建立、使用與銷毀,且對於日後閱讀程式碼的人來說,意圖非常明顯。設計階段的替代方案只有在同一個表單驅動連續的工作流程時才合理,例如一個列印預覽面板,當使用者變更設定時會重建文件;在這種情況下,讓元件保持存活並重複呼叫 BeginDoc/EndDoc,會比反覆配置與釋放堆積物件所帶來的干擾小

對於循序產生許多文件的批次任務來說,每個文件限制一個 THotPDF 作用域是值得付出配置開銷的。如果沒有物件承載,狀態就不會在文件之間傳遞,這是您永遠不必除錯的一類間歇性錯誤。配置、產生、刪除、重複

在一些 HotPDF 範例中出現的一個屬性是 AutoLaunch,它會在 EndDoc 之後立即在系統 PDF 檢視器中開啟產生的檔案。這在撰寫佈局初稿時很有用。但在生產環境中,請跳過它:明確地開啟輸出路徑,驗證檔案存在且大小不為零,記錄結果,然後讓呼叫的工作流程來決定是否需要檢視器。在批次任務中,AutoLaunch 會為每份文件啟動一個檢視器視窗,並且在某些系統上會阻塞程序直到檢視器關閉

此處所展示的 THotPDF 元件及所有繪製呼叫皆是 HotPDF Component(適用於 Delphi 與 C++Builder)的一部分