技術記事

HotPDFを使用したDelphiでのデータテーブルのPDFへのレンダリング

データセットは行と列です。PDFページは空白の座標グリッドであり、どちらの概念もありません。そのギャップを埋めるのがここでの全作業です。HotPDFには、データセットを受け取り、フォーマットされたグリッドを返す DrawTable のような呼び出しはありません。代わりに提供されるのは、グリッドを構成するプリミティブです。ポイントに文字列を配置する TextOut 、書体を選択する SetFont 、帯をシェーディングする RectangleFill 、そして罫線を描画する MoveTo / LineTo / Stroke です。機能するテーブルエクスポーターとは、行と列の考え方を明示的なx座標とy座標に変換し、データがページの下部を超えたときにそれらの座標を正確に保つための規律です

以下の例では顧客レコードをレポートしますが、描画コード内の何も、行がどこから来たのかを知ることも気にかけることもありません。元のコードはレガシーな TTable を使用していましたが、FireDACクエリ、インメモリデータセット、または単純なレコードの配列でも、同じルーチンにそのまま供給されます。重要なのは、データを一度に1行ずつ歩き、各行から4つの文字列フィールドを読み取れることだけです。レンダリングをデータソースから分離しておけば、どちらの側を変更しても他方を妨げることはありません

列のジオメトリが最初

文字を一つ描画する前に、各列がどこに位置するかを決定します。ここでのテーブルには4つの列があるため、4つの左端と既知の右マージンが必要です。クイックサンプルによくあるように、すべての TextOut 呼び出しにマジックナンバーをハードコーディングすることは、まさに後でテーブルを広げるのを苦痛にする原因です。左下の原点からのポイント単位でエッジに一度名前を付ければ、すべての描画呼び出しは名前でそれらを参照します

const
  ColNo   = 70;    // left edge of the "No." column
  ColName = 110;   // company name
  ColAddr = 300;   // street address
  ColCity = 480;   // city
  RowLeft = 50;    // table frame: left rule
  RowRight = 570;  // table frame: right rule
  RowStep = 20;    // vertical distance between baselines

procedure PrintRow(Page: THPDFPage; Y: Single;
  const ANo, AName, AAddr, ACity: string; Shaded: boolean);
begin
  if Shaded then
  begin
    // A shaded band behind the row. Rectangle takes X, Y, Width, Height.
    Page.SetRGBFillColor($00FFF3DD);
    Page.Rectangle(RowLeft, Y - 4, RowRight - RowLeft, RowStep);
    Page.Fill;
    Page.SetRGBFillColor(clBlack);
  end;
  Page.TextOut(ColNo,   Y, 0, ANo);
  Page.TextOut(ColName, Y, 0, AName);
  Page.TextOut(ColAddr, Y, 0, AAddr);
  Page.TextOut(ColCity, Y, 0, ACity);
end;

ここで2つの詳細がその価値を証明しています。影付きの帯が最初に描画され、その上にテキストが描画されます。PDFでは描画順序がzオーダーだからです。テキストの後に四角形を塗りつぶすと、行が埋もれてしまいます。そして、交互に繰り返される影は、それ自体を目的とした装飾ではありません。密度の高いレポートでは、視線が間違った行に滑り落ちるのを防ぐ最も安価な方法です。そのため、ループは行ごとにブール値を反転させ、それを Shaded に直接渡しています

上記の列の位置は固定されており、スキーマを制御できるレポートとしては妥当です。データが可変の場合は、推測する代わりに測定します。HotPDFはページオブジェクト上でテキスト幅の測定を公開しているため、 PrintRow の製品版では、各列で予想される最も長い値を受け取り、選択したフォントサイズで一度測定し、それらの幅にガターを加えたものから左端を導出できます。ルーチンの形は変わりません。定数のソースのみが変わります

ヘッダー、罫線、そしてそれらを所有する1つの場所

ページからスクロールアウトし、列ラベルなしで次のページで再開するテーブルは読めません。この修正は、ヘッダーを一度描画するものではなく、再描画するものとして扱うことです。列のタイトルとそれを囲む水平方向の罫線を1つのルーチンに配置し、最初と、新しいページを開くたびにそのルーチンを呼び出します。ヘッダーと本文は同じ列定数を共有するため、構造によって整列します

procedure DrawHeader(Page: THPDFPage; var Y: Single; PageNo: Integer);
begin
  // Left: source label and page number. Right: generation time.
  Page.SetFont('Arial', [fsItalic], 10);
  Page.TextOut(RowLeft, Y, 0, 'customer.db   Page ' + IntToStr(PageNo));
  Page.TextOut(ColCity, Y, 0, DateTimeToStr(Now));

  // Two horizontal rules that box the column titles.
  Page.MoveTo(RowLeft, Y + 15);
  Page.LineTo(RowRight, Y + 15);
  Page.MoveTo(RowLeft, Y + 45);
  Page.LineTo(RowRight, Y + 45);
  Page.Stroke;

  // The column titles, in a heavier face so they read as headings.
  Page.SetFont('Times New Roman', [fsBold], 12);
  Page.SetRGBFillColor(clNavy);
  PrintRow(Page, Y + 25, 'No.', 'Company', 'Address', 'City', False);
  Page.SetRGBFillColor(clBlack);

  Y := Y + RowStep + 45;  // advance past the boxed header before the first body row
end;

DrawHeaderY を参照渡しで受け取り、前方に移動させることに注意してください。呼び出し元は、ヘッダーの高さがどれくらいかを覚えておく必要はありません。それを描画するルーチンがそれを知っているルーチンです。後でヘッダーバンドにロゴやフィルターの要約を追加したときに、この単一所有権のルールにより、レイアウトのずれを防ぐことができます。本文のループはそれを認識しません。 Y が現在指している場所から行を描画し続けるだけです

罫線自体が、リストとテーブルの違いです。垂直方向の列区切り文字は、同じアイデアをx軸に適用したものです。各列の端にある MoveTo / LineTo / Stroke が、一番上の罫線からページ上の最後の行の下部まで走ります。サンプルでは読みやすさを保つために水平方向の罫線のみにとどめていますが、列の定数が存在すれば、製品版の手順は機械的なものです

カーソルループは改ページを所有する

描画は簡単な半分です。おもちゃとレポートを分ける半分はページネーションです。つまり、行を描画する前に、それがまだ収まるかどうかを知り、収まらない場合は新しいヘッダー付きの新しいページを開始することです。その決定は、データを歩くループという、ただ1つの場所に属し、他の場所には属しません

var
  Pdf: THotPDF;
  Page: THPDFPage;
  Y: Single;
  PageNo: Integer;
  Shaded: boolean;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'CustomerReport.pdf';
    Pdf.BeginDoc;
    Page := Pdf.CurrentPage;

    // Report title, once, at the top of the first page.
    Page.SetFont('Arial', [fsBold], 24);
    Page.TextOut(200, 800, 0, 'Customer Report');

    PageNo := 1;
    Y := 760;
    DrawHeader(Page, Y, PageNo);
    Shaded := False;

    CustomerTable.First;
    while not CustomerTable.Eof do
    begin
      // Out of room? Open a new page and repeat the header there.
      if Y < 60 then
      begin
        Pdf.AddPage;
        Page := Pdf.CurrentPage;   // AddPage moves CurrentPage forward
        Inc(PageNo);
        Y := 760;
        DrawHeader(Page, Y, PageNo);
      end;

      Shaded := not Shaded;
      Page.SetFont('Arial', [], 10);   // SetFont must be reissued on every new page
      PrintRow(Page, Y,
        VarToStr(CustomerTable['CustNo']),
        VarToStr(CustomerTable['Company']),
        VarToStr(CustomerTable['Addr1']),
        VarToStr(CustomerTable['City']),
        Shaded);

      Y := Y - RowStep;
      CustomerTable.Next;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

2つの座標の事実がループ全体を駆動します。PDFは左下隅から上に向かってyを測定するため、行は毎回 Y から RowStep を減算することでページを下へ進み、ページがいっぱいになったかどうかのテストは、上部を超えたときではなく、 Y が下部マージンを下回ったときに発生します。方向を間違えると、ループがまだ1ページ分のスペースがあると見なしている間に、最初の行が下端を越えて印刷されてしまいます

もう1つの事実は、ほとんどの人が一度は引っかかるものです。 AddPage は新しいページを作成し、 CurrentPage をそこに再指定しますが、フォントも、塗りつぶしの色も、位置も引き継ぎません。そのため、 AddPage の後に毎回 CurrentPage から Page が再読み込みされ、本文の行の前に SetFont が再発行されるのです。再読み込みをスキップすると、置き去りにしたページに描画し続けることになります。フォントをスキップすると、新しいページはビューアがフォールバックするデフォルトでレンダリングされます

テーブルエクスポーターを壊すケース

ほとんどのテーブルのバグは、数十の整然とした行というハッピーパスには現れません。それらはエッジ(境界)に存在し、どこにあるかがわかれば、エッジのテストは安価です

  • 空のデータセット。 ゼロ行のループは、ヘッダーがありその下に何もないページを生成しますが、これは少なくとも意図的であるように見えます。ヘッダーのない空白のページは失敗のように見えます。出荷する前にどちらが必要かを決定してください
  • 境界に正確に着地する行。 最後の行がマージンの1ステップ上にあるレポートを生成し、次に、次の行がマージンの1ステップ下にあるレポートを生成します。オフバイワン(1つずれ)のページネーションは、データが正確に間違った長さになるまで隠れています
  • 長すぎる値。 列よりも幅の広い会社名は、次の列にはみ出します。フィールドを測定し、ポリシーを決定します:2行目に折り返すか、クリップするか、または省略記号で切り捨てます。沈黙はポリシーではありません
  • Nullフィールド。 Nullを TextOut に直接読み込むと、変換方法によっては、リテラルテキストの Null または空白として表面化する可能性があります。バリアント変換に選択させるのではなく、レンダリングを意図的に選択してください

完了したと判断する前に、複数のビューアで結果を実行してください。フォントの置換とクリッピングはレンダラー間で動作が異なり、あるPDFリーダーで四角く見えるテーブルが、別のリーダーではずれた列やクリップされた都市を示すことがあります。繰り返されるヘッダー、行のシェーディング、およびマージンが移動しても残っていること、およびデータが境界を越えた後もページ番号が連続していることを確認してください

ビジュアルなレポートデザイナーに頼るのではなく、自分でグリッドを描画することは、より多くのコードを記述することを意味し、そのトレードオフは率直に名付ける価値があります。すべての座標を自分で所有するということです。これは、すべてのマシンで同一にレンダリングする必要があるサーバー側のバッチジョブ、請求書、および監査エクスポートにまさに必要なものであり、一度きりの内部リストではまさに避けたいオーバーヘッドです。前者については、レポートがあなたのデスク上と同じように本番環境でも見えなければならなくなった最初の時点で、コントロールはそのコストに見合うだけの見返りをもたらします

上記の罫線と影付きの帯は、 キャンバス描画のウォークスルー で取り上げられているのと同じベクトルプリミティブとカラープリミティブに依存しています。 RectangleMoveTo 、および LineTo の呼び出しを最初から単独で扱いたい場合は、そちらを参照してください。ここで使用されている描画プリミティブは、DelphiおよびC++Builder用のHotPDFコンポーネントの一部です