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 对象模型一同提供