Delphi から見た目の整った Excel レポートを確実に生成する方法は、デザイナーがすでに作り込んだワークブックから出発することです。経理部門の誰かが Excel で請求書のレイアウトを組みます。ロゴ、列見出し、明細帯の罫線、太字の合計行、通貨書式などです。あなたのコードはそのファイルを開き、デザイナーが確保しておいたセルにライブデータを流し込み、結果を保存します。見た目はデザイナーのものであり、数値はあなたのものです。HotXLS は Excel を動かさずに XLS と XLSX のワークブックを読み書きするネイティブの Delphi/C++Builder ライブラリであり、このアプローチに必要な 3 つの操作を提供します。テキストによるセルの検索、スタイルと数式を保ったままの範囲のコピー、そして下にあるすべてをデータとともにシフトさせる行の挿入です
テンプレートの編集を生き延びるジェネレーターと、最初の編集で壊れてしまうジェネレーターを分けるたった 1 つのルールは、セルを決してリテラルな行番号と列番号で参照しないことです。テンプレートは他の人が編集する文書です。経理チームは税額行を追加し、ロゴ行の高さを上げ、住所ブロックを並べ替えます。ファイルフォーマットはそれについて何も助けてくれません。行10が前四半期と同じ意味を保っているかどうかにかかわらず、BIFF や OOXML の保存は成功します。最初の明細行をハードコードされた行10に書き込むジェネレーターは、誰かが明細セクションの上にブロックを挿入した瞬間、明細項目を誤ったセルに刻印し、もうデータをカバーしていない合計範囲を合計してしまいます。何も例外を投げず、すべての保存が成功を返し、唯一のシグナルは顧客が誤った請求書に気づくことです
すべての座標をプレースホルダートークンに固定する
解決策は、テンプレート自身に座標を運ばせることです。デザイナーは {{CUSTOMER}}、{{DATE}}、{{DETAIL_START}} のようなトークンを、ジェネレーターが触れる必要のあるセルに書き込み、ジェネレーターはそれらのトークンが見つかった場所から、実行時にすべての位置を割り出します。レイアウトの編集はもはや問題になりません。トークンは自分が置かれているセルとともに移動するからです。この契約のもう半分は失敗時のルールです。必須トークンが見つからなければ、顧客データがファイルに到達する前にジョブを停止します。ずれてしまったテンプレートは、配信済みの文書ではなく、失敗したジョブチケットを生むべきです
トークンを見つける: FindText と ReplaceText
HotXLS の 2 つのクラスファミリーはどちらもワークシートレベルの検索を公開しています。FindText はテキストが一致する最初のセルの行と列を返し、大文字小文字を区別するオーバーロードも用意されています。ReplaceText はすべての出現箇所を置き換え、何個変更したかを返します。この 2 つで、通常出会う 2 種類のトークンをカバーできます。顧客名のような単一のアンカーは一度見つけてその隣に書き込み、レポート日付のようにちょうど 1 回だけ出現するべきトークンは置き換えたうえで件数を確認します。XLSX 側でこの方式で自己アンカーする書き込みは次のようになります
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
R, C: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('invoice-template.xlsx') <> 1 then
raise Exception.Create('Cannot open invoice template');
Sheet := Book.Sheets[0]; // TXLSXSheets.Items は 0 始まり
if not Sheet.FindText('{{CUSTOMER}}', R, C) then
raise Exception.Create('Template drift: {{CUSTOMER}} anchor missing');
Sheet.Cells[R, C].Value := 'ACME Corp';
if Sheet.ReplaceText('{{DATE}}',
FormatDateTime('yyyy-mm-dd', Date)) = 0 then
raise Exception.Create('Template drift: {{DATE}} token missing');
// この後で明細の展開と保存が続く
finally
Book.Free;
end;
end;
重要な点が 2 つあります。まず、FindText と ReplaceText はセルのテキスト値と一致します。数式文字列の中に埋め込まれたトークンはこれらから見えないため、プレースホルダートークンは必ずプレーンなセルに置き、決して数式の中には置かないでください。次に、置換件数があなたのドリフト検出器です。本来ちょうど 1 つの {{DATE}} トークンを含むはずのテンプレートが 0 件の置換を報告したなら、それは編集されてしまっています。その瞬間に例外を投げることこそが、静かなレイアウトのずれを目に見える失敗へと変えます
スタイルや数式を失わずに明細行を複製する
請求書の明細セクションはデータとともに伸びます。サンプル行の下にある空の行に値をそのまま書き込むと、デザイナーが用意したものすべて、罫線、数値書式、行ごとの数式が失われます。それらすべてを保つパターンは、テンプレート内にスタイル済みのサンプル行を 1 行だけ残し、項目ごとにそれを複製することです。CopyRange は 1 回の呼び出しでスタイルと数式を複製し、その後ジェネレーターは値のセルだけを上書きします
const
DetailRow = 10; // テンプレート内の書式済みサンプル行
var
I: Integer;
begin
// まず合計ブロックの手前にスペースを開き、SUM 範囲が
// 明細帯の下でデータと一緒に伸びるようにする
if Length(Items) > 1 then
Sheet.InsertRows(DetailRow + 1, Length(Items) - 1);
for I := 0 to High(Items) do
begin
if I > 0 then // サンプル行からスタイルと数式を複製する
Sheet.CopyRange(DetailRow, 1, DetailRow, 5, DetailRow + I, 1);
Sheet.Cells[DetailRow + I, 1].Value := Items[I].Name;
Sheet.Cells[DetailRow + I, 2].Value := Items[I].Qty;
Sheet.Cells[DetailRow + I, 3].Value := Items[I].UnitPrice;
Sheet.Cells[DetailRow + I, 4].Formula :=
Format('B%d*C%d', [DetailRow + I, DetailRow + I]); // 先頭に '=' は付けない
end;
end;
数式の代入部分をよく見てください。XLSX の Formula プロパティは先頭のイコール記号なしで式を受け取りますが、XLS ファサードは '=B10*C10' を Value 経由で代入することを期待します。この 2 つの流儀を混同することは、クラスファミリー間の移植で最もよくある間違いであり、これは何の文句も言わずに失敗します。セルは単にリテラル文字列を保持するだけで、Excel はそれをテキストとして表示します。テンプレートが結合されたタイトル行で明細帯を飾っている場合は、結合領域の左上のセルだけが値を持つことを忘れないでください。レイアウト駆動のレポートテンプレートにおける結合セルに関する関連記事は、結合領域がデータ帯の完全に外側に属する理由を説明しています
InsertRows が移動させるもの、取り残すもの
合計ブロックの手前に行を挿入することが、明細セクションが伸びるのに合わせて SUM 範囲を伸ばし続ける方法です。XLSX 側では、InsertRows はセルとともに長いリストの依存構造も下へ運びます。結合範囲、行の高さ、ハイパーリンク、コメント、ウィンドウ枠の固定、オートフィルタ範囲、条件付き書式、データの入力規則、テーブル、定義名、そして画像とグラフのアンカーです。このリストの中に、記憶しておく価値のある境界が 1 つあります。数式の書き換えは同じシート内の参照にしか及びません。移動した領域を指すサマリーシート上の数式は古い座標を保ったままとなり、静かに誤ったセルを読み続けます。だからこそ、シートをまたぐ合計はワークブックレベルの名前を通じて表現するほうが安全です。定義名とシート横断数式に関する関連記事がこのパターンを扱っています
レガシーの XLS フォーマットは、この線をさらに厳しいところに引きます。HotXLS は BIFF ファイル内のピボットテーブル、クエリテーブル、外部データ接続を生のバイトブロックとして保持します。それらは開いて保存しても変化せずに生き残りますが、モデル化されていないため、行の挿入がそれらに触れることは決してありません。伸び続ける明細ブロックの下にピボットテーブルを配置したテンプレートは、何の警告もなく保存され、その間にピボットのソース矩形はデータからずれていきます。抜け道は防御的なものではなく構造的なものです。ピボットやクエリのコンテンツを、ジェネレーターが決して挿入を行わないシートに置いておけば、この陳腐化は起こり得ません
配信前に再計算する、あるいはなぜそれを省いたのかを把握しておく
HotXLS は SaveAs の間に数式を評価しません。人がファイルを開くと Excel がすべてを再計算するため(それを制御する必要があれば、XLS ファサードは CalculationMode と RecalcOnSave を公開しています)、人間の受信箱に届くレポートにはそれ以上何もする必要がありません。状況が変わるのは、そのワークブックが別のプログラムに供給される瞬間です。CSV エクスポートは数式をそのリテラルなテキストとして書き出すだけで決して計算せず、キャッシュされた値を信頼する下流のパーサーは古い数値や空欄を読み込んでしまいます。そうした経路のためには、サーバー側で Calculate を使って計算してください。これは読み込まれたワークブックに対して任意の式を評価し、結果を返します
var
Total: Variant;
LastDetail: Integer;
begin
LastDetail := DetailRow + Length(Items) - 1;
Total := Book.Calculate(Format('SUM(Invoice!D%d:D%d)',
[DetailRow, LastDetail]));
if (not VarIsNumeric(Total)) or
(Abs(Total - ExpectedTotal) > 0.005) then
raise Exception.Create('Invoice total does not match the order record');
if Book.SaveAs('invoice-2026-0611.xlsx') <> 1 then
raise Exception.Create('Save failed: check output path and permissions');
end;
保存の前に計算済みの合計を注文レコードと突き合わせることは、安価な保険であり、見返りも大きいです。それは誤った請求書を失敗したジョブへと変えます。オペレーターは失敗したジョブを数秒で再試行できますが、すでに顧客のメールボックスに届いてしまった誤った請求書は、アカウントマネージャーに謝罪と訂正の手間を負わせます
2 つのクラスファミリー、1 つのアルゴリズム
同じロジックはフォーマット間で移植できますが、同じコードではありません。レガシーの .xls 向けの TXLSWorkbook はインターフェースベースで参照カウント方式であり、シートのインデックスは 1 始まりで、手動で解放することは決してありません。.xlsx 向けの TXLSXWorkbook は普通のオブジェクトであり、try..finally の中で解放しなければならず、シートのインデックスは 0 始まりで、数式の慣習は前述のとおりです。FindText、ReplaceText、CopyRange、InsertRows はすべて両方の側に存在するため、アンカー・複製・再計算という形はきれいに引き継がれます。実務的なアドバイスとしては、パイプラインごとに 1 つのフォーマットに絞るか、あるいはこの違いをジェネレーター全体に散りばめるのではなく、自作の薄いアダプターの背後にこの 2 つのオブジェクトのライフサイクルを隠すことです
このパターンが生成するような種類のレポートでは、サイズが問題になることはめったにありません。スタイル済みの行を数千回複製するくらい、現在のハードウェアにとっては何でもありません。保存パスがボトルネックになるのは、明細帯の行数が6桁に達したときだけであり、その時点で StreamingWrite を設定すると、ワークシートの XML をバッファリングせずに直接出力パッケージへ送り込むようになります。サーバーバッチジョブ向けストリーミング書き込みの記事は、そのトレードオフがいつ価値を持つかを扱っています。グラフは残りのレイアウトと同じように振る舞います。XLSX 側では、InsertRows がグラフの上で実行されるとチャートのアンカーとその系列参照の両方が移動するため、合計行の下にあるグラフは正しいデータに結びついたままです。一方 XLS 側では、グラフは自分専用のチャートシート上にあり、ピボットテーブルと同様に決して移動しません。これは、ジェネレーターが展開するシートからプレゼンテーション用のシートを切り離しておくべきもう1つの理由です
このアンカー・複製・再計算のアプローチによって、デザイナーはワークブックの見た目を、あなたのコードはその中身を、それぞれ所有できます。これはたいてい、生成された Excel の出力を保守する価値のあるものにしてくれます。ここで示した検索、コピー、挿入の各呼び出しは、配信前の合計チェックに使われる数式エンジンとともに、Delphi と C++Builder 向けの HotXLS Delphi Component に同梱されています