技术文章

从 Delphi 创建 Excel Camera 对象:实时快照

HotXLS 通过 AddCamera 从 Delphi 在 XLSX 工作表上创建 Excel camera 对象。camera 对象是一张永久链接到某个单元格区域的图片:每当工作簿打开,或源数据发生变化,Excel 都会从源区域重新渲染它,因此仪表盘可以实时展示另一张工作表上某张表格的内容,尺寸随意,需要的话还可以旋转

这项功能在 Excel 中以 Camera 工具的形式存在,是大多数用户从未见过的一个按钮,因为它默认不在功能区上。它比其他替代方案更好地解决了一个真实的仪表盘问题:复制的区域会过时,图表无法展示任意的单元格内容,而手工粘贴的链接图片又无法由代码生成。camera 对象是唯一一个既能保持实时、又能承载任意内容的构造

从 Delphi 创建的 HotXLS camera 对象:一张放置在仪表盘工作表上的 Data!$B$2:$D$4 实时图片,Excel 会在打开时以及每次源数据编辑后重新渲染它
camera 对象始终与它的源区域保持永久链接,因此 Excel 会在打开时以及每次编辑后重新渲染这张图片,尺寸任意,源区域也可以来自另一张工作表

这项功能记录在哪里,为什么这一点很重要?

主电子表格规范并没有描述 camera 对象,它被当作一个实现细节处理。权威描述位于绘图标记文档中的 Camera Tool 一节,知道这一点能省下一下午在错误文档里查找的时间

从结构上看,camera 对象就是一个普通的图片元素,只是它的非视觉图片属性携带了一个扩展列表。该扩展由一个固定的 GUID 标识,其内部有一个来自 2010 绘图命名空间的元素,记录了两件事:以 A1 样式绝对引用表示的源区域(可选带工作表限定),以及一个形状标识符。图片的其余部分都是普通的

HotXLS 从 Delphi 写出的 camera 对象在 XLSX 中的结构:一个普通图片元素,其扩展列表携带一个 2010 绘图命名空间元素,记录绝对源区域和形状标识符
从结构上看,camera 就是一张普通图片,外加一个来自 2010 绘图命名空间的扩展元素,记录以 A1 样式表示的绝对源区域和一个形状标识符

创建一个 camera 对象

存在两个重载,因为有两种指定区域的方式都很方便。文本形式接受的引用会原样存储,这正是跨工作表源所需要的形式。坐标形式接受同一张工作表上的四个单元格坐标,并为你构建出绝对引用:

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 像素列宽和 20 像素行高;
    // 如有需要,之后可以用 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 的普通图片对象,当图片是 camera 时该属性非空。这也是你在一份你并非作者的工作簿中检测 camera 对象的方法:枚举所有图片并检查这个属性

为什么占位图像不需要是真的?

XLSX 包中的每一张图片都需要一个图像部件,camera 对象也不例外。但 Excel 会忽略这张图像:加载时它会重新渲染被链接的区域并绘制结果。内嵌的字节纯粹是为那些没有实现 camera 行为的工具提供的一份显示缓存

这一事实使得实现里可以省掉一整个子系统。既不需要对源区域做栅格化,也不需要用渲染引擎去生成快照,更没有缓存图像与 Excel 中实时图像不一致的风险。HotXLS 写出一个最小化的占位元文件——一条头记录和一条文件结束记录,总共 108 字节——这既让包结构保持完整,又几乎不花任何代价

有一个后果需要提前考虑:一个能渲染 XLSX 但未实现 camera 对象的查看器,会显示这个占位图,而它是空白的。如果你的工作簿会被这样的工具消费,camera 对象就是错误的选择,一张渲染好的区域图像才是对的

HotXLS camera 对象的占位图像部件:Excel 会在加载时重新渲染被链接的区域,并忽略这个 108 字节的元文件,而不支持 camera 的查看器则会显示一张空白图片
Excel 会忽略内嵌字节,并在加载时重新渲染被链接的区域,因此 HotXLS 只写出一个 108 字节的占位元文件,而不支持 camera 的查看器会显示这个空白占位图

容易出错的单位换算

Open XML 中的绘图几何单位是英制度量单位(EMU),一英寸等于 914,400 EMU,一厘米等于 360,000。然而元文件头记录其画幅时使用的单位是 0.01 毫米。两者之间的换算是除以 360

这一点值得专门提出来,因为出错时不会有任何提示。如果改用按 96 DPI 经像素换算——乘以 2540 再除以 9525——得到的数值会大出 96 倍,而且没有任何环节会拒绝它:包依然有效,图片依然会被锚点正确放置,只有元文件声明的画幅是荒谬的。EMU 模型及其舍入行为见 图像几何、EMU 单位与缩放

往返读写一份已有 camera 对象的工作簿

打开并保存一份工作簿会保留 camera 对象,包括扩展元素及其区域引用。这一点比创建它们更重要:大多数带 camera 对象的工作簿都是分析师在 Excel 中制作的,如果某个库在保存时悄悄把它们转换成普通图片,就摧毁了分析师所依赖的实时行为

// 审计哪些图片是 camera,以及它们各自指向哪里
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]));

当你以编程方式生成仪表盘时,这份审计同时也是回归测试:经过一轮保存再重新打开之后,camera 的数量必须相同,并且各自指向的区域也必须不变。通用的图片与绘图处理,包括为它们定位的锚定模型,见 图表、图片与绘图

什么时候 camera 胜过其他方案

当同一张实时表格需要以不同尺寸出现在多张工作表上时,当一份打印版式需要把某张工作表的一个区域与其他工作表的区域组合在一起时,或者当一个摘要区块应该跟随其他地方所做的编辑而变化、又不想用公式逐个链接每一个单元格时,就该使用 camera 对象

当目标只是少数几个单元格时,优先使用普通公式,因为跨工作表公式更简单,而且每个工具都能理解它。当数据本质上是一个数据系列而不是一个格式化区块时,优先使用图表。而当工作簿会被 Excel 以外的工具消费,或者快照在交付后不应再变化时——一份归档报告本就不该是实时的——优先使用渲染好的图像

一条来自实践的排版提示:camera 会完全按源区域的格式展示,包括合并单元格、条件格式和列宽。因此,要得到一个外观整洁的仪表盘区块,第一步就是把源区域按最终成品的样子来格式化,这与 合并单元格与报表模板排版 中所述的做法是同一套准则

camera 对象、绘图及其背后的 XLSX 包写入器,都打包在同一个面向 Delphi 与 C++Builder 的库中;完整功能列表见 HotXLS Delphi 电子表格组件页面