在 SpreadsheetML 裡,帶貨幣格式的一欄只需要一個元素:<col min="2" max="2" style="3"/>。ECMA-376 Part 1 只在三個地方附加儲存格樣式:欄(§18.3.1.13)、列(§18.3.1.73)與儲存格(§18.3.1.4)。把格式收窄到 B2:D500000,SpreadsheetML 就沒有任何單一元素能表達這個形狀:欄樣式會超出第 500000 列,列樣式會蓋住每一欄,於是寫入端只能把它發成純樣式儲存格——1,499,997 個
記憶體內的試算表模型在更早一步就撞上同一堵牆。如果「格式化」意味著對每個儲存格物件設屬性,在工作表標頭上點一下,就要配置一百萬個物件來帶一個小整數;只想設個數字格式的 Delphi 行程,會在一份磁碟上只有幾 KB 的活頁簿上用光位址空間。真正的設計問題是:編輯期間格式住在哪裡、何時才變成逐儲存格資料
HotXLS 如何不建立儲存格就格式化一個範圍
HotXLS 把範圍格式保存為區間。TXLSXRange 上的每個樣式 setter——SetNumberFormat、SetNumberFormatIndex、SetFontIndex、SetFillIndex、SetBorderIndex、SetAlignmentIndex——都會在工作表的 StyleOverlays 清單加一筆記錄:四個座標、該呼叫擁有的樣式部件,以及那些部件的池索引。矩形內已存在的儲存格立刻拿到新部件;空座標保持為空,不為它們配置任何東西
var
Workbook: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Edge: TXLSXBorder;
BorderIdx, Row: Integer;
Stats: TXLSXCellStorageStatistics;
begin
Workbook := TXLSXWorkbook.Create;
try
Sheet := Workbook.Sheets.Add('Ledger');
Sheet.WriteStringRowValues(1, 2, ['Net', 'Tax', 'Gross']);
for Row := 2 to 1001 do
Sheet.WriteFloatRowValues(Row, 2, [Row * 1.5, Row * 0.3, Row * 1.8]);
// B..D 欄涵蓋全部 1,048,576 列:每次呼叫一個區間
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetNumberFormat('#,##0.00 "EUR"');
// 邊框只建一次,之後按索引套用
Edge := TXLSXBorder.Create;
try
Edge.SetAll(xlsxBorderThin);
BorderIdx := Workbook.Borders.LookupOrAdd(Edge); // 從 1 起算
finally
Edge.Free;
end;
Sheet.RCRange[1, 2, XlsxMaxRow, 4].SetBorderIndex(BorderIdx);
Stats := Sheet.Cells.GetStorageStatistics;
// 預期 3,003:三個標題儲存格加 3,000 個值
WriteLn('Cells: ', Stats.CellCount);
if Workbook.SaveAs('ledger.xlsx') <> 1 then
raise Exception.Create('Save failed');
finally
Workbook.Free;
end;
end;
那兩次格式化呼叫涵蓋 3,145,728 個座標,卻沒讓計數增加分毫。區間刻意從第 1 列開始:整欄區間會存成一個 <col> 元素,而且第 1 列的文字標題反正不受數字格式影響。RCRange、Range['A1:C5']、EntireColumn 與 EntireRow 回傳的範圍由工作表擁有,所以範例從不釋放它們
TXLSXFormatParts 是讓多個區間不打架的機制。它的成員是 xfpFont、xfpFill、xfpBorder、xfpNumberFormat、xfpAlignment、xfpProtection 與 xfpCellControl,每個 TXLSXRange setter 恰好認領其中一個。解析某個儲存格時,覆蓋它的區間按加入順序套用,每個只寫自己的部件,而儲存格本身的明確格式壓過全部。設數字格式的欄區間與設填滿的列區間,都能完好抵達 B5
一筆記錄要帶多個部件時,Sheet.StyleOverlays.Add(Row1, Col1, Row2, Col2, Parts, State) 吃一個填好池索引的 TXLSXCellStyleState。Add 會把座標夾進 1,048,576 × 16,384 的格線、對調顛倒的角,Parts 為空時回傳 nil、什麼都不記。結構編輯讓記錄保持同步:在已格式化區塊內 InsertRows 會把區間加寬插入的列數,在其上方插入則把整筆記錄向下移
為什麼 SetBorders 會建立儲存格,SetBorderIndex 不會?
TXLSXRange.SetBorders(Kind, Style, Color) 必須逐一檢視每個儲存格,因為答案取決於位置。xlsxEdgeOutline 時頂列得上邊、左欄得左邊、內部無事;xlsxEdgeInside 大致相反。這個呼叫還要把新邊合併進儲存格既有的邊框,所以先 Outline 再 Inside 會織出完整格線。這種逐儲存格的合成走 Cells[R, C],儲存格不存在時就建一個。在 10 × 40 的報表區塊上,這恰恰是對的。在 EntireColumn(3) 配 xlsxEdgeAll 上,是 1,048,576 個新儲存格物件
所以分工很簡單。比一個螢幕的資料更高的東西,邊框建一次、按索引套用,如同第一個範例:TXLSXBorder.SetAll,然後 Workbook.Borders.LookupOrAdd 取得從 1 起算的索引,然後 SetBorderIndex。SetBorders 留給外框與內線不同的區塊,例如上面的 10 × 40 報表區塊
讀回格式而不白做工
同一個陷阱也逮得到只讀的程式碼。Cells[R, C] 是查無即建的存取器:問它 B500000 的數字格式,你得到正確答案,外加一個新的、帶樣式的空儲存格落在 B500000,並且從此會被保存與寫出。對一整欄跑這種迴圈稽核格式,等於把區間原本要避免的那一百萬個物件重建回來,一次一個無辜的讀取
TXLSXWorksheet.GetCellVisibleFormat 解析座標的生效樣式而不新增儲存格。它合併工作表預設、列與欄區間、矩形區間、表格樣式,以及儲存格上的任何明確格式。帶 out ABuiltinNumberFormat 參數的多載,把 14(短日期)這類 Excel 內建格式與 NumberFormatIndex 分開回報——後者指向活頁簿自訂的 NumberFormats 池。Cells.HasCell(R, C) 回答實體儲存格到底存不存在
var
Sheet: TXLSXWorksheet;
Before, After: TXLSXCellStorageStatistics;
State: TXLSXCellStyleState;
Row, Builtin, Unformatted, Physical: Integer;
begin
Sheet := Workbook.Sheets[1]; // Sheets 從 1 起算
Before := Sheet.Cells.GetStorageStatistics;
Unformatted := 0;
Physical := 0;
for Row := 1 to 200000 do
begin
// 生效樣式,解析時不往工作表加儲存格
State := Sheet.GetCellVisibleFormat(Row, 2, Builtin);
if (State.NumberFormatIndex = 0) and (Builtin = 0) then
Inc(Unformatted);
if Sheet.Cells.HasCell(Row, 2) then
Inc(Physical);
// Sheet.Cells[Row, 2].NumberFormatIndex 回答同一個問題,
// 但每走訪一列就留下一個新儲存格物件
end;
After := Sheet.Cells.GetStorageStatistics;
// 迴圈前後兩個計數應完全相同
WriteLn(Format('cells %d -> %d, styled blanks %d -> %d',
[Before.CellCount, After.CellCount,
Before.StyledBlankCellCount, After.StyledBlankCellCount]));
end;
這種檢查對著你的真實工作負載跑一次就好。StyledBlankCellCount 數的是有樣式而沒有值、公式或富文字的儲存格。若它在理應唯讀的一趟裡攀升,某個輔助常式正為了放樣式而建儲存格,典型手法是呼叫 Cells[R, C] 來判斷儲存格需不需要格式
256 列區塊對你的程式碼意味著什麼
實體儲存格住在 256 列的區塊裡。區塊內,每個有內容的列把儲存格按欄排序存在清單;沒有儲存格的列只是一個空槽;整整 256 列都沒有儲存格的區段連區塊都沒有。資料落在第 3、7、512 列的稀疏表因此只佔兩個區塊。儲存以列為主序走訪同一結構——這正是 SpreadsheetML 對 <row> 元素與其 <c> 子元素要求的順序
你握住的 TXLSXCell 參照在周圍儲存格增刪時仍是同一個物件;區塊重整的是自己的簿記,不是你的控制代碼。WriteFloatRowValues、WriteStringRowValues 這類批次寫入器在全新工作表上走更快的路,把列主序的值追加進一個緊湊存放。第一次經由 Cells[R, C] 的逐格存取會把那個存放一次性轉成儲存格物件。GetStorageStatistics 透過 PackedStorageActive 與 PackedCellCount 顯示你處於哪個狀態,旁邊還有 RowBlockCount 與 PopulatedRowCount
樣式池在儲存時修剪。寫出 styles.xml 之前,HotXLS 重建字型、填滿、框線、數字格式、對齊與保護池,只留下仍在使用的項目,被使用者改了一小時樣式的活頁簿不會把每個棄用的填滿一起出貨。釘在絕對 FormatIndex 上的儲存格保住自己的槽位
逐列串流儲存,以及能看見它的數字
TXLSXWorkbook.StreamingWrite 預設為 True。開著時,SaveAs 逐列產生工作表 <row>、直接寫進所屬 ZIP 項目的 deflate 串流,任何時刻記憶體裡最大的工作表 XML 片段就是單獨一列。關掉時,每張工作表的 XML 先組成完整字串再壓縮,峰值記憶體跟著最大的那張表走。矩形區間變成的那些純樣式儲存格——開頭那個 B2:D500000 案例——也在那個逐列迴圈裡產生,所以它們增加檔案大小、卻從不以一整塊 XML 坐在記憶體裡
共用字串與樞紐分析快取記錄遵循同一模式。每個 <si> 或 <r> 項目各自產生、各自寫出,所以寬大的分析活頁簿上,比任何單張工作表還大的共用字串表不必以一整塊 XML 存在
這些都不必憑信心接受。每次儲存之後,活頁簿暴露四個唯讀屬性:LastSaveUsedStreaming、LastSavePeakRowXmlChars(最長單列的字元數)、LastSaveBufferedWorksheetXmlChars(記憶體中持有的工作表 XML 總量,串流時為零)與 LastSavePeakAuxiliaryXmlChars(最長的共用字串或樞紐項目)。LastSaveUsedStreaming 對 .xlsx、.xltx、.xltm 輸出與二進位 .xlsb 寫入器為 True;OpenDocument 輸出走自己的寫入器、回報 False
procedure ReportSave(Workbook: TXLSXWorkbook; const FileName: string;
Streaming: Boolean);
begin
Workbook.StreamingWrite := Streaming;
if Workbook.SaveAs(FileName) <> 1 then
raise Exception.CreateFmt('Save failed: %s', [FileName]);
WriteLn(Format('streaming=%s peakRow=%d buffered=%d peakAux=%d',
[BoolToStr(Workbook.LastSaveUsedStreaming, True),
Workbook.LastSavePeakRowXmlChars,
Workbook.LastSaveBufferedWorksheetXmlChars,
Workbook.LastSavePeakAuxiliaryXmlChars]));
end;
// 同一份活頁簿、兩種模式;在你的分析器裡觀察行程記憶體
ReportSave(Workbook, 'ledger-stream.xlsx', True);
ReportSave(Workbook, 'ledger-buffered.xlsx', False);
在自己的資料上,緩衝那行會顯示出與整張工作表 XML 同一數量級的 buffered 數字,串流那行則是零,外加不超過最寬那一列的 peakRow。讓 StreamingWrite 保持開啟。這個旗標只決定工作表 XML 是否在壓縮前整份組裝,LastSaveBufferedWorksheetXmlChars 回報那次組裝的字元大小
其餘記憶體去了哪裡
儲存格與樣式都緊湊之後,大型活頁簿上下一批最大的消耗者是共用字串表,以及檔案帶著的任何衛星部件:樞紐分析快取、繪圖,以及物件模型沒有建模之部件的保留 XML。那些各有自己的槓桿,沒有任何單一設定能罩住全部
如果瓶頸在開檔而非儲存,先從選擇性載入下手:僅中繼資料與選擇性工作表載入 的逐步解說,涵蓋不為碰不到的工作表付錢的讀法。超大檔案的讀取路徑吞吐量,見 平行 XLSX 剖析與記憶體配置器 的筆記;永遠不需要物件模型的純輸出工作,伺服器批次工作的串流寫入 比這裡任何調校都更合身
HotXLS 從原生 Delphi 與 C++Builder 程式碼讀寫 XLS 與 XLSX,不必 Excel 也不必 OLE 自動化,所以上面每個數字都出自你自己的行程、也能在那裡驗證。支援的格式與 RAD Studio 版本列於 HotXLS Delphi 試算表元件頁面