技術文章

從 Delphi 建立 Excel 相機物件:即時快照

HotXLS 可在 Delphi 中透過 AddCamera,在 XLSX 工作表上建立 Excel 相機物件。相機物件是一張永久連結到某個儲存格範圍的圖片:每當活頁簿開啟或來源變更時,Excel 都會依來源範圍重新渲染這張圖片,因此儀表板可以顯示另一張工作表上某個表格的即時畫面,可以是任意尺寸,甚至可以旋轉

這項功能在 Excel 中以「相機」工具存在,是大多數使用者從未見過的按鈕,因為它預設並未出現在功能區中。它解決的是一個真實的儀表板問題,而且比其他替代方案都做得更好:複製的範圍會過期失效,圖表無法顯示任意的儲存格內容,而手動貼上的連結圖片又無法用程式碼產生。相機物件是唯一一種既即時、又能任意呈現內容的建構方式

HotXLS 在 Delphi 中建立的相機物件:一張 Data!$B$2:$D$4 的即時圖片被放置在儀表板工作表上,Excel 會在開啟與每次來源編輯時重新渲染它
相機物件會永久連結至其來源範圍,因此 Excel 會在開啟與每次編輯時重新渲染這張圖片,可以是任意尺寸,也可以來自另一張工作表上的範圍

這項功能記載於何處,這一點為何重要?

主要的試算表規範並未描述相機物件;它們被視為一項實作細節。權威說明實際上位於繪圖標記文件中的「Camera Tool」標題之下,知道這一點能省下一整個下午在錯誤文件中搜尋的時間

就結構而言,相機物件是一個普通的圖片元素,只是它的非視覺圖片屬性帶有一份延伸清單。該延伸以一個固定的 GUID 識別,其中一個來自 2010 繪圖命名空間的元素,記錄了兩件事:以 A1 樣式絕對參照表示的來源範圍(可選擇附上工作表限定名稱),以及一個圖形識別碼。這張圖片其餘的一切都與一般圖片無異

HotXLS 在 Delphi 中寫入的相機物件之 XLSX 結構解剖:一個普通的圖片元素,其延伸清單帶有一個 2010 繪圖命名空間元素,記錄絕對來源範圍與圖形識別碼
就結構而言,相機物件是一張普通圖片,外加一個來自 2010 繪圖命名空間的延伸元素,記錄 A1 樣式的絕對來源範圍與圖形識別碼

建立一個相機物件

存在兩種多載,因為命名範圍有兩種都很方便的方式。文字形式接受的參照就是最終儲存的樣子,這正是跨工作表來源所需要的。座標形式則接受同一張工作表上的四個儲存格座標,並自動為你建構絕對參照:

uses
  lxHandleX;

var
  Book: TXLSXWorkbook;
  Dashboard: TXLSXWorksheet;
  Cam: TXLSXImage;
begin
  Book := TXLSXWorkbook.Create;
  try
    if Book.Open('reporting.xlsx') <> 1 then
      Exit;
    Dashboard := Book.Sheets[0];

    // Data!$B$2:$D$4 的即時畫面,放置於儀表板上的 B10:F20 位置
    Cam := Dashboard.AddCamera('Data!$B$2:$D$4', 10, 2, 20, 6);

    // 放置框預設採用 Excel 自身的 64 px 欄寬與
    // 20 px 列高;如有需要,之後可用 EMU 調整繪製尺寸
    Cam.WidthEMU := Round(12.5 * 914400 / 2.54);   // 12.5 公分
    Cam.HeightEMU := Round(6.0 * 914400 / 2.54);

    Book.SaveAs('reporting-dashboard.xlsx');
  finally
    Book.Free;
  end;
end;

傳回的物件是一個普通的圖片物件,只多了一個屬性 CameraRange,這個屬性只有在該圖片是相機物件時才會非空。這也是你在一份非自己建立的活頁簿中偵測相機物件的方法:列舉所有圖片並檢查這個屬性

為何佔位圖片不需要是真實的?

XLSX 封裝中的每一張圖片都需要一個影像部件,相機物件也不例外。但 Excel 會忽略那張影像:載入時它會重新渲染連結的範圍,並繪出結果。內嵌的位元組純粹是為了不支援相機行為的工具而存在的顯示快取

這個事實讓實作可以省去整整一個子系統。不需要點陣化來源範圍,不需要渲染引擎產生快照,也不會有快取圖片與 Excel 中即時畫面不一致的風險。HotXLS 只寫入一個最小化的佔位中繼檔——一筆標頭記錄與一筆檔案結束記錄,總共 108 位元組——這讓封裝在結構上維持完整,且幾乎不花任何成本

有一項後果需要事先規劃:任何渲染 XLSX 卻未實作相機物件的檢視器,顯示出來的都會是那個空白佔位圖。若你的活頁簿是由這類工具消費,相機物件就不是合適的建構方式,該範圍的實際渲染圖片才是正確做法

HotXLS 相機物件的佔位影像部件:Excel 在載入時重新渲染連結範圍並忽略這個 108 位元組的中繼檔,而不支援相機功能的檢視器則會顯示空白圖片
Excel 會忽略內嵌位元組,並在載入時重新渲染連結範圍,因此 HotXLS 只需寫入一個 108 位元組的佔位中繼檔,不支援相機功能的檢視器則會顯示這個空白佔位圖

容易出錯的單位換算

Open XML 中的繪圖幾何以英制度量單位(EMU)計量,其中一英吋等於 914,400 EMU,一公分等於 360,000 EMU。然而中繼檔標頭記錄邊框時使用的單位是 0.01 公釐。兩者之間的換算,是除以 360

這一點值得特別指出,因為犯這個錯誤時不會有任何警示。若改成透過 96 DPI 的像素進行換算——乘以 2540 再除以 9525——會得到一個大了 96 倍的數值,而且不會有任何機制拒絕它:封裝仍然有效,圖片仍會由錨點正確放置,唯一出錯的是中繼檔宣告的邊框變得毫無意義。EMU 模型及其捨入行為,說明於影像幾何、EMU 單位與縮放一文

往返存取已含有相機物件的活頁簿

開啟並儲存活頁簿時,相機物件會被保留,包含延伸元素及其範圍參照。這一點比建立相機物件本身更重要:大多數含有相機物件的活頁簿,都是分析師在 Excel 中製作的,而一套在儲存時悄悄把它們轉換成一般圖片的函式庫,等於破壞了分析師原本仰賴的即時行為

// 稽核哪些圖片是相機物件,以及它們指向何處
for I := 0 to Sheet.Images.Count - 1 do
  if Sheet.Images[I].CameraRange <> '' then
    Writeln(Format('camera %d -> %s',
      [I, Sheet.Images[I].CameraRange]));

當你以程式方式產生儀表板時,這項稽核同時也是回歸測試:經過一次儲存並重新開啟的循環後,相機物件的數量必須相同,且各自指向的範圍也必須不變。一般的圖片與繪圖處理,包含用來定位它們的錨定模型,涵蓋於圖表、圖片與繪圖一文

何時相機物件優於其他替代方案

當同一張即時表格必須以不同尺寸出現在多張工作表上,或當列印版面需要把某張工作表的某個區域與其他工作表的區域組合在一起,或當摘要區塊需要跟隨其他地方的編輯而更新,卻不想用公式逐一連結每個儲存格時,就該使用相機物件

當目標只是少數幾個儲存格時,優先選用一般公式,因為跨工作表公式更簡單,而且每種工具都能理解它。當資料本質上是一組數列而非格式化區塊時,優先選用圖表。而當活頁簿會被 Excel 以外的工具消費,或快照在交付後不應再變動——例如一份封存報告不該是即時的——時,優先選用渲染後的圖片

實務上有一個版面配置要點:相機物件會完全依照格式呈現來源範圍,包含合併儲存格、條件式格式與欄寬。因此要做出整潔的儀表板區塊,第一步就是把來源範圍格式化得如同最終輸出一樣,這與合併儲存格與報表範本版面配置一文所描述的做法遵循相同原則

相機物件、繪圖與背後的 XLSX 封裝寫入器,在 Delphi 與 C++Builder 中同屬一套函式庫;完整功能清單請見HotXLS Delphi 試算表元件頁面