開いたばかりのレポートテンプレートのセルを順に読んでいくと、結合されたタイトルはまるで漏れのように振る舞います。A1 を読むと「Quarterly Statement」が返ってきますが、同じバナーの下に見えている B1 から F1 を読んでも、何も返ってきません。ヘッダーを直そうと C1 に値を書き込んでも、画面には決して現れません。グリッドはあなたのデータを失ったわけではありません。これは結合が意味するとおりのことをしているだけです。XLS でも XLSX でも、結合された矩形は左上のアンカーである 1 つのセルの内容だけを描画し、残りは値を保持しはするものの決して表示されない「覆われた領域」として扱われます。Excel ユーザーはこれを試行錯誤の中で体得しますが、レポートジェネレーターはこれをルールとしてコードに落とし込まなければなりません。生成コードでの症状は、追跡すべき例外を何も伴わない空白の領域だからです。HotXLS は Delphi と C++Builder から両方の Excel フォーマットを読み書きするネイティブの Object Pascal ライブラリであり、結合テーブルを十分に明示的な形で公開しているため、サポートチケットの中で再発見するのではなく、そのルールに沿ってプログラムを組むことができます
値は 1 つ、アンカーも 1 つ
結合とは、形を変えないグリッドの上に重ねられた表示上の指示です。覆われたセルはそれぞれ、ファイル内では独立したスロットとして存在し続けます。結合レコードは、消費者に対してアンカーの内容をその矩形全体に描画するよう伝えるだけです。この区別が、レイアウトコードを書く前に体に染み込ませておく価値のある 3 つの挙動を導きます。覆われたセルを読むとそれ自身に保存された値が返ってきますが、あなたが作ったバナーの場合それは通常空であるため、結合されたタイトルを調べるコードは必ずアンカーを解決して読む必要があります。覆われたセルへの書き込みはファイルレベルでは成功しますが、どこにも表示されません。これが冒頭で触れた見えないヘッダーの罠です。そして領域の結合を解除すると、それまでその下にあったものがそのまま露出するため、覆われた空間に紛れ込んだ値は、誰かが結合を解いた日に目に見える不具合へと姿を変えます
XLSX 側では、このテーブルは一級オブジェクトです。Sheet.MergedCells は Add('A1:C1')、FindAt(Row, Col)、DeleteAt、Items を備えており、最もよく使う呼び出しは FindAt です。任意の座標を渡すと、そのセルを覆う結合領域を返し、セルが単独で存在する場合は nil を返します。この 1 回の検索が、正しい結合の扱いを構成する 2 つの半分、安全な読み取りと書き込みガードの両方の土台となり、どちらも後ほど登場します
2 つのファサード、2 つの結合イディオム
HotXLS は、クラシックな BIFF8 の .xls エンジンと OOXML の .xlsx エンジンを別々のオブジェクトモデルとして保っており、由来する慣習が異なるため結合の書き方も異なります。XLS ファサードは Excel の COM イディオムに従います。2 引数のインデックス付きプロパティから範囲を取得し、値によって最終的な形状が決まる OleVariant を伴って Merge を呼び出します
var
Book: IXLSWorkbook; // インターフェース参照カウント: 手動での Free は不要
Sh: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
Sh := Book.Sheets[1]; // XLS のシートコレクションは 1 始まり
Sh.Range['A1', 'F1'].Merge(False); // False = 1 つの結合ブロック
Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
Sh.Range['A3', 'F4'].Merge(True); // True = merge across: 行ごとに 1 つの結合
Book.SaveAs('layout.xls');
end;
Merge の引数こそ、人がよく誤解する部分です。2 行にまたがる範囲に対して Merge(True) を使うと、独立した 1 行の結合が 2 つ生成されます。これが Excel の「Merge Across(横方向に結合)」であり、各行を分離可能なまま保ちたい積み重ね式のヘッダー帯にはまさに求めている挙動です。Merge(False) は矩形全体を 1 つのブロックに融合します。範囲はまた、状態フラグとして MergeCells を報告し、MergeArea を通じて含まれる領域を返し、Unmerge で自身を解除します。XLSX ファサードは同じ操作を別の名前で公開します。Sheet.MergeCells(Row1, Col1, Row2, Col2) は整数の境界を取り、TXLSXRange.Merge は同等の Across バリアントを受け付け、MergedCells コレクションがその結果を保持します
データとともに伸びるテンプレート
実際のレポートテンプレートは固定されたグリッドではありません。ヘッダーと合計は固定されていますが、その間の明細セクションはクエリが返す件数に合わせて伸縮します。堅実に機能するパターンは、テンプレート内にスタイル済みの明細行を 1 行だけ用意し、レコードごとにそれを複製し、その後に合計ブロックの手前にギャップを開くことで、その下にアンカーされたすべてがスタイルを失わずに下へスライドするというものです
Sheet.Range['A1:F1'].Merge;
Sheet.Cells[1, 1].Value := 'INVOICE #2026-0611'; // 値はアンカーである A1 に入る
Sheet.RowHeight[1] := 28;
TitleFont := Book.Fonts.Add('Calibri', 16, True, False);
Sheet.Cells[1, 1].FontIndex := TitleFont + 1; // プール側のインデックスは 0 始まり、セル側は 1 始まり
// 5 行目はスタイル済みの明細テンプレート行
for I := 0 to ItemCount - 1 do
Sheet.CopyRange(5, 1, 5, 6, 6 + I, 1); // スタイルと数式も一緒に運ばれる
// 合計ブロックの手前にギャップを開く。以下の内容は下にシフトする
Sheet.InsertRows(6 + ItemCount, 1);
Sheet.Range['A1:F1'].SetBorders(xlsxEdgeOutline, xlsxBorderMedium);
見直す価値がある行が 2 つあります。フォントの割り当てには、静かに噛みつくオフバイワンの問題があります。Fonts.Add は 0 始まりのプール位置を返しますが、セルは 1 始まりのフォント参照を保持し、0 は既定フォントを意味します。そのため + 1 を落としても何も例外は発生せず、単にタイトルのフォントが間違って表示されるだけです。もう1つは CopyRange で、これは値と一緒に書式や数式も移動させます。これこそが、テンプレート行の見た目をコードで再構築するのではなく、手作りのテンプレート行を複製する理由のすべてです。デザイナーは外観をテンプレート内で一度だけ所有し、ジェネレーターはその複製にデータを注ぎ込むだけです
この分離は、再利用可能なレイアウトが自分専用のワークブック内にある場合、たとえば複数のレポートで共有されるヘッダーとフッターの帯を集めたシートなどにおいて、さらにスケールします。CopyRangeTo はワークシートの境界を越えて同じ複製を実行し、ターゲットシートと配置先の座標を受け取るため、ジェネレーターは 1 枚の汚れのないテンプレートシートを保持したまま、ジョブが必要とするだけ多くの出力シートにその領域を刻印できます。代わりにテンプレートをその場で変更してあとで復元しようとする方法は、実行が途中で中断される日まではうまく動くというたぐいのものです
InsertRows が移動させるもの、させないもの
テンプレートを伸ばすこのパターンが機能するのは、XLSX の InsertRows がセルのシャッフルではなく構造的な編集だからです。ギャップを開くとき、これはセル値だけでなく、挿入位置より下にある結合領域、行の高さ、ハイパーリンク、コメント、ウィンドウ枠の固定、オートフィルタ範囲、条件付き書式、データの入力規則、テーブル、定義名、画像アンカー、グラフアンカーを再配置します。だからこそ、合計ブロックは結合や数値書式を保ったまま新しい行にたどり着くのであり、剥がされた状態で到着することはありません
ドキュメント化されている 2 つの制限は、それを前提に設計すべきものです。数式の調整は編集対象のシートに限定されます。そのシート内の参照は書き換えられ、シフトされた領域を指す別シート上の数式も書き換えられますが、この調整は編集対象のシートを参照するものにしか及ばないため、シートをまたぐ参照方式は、盲目的に信頼するのではなく独自の監査に値します。2 つ目の制限はより厳しく、XLS 側にあります。ピボットテーブルは開いて保存するサイクルを、HotXLS が移動できるモデル化されたオブジェクトとしてではなく、生のまま保持されたレコードとして生き延びます。そのため、行の挿入はピボットの占有領域を移動させません。.xls フォーマット向けに構築するテンプレートでは、ピボット領域を伸びるどの帯からも十分に離して配置すべきです
レイアウト領域へのデータ書き込みを拒否する
実際に本番環境に到達する結合セルの不具合は、見た目だけの問題ではありません。構造的なものです。明細行が結合されたレイアウト帯へとずれ込み、その値が覆われたセルに落ちて見えなくなり、列の合計が誰の目にも見える表示と静かに一致しなくなります。FindAt は任意の座標に対して「覆っている領域はどれか」という問いに答えるため、ジェネレーターは静かに件数が合わないレポートを出荷するのではなく、その書き込みが起きる瞬間にそれを拒否できます
// 結合されたレイアウト領域への明細データの書き込みを拒否する
if Sheet.MergedCells.FindAt(Row, 1) <> nil then
raise Exception.CreateFmt('row %d overlaps a merged layout region', [Row]);
Sheet.Cells[Row, 1].Value := Detail.Description;
同じ境界チェックは、後でユーザーが出力を並べ替えたりフィルタしたりする場所ならどこにでも当てはまります。内部に結合を含む範囲はきれいに並べ替えることができません。並べ替えは行を個別に移動させますが、複数行にまたがる結合には一緒に移動すべき単一の行が存在しないからです。Excel はエラーを返すか、レイアウトを崩して応答します。レポートを正しく保つ規律は地理的なものです。結合はタイトル帯、セクションの区切り、署名欄に限定し、シートの表形式の中央部分はフラットに保ってください。テンプレートレポート生成の記事は、このレイアウトとデータの分離をプレースホルダー駆動の完全なワークフローへと発展させており、条件付き書式とリッチテキストの記事はそのフラットなデータ帯のスタイリングを扱っています
出力時に結合はどのように劣化するか
結合はワークブックの概念であり、テキスト志向の各エクスポート形式はそれぞれ異なる度合いでこれを尊重します。この 3 つの挙動をあらかじめ知っておくことで、QA サイクルを 1 回節約できます。HTML エクスポートは結合を忠実に再現し、1 つのテーブル上に colspan と rowspan を出力するため、ブラウザ向けのレポートは帯状の見た目を保ちます。RTF エクスポートは列方向にまったくスパンしません。アンカーのテキストは自身のセルに収まり、結合の残りの幅は空のセルとして出力されるため、ワードプロセッサ上では幅の広いタイトルが視覚的に左に寄って見えます。CSV には結合という概念自体が存在しないため、アンカーの値は 1 つのフィールドを占め、覆われたセルはそれぞれ空のフィールドとして出力されます。区切り形式のエクスポートも同時に行うワークブックにとっての教訓は、耐荷重となるものは何であれ結合された形状の外に置いておくことです。CSV、TSV、HTML エクスポートの記事は各形式を詳しく解説しています
ファイルサイズとの兼ね合いを気にしている人への安心材料が 1 つあります。結合は、レポートの規模においてはほとんどコストがかかりません。結合テーブルはセルデータに比べて非常に小さく、覆われたセルを読み取る際も走査ではなく FindAt を経由します。大きなワークブックに対する性能上の圧力は別のところ、主にスタイルプールの肥大化と保存パスが保持するメモリから来るものであり、大規模ワークブックの性能に関する記事がそれを直接取り上げています。両方の結合 API、構造的な編集操作、そしてテンプレートのデモはすべて HotXLS Delphi Component に同梱されています