技術記事

C++Builder での HotPDF コンポーネントの動的な作成と解放

設計時に THotPDF をフォームにドロップすることは、簡単なプロトタイプには適していますが、コンポーネントがフォームのライフタイムに結びついてしまうため、本番コードで望まれることはほとんどありません。ボタンのクリックごとに1回実行されるレポートジェネレーター、夜間のエクスポートを一括処理するサービススレッド、フォームをまったく持たないヘルパークラスなど、これらの各状況では、コンポーネントが1つの PDF ジョブの期間中だけ存在し、その後消滅するようにしたいはずです。つまり実行時の割り当てが必要になりますが、コードを書き始める前に理解しておくべき2つの重要な点が変わります。それは、誰がオブジェクトを所有するのか、そして何か問題が発生したときにクリーンアップがどのように実行されるのかということです

VCL における所有者のセマンティクス

VCL コンポーネントのすべてのコンストラクターは、TComponent* 型の Owner パラメーターを受け取ります。this(フォーム)を渡すと、新しいオブジェクトがフォームの所有コンポーネントリストに登録されるため、コンポーネントがまだ生きている間にフォームが破棄された場合、VCL は自動的にコンポーネントを解放します。nullptr を渡すことは所有者がいないことを意味します。ポインターの管理はすべてあなた自身の責任となり、明示的な delete の前に例外がスタックをアンワインドした場合、代わりにクリーンアップしてくれるものは何もありません

1つの関数内で完了する単発のエクスポートの場合、どちらの選択肢も機能しますが、2つの障害モードが異なります。所有者として this を指定した場合、フォームが最終的に閉じる限り、メモリリークは不可能です。nullptr の場合、ポインターは必ず __finally ブロックに到達する必要があります。実際には、有効期間が短いオブジェクトの場合、nullptr__finally を組み合わせるパターンの方がわずかにクリーンです。なぜなら、ライフタイムの境界が一目でわかり、一時的であるはずの所有オブジェクトがフォームに蓄積されるのを防ぐことができるからです

例外セーフな構造

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 であり、ライフタイムが明示的になっています。CompressionFontEmbeddingBeginDoc の前に設定されています。これらはどちらも HotPDF がドキュメントを開くときに確定するドキュメントレベルのオプションであり、後から割り当てても効果がありません。TextOut は、ページの下の左隅からポイント単位で測定された座標を受け取り、Y 軸は上に向かって増加します。72, 720 のペアは、左マージンが1インチのレターサイズページの左上近くにテキストを配置します。__finally ブロック内の delete Pdf は、BeginDoc、描画、または EndDoc が例外を発生させたかどうかに関係なく実行されます

delete 後に Pdf のメソッドを呼び出さないようにしてください。ポインターがメンバー変数に保存されている場合は、削除直後に nullptr に設定して、後で誤ってアクセスした場合に、静かに破損するのではなくクリーンなクラッシュが発生するようにします

プロジェクトの構成

C++Builder は、インクルードパス、ライブラリパス、および pragma ディレクティブを組み合わせて THotPDF の場所を特定します。生成されたヘッダーは、HotPDF のソースディレクトリ内の HPDFDoc.pas と並んで配置されています。そのディレクトリを Project > Options > C++ Compiler > Include path に追加してください。#pragma link "HPDFDoc" ディレクティブは、プロジェクトファイルに手動で記述しなくても、コンパイルされたユニットを引き込むようにリンカーに指示します。静的リンクの代わりにランタイムパッケージを使用している場合は、まず HotPDF のデザインパッケージとランタイムパッケージをインストールしてください。pragma は引き続き適用されます

ユニット名 HPDFDoc は変更しないでください。C++Builder は Pascal のユニット名からヘッダー名を派生させるため、ファイルの名前を変更したり、pragma でパスエイリアスを使用したりすると、ルックアップが静かに失敗します

スコーピングと複数ドキュメントのジョブ

ユーザーアクションによってトリガーされる単一のエクスポートの場合、ボタンハンドラーをスコープとするローカル変数が正しい答えです。つまり、1つのコールフレーム内で作成、使用、破棄され、後でコードを読む誰にとってもその意図が明確です。設計時の代替手段が正当化されるのは、ユーザーが設定を変更するたびにドキュメントを再構築する印刷プレビューパネルなど、同じフォームが継続的なワークフローを推進する場合です。その場合、コンポーネントを有効にしたまま BeginDoc/EndDoc を繰り返し呼び出す方が、ヒープオブジェクトを繰り返し割り当てて解放するよりも中断が少なくなります

多くのドキュメントを順番に生成するバッチジョブの場合、ドキュメントごとに1つの THotPDF をスコープすることで、割り当てのオーバーヘッドに見合う価値があります。状態を保持するオブジェクトが存在しなければ、ドキュメント間で状態が引き継がれることはなく、デバッグする必要のない断続的なバグの1つとなります。割り当て、生成、削除、繰り返しの手順です

いくつかの HotPDF のデモに登場するプロパティの1つが AutoLaunch であり、これは EndDoc の直後に、生成されたファイルをシステムの PDF ビューアーで開きます。これは、レイアウトの最初のドラフトを作成している間に役立ちます。本番環境ではこれはスキップしてください。出力パスを明示的に開き、ファイルが存在し、サイズがゼロではないことを確認し、結果をログに記録して、呼び出し元のワークフローでビューアーが関連するかどうかを決定させます。バッチジョブでは、AutoLaunch はドキュメントごとに1つのビューアーウィンドウを起動し、一部のシステムではビューアーが閉じるのを待機してプロセスがブロックされます

ここに示されている THotPDF コンポーネントとすべての描画呼び出しは、Delphi および C++Builder 用の HotPDF Component の一部です