技術文章

用 HotXLS 編輯 Excel 圖表而不流失 ChartML

HotXLS 會保留匯入 Excel 圖表的原始 ChartML,並在強型別模型沒有任何變動時,逐位元組把它原樣重播,所以開啟並重新儲存一份活頁簿,不會悄悄剝除一個圖表中物件模型無法表達的部分。編輯一個標題、一條數列或一個座標軸時,HotXLS 會在結構上把新的強型別節點合併進原始樹,而不是從頭重新產生圖表

這條區別——未動到時重播、被編輯時合併——正是一份 Delphi 應用程式能安全地指向分析師在 Excel 裡建立的圖表的原因。從強型別模型重新產生圖表,永遠會流失模型所不知道的任何東西,而 Excel 圖表裡滿是函式庫不知道的東西:自訂樣式、延伸清單、替代內容分支、來自增益元件的命名空間限定格式

為什麼重新儲存活頁簿會把它的圖表壓平?

因為常見的實作是讀它懂的東西、寫它懂的東西。中間的一切——來自較新 Excel 組建的 extLst、一條 mc:AlternateContent 分支、一個模型沒有對應屬性的形狀效果——存在於檔案裡、在物件模型裡沒有歸宿,並在儲存時消失。使用者看到一個失去了漸層、自訂資料標籤或 3D 效果的圖表,而活頁簿無法解釋為什麼

在活頁簿上啟用 PreserveUnsupportedParts 後,HotXLS 會在匯入時一併儲存原始 UTF-8 圖表位元組,以及一個長度和一份已建模內容的 64 位元指紋。如果到儲存時強型別模型沒有變動,它會把原始位元組原樣寫回。沒有 XML 重排、沒有空白漂移、也不會為了一個沒人編輯的圖表,而在記憶體裡留下一份 UTF-16 副本

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create(nil);
  try
    Workbook.PreserveUnsupportedParts := True;   // set before Open
    Workbook.Open('quarterly-review.xlsx');
    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] is 1-based
    if Chart.HasPreservedXml then
      Log('original ChartML retained; untouched charts replay exactly');
    Chart.Title := 'Revenue by region, Q3 2026';  // now the merge path runs
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

合併保留了什麼,模型擁有什麼

規則是擁有權。公開的模型節點——標題、數列、快取、繪圖群組、座標軸——由新產生的樹決定。未建模的子樹則從原始內容攜帶過來。這種不對稱是刻意的,也是需要花點時間才能內化的部分:刪除一條數列,絕不能從保留的 XML 裡把舊數列救回來,所以凡是模型擁有的東西,即使模型說「沒了」,也一律取自模型

未擁有的內容會重新插入到它原本相鄰的相符節點旁,而 extLst 會留在其父節點內的最後位置,因為這是綱要規定的位置。清除一個已知屬性——拿掉一個粗體旗標、丟掉一個顏色——會被當作一次清除,而不是一個要從原始樹填補的缺口。同一個鄰域裡的未知屬性則保有各自的位置

標記相容性內容會得到特定處理。剖析時,HotXLS 會跳過 mc:Choice 並讀取 mc:Fallback;如果該後援分支包裹著一個模型認得的繪圖群組,合併會把新的強型別內容寫回後援子樹。少了這一步,儲存時會發出新的繪圖群組、卻把舊的留在後援裡,圖表於是會帶著兩份

把單一圖表排除在外

ClearPreservedXml 會丟棄某個圖表的原始 XML 與指紋,於是下次儲存時會純粹從強型別模型重建該圖表。這是一個以圖表為單位的決定;活頁簿層級的 PreserveUnsupportedParts 仍然治理著是否在匯入時建立保留狀態

當你想要一份乾淨的圖表、而不是一份忠實的圖表時——例如一份範本圖表帶有舊企業佈景主題的格式,而這次操作的重點正是丟掉它——就伸手拿它。不要把它當作除錯反射動作:一個圖表在編輯後算繪錯誤,比較常是建模問題而非保留問題,而清除保留的 XML 會毀滅證據

組合圖表與第二座標軸

AddPlotGroup 會建構有序的組合圖表,其中每個繪圖群組保有自己的圖表家族,以及自己的主要或次要座標軸指派。這正是無所不在的商業圖表所需要的:左軸上的營收以直條呈現,右軸上的利潤百分比以折線呈現

圖表會公開主要與次要類別及數值座標軸,外加一條數列座標軸,而強型別的類別、數值、日期與數列座標軸設定涵蓋了位置、識別碼、交叉、日期單位、標籤區間與可見性。較舊的平面 API——ChartTypeAddSeries、平面的 Series 集合——仍然定址到主要繪圖群組,所以既有程式碼能繼續運作,而底下的模型則持有每一個有序群組

var
  Chart: TXLSXChart;
  Line: TXLSXChartPlotGroup;
begin
  // Column chart anchored over rows 1..15, columns 5..12
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Chart.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');
  // Second plot group: a line, read against the secondary value axis
  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary);
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
end;

在這套模型出現之前,開啟並重新儲存一份組合圖表,會把它塌陷成單一圖表家族,並失去次要、日期與數列座標軸。如今圖表公式參照與快取數列值也能在活頁簿複製與開啟儲存循環中存活,而圖表錨點會依關聯識別碼與其圖表零件對應——所以一份重新排序關聯項目的應用程式,不再會把圖表內容在工作表位置之間對調

手動版面的陷阱

如果你要手動定位繪圖區、標題、座標軸標題或圖例,請把四個座標全部寫上。只要其中任何一個出現,Office 就要求 xywh 作為一個完整群組,否則會整個忽略 manualLayout。HotXLS 因此永遠發出完整的矩形,而 SetPositionAndSize 會在一次呼叫裡設定並啟用它

還有兩個細節遵循 Excel,而不是規格的完整自由度。位置會在 xModeyMode 設為 edge 的情況下寫入,而尺寸則對 wModehMode 使用 factor,因為這正是 Excel 寫入的內容,也是 Excel 把其他組合轉換成的結果。而 layoutTarget 只會為繪圖區發出,絕不為標題或圖例發出,因為 Microsoft 的實作註記禁止在那些位置使用它

把這一點嵌進活頁簿往返裡

圖表保留是一條通用政策的一個實例:一套編輯他處所撰寫文件的函式庫,應該只改變它被要求改變的東西,其他一概不動。同樣的推理驅動著 HotXLS 對佈景主題、計算鏈與工作表延伸的處理,相關內容描述於 佈景主題、extLst 與 calcChain 的無損往返,以及 VBA 專案與外部連結的 保留 VBA 與外部連結。至於圖表撰寫面本身——從 Delphi 建立圖表、擺放圖片與繪圖——請見 HotXLS 的圖表、圖片與繪圖逐步解說

HotXLS 從 Delphi 與 C++Builder 的原生程式碼讀取、編輯並寫入 XLSX 圖表,機器上不需要安裝 Excel,而這正是把保真度變成函式庫問題、而非自動化問題的原因——HotXLS 試算表元件頁提供圖表功能清單與試用下載