逐一走訪一個剛開啟的報表範本的儲存格,一個合併的標題表現得就像一個漏洞。你讀 A1 並得到「Quarterly Statement」;你讀 B1 到 F1——它們明明坐在同一面橫幅底下——卻什麼也得不到。把一個數值寫進 C1 想補上標頭,它卻從不在螢幕上出現。格線沒有弄丟你的資料。它做的正是合併的意思:在 XLS 與 XLSX 裡,一個合併矩形只渲染一個儲存格的內容,即左上角錨點,並把其餘部分當成持有數值卻從不顯示的覆蓋空間。Excel 使用者透過試誤把這件事吸收進去。一個報表產生器必須把它編碼成一條規則,因為在生成的程式碼裡,症狀是一片空白區域,卻沒有任何例外可以追溯。HotXLS 是一個原生 Object Pascal 函式庫,能從 Delphi 與 C++Builder 讀寫兩種 Excel 格式,它把合併表呈現得夠明確,讓你能針對規則來寫程式,而不是在一張支援工單裡重新發現它
一個數值,一個錨點
合併是一道疊在一個不改變形狀的格線之上的顯示指令。每個被覆蓋的儲存格在檔案裡仍以它自己的欄位存在;合併紀錄只是告訴消費者,把錨點的內容繪製到整個矩形上。這個區別驅動了三個在你寫任何版面程式碼之前都值得內化的行為。讀取一個被覆蓋的儲存格會回傳它自己儲存的數值,而對你建好的橫幅來說那通常是空的,所以任何檢視合併標題的程式碼都必須解析並讀取錨點。寫入一個被覆蓋的儲存格在檔案層級會成功,卻哪裡都不出現,這就是開頭那個看不見標頭的陷阱。而取消合併一個區域,會把一直坐在它底下的東西整個暴露出來,所以一個寫進覆蓋空間的多餘數值,會在某一天有人解散這個合併時變成一個看得見的缺陷
在 XLSX 那一側,那張表是一級物件。Sheet.MergedCells 帶有 Add('A1:C1')、FindAt(Row, Col)、DeleteAt 與 Items,而你最常伸手去用的那一個呼叫是 FindAt:交給它任何座標,它就回傳覆蓋該儲存格的合併區域,或當儲存格獨立時回傳 nil。這單一次查詢,是正確合併處理兩半——安全讀取與寫入守衛——的基礎,而兩者稍後都會出現
兩個外觀,兩種合併慣用法
HotXLS 把經典的 BIFF8 .xls 引擎與 OOXML .xlsx 引擎保留為兩個分開的物件模型,而它們拼寫合併的方式不同,因為它們源自不同的慣例。XLS 外觀遵循 Excel COM 慣用法:你從一個雙引數的索引屬性取一個範圍,並用一個 OleVariant 呼叫 Merge,而該數值決定了你最終得到的幾何形狀
var
Book: IXLSWorkbook; // interface-counted: no manual Free
Sh: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
Sh := Book.Sheets[1]; // XLS 工作表集合採 1-based
Sh.Range['A1', 'F1'].Merge(False); // False = 一個合併區塊
Sh.Cells.Item[1, 1].Value := 'Quarterly Statement';
Sh.Range['A3', 'F4'].Merge(True); // True = 跨欄合併:每列一個合併
Book.SaveAs('layout.xls');
end;
Merge 的引數是人們會搞錯的部分。在兩列的範圍上,Merge(True) 會產生兩個獨立的單列合併,這就是 Excel 的「橫跨合併」,也正是你為了一個要保持各列可分離的堆疊標頭帶所想要的。Merge(False) 把整個矩形熔成一個區塊。該範圍還以 MergeCells 作為狀態旗標來回報、透過 MergeArea 回傳所屬區域,並用 Unmerge 把自己解散。XLSX 外觀以不同的名稱公開相同的操作:Sheet.MergeCells(Row1, Col1, Row2, Col2) 接受整數邊界、TXLSXRange.Merge 接受等價的 Across 變體,而 MergedCells 集合持有結果
一個會跟著資料增長的範本
一份真正的報表範本不是一個固定的格線。標頭與總計是固定的,但它們之間的明細區會伸展到查詢回傳什麼就是什麼。站得住腳的 pattern,是在範本裡保留一列完整樣式的明細列,每筆紀錄複製它一次,然後在總計區塊前方開一道缺口,好讓錨定在下方的一切向下滑動而不失去格式
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; // pool index 0-based, cell side 1-based
// 第 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);
有兩行值得再看一眼。字型指派帶著一個會悄悄咬人的差一錯誤:Fonts.Add 交回的是 0 起算的集區位置,而儲存格儲存的是 1 起算的字型參照,其中 0 表示預設字型,所以漏掉 + 1 不會引發任何東西,它只是用錯字型來為你的標題套樣式。另一行是 CopyRange,它連同數值一起搬移格式與公式。這正是要複製一列手工建好的範本列、而不是在程式碼裡重建其外觀的全部理由。設計師在範本裡一次性擁有外觀;產生器只是把資料倒進它的副本
當可重複使用的版面住在它自己的活頁簿裡——比如一張跨報表共享的標頭與頁尾帶工作表——這個分割還能進一步擴充。CopyRangeTo 跨越工作表邊界執行同樣的複製,接受一張目標工作表加上目的座標,於是產生器可以保留一張原始乾淨的範本工作表,並把它的一些區域蓋章到一件工作所需的任意數量的輸出工作表上。另一種做法——就地變更範本並事後試圖還原它——是那種一直有效、直到某天一次執行跑到一半中止的東西
InsertRows 移動了什麼,又沒移動什麼
讓範本增長的 pattern 之所以行得通,只因為 XLSX 的 InsertRows 是一次結構性編輯,而不是一次儲存格洗牌。當它開一道缺口,它重新安置的是位於插入點下方的合併區域、列高、超連結、註解、凍結窗格、自動篩選範圍、條件式格式設定、資料驗證、表格、定義名稱、圖片錨點與圖表錨點,而不只是儲存格數值。這正是讓總計區塊抵達它的新列時,帶著完整的合併與數字格式、而非一絲不掛地抵達的原因
它兩個記載下來的限制,是你在設計時要繞開的。公式調整的範圍限於正在編輯的那張工作表:該工作表內部的參照會被改寫,而另一張工作表上一個指向被移動區域的公式也會被改寫,但這個調整只跟隨以被編輯工作表為目標的參照,所以任何跨活頁簿的參照機制都值得自己審核一次,而不是盲目信任。第二個限制更尖銳,而且是在 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 會以一個錯誤或一個打亂的版面來回應。讓報表保持正確的紀律是地理性的。把合併限制在標題帶、章節分隔與簽名區塊,並讓工作表的表格狀中段保持平坦。範本報表生成文章把這個版面對資料的分割,發展成一套完整的佔位符驅動工作流程,而條件式格式設定與富文字文章則涵蓋那條平坦資料帶的樣式
合併在輸出的路上如何退化
合併是一個活頁簿概念,而每一種文字導向的匯出格式對它的尊重程度不同。事先知道這三種行為能省下一個 QA 循環。HTML 匯出忠實地重現合併,在單一表格上發出 colspan 與 rowspan,所以一份送往瀏覽器的報表能保住它的帶狀外觀。RTF 匯出完全不跨越欄:錨點文字落在它自己的儲存格裡,而合併的剩餘寬度以空白儲存格出現,這讓一個寬標題在文書處理器裡視覺上被推到左邊。CSV 沒有合併的概念,所以錨點數值佔用一個欄位,而每個被覆蓋的儲存格以一個空白欄位發出。對於一份也餵給分隔式匯出的活頁簿,重點是把任何承重的東西移出合併幾何之外;CSV、TSV 與 HTML 匯出文章逐一走過每種格式的細節
給任何拿這個來權衡檔案大小的人一點安心:合併在報表規模上幾乎不花成本。合併表在儲存格資料旁邊微不足道,而讀取一個被覆蓋的儲存格仍是走 FindAt 而非掃描。大型活頁簿上的效能壓力來自別處,主要是樣式集區增長與儲存路徑持有的記憶體,而大型活頁簿效能文章直接處理這一點。兩個合併 API、結構性編輯操作與範本展示,都隨HotXLS Delphi Component一起出貨