HotXLS 提供 TXLSWorkbookViewer 原生 VCL 控制項,可在 Delphi 或 C++Builder 視窗體內將 XLS、XLSX、XLSM 和 ODS 活頁簿渲染為互動式試算表網格,無需安装 Excel,也無需透過 OLE 自動化驅動 Excel。要把這類控制項做好,需要解决三個具體問題:把落在合併儲存格內的滑鼠點擊映射到正確的逻辑儲存格;用户浏覽遠大於可見視窗口的工作表時,讓滾動位置、表頭區域和儲存格選擇維持一致;以及明確點擊批註標記或超連結儲存格後應執行什麼操作
多數 Delphi 团隊使用試算表檢視器,並不是為了編輯,而是為了在活頁簿進入處理流程前預覽上傳檔案,在部署映像沒有 Microsoft Office 的自助終端或報表檢視器中展示內容,或為 QA 工具提供一種不必透過 COM 不穩定地自動化真實 Excel 進程的檢視方式。普通字串網格可以讓儲存格快速顯示文字,但試算表檔案並不是普通網格:儲存格會合併成只有底層模型確定後才存在的區域,工作表帶有固定表頭以及彼此獨立的水平和垂直滾動位置,儲存格還可能帶有需要獨立互動模型的批註和超連結。TXLSWorkbookViewer 正是 HotXLS 對這一缺口的回答,其內部設計也可以作為從零建立類似控制項的合理蓝圖
活頁簿檢視器如何避免依賴 Excel?
TXLSWorkbookViewer 完全不依賴 Excel,而是讀取 HotXLS 自己解析出的對象模型,不借助 Excel 打開並操控檔案。對於經典 XLS 檔案,Workbook 屬性绑定現有的 TXLSWorkbook;對於 XLSX、XLSM 和模板變體,XlsxWorkbook 绑定 TXLSXWorkbook;這兩類活頁簿都可以已經在應用程序的其他位置打開,而檢視器只负责讀取。當控制項需要自行管理檔案時,LoadFromFile 會檢查擴展名,將 XLSX、XLSM、XLTX、XLTM 和 ODS 交給現代引擎,其余格式交給經典引擎,並在控制項被清空或销毁時釋放它所創建立的活頁簿
var
Viewer: TXLSWorkbookViewer;
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create;
if Book.Open('quarterly-report.xlsx') <> 1 then
raise Exception.Create('Could not open workbook');
Viewer := TXLSWorkbookViewer.Create(Self);
Viewer.Parent := Self;
Viewer.Align := alClient;
Viewer.XlsxWorkbook := Book; // the viewer does not take ownership
Viewer.GoToCell(1, 1);
Caption := Viewer.WorksheetName + ': ' + Viewer.SelectedCellText;
end;
如何在合併區域內定位正確的儲存格?
要將點擊解析為 TXLSWorkbookViewer 中的正確儲存格,需要進行兩阶段尋找。之所以要拆開,是因為像素几何位置和試算表語義確實是兩個不同的問題。第一阶段是純几何計算:私有 CellAtPoint 方法從目前滾動位置開始遍歷列寬和行高,直到找到包含點擊 X、Y 坐標的行列區域,完全不感知合併儲存格。第二阶段是語義解析:所有會改變選擇的路徑——滑鼠點擊、方向鍵、Tab 或直接呼叫 GoToCell——都會進入一個內部 ChangeSelection 例程;該例程會根據合併關係規範化原始行列,並在真正改變選擇前將其吸附到合併區域的锚點儲存格
锚點是合併區域左上角的儲存格,也是底層活頁簿模型中真正儲存值、格式、批註或超連結的唯一儲存格;合併區域視觉覆盖的其他儲存格在資料中都是空的。對於經典 XLS 活頁簿,锚點來自 Cell.MergeArea,這是一個 IXLSRange,其 Row 和 Column 指向所屬儲存格;對於 XLSX 和 ODS 活頁簿,MergedCells.FindAt 回傳 TXLSXMergedRange,透過 Row1 和 Col1 暴露同一個锚點。繪製時也會獨立解决相同問題:將合併儲存格的矩形擴展到完整的行列跨度,並跳過該跨度內的儲存格,從而讓選擇轮廓包住整個合併區塊,而不只是锚點角落。寫入合併佈局而不只是讀取佈局,则是另一個相關但獨立的問題,詳見配套文章報表模板中的合併儲存格佈局
var
Sheet: TXLSXWorksheet;
begin
Sheet := Book.Sheets.Add('Summary');
Sheet.MergeCells(2, 2, 3, 4); // B2:D3
Sheet.Cells[2, 2].Value := 'Region totals';
Viewer.XlsxWorkbook := Book;
Viewer.GoToCell(3, 4); // targets the bottom-right corner of the merge
// SelectedRow is now 2 and SelectedCol is now 2: normalized to the anchor cell
end;
如何讓滾動、表頭和選擇維持同步?
TXLSWorkbookViewer 會維持三部分獨立狀態的一致性:由 TopRow 和 LeftCol 儲存的逻辑滾動位置;控制項透過 CreateParams 中的 WS_HSCROLL 和 WS_VSCROLL 请求的原生 Windows 滾動條;以及由 SelectedRow 和 SelectedCol 表示的目前選擇。拖動滾動條或滾動滑鼠滾轮會觸發 WM_HSCROLL、WM_VSCROLL 或 WM_MOUSEWHEEL,更新 TopRow 或 LeftCol 並重繪;選擇不會移動,這與 Excel 將浏覽和選擇分開的行為一致。每次更新後,UpdateScrollBars 會透過 SetScrollInfo 將新位置寫回原生滾動條,因此滑區塊不會偏離網格實際顯示的內容
鍵盤導航會反向執行同样的同步:當選擇越過可見網格邊緣時,會呼叫 EnsureSelectionVisible,透過累加實際列寬和行高來調整 TopRow 或 LeftCol,而不是簡單加一,因為行列可能具有自訂尺寸;隨後它會呼叫 UpdateScrollBars,讓滑區塊反映鍵盤刚刚帶到的位置。透過 RowHeaderWidth 和 ColumnHeaderHeight 設定的行號和列字母表頭,會在 TopRow 和 LeftCol 滾動下方資料時固定在螢幕幕上,這就是該控制項自行提供的凍結範圍:它不是 Excel 的 Freeze Panes 功能,也沒有內置方式在其余工作表滾動時固定任意資料行或列。發佈用於檢視未知檔案的檢視器前,還應測試一個邊界:TopRow 和 LeftCol 不會根據工作表實際使用範圍進行限制,因此將滑區塊拖到結構上限時,可能落在第 1,048,576 行或第 16,384 列,顯示空白網格,而不是顯示實際有資料的最後一行或最後一列;如果活頁簿大到足以暴露這一點,通常也大到需要關註大型活頁簿性能文章中讨论的加載侧問題
如何將批註和超連結連接到滑鼠及選擇事件?
TXLSWorkbookViewer 將批註和超連結視為目前選中儲存格的屬性,而不是懸停目標,因此每次觸發 OnSelectionChange 時,SelectedCellCommentText、SelectedCellCommentAuthor 和 SelectedCellHyperlink 都會更新,無论選擇是透過滑鼠點擊、方向鍵還是呼叫 GoToCell 移動的。帶批註的儲存格會在右上角繪製一個小红三角作為視觉提示,類似 Excel 自己的批註標記,但該標記純粹是視觉效果;控制項沒有內置懸停觸發的工具提示,因此如果應用程序希望在滑鼠移入時而不是選中時弹出提示,就必須自行建立這一層。超連結激活也遵循先選擇後處理的方式:双擊儲存格會呼叫 ActivateSelectedCell,讀取 SelectedCellHyperlink,如果不為空,则透過目標地址和 var Handled: Boolean 參數觸發 OnHyperlinkClick,由處理程序設定該參數
OnHyperlinkClick 不會做什麼同样重要:無论處理程序將 Handled 設為 true 還是維持 false,TXLSWorkbookViewer 都不會自行呼叫 ShellExecute 或打開瀏覽器。導航以及判斷目標是否安全,完全由宿主應用程序负责;對於一個無法判斷自身嵌入的是受信任的內部工具還是客户刚上傳檔案的檢視器的元件來說,這是合理的預設行為
procedure TMainForm.ViewerSelectionChange(Sender: TObject; Row, Col: Integer);
begin
if Viewer.SelectedCellCommentText <> '' then
StatusBar.SimpleText := Viewer.SelectedCellCommentAuthor + ': ' +
Viewer.SelectedCellCommentText
else
StatusBar.SimpleText := Viewer.SelectedCellHyperlink;
end;
procedure TMainForm.ViewerHyperlinkClick(Sender: TObject;
const Target: WideString; var Handled: Boolean);
begin
ShellExecute(0, 'open', PWideChar(Target), nil, nil, SW_SHOWNORMAL);
Handled := True;
end;
選擇範圍和鍵盤導航有哪些限制?
TXLSWorkbookViewer 中的選擇始终是單個逻辑儲存格,由 SelectedRow 和 SelectedCol 跟踪;基礎控制項不支援矩形多儲存格區域選擇,因此任何需要對儲存格區塊操作的功能,都必須建立在控制項之上,而不能從一個選擇對象中直接讀取。鍵盤支援刻意維持基礎:方向鍵每次移動一個儲存格,Home 回傳行首,配合 Ctrl 時回傳 A1 儲存格,Page Up 和 Page Down 跳轉十行,Tab 和 Shift+Tab 横向移動;控制項沒有 Ctrl+Arrow 跳轉到資料區域邊緣的功能,也沒有 Shift 擴展區域選擇,因此直接從 Excel 轉來的用户在密集工作表上會明顯感到差異
列限制與處理合併規範化的同一個 ChangeSelection 關鍵入口中執行,並且按引擎有意區分:绑定經典 TXLSWorkbook 的檢視器會將列限制在 256 列,這是 BIFF8 格式的結構上限;绑定 TXLSXWorkbook 的檢視器则遵循現代的 16,384 列限制,這是 XLSX 從 Excel 2007 起繼承的上限。無论哪種引擎,行數都限制為 1,048,576 行,因此在同一個檢視器中打開舊版 XLS 檔案和 XLSX 檔案時,實際差異完全體現在網格允許向右移動多遠
將問題拆分為像素尋找、锚點規範化和少量訊息處理程序後,這些工作並不神秘;但要讓三者在真實檔案、真實合併區域、批註和超連結下維持一致,才是這類控制項的大部分工作。TXLSWorkbookViewer 屬於面向 Delphi 和 C++Builder 的標準HotXLS Excel 元件,與它所渲染的經典和 XLSX 對象模型一同提供