在 Excel 2007 中开发加载项 (XLL)发布日期 : 2006-11-09 | 更新日期 : 2006-11-09
Steve Dalton, Eigensys Ltd. 适用于:Microsoft 2007 Office 套件、Microsoft Office Excel 2007 摘要:学习 Microsoft Office Excel 2007 中影响 XLL 加载项和支持新的 XLL 功能性的功能,了解 XLL C API 本身的一些变化。 本页内容 在 Excel 2007 中开发 XLL 在 Excel 2007 中开发 XLL本 文的目标受众是已经具备 Microsoft Office Excel 加载项或 XLL 开发经验的 Microsoft Visual C 和 Microsoft Visual C++ 开发人员。本文尽管对 XLL 开发有一些简要的概述,但其宗旨并非介绍 XLL 的开发。要完全掌握本文要旨,读者应熟悉以下内容:
Excel 2007 中 XLL 相关功能概述Microsoft Office Excel 2007中 XLL 相关功能最明显的变化是工作表单元格从 224 个扩展到 234 个,或者更准确地说,工作表从 256 列 x 65,536 行 (28 x 216) 扩展到 16,384 x 1,048,576 (214 x 220)。这些新限制会溢出以旧的范围和数组结构来容纳它们的整数类型。这一变化需要支持数位更多的整数的新数据结构来定义范围和数组大小。 函数可使用的最大参数数从 30 增加到 255。此外,XLL 现在可与 Excel 交换长 Unicode 字符串而不是长度有限的字节串。 单 处理器和多处理器均支持多线程工作簿重新计算。与 Microsoft Visual Basic for Applications (VBA) 用户定义函数 (UDF) 不同的是,XLL UDF 可以注册为线程安全模式。而与 Excel 中的大多数内置工作表函数一样的是,它们可以分配给并发线程来加快重新计算的速度。上述改进带来益处的同时也附带一些限制,而且不可以滥用多线程权限进行 不安全操作。 现在,尽管数据分析工具仍需要 Analysis Toolpak 加载项,但其函数实际已经完全集成到 Excel 中。这就使得针对早期版本开发的 XLL(使用 xlUDF 调用 ATP 函数)出现不兼容情况。 用户界面也有了显著的变化。对于用户界面的定制,本文将不予介绍,但要说明的是,旧的 XLL 自定义菜单和菜单项仍然可用,但并未放在旧的 C 应用程序编程接口 (API) 函数所在位置。 XLL 概述Microsoft Excel 自 4.0 版本以来,一直支持 XLL 与 Excel 的链接。此外,这些软件还支持 XLL 用来访问 Excel 函数和命令的接口(又称 C API)。XLL 是指包含 Excel 加载项管理器所需回调以及 XLL 导出命令和工作表函数的 DLL。自 Excel 5.0 以来,此接口一直没有明显的变化。目前,Excel 2007 的许多新功能以及早期版本的一些先前不支持的功能在更新的 C API 中都可以使用。本文介绍了新 C API 的新增功能并讨论了开发人员所遇到的一些有关迁移的问题。 Microsoft 随 Excel 97 发布了一个软件开发工具包 (SDK),这是先前 SDK 版本的升级版,其中包括以下组件:
最 简单的 XLL 是在加载加载项时用于导出 Excel 所调用函数的 XLL:xlAutoOpen。此函数执行初始化任务并注册 XLL 所导出的任何函数和命令。这要求加载项调用 XLM 函数 REGISTER() 的 C API 等效命令。Excel 库 (xlcall32.dll) 负责将允许 XLL 回调的函数导出到 Excel,这些函数包括:Excel4 和 Excel4v(表示最初引入 XLL 功能时其版本为 4,而目前 Excel 2007 中的 Excel12 和 Excel12v 对其进行了补充)。 Excel 加载项管理器负责加载并管理 XLL。它会寻找 XLL 所导出的以下函数:
后三个函数可接受或返回 XLOPER。在 Excel 2007 中,XLOPER 和 XLOPER12 均支持这三个函数。 这些函数中唯一必需的是 xlAutoOpen,没有它就无法加载 XLL。在 DLL 内分配内存或其他资源时,还应实现 xlFree (xlFree12) 来避免内存泄漏。缷载 XLL 时,还应实现 xlAutoClose 来清理内存。其他的均可忽略。 C API 因以下原因而得名:Excel 使用一些标准的 C 数据类型来交换数据;库函数用 C 名称修饰;数据结构为 ANSI C。在具备了适当经验的情况下,使用 C++ 可增强 XLL 项目的易管理性、可读性和稳定性。因此,本文的剩余部分假定读者对 C++ 类有一定的了解。 表 1 总结了 Excel 用来与 XLL 交换数据的数据结构,并提供了在注册工作表 UDF 时在第三个参数中传递给 xlfRegister 的类型字母。 表 1. 用来与 XLL 交换数据的 Excel 数据结构
类 型 C%、F%、D%、G%、K%、Q 和 U 都是 Excel 2007 中新增的类型,早期版本不支持这些类型。字符串类型 F、F%、G 和 G% 用于即时修改的参数。当 XLOPER 或 XLOPER12 UDF 函数参数分别注册为类型 P 或 Q 时,Excel 会在准备这些参数时将单单元格引用转换为单个值而将多单元格引用转换为数组。P 和 Q 类型始终以下列类型到达您的函数中:xltypeNum、xltypeStr、xltypeBool、xltypeErr、xltypeMulti、 xltypeMissing 或 xltypeNil,但不能采用 xltypeRef 或 xltypeSRef 类型,因为他们始终是解除引用的。 传 递给 xlfRegister 的第三个参数 type_text 是以上代码组成的字符串。该字符串也可带有英镑符号 (#) 的后缀,表示函数是宏表函数。字符串也可带有感叹号 (!) 的后缀,表示函数是宏表函数且/或被视为具有不稳定性。将函数声明为宏表函数后,函数可以获得未重新计算的单元格的值(包括调用单元格的当前值)并且可以 调用 XLM 信息函数。注册为 # 和采用 R 或 U 类型参数的函数在默认情况下是不稳定的。 Excel 2007 还允许您附加美元符号 ($) 来表示函数具有线程安全性。但宏表函数则不会被视为具有线程安全性。因此,不能将 # 和 $ 符号同时附加到函数的 type_text 类型字符串。如果 XLL 尝试以 # 和 $ 注册函数,将会失败。 Excel 2007 中 XLL 的变化Excel 2007 可加载并运行任何为早期版本创建的加载项。这并不意味着每个 XLL 都可以在 Excel 2007 中按预期运行。在考虑使 XLL 与 Excel 2007 完全兼容之前,应避免某些危险。本节介绍了一些已不再适用的明示或暗含的假设。 新结构所带来的两大变化之一是引入了更大的网格,为此,引入了两种新的数据 typedef 来对行和列计数: C++ typedef INT32 RW; /* XL 12 行 */ 这些带符号的 32 位整数应用于新的 XLOPER12 和 FP12 结构中后,取代了 XLOPER 范围中使用的 WORD 行和 BYTE 列以及 XLOPER 数组和 FP 数据结构中使用的 WORD 行。另一大变化是 XLL 目前支持 Unicode 字符串。XLOPER12 只是一个包括 RW 和 COL 类型的 XLOPER,其中的 ASCII 字节字符串被 Unicode 字符串所取代。 新工作表函数Analysis Toolpak (ATP) 函数现在是 Excel 2007 的一部分。以前 XLL 使用 xlUDF 来调用 ATP 加载项函数。在 Excel 2007中,应通过调用某函数(例如 xlfPrice)来取代上述调用。另外还有许多只能在运行 Excel 2007 时调用的新工作表函数。如果在早期版本中调用这些函数,C API 会返回 xlretInvXlfn。有关详细信息,请参阅编写跨版本 XLL。 字符串Excel 2007 率先允许 XLL 直接访问长度可达 32,767 (215–1) 个字符的宽字符 Unicode 字符串。目前,有多个版本的工作表都支持这些字符串。这是对先前字符串长度不能超过 255 个 ASCII 字节的 C API 的重大改进。但目前仍然通过 C、D、F 和 G 参数类型和 XLOPER xltypeStr 支持字节字符串,其长度限制依旧。 在 Microsoft Windows 中,字节字符串和 Unicode 字符串之间的转换与区域设置有关。也就是说,255 个字节的字符会根据系统的区域设置转换为宽 Unicode 字符或从宽 Unicode 字符转换回来。Unicode 标准为每个代码分配唯一的字符,但扩展的 ASCII 码却非如此。您应记住这一特定于区域设置的转换。例如,两个不等的 Unicode 字符串在转换为字节字符串之后有可能变为相等的。 在 Excel 2007 中,用户看到的任何字符串在内部通常是以 Unicode 表示的。因此,在 Excel 2007 中交换字符串的最有效方式是使用这些宽 Unicode 字符串。早期版本只允许在通过 C API 进行交互时访问字节字符串,尽管宽 Unicode 字符串也可以通过字符串 Variant 来使用。它们可以从 VBA 或使用 Excel 2007 COM 接口传递到 DLL 或 XLL。运行 Excel 2007 时,您应尽可能有意识地使用 Unicode 字符串。 Excel C API 可使用的字符串类型表 2 显示了 C API xltypeStr XLOPER。 表 2. C API xltypeStr XLOPER
重要事项: 不要采用以 Null 值结尾的字节串。 表 3 显示了 C/C++ 字符串。 表 3. C/C++ 字符串
将一个字符串类型转换为另一个XLL 中新增字符串类型后,您可能会需要将字节字符串转换为宽字符或将宽字符转换为字节字符串。 复制字符串时,应确保源字符串长度不要长于目标字符串缓冲区。否则会导致执行失败或字符串截断。表 4 显示了转换和复制库例程。 表 4 转换和复制库例程 请注意,表 4 中显示的所有库函数均带有(最大)字符串长度参数。您务必要提供此参数值以避免溢出 Excel 限制的缓冲区。 请注意以下内容:
以下函数安全地将长度计数字节字符串与以 Null 值结尾的 C 字节字符串互相转换。第一个函数假设传递的目标缓冲区足够大,第二个函数则采用最多 256 个字节的缓冲区(包括长度字节): C++ char *BcountToC(char *cStr, const char *bcntStr) 代码中更大网格的含义在 Microsoft Office Excel 2003 中,单个块范围的最大尺寸为 224 个单元格,正好在 32 位整数的范围内。而在 Excel 2007 中,这一限制变为 234 个单元格。一个涵盖约 228 个单元格的双精度型数组可达到所有应用程序都不能超出的 2G 字节的内存限制,因此用有符号的 32 位整数记录 xltypeMulti 数组或 FP/FP12 是安全的。但是,要安全地获得更大的范围(例如,整个 Excel 2007 网格),您需要 64 位整数类型(例如,__int64 或 INT64)。 范围名称Excel 2007 中,确定有效范围名称的规则发生了变化。目前最大的列是 XFD。例如,公式 =RNG1 现在被解释为第 371 列中的第一个单元格,除非工作簿在“兼容模式”下工作(即打开 Excel 2003 格式工作簿时)。如果用户以 Excel 2007 格式保存 2003 工作簿,Excel 将重新定义名称(例如将 RNG1 定义为 _RNG1),并就所作更改提醒用户。除了 VBA 代码中的名称外,所有的工作簿名称都会被替换,该代码和其他工作簿中可能的外部引用会被断开。您应检查创建、验证或查找这种名称的任何代码。在 RNG1 没有被定义的情况下,有一种修改代码的方法可以使代码在两种保存的格式中都能运行,那就是,查找 _RNG1。 处理大数组和内存不足状况在 Excel 2007 中,被强制使用 XLOPER 范围类型的 XLOPER 数组 (xltypeMulti) 其大小由 Excel 32 位地址空间限定,而不是由用于计算它们的行数和列数或整数的宽度来限定。由于具有了更大的网格,Excel 2007 会更容易突破内存限制。对于使用 xlCoerce 在代码内显式地强制范围引用,或者通过将导出的函数 XLOPER 参数注册为类型 P 或将 XLOPER12 注册为类型 Q 来隐式地强制范围引用,如果范围超出可用内存,这两种操作就会失败。在第一种情况下,对 Excel4 或 Excel12 调用将会失败,并返回 xlretFailed。在后一种情况下,Excel 将返回 #VALUE! 至调用单元格。 出于性能考虑,如果存在用户向加载项传递大数组的重大风险,您就应检测数组的大小并在其超过一定范围时予以拒绝,或使用 xlAbort 启用工作表函数的用户中断。 处理大范围引用和重新计算在 Excel 2003 中,一个引用最多能够指向工作表内的全部 224 个单元格。即使是在处理其中的一部分时,也能根据用户的需要有效地挂起计算机。因此,您应检查范围的大小来确定是否应以更小的块进行处理,对于大数组的处 理,则应使用 xlAbort 来检测用户中断。在 Excel 2007 中,最大范围大约可以达到从前的 1000 倍,因此仔细的检查就更加显得重要。在 Excel 2003 中,某个命令处理整个工作表可能需要一秒钟,而在同等条件下,在 Excel 2007 中则需要 15 分钟。 更多函数参数Excel 2007 允许 XLL 导出最多带有 255 个参数的函数。而事实上,如果一个函数带有那么多的参数,并且每个参数都具有不同的意思,可能就过于复杂,而且会出现故障。这个参数数量更可能由处理输入参数类型相似且数目不定的函数使用。 多线程重新计算Excel 2007 可以被配置为使用多线程来重新计算注册为线程安全的工作表函数。这样可以减少多处理器计算机上的计算时间,但对于单处理器计算机也十分有用,尤其在使用 UDF 访问多线程服务器上的功能的情况下。XLL 胜过 VBA、COM 和 C# 加载项的优势在于它们可将函数注册成线程安全。 访问新命令和工作表函数扩展后的枚举函数定义包括自 Excel 97(版本 9)以来新增的所有工作表函数和许多新命令。新函数如表 5 所示。 表 5. 新函数
新命令如表 6 所示。 表 6. 新命令
表 7 中所示的命令已被删除。 表 7. 删除的命令
C API 函数:Excel4、Excel4v、Excel12、Excel12v有经验的 XLL 开发人员应该非常熟悉旧有的 C API 函数: C++ int _cdecl Excel4(int xlfn, LPXLOPER operRes, int count,... ); 在 Excel 2007 中,新的 SDK 还包括一个源代码模块,该模块包含另外两个可同样运行但带有 XLOPER12 参数的 C API 函数的定义。如果在先前的 Excel 版本中对这些函数进行调用,它们将返回 xlretFailed: C++ int _cdecl Excel12(int xlfn, LPXLOPER12 operRes, int count,... ); 在较早版本中,两个函数与 Excel4 和 Excel4v 都返回相同的成功或错误值,但在 Excel 2007 中,所有这四个函数都能够返回一个新错误:xlretNotThreadSafe,定义为 128。每当注册为线程安全的函数试图调用非线程安全函数(通常是宏表函数或非安全 UDF)时,就会返回该值。 加载项管理器接口函数一些 xlAuto 函数接受或返回 XLOPER。这在 Excel 2007 中仍有效,但除此之外目前还识别 XLOPER12 版本。受影响的函数是: C++ xloper * __stdcall xlAutoRegister(xloper *p_name); 新函数是: C++ xloper12 * __stdcall xlAutoRegister12(xloper12 *p_name); 这四个函数都不是必需的,如果它们不存在,Excel 会使用默认行为。在 Excel 2007 中,如果存在 XLOPER12 版本,它们将会优先于 XLOPER 版本而被调用。创建多版本 XLL 时,应确保两个版本能够等效运行。 浮点矩阵类型在多个版本的 Excel 中都支持注册为类型 K 的以下结构。Excel 2007 xlcall.h SDK 头文件中也定义了这一结构: C++ typedef struct 以下示例是能够引发问题的假设。在 Excel 2007 之前,您可以假定以下代码尽管在风格上差强人意,但是安全的。 C++ // 不安全函数:返回列偏移量(以 0 为基值)和最大合计值。 尽管 FP 类型并不是一种新的数据类型,在 Excel 2007 中,这一结构所容纳的数组可以涵盖新网格的整个宽度(214 列),但至多涵盖 216 行就会被截断。在这种情况下,解决办法非常简单:动态分配一个大小始终适当的缓冲区: C++ double *column_sums = new double[columns]; 为了能够传递多于 216 的行,Excel 2007 还支持一种新数据类型,该类型注册为 K%: C++ typedef struct XLCallVer用于 XLCallVer 函数的签名为: C++ int pascal XLCallVer(void); 在 Microsoft Excel 97 到 Excel 2003 中,XLCallVer 返回 1280 = 0x0500 = 5*256,这表示 Excel 版本 5(最后一次对 C API 做出更改的版本)。在 Excel 2007 中,它返回 0x0C00,同样这表示版本 12。 尽管可以通过这种方法来确定在运行时可否使用新的 C API,但更好办法是使用 Excel4(xlfGetWorkspace, &version, 1, &arg) 来检测 Excel 的运行版本,其中 arg 是设为 2 的数值型 XLOPER,version 是字符串型 XLOPER,之后可将其强制成为一个整数。这是因为加载项可能也需要检测 Microsoft Excel 2000、Microsoft Excel 2002 和 Excel 2003 之间的一些不同。例如,一些统计函数的精确度发生了改变,您可能需要对这种情况进行检测。 在不同版本中有不同表现的 C API 函数通常情况下,尽管数值型函数的精确度会有所提高,但工作表函数在各版本中的工作方式并未改变。但在 C API 函数中,以下三个函数在 Excel 2007 中的工作方式与其在较早版本中的有所不同。 xlStack 此函数目前返回实际的堆栈空间或 64K 字节(其中较小的一个)。以下代码示例说明如何在任一版本中获得堆栈空间。 C++ double __stdcall get_stack(void) xlGetHwnd 在 Excel 2003 中,xlGetHwnd 返回一个包含 2 字节短整型数值的 xltypeInt XLOPER,代表 Excel 的整个 Windows HWND 句柄的下半部分。必须如下面所示使用 Windows API EnumWindows 才能获得整个句柄。而在 Excel 2007 中,如果使用 Excel12 调用此函数,则返回的 xltypeInt XLOPER12 包含一个 4 字节的带符号整数,表示整个句柄。值得注意的是,即使是在 Excel 2007 中进行调用,Excel4 也只能返回句柄的下半部分。 C++ HWND get_xl_main_handle(void) xlGetInst 对于 xlGetHwnd,当使用 Excel12 进行调用时,返回的 xltypeInt XLOPER12 包含整个运行实例句柄,而使用 Excel4 进行调用时所返回的 xltypeInt XLOPER 则仅包含句柄的下半部分。 用户界面定制在 Excel 的早期版本中,可以使用 XLL 代码通过 xlcAddBar、xlcAddMenu、xlcAddCommand、xlcShowBar、xlcAddToolbar、xlcAddTool 和 xlcShowToolbar 等命令来定制菜单栏、菜单、命令栏或工具栏。这些命令目前仍得到支持,但由于旧的菜单栏和命令栏结构已被取代,它们将 XLL 命令的访问点放置于“功能区”的“加载项”组中,因此不能为用户提供预定的界面。 您只能使用受管代码来定制 2007 Microsoft Office 版本中的 UI。在 Excel 2007 中定制 UI 的一种方法是获得独立的代码资源或加载项,用于定制 UI 的函数驻留其中。然后,可以将其紧密结合到 XLL 中,回调到 XLL 代码以调用其所包含的命令和函数。 编写跨版本 XLL以下部分介绍如何编写在 Excel 的多个版本中都能兼容的 XLL。 一些有用的常量定义应考虑在 XLL 项目代码中加入一些定义(例如下面的代码)并取代在此上下文中使用的所有字面数值实例。这样会使特定于版本的代码更加清晰明了,减少以乏味的数字形式存在的、与版本相关的错误。 C++ #define MAX_XL11_ROWS 65536 获得运行版本应使用 Excel4(xlfGetWorkspace, &version, 1, &arg) 来检测正在运行哪个 version,其中 arg 是设为 2 的数值型 XLOPER,version 是字符串型 XLOPER,之后可将其强制成为一个整数。在 Excel 2007 中,此版本为 12。应该在 xlAutoOpen 中或者从其中进行检测。也可以调用 XLCallVer,但是这样不会指示出正在运行的是 2007 版本之前的哪一个版本。 链接到 xlcall32 库和 C API 函数基 于 Excel 97 SDK 和 Framework 项目,标准的 xlcall32 库链接方法是在项目中加入对 xlcall32.lib 导入库的引用。(或者也可以在运行时使用 LoadLibrary 和 GetProcAddress 明确链接到 xlcall32.dll。对于以这种方式构建的项目,库在编译时被链接,其输出以一般方式实现原型。例如: C++ #ifdef __cplusplus 在运行时,当 XLL 由 Excel 加载时,它会隐式地链接到 xlcall32.dll。 通过早期版本的导入 库构建(在编译时链接)的任何加载项都可以与 Excel 2007 配合运行,但不能访问 Excel12 和 Excel12v 回调,因为它们没有被定义。使用 Excel 2007 SDK 版本的 xlcall.h 和 C++ 源文件 [name?].cpp 以及链接到 Excel 2007 版本的 xlcall32.lib 的代码,都能安全地在所有最近版本的 Excel 中调用这些函数。如果从 Excel 2007 之前的版本调用这些函数,它们只返回 xlretFailed。这仅仅是一个自动防故障机制,因此应确保代码知道运行版本并调用合适的回调。 创建导出双接口的加载项以一个 XLL 函数为例,此函数接受字符串参数并返回可以是任何工作表数据类型或一个范围的单一参数。在 Excel 2003 和 Excel 2007 中,可以导出注册为类型 RD 且原型如下的函数,其中字符串作为长度计数字节字符串传递。 C++ xloper * __stdcall my_xll_fn(unsigned char *arg); 首先,这在所有最近版本的 Excel 中都能正常运行,但受旧 C API 字符串限制的约束。其次,尽管 Excel 2007 能够传递和接受 XLOPER,但是它在内部将其转换为 XLOPER12,因此 Excel 2007 中有一个隐含的转换开销,而当代码在 Excel 2003 中运行时,则不会出现这种开销。第三,此函数可以是线程安全的,但如果类型字符串更改为 RD$,在 Excel 2003 中的注册就会失败。出于以上原因的考虑,最好为 Excel 2007 用户导出注册为 UD%$ 且原型如下的函数: C++ xloper12 * __stdcall my_xll_fn_v12(wchar_t *arg); 运行 Excel 2007 时最好注册一个不同函数的另外一个原因是,此软件允许 XLL 函数至多接受 255 个参数(而旧版中此限制为 30)。幸运的是,您可以通过从项目中导出两个版本来享受这两者的优点。然后,您可以检测 Excel 的运行版本,并根据具体条件注册最合适的函数。 在一个项目中,对于在注册 XLL 的输出时所传递的数据,存在许多种管理方法。 一 种简单方法是定义称为 ws_func_export_data 的数据结构(如下面的代码所示),然后声明并初始化一个 ws_func_export_data 数组,之后 XLL 代码可利用此数组来初始化传递给 xlfRegister 的 XLOPER 或 XLOPER12。例如: C++ #define XL12_UDF_ARG_LIMIT 255 请注意:无论注册函数怎样处理数据,都只提供一个工作表函数名称,使工作表并不知道(也不需要知道)所调用的是哪个函数。以下是一个函数的示例,该函数调用标准库函数来颠倒工作表字符串: C++ // Excel 11-:注册为“1F”类型 然后,您可以用以下方式为该函数初始化结构: C++ ws_func_export_data WsFuncExports[1] = 以上字符串是以 Null 值结尾的字节字符串。任何使用它们来初始化 XLOPER 的代码都必须首先将其转换为长度计数字符串。如果使用它们来初始化 XLOPER12,则还需要将其从字节转化为 Unicode。或者也可以将这些字符串初始化为带一个前导空格,其他代码会将此空格替换为字符串长度。但这会使一些在调试模式下运行的编译器出现问题。 您可以轻松地修改以上结构定义,以便在运行 Excel 2007 时传递 Unicode 字符串。当然,这也需要您修改使用该结构的代码。 这 种方法有可能使同一工作表在 Excel 2003 和 Excel 2007 中显示不同的运行结果。例如,Excel 2003 将 Excel 2003 工作表单元格中的 Unicode 字符串映射至 ASCII 字节字符串,并在将其传递到 XLL 函数调用前将其截断。而 Excel 2007 将未转换的 Unicode 强类型数据传递到正确注册的 XLL 函数。这样就会使返回结果有所不同。您应了解这种可能性及其为用户带来的后果,不应一味地升级到 Excel 2007。例如,一些内置的数值型函数在 Excel 2000 和 Excel 2003 之间得到了增强。 将数据类型封装到公共容器类或结构中一般而言,XLL 工作表函数会执行以下操作:
您 要提供两个导出接口(如上所述,这两个导出接口必须复制此逻辑的全部)的位置与理想相差甚远,但如果参数数据类型全都不同,还有哪种方法可供选择?答案就 是,将 Excel 数据类型封装到一个公共容器中。有许多可供使用的方法,但让我们暂且不去讨论包含 XLOPER 和 XLOPER12 的问题。这里所概述的解决方案是要创建一个了解 XLOPER 和 XLOPER12(本例中包含了一个涉及到这两者的实例)的 C++ 类。下例讨论了一个 C++ 类 cpp_xloper,在文章结尾列出的作者书籍的第二版中对其进行了全面介绍。 理论上说,该类应拥有 一个在默认情况下对所提供的 XLOPER 或 XLOPER12 初始化表达式进行浅复制的构造函数。(复制为浅复制是为了加速对只读 UDF 参数的解译。)它还应提供存取器函数以实现对类型和值的提取及修改。然后导出函数只需将其 XLOPER 或 XLOPER12 参数转换为该公共类型,调用一个执行实际任务的公共函数,再处理该函数的返回值。以下是使用 cpp_xloper 类的一个示例: C++ xloper * __stdcall my_function_xl4(xloper *arg) 假定方法 ExtractXloper 和 ExtractXloper12 分别返回指向线程安全静态 XLOPER 和 XLOPER12 的指针,并在必要位置设置相应的内存释放位。 要 将该封装整个过程的开销降至最低,构造函数不仅要进行浅复制,还要在运行 Excel 2007 时在内部识别运行的版本、将 XLOPER 转换为 XLOPER12,并调用 Excel12 而不是 Excel4。这是因为在 Excel 2007 中,如果调用 Excel4,则 XLOPER 参数将一直转换到 XLOPER12,并且返回值将一直转换回到 XLOPER。让该类使用相应的类型,并且回调避免在每次调用时进行该转换。 封装 C API 函数继 续前一部分的内容,如果 my_core_function 需要通过 C API 回调到 Excel 中,则它必须将 cpp_xloper 转换回 XLOPER 或 XLOPER12,然后根据所运行版本调用 Excel4 或 Excel12。对此问题的一种解决方案就是将函数 Excel4、Excel4v、Excel12 和 Excel12v 作为类成员函数封装到 cpp_xloper 中。然后可以按以下代码重写 my_core_function: C++ void my_core_function(cpp_xloper &RetVal, cpp_xloper &Arg) 其中,cpp_xloper::Excel 将返回值直接放到 Arg 中。要实现此操作并仍提供灵活性,以便您能够通过 XLOPER、XLOPER12 或 cpp_xloper 参数调用此函数,则应创建多个重载成员函数: C++ int Excel(int xlfn); // 不是严格必需的,但可以简化其他项 请注意,上述代码假定这些函数的变量参数版本的调用方没有将各参数类型混杂在一起。另请注意,由于在这里使用了 const,因此在 Excel4v 和 Excel12v 的定义中也必须添加 const。 实 现这样一个包装后,就不得直接调用 C API 函数。使用此方法的另一个优点是,您可以将返回值的内存管理包含在该类内。如果 Excel 返回一个字符串,则该类可以设置一个标志,告知其在重写或销毁此实例之前调用 xlFree。您还可以将附加的检查内置到这些包装中。例如,可以检查计数值是否没有小于零或大于版本特定的限制值。在这种情况下,您可能希望定义和返回 一个附加错误: C++ #define xlretNotCalled -1 // 未调用 C API 以 下是这些函数之一的实现代码示例,其中,带有 m_ 前缀的变量是类成员变量,标志 m_XLtoFree12 和 m_XLtoFree 用于向该类指明通过调用 xlFree 释放内存,而 m_Op 和 m_Op12 分别为该类在内部复制的 XLOPER 和 XLOPER12 数据结构的副本: C++ int cpp_xloper::Excel(int xlfn, int count, const cpp_xloper *p_op1, ...) 新工作表函数和 Analysis Toolpak 函数与 Excel 的先前版本不同,“分析工具库”(ATP) 函数已并入 Excel 2007 中。在以前,XLL 可以按以下方式用 xlUDF 调用 ATP 函数: C++ double call_ATP_example(void) 在 Excel 2007 中,将用如下所示的代码替换向 Excel 中的调用,其中 gExcelVersion 为项目内具有全局作用域的整数变量,并在调用 xlAutoOpen 期间进行初始化。 C++ int xl_ret_val; 可以通过使用 cpp_xloper 之类的容器使函数在 Excel 2003 和 Excel 2007 中都更独立于版本且更高效。例如: C++ double call_ATP_example_3(void) 如果尝试在较早版本中调用新的 C API 工作表函数,则会得到一个 xlretInvXlfn 错误。 编写线程安全 XLL 和工作表函数Excel 的先前版本是将单个线程用于所有的工作表计算。在多处理器计算机或用户已明确配置为使用多线程的单处理器计算机上,Excel 2007 尝试在主线程与一个或多个附加线程(操作系统对所有处理器分配的线程)之间平衡负载。在双处理器(或双核)计算机上,这最多可将重计算时间加速两倍,具体 要视工作簿内依存关系树的拓扑和所涉及函数中线程安全函数的数量而定。 Excel 2007 使用一个线程(其主线程)调用所有命令、非线程安全函数、xlAuto 函数(除 xlAutoFree 之外),以及 COM 和 VBA 函数。 只要 XLL 开发人员遵守以下几条简单规则,就可创建出线程安全函数:
当 设置了 xlbitDllFree 的 XLOPER 或 XLOPER12 返回到 Excel 后,Excel 在调用该线程上的其他任何函数之前会调用同一线程上的 xlAutoFree。这对所有函数(无论是线程安全还是非线程安全)都是如此,并且避免了线程本地 XLOPER 在其所关联内存释放之前被重用的风险。 从线程安全函数调用 C API 函数VBA 和 COM 加载项函数不被视为线程安全函数。除 C API 命令(例如,任何工作表函数都不允许调用的 xlcDefineName)外,线程安全函数不能访问 XLM 信息函数。也不能通过在类型字符串后面附加磅符号 (#) 来将线程安全 XLL 函数注册为宏表等效项。这样做的结果是,线程安全函数无法执行以下操作:
XLM 的一个例外就是 xlfCaller,它是线程安全函数。如果调用方是一个工作表单元格或范围,则 xlfCaller 会返回一个引用。但是,您不能使用某线程安全函数中的 xlCoerce 以安全方式将所得到的引用强制为一个值,因为这将返回 xlretUncalced。注册带有 # 的该函数可以解决此问题,但在此例中,该函数是宏表等效项,因此不将其视为线程安全函数。这就防止了返回先前值(例如在某一错误条件存在时)的函数成为线 程安全函数。 应注意的是,纯粹的 C API 函数都是线程安全函数:
作为命令等效项的 xlSet 是一个例外,因此不能从任何工作表函数对其进行调用。 除了以下几项外,Excel 2007 所有的内置工作表函数及其 C API 等效项都是线程安全函数:
多线程使用的资源应 使用临界区对可被多个线程访问的读/写内存进行保护。对于每个要保护的内存块,都需要一个命名的临界区。您可以在调用 xlAutoOpen 时对其进行初始化,并在调用 xlAutoClose 时将其释放并设置为空值。应通过调用 EnterCriticalSection 和 LeaveCriticalSection 来包含对受保护块的每个访问。任何时候都只允许在临界区内存在一个线程。以下是对一个名为 g_csSharedTable 的临界区的初始化、取消初始化及使用示例: C++ CRITICAL_SECTION g_csSharedTable; // 全局范围(如果需要) 另一个保护内存块的可能更为安全的方法就是创建一个包含其自身 CRITICAL_SECTION 的类,由该类的构造函数、析构函数和存取器方法来管理其使用情况。此方法的另外一个好处是,保护可能在 xlAutoOpen 运行之前进行初始化或在调用 xlAutoClose 之后仍然存在的对象,但应注意,不要创建过多临界区,无谓地降低操作系统运行速度。 在使用需要同时访问多个受保护内存块的代码时,应特别注意各临界区的输入和退出顺序。例如,以下两个函数可能会创建一个死锁: C++ bool copy_shared_table_element_A_to_B(unsigned int index) 如果某一线程上的第一个函数输入 g_csSharedTableA,而另一个线程上的第二个函数输入 g_csSharedTableB,则两个线程都将挂起。正确的方法是按一致顺序输入,再按相反顺序退出,如下所示: C++ EnterCriticalSection(&g_csSharedTableA); 在可能的情况下,最好从线程合作的角度出发来隔离对不同块的访问,代码如下所示: C++ bool copy_shared_table_element_A_to_B(unsigned int index) 当存在对共享资源的大量争用(例如,频繁的短期访问请求)时,应考虑利用临界区的旋转能力。此方法可减轻资源等待的处理器密集度。为 此,可在临界区初始化时使用 InitializeCriticalSectionAndSpinCount 或在临界区初始化后使用 SetCriticalSectionSpinCount 来设置线程在等到资源可用之前所循环的次数。等待操作的成本比较昂贵,如果资源在此期间被释放,则旋转可以避免等待该资源。在单处理器系统上,旋转计数实 际上是忽略不计的,但也可以指定它且不会带来任何不利影响。内存堆管理器使用的旋转计数为 4000。有关使用临界区的更多信息,可参考“平台 SDK”文档中的 Critical Sections Object(英文)主题。 声明和使用线程本地内存例如,设想一个返回指向 XLOPER 的指针的函数: C++ xloper * __stdcall mtr_unsafe_example(xloper *arg) 此函数不是线程安全函数,因为可能出现一个线程返回静态 XLOPER 而另一个线程将其重写的情况。如果需要将 XLOPER 传递给 xlAutoFree,则发生这种情况的可能性还要更大。一个解决方案就是为返回的 XLOPER 分配内存并实现 xlAutoFree,以便释放 XLOPER 内存其本身: C++ xloper * __stdcall mtr_safe_example_1(xloper *arg){ xloper *p_ret_val = new xloper; // 必须由 xlAutoFree 释放// 代码将 ret_val 设置为 arg 的函数... p_ret_val.xltype |= xlbitDLLFree; // 不管是何类型始终都需要 return p_ret_val; // xlAutoFree 必须释放 p_ret_val} 此方法比下述依赖于 TLS API 的方法要简单一些,但它有两个缺点。第一个缺点是,无论返回的 XLOPER 是什么类型,Excel 都必须调用 xlAutoFree。第二个缺点是,如果新分配的 XLOPER 是在调用 Excel4 时填充的字符串,则没有便捷的方法来通知 xlAutoFree 在使用 delete 释放 p_ret_val 之前需要先使用 xlFree 释放该字符串,这就需要该函数创建一个分配给 DLL 的副本。 避免这些局限性的解决方案是填充并返回一个线程本地 XLOPER,该方法要求 xlAutoFree 不释放 XLOPER 指针其本身。 C++ xloper *get_thread_local_xloper(void); 下一个问题是如何建立和检索线程本地内存。这可利用线程本地存储 (TLS) API 来实现。第一步是使用 TlsAlloc 获得一个 TLS 索引,该索引最终必须使用 TlsFree 得以释放。这两者都可从 DllMain 成功完成: C++ // 此实现只是调用一个函数来设置线程局部存储 获得索引后,下一步是为每个线程分配一个内存块。“动态链接库参考”中的 DllMain 建议,在每次随 DLL_THREAD_ATTACH 事件调用 DllMain 时都执行该操作,并在每个 DLL_THREAD_DETACH 发生时释放内存,但是遵照此建议将导致 DLL 为 Excel 未用于重计算的那些线程执行不必要的分配。最好改为使用“在第一次使用时分配”的策略。首先,需要定义一个要分配给每个线程的结构。对于上述简单示例,使 用以下代码就足够了: C++ struct TLS_data 下面的函数获得一个指向线程本地实例的指针,或在第一次调用时分配一个指针: C++ TLS_data *get_TLS_data(void) 现在我们可以看到线程本地 XLOPER 内存是如何获得的:首先,获得一个指向 TLS_data 线程实例的指针,然后返回一个指向内含于其中的 XLOPER 的指针: C++ xloper *get_thread_local_xloper(void) 通过将 XLOPER12 添加到 TLS_data 并添加一个 get_thread_local_xloper12 访问函数,您可以编写 mtr_safe_example 的 XLOPER12 版本。 应 当明确的是,mtr_safe_example_1 和 mtr_safe_example_2 均为线程安全函数,可以在运行 Excel 2007 时注册为“RP$”,在运行 Excel 2003 时注册为“RP”。您在运行 Excel 2007 时可以创建该 XLL 函数使用 XLOPER12 的版本并注册为“UQ$”,而在 Excel 2003 中根本无法对其注册。 结束语新的 XLL SDK 将在 Office 2007 发布后的某一时间发布,届时您将能够利用到本文中所介绍的新功能。 关于作者 Steve Dalton 是英国 Eigensys Ltd. 的创始人。Eigensys 一直致力于 Excel 开发领域,专门面向财务分析。Dalton 先生是《Excel Add-in Development in C/C++: Applications in Finance》(Excel 加载项开发(C/C++ 版):财务应用软件)(Wiley,2004)和《Financial Applications Using Excel Add-in Development in C/C++》(使用 Excel 加载项开发财务应用软件(C/C++ 版))(Wiley,2007)两书的作者。 本文是与 A23 Consulting 合作完成的。 参考资料 若要了解有关在 Excel 2007 中开发加载项的更多信息,请参阅以下资料:
© 2006 Microsoft Corporation 版权所有。保留所有权利。使用规定。 |
|