分享

在 Excel 2007 中开发加载项 (XLL)

 weicat 2011-04-09

在 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
Excel 2007 中 xll 相关功能概述 Excel 2007 中 XLL 相关功能概述
XLL 概述 XLL 概述
Excel 2007 中 xll 的变化 Excel 2007 中 XLL 的变化
编写跨版本 xll 编写跨版本 XLL
编写线程安全 xll 和工作表函数 编写线程安全 XLL 和工作表函数
结束语 结束语

在 Excel 2007 中开发 XLL

本 文的目标受众是已经具备 Microsoft Office Excel 加载项或 XLL 开发经验的 Microsoft Visual C 和 Microsoft Visual C++ 开发人员。本文尽管对 XLL 开发有一些简要的概述,但其宗旨并非介绍 XLL 的开发。要完全掌握本文要旨,读者应熟悉以下内容:

  • C 和 C++ 语言概念和结构。本文的代码示例是用 C++ 编写的。

  • 构建用于导出函数的 DLL。

  • XLOPER 数据结构和其他 Excel 数据类型,例如浮点矩阵结构 (FP)。

  • 加载项管理器接口函数,例如 xlAutoOpen 和 xlAutoClose。

  • XLOPER 内存管理(xlAutoFree、xlFree以及 xlbitDLLFree 和 xlbitXLFree 的应用)。

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 版本的升级版,其中包括以下组件:

  • A C 头文件 xlcall.h,包含 Excel 用来与 XLL 交换数据的数据结构定义;与内置 Excel 工作表函数、XLM 信息函数和许多命令相应的枚举函数和命令定义;以及 Excel 回调函数 Excel4、Excel4v 和 XLCallVer 的原型。

  • 静态导入库,xlcall32.lib。Excel 回调以 C 名称修饰从此库导出。

  • XLL SDK Framework 项目,其中包含完整的 XLL 项目和许多用于处理 Excel 数据类型和帮助调用 Excel4 和 Excel4v 的例程。

最 简单的 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 所导出的以下函数:

  • xlAutoOpen:加载 XLL 时调用。最适于注册 XLL 函数和命令、初始化数据结构和自定义用户界面。

  • xlAutoClose:缷载 XLL 时调用。用于取消函数和命令注册、释放资源和撤消自定义。

  • xlAutoAdd:会话期间激活或加载 XLL 时调用。

  • xlAutoRemove:会话期间取消激活或缷载 XLL 时调用。

  • xlAddInManagerInfo (xlAddInManagerInfo12):首次调用加载项管理器时调用。如果所传递的参数为 1,它会返回一个字符串(加载项名称),否则将返回 #VALUE!

  • xlAutoRegister (xlAutoRegister12):在调用了 REGISTER (XLM) 或 xlfRegister (C API) 时调用,无函数返回值和参数类型。它会在内部搜索 XLL 以便用所提供的信息注册函数。

  • xlAutoFree (xlAutoFree12):Excel 收到 XLOPER(标记为指向 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 数据结构

数据类型

传递数值

传递引用(指针)

注释

Boolean

A

L

short (0=false or 1=true)

Double

B

E

-

char *

-

C, F

以 Null 值结尾的 ASCII 字节串

unsigned char *

-

D, G

计数 ASCII 字节字符串

[v12+] unsigned short *

-

C%, F%

以 Null 值结尾的 Unicode 宽字符字符串

[v12+] unsigned short *

-

D%, G%

计数 Unicode 宽字符字符串

unsigned short [int]

H

-

DWORD、size_t、wchar_t

[signed] short [int]

I

M

16 位

[signed long] int

J

N

32 位

FP

-

K

浮点数组结构

[v12+] FP12

-

K%

更大网格浮点数组结构

XLOPER

P

R

变量类型的工作表值和数组

值、数组和范围引用

[v12+] XLOPER12

-

-

Q

U

变量类型的工作表值和数组

值、数组和范围引用

类 型 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 行 */
typedef INT32 COL; /* 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

字节字符串:XLOPER

宽字符字符串:XLOPER12

所有版本的 Excel

仅限 Excel 2007 及以上版本

最大长度:255 个扩展的 ASCII 字节

最大长度 32,767 个 Unicode 字符

首个(无符号)字节 = 长度

首个宽字符 = 长度

重要事项:

不要采用以 Null 值结尾的字节串。

表 3 显示了 C/C++ 字符串。

表 3. C/C++ 字符串

字节字符串

宽字符字符串

以 Null 值结尾的 (char *) "C" 最大长度:255 个扩展的 ASCII 字节

以 Null 值结尾的 (wchar_t *) "C%" 最大长度为 32,767 个 Unicode 字符

长度计数 (unsigned char *) "D"

长度计数 (wchar_t *) "D%"

将一个字符串类型转换为另一个

XLL 中新增字符串类型后,您可能会需要将字节字符串转换为宽字符或将宽字符转换为字节字符串。

复制字符串时,应确保源字符串长度不要长于目标字符串缓冲区。否则会导致执行失败或字符串截断。表 4 显示了转换和复制库例程。

表 4 转换和复制库例程

.

请注意,表 4 中显示的所有库函数均带有(最大)字符串长度参数。您务必要提供此参数值以避免溢出 Excel 限制的缓冲区。

请注意以下内容:

  • 使用声明为 [signed] char * 的长度计数字节字符串时,请将长度转换为 BYTE 以避免长于 127 的字符串产生负数结果。

  • 将以 Null 值结尾的最大长度字符串复制到长度计数字符串缓冲区时,不要复制以 Null 值结尾的字符,因为这样可能会溢出缓冲区。

  • 为以 Null 值结尾的字符串分配新内存时,请为 Null 值结尾分配空间。

  • 将长度计数字符串复制到以 Null 值结尾的字符串时,请明确设置 Null 值结尾,除非使用始终替您进行此设置的 API 函数,例如 lstrcpynA()。

  • 在速度要求较高环境中,如果您知道所复制的字节数,请使用 memcpy() 代替 strcpy()、strncpy()、wcscpy() 或 wcsncpy()。

以下函数安全地将长度计数字节字符串与以 Null 值结尾的 C 字节字符串互相转换。第一个函数假设传递的目标缓冲区足够大,第二个函数则采用最多 256 个字节的缓冲区(包括长度字节):

C++

char *BcountToC(char *cStr, const char *bcntStr)
{
BYTE cs_len = (BYTE)bcntStr[0];
if(cs_len) memcpy(cStr, bcntStr+1, cs_len);
cStr[cs_len] = '\0';
return cStr;
}
#define MAX_XL4_STR_LEN 255u
char *CToBcount(char *bcntStr, const char *cStr)
{
size_t cs_len = strlen(cStr);
if(cs_len > MAX_XL4_STR_LEN) cs_len = MAX_XL4_STR_LEN;
memcpy(bcntStr+1, cStr, cs_len);
bcntStr[0] = (BYTE)cs_len;
return 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. 新函数




xlfAccrint

xlfCumprinc

xlfImlog10

xlfQuotient

xlfAccrintm

xlfDec2bin

xlfImlog2

xlfRandbetween

xlfAmordegrc

xlfDec2hex

xlfImpower

xlfReceived

xlfAmorlinc

xlfDec2oct

xlfImproduct

xlfRoundbahtdown

xlfAveragea

xlfDelta

xlfImreal

xlfRoundbahtup

xlfAverageif

xlfDisc

xlfImsin

xlfRtd

xlfAverageifs

xlfDollarde

xlfImsqrt

xlfSeriessum

xlfBahttext

xlfDollarfr

xlfImsub

xlfSqrtpi

xlfBesseli

xlfDuration

xlfImsum

xlfStdeva

xlfBesselj

xlfEdate

xlfIntrate

xlfStdevpa

xlfBesselk

xlfEffect

xlfIseven

xlfSumifs

xlfBessely

xlfEomonth

xlfIsodd

xlfTbilleq

xlfBin2dec

xlfErf

xlfIsthaidigit

xlfTbillprice

xlfBin2hex

xlfErfc

xlfLcm

xlfTbillyield

xlfBin2oct

xlfFactdouble

xlfMaxa

xlfThaidayofweek

xlfComplex

xlfFvschedule

xlfMduration

xlfThaidigit

xlfConvert

xlfGcd

xlfMina

xlfThaimonthofyear

xlfCountifs

xlfGestep

xlfMround

xlfThainumsound

xlfCoupdaybs

xlfGetpivotdata

xlfMultinomial

xlfThainumstring

xlfCoupdays

xlfHex2bin

xlfNetworkdays

xlfThaistringlength

xlfCoupdaysnc

xlfHex2dec

xlfNominal

xlfThaiyear

xlfCoupncd

xlfHex2oct

xlfOct2bin

xlfVara

xlfCoupnum

xlfHyperlink

xlfOct2dec

xlfVarpa

xlfCouppcd

xlfIferror

xlfOct2hex

xlfViewGet

xlfCubekpimember

xlfImabs

xlfOddfprice

xlfWeeknum

xlfCubemember

xlfImaginary

xlfOddfyield

xlfWorkday

xlfCubememberproperty

xlfImargument

xlfOddlprice

xlfXirr

xlfCuberankedmember

xlfImconjugate

xlfOddlyield

xlfXnpv

xlfCubeset

xlfImcos

xlfPhonetic

xlfYearfrac

xlfCubesetcount

xlfImdiv

xlfPrice

xlfYield

xlfCubevalue

xlfImexp

xlfPricedisc

xlfYielddisc

xlfCumipmt

xlfImln

xlfPricemat

xlfYieldmat

新命令如表 6 所示。

表 6. 新命令




xlcActivateNotes

xlcInsertdatatable

xlcOptionsSettings

xlcUnprotectRevisions

xlcAddPrintArea

xlcInsertMapObject

xlcOptionsSpell

xlcVbaactivate

xlcAutocorrect

xlcLayout

xlcPicklist

xlcViewDefine

xlcClearPrintArea

xlcMoveBrk

xlcPivotTableChart

xlcViewDelete

xlcDeleteNote

xlcNewwebquery

xlcPostDocument

xlcViewShow

xlcEditodc

xlcNormal

xlcProtectRevisions

xlcWebPublish

xlcHideallInkannots

xlcOptionsMe

xlcRmPrintArea

xlcWorkgroupOptions

xlcHideallNotes

xlcOptionsMenono

xlcSheetBackground

-

xlcHidecurrNote

xlcOptionsSave

xlcTraverseNotes

-

表 7 中所示的命令已被删除。

表 7. 删除的命令


xlcStart

xlcVbaObjectBrowser

xlcVbaAddWatch

xlcVbaReferences

xlcVbaClearBreakpoints

xlcVbaReset

xlcVbaDebugWindow

xlcVbaStepInto

xlcVbaEditWatch

xlcVbaStepOver

xlcVbaEnd

xlcVbaToggleBreakpoint

xlcVbaInstantWatch

-

C API 函数:Excel4、Excel4v、Excel12、Excel12v

有经验的 XLL 开发人员应该非常熟悉旧有的 C API 函数:

C++

int _cdecl Excel4(int xlfn, LPXLOPER operRes, int count,... ); 
/* 后跟计数 LPXLOPER */
int pascal Excel4v(int xlfn, LPXLOPER operRes, int count, LPXLOPER opers[]);

在 Excel 2007 中,新的 SDK 还包括一个源代码模块,该模块包含另外两个可同样运行但带有 XLOPER12 参数的 C API 函数的定义。如果在先前的 Excel 版本中对这些函数进行调用,它们将返回 xlretFailed:

C++

int _cdecl Excel12(int xlfn, LPXLOPER12 operRes, int count,... ); 
/* 后跟计数 LPXLOPER12 */
int pascal Excel12v(int xlfn, LPXLOPER12 operRes, int count, LPXLOPER12 opers[]);

在较早版本中,两个函数与 Excel4 和 Excel4v 都返回相同的成功或错误值,但在 Excel 2007 中,所有这四个函数都能够返回一个新错误:xlretNotThreadSafe,定义为 128。每当注册为线程安全的函数试图调用非线程安全函数(通常是宏表函数或非安全 UDF)时,就会返回该值。

加载项管理器接口函数

一些 xlAuto 函数接受或返回 XLOPER。这在 Excel 2007 中仍有效,但除此之外目前还识别 XLOPER12 版本。受影响的函数是:

C++

xloper * __stdcall xlAutoRegister(xloper *p_name);
xloper * __stdcall xlAddInManagerInfo(xloper *p_arg);

新函数是:

C++

xloper12 * __stdcall xlAutoRegister12(xloper12 *p_name);
xloper12 * __stdcall xlAddInManagerInfo12(xloper12 *p_arg);

这四个函数都不是必需的,如果它们不存在,Excel 会使用默认行为。在 Excel 2007 中,如果存在 XLOPER12 版本,它们将会优先于 XLOPER 版本而被调用。创建多版本 XLL 时,应确保两个版本能够等效运行。

浮点矩阵类型

在多个版本的 Excel 中都支持注册为类型 K 的以下结构。Excel 2007 xlcall.h SDK 头文件中也定义了这一结构:

C++

typedef struct
{
WORD rows;
WORD columns;
double array[1]; // array[行 * 列] 的开始
}
FP;

以下示例是能够引发问题的假设。在 Excel 2007 之前,您可以假定以下代码尽管在风格上差强人意,但是安全的。

C++

// 不安全函数:返回列偏移量(以 0 为基值)和最大合计值。
int __stdcall max_column_index(FP *pArray)
{
int c, columns = pArray->columns;
int r, rows = pArray->rows;
double *p, column_sums[256]; // 显式假定!!!

for(c = 0; c < columns; c++)
column_sums[c] = 0.0; // 如果列数 > 256,则可能溢出!!!

for(r = 0, p = pArray->array; r < rows; r++)
for(c = 0; c < columns; c++)
column_sums[c] += *p++; // 可能溢出!!!

int max_index = 0;
for(c = 1; c < columns; c++)
if(column_sums[c] > column_sums[max_index]) // 溢出!!!
max_index = c;
return max_index;
}

尽管 FP 类型并不是一种新的数据类型,在 Excel 2007 中,这一结构所容纳的数组可以涵盖新网格的整个宽度(214 列),但至多涵盖 216 行就会被截断。在这种情况下,解决办法非常简单:动态分配一个大小始终适当的缓冲区:

C++

    double *column_sums = new double[columns];
// ...
delete[] column_sums;
return max_index;

为了能够传递多于 216 的行,Excel 2007 还支持一种新数据类型,该类型注册为 K%:

C++

typedef struct
{
RW rows;
COLS columns;
double array[1]; // array[行 * 列] 的开始
}
FP12;

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)
{
if(gExcelVersion12plus)
{
xloper12 retval;
if(xlretSuccess != Excel12(xlStack, &retval, 0))
return -1.0;

if(retval.xltype == xltypeInt)
return (double)retval.val.w; // 返回 min(64Kb,实际空间)
// 这不是目前版本的返回类型,但在 Excel 12 beta 版
// 中返回,所以代码在此仍保留。
if(retval.xltype == xltypeNum)
return retval.val.num;
}
else
{
xloper retval;
if(xlretSuccess != Excel4(xlStack, &retval, 0))
return -1.0;

if(retval.xltype == xltypeInt)
return (double)(unsigned short)retval.val.w;
}
return -1.0;
}

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)
{
if(gExcelVersion12plus) // xlGetHwnd 返回整个句柄
{
xloper12 main_xl_handle;
if(Excel12(xlGetHwnd, &main_xl_handle, 0) != xlretSuccess)
return 0;
return (HWND)main_xl_handle.val.w;
}
else // xlGetHwnd 仅返回句柄低位
{
xloper main_xl_handle;
if(Excel4(xlGetHwnd, &main_xl_handle, 0) != xlretSuccess)
return 0;
get_hwnd_enum_struct eproc_param = {main_xl_handle.val.w, 0};
EnumWindows((WNDENUMPROC)get_hwnd_enum_proc, (LPARAM)&eproc_param);
return eproc_param.full_handle;
}
}

#define CLASS_NAME_BUFFER_SIZE 50

typedef struct
{
short main_xl_handle;
HWND full_handle;
}
get_hwnd_struct;

// Windows 针对每个顶层窗口调用的回调函数
BOOL __stdcall get_hwnd_enum_proc(HWND hwnd, get_hwnd_struct *p_enum)
{
// 检查句柄的低字是否与 Excel 的匹配
if(LOWORD((DWORD)hwnd) != p_enum->main_xl_handle)
return TRUE; // 保持迭代

char class_name[CLASS_NAME_BUFFER_SIZE + 1];
// 确保 class_name 始终以 null 结尾
class_name[CLASS_NAME_BUFFER_SIZE] = 0;
GetClassName(hwnd, class_name, CLASS_NAME_BUFFER_SIZE);
// 对 Excel 主窗口类名执行一次不区分大小写的比较
if(_stricmp(class_name, "xlmain") == 0)
{
p_enum->full_handle = hwnd;
return FALSE; // 告诉 Windows 停止迭代
}
return TRUE; // 告诉 Windows 继续迭代
}

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
#define MAX_XL11_COLS 256
#define MAX_XL12_ROWS 1048576
#define MAX_XL12_COLS 16384
#define MAX_XL11_UDF_ARGS 30
#define MAX_XL12_UDF_ARGS 255
#define MAX_XL4_STR_LEN 255u
#define MAX_XL12_STR_LEN 32767u

获得运行版本

应使用 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
extern "C" {
#endif
int _cdecl Excel4(int xlfn, LPXLOPER operRes, int count,... );
//...
#ifdef __cplusplus
} // extern“C”块结束
#endif

在运行时,当 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
typedef struct
{
// 必需(如果未定义 v12+ 字符串,则使用 v11- 字符串):
char *name_in_code; // RegArg2:代码中的函数名 (v11-)
char *types; // RegArg3:返回类型和参数类型 (v11-)
char *name_in_code12; // RegArg2:代码中的函数名 (v12+)
char *types12; // RegArg3:返回类型和参数类型 (v12+)
char *ws_name; // RegArg4:工作表中函数名
char *arg_names; // RegArg5:参数名(Excel 11-:最大 30)
char *arg_names12; // RegArg5:参数名(Excel 12+:最大 64)
// 可选:
char *fn_category; // RegArg7:函数向导的函数类别
char *help_file; // RegArg9:帮助文件(可选)
char *fn_description; // RegArg10:函数说明文本(可选)
char *arg_help[MAX_XL12_UDF_ARGS - 11]; // RegArg11:参数帮助文本
}
ws_func_export_data;

请注意:无论注册函数怎样处理数据,都只提供一个工作表函数名称,使工作表并不知道(也不需要知道)所调用的是哪个函数。以下是一个函数的示例,该函数调用标准库函数来颠倒工作表字符串:

C++

// Excel 11-:注册为“1F”类型
void __stdcall reverse_text_xl4(char *text) {strrev(text);}

// Excel 12+:注册为“1F%$”类型(如果与线程安全的库链接)
void __stdcall reverse_text_xl12(wchar_t *text) {wcsrev(text);}

然后,您可以用以下方式为该函数初始化结构:

C++

ws_func_export_data WsFuncExports[1] =
{
{
"reverse_text_xl4",
"1F",
"reverse_text_xl12",
"1F%$",
"Reverse",
"Text",
"", // arg_names12
"Text", // 函数类别
"", // 帮助文件
"Reverse text",
"Text ",
},
};

以上字符串是以 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。

您 要提供两个导出接口(如上所述,这两个导出接口必须复制此逻辑的全部)的位置与理想相差甚远,但如果参数数据类型全都不同,还有哪种方法可供选择?答案就 是,将 Excel 数据类型封装到一个公共容器中。有许多可供使用的方法,但让我们暂且不去讨论包含 XLOPER 和 XLOPER12 的问题。这里所概述的解决方案是要创建一个了解 XLOPER 和 XLOPER12(本例中包含了一个涉及到这两者的实例)的 C++ 类。下例讨论了一个 C++ 类 cpp_xloper,在文章结尾列出的作者书籍的第二版中对其进行了全面介绍。

理论上说,该类应拥有 一个在默认情况下对所提供的 XLOPER 或 XLOPER12 初始化表达式进行浅复制的构造函数。(复制为浅复制是为了加速对只读 UDF 参数的解译。)它还应提供存取器函数以实现对类型和值的提取及修改。然后导出函数只需将其 XLOPER 或 XLOPER12 参数转换为该公共类型,调用一个执行实际任务的公共函数,再处理该函数的返回值。以下是使用 cpp_xloper 类的一个示例:

C++

xloper * __stdcall my_function_xl4(xloper *arg)
{
cpp_xloper Arg(arg); // 构造函数进行浅复制
cpp_xloper RetVal; // 通过引用传递以检索返回值
my_core_function(RetVal, Arg);
return RetVal.ExtractXloper();
}

xloper12 * __stdcall my_function_xl12(xloper12 *arg)
{
cpp_xloper Arg(arg); // 构造函数进行浅复制
cpp_xloper RetVal; // 通过引用传递以检索返回值
my_core_function(RetVal, Arg);
return RetVal.ExtractXloper12();
}

void my_core_function(cpp_xloper &RetVal, cpp_xloper &Arg)
{
double d;
if(Arg.IsMissing() || Arg.IsNil())
RetVal.SetToError(xlerrValue);
else if(Arg.IsNum() && (d = (double)Arg) >= 0.0)
RetVal = sqrt(d); // 重载赋值运算符
else
RetVal.SetToError(xlerrNum);
}

假定方法 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)
{
if(!Arg.IsNum() || Arg.Excel(xlfSqrt, 1, &Arg) != xlretSuccess)
RetVal.SetToError(xlerrValue);
}

其中,cpp_xloper::Excel 将返回值直接放到 Arg 中。要实现此操作并仍提供灵活性,以便您能够通过 XLOPER、XLOPER12 或 cpp_xloper 参数调用此函数,则应创建多个重载成员函数:

C++

int Excel(int xlfn); // 不是严格必需的,但可以简化其他项
int Excel(int xlfn, int count, const xloper *p_op1, ...);
int Excel(int xlfn, int count, const xloper12 *p_op1, ...);
int Excel(int xlfn, int count, const cpp_xloper *p_op1, ...);
int Excel(int xlfn, int count, const xloper *p_array[]);
int Excel(int xlfn, int count, const xloper12 *p_array[]);
int Excel(int xlfn, int count, const cpp_xloper *p_array[]);

请注意,上述代码假定这些函数的变量参数版本的调用方没有将各参数类型混杂在一起。另请注意,由于在这里使用了 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, ...)
{
if(xlfn < 0 || count < 0 || count > (gExcelVersion12plus ?
MAX_XL12_UDF_ARGS : MAX_XL11_UDF_ARGS))
return xlretNotCalled;

if(count == 0 || !p_op1) // 默认值为 0,如果忽略则为 NULL
return Excel(xlfn); // 调用此函数更简单的版本

va_list arg_ptr;
va_start(arg_ptr, p_op1); // 初始化

if(gExcelVersion12plus)
{
const xloper12 *xloper12_ptr_array[MAX_XL12_UDF_ARGS];
xloper12_ptr_array[0] = &(p_op1->m_Op12);
cpp_xloper *p_cpp_op;

for(int i = 1; i < count; i++) // 作为 cpp_xlopers 的指针检索
{
p_cpp_op = va_arg(arg_ptr, cpp_xloper *);
xloper12_ptr_array[i] = &(p_cpp_op->m_Op12);
}
va_end(arg_ptr); // 重设

xloper12 temp;
m_ExcelRtnCode = Excel12v(xlfn, &temp, count, xloper12_ptr_array);
Free();

if(m_ExcelRtnCode == xlretSuccess)
{
m_Op12 = temp; // 浅复制
m_XLtoFree12 = true;
}
}
else // gExcelVersion < 12
{
const xloper *xloper_ptr_array[MAX_XL11_UDF_ARGS];
xloper_ptr_array[0] = &(p_op1->m_Op);
cpp_xloper *p_cpp_op;

for(int i = 1; i < count; i++) // 作为 cpp_xlopers 的指针检索
{
p_cpp_op = va_arg(arg_ptr, cpp_xloper *);
xloper_ptr_array[i] = &(p_cpp_op->m_Op);
}
va_end(arg_ptr); // 重设

xloper temp;
m_ExcelRtnCode = Excel4v(xlfn, &temp, count, xloper_ptr_array);
Free();

if(m_ExcelRtnCode == xlretSuccess)
{
m_Op = temp; // 浅复制
m_XLtoFree = true;
}
}
return m_ExcelRtnCode;
}

新工作表函数和 Analysis Toolpak 函数

与 Excel 的先前版本不同,“分析工具库”(ATP) 函数已并入 Excel 2007 中。在以前,XLL 可以按以下方式用 xlUDF 调用 ATP 函数:

C++

double call_ATP_example(void)
{
xloper settlement, maturity, coupon, yield,
redepmtion_value, num_coupons, rate_basis, price;

// 初始化传递给 ATP 函数 PRICE 的数据类型
settlement.xltype = maturity.xltype = coupon.xltype =
yield.xltype = redepmtion_value.xltype =
num_coupons.xltype = rate_basis.xltype = xltypeNum;

// 设置传递给 ATP 函数 PRICE 的值
settlement.val.num = 39084.0; // 2007 年 1 月 2 日
maturity.val.num = 46706.0; // 2027 年 11 月 15 日
coupon.val.num = 0.04;
yield.val.num = 0.05;
redepmtion_value.val.num = 1.0; // 面值的 100%
num_coupons.val.num = 1.0; // 年优待券
rate_basis.val.num = 1.0; // Act/Act

xloper fn;
fn.xltype = xltypeStr;
fn.val.str = "\005" "PRICE";
if(Excel4(xlUDF, &price, 8, &fn, &settlement, &maturity,
&coupon, &yield, &redepmtion_value, &num_coupons,
&rate_basis) != xlretSuccess || price.xltype != xltypeNum)
return -1.0; // 错误值
return price.val.num;
}

在 Excel 2007 中,将用如下所示的代码替换向 Excel 中的调用,其中 gExcelVersion 为项目内具有全局作用域的整数变量,并在调用 xlAutoOpen 期间进行初始化。

C++

    int xl_ret_val;
if(gExcelVersion12plus)
{
xl_ret_val = Excel4(xlfPrice, &price, 7, &settlement,
&maturity, &coupon, &yield, &redepmtion_value,
&num_coupons, &rate_basis);
}
else // gExcelVersion < 12
{
xloper fn;
fn.xltype = xltypeStr;
fn.val.str = "\005" "PRICE";
xl_ret_val = Excel4(xlUDF, &price, 8, &fn, &settlement,
&maturity, &coupon, &yield, &redepmtion_value,
&num_coupons, &rate_basis);
}
if(xl_ret_val != xlretSuccess || price.xltype != xltypeNum)
return -1.0; // 错误值
return price.val.num;

可以通过使用 cpp_xloper 之类的容器使函数在 Excel 2003 和 Excel 2007 中都更独立于版本且更高效。例如:

C++

double call_ATP_example_3(void)
{
cpp_xloper Settlement(39084.0); // 2007 年 1 月 2 日
cpp_xloper Maturity(46706.0); // 2027 年 11 月 15 日
cpp_xloper Price, Coupon(0.04), YTM(0.05);
cpp_xloper RedepmtionValue(1.0); // 面值的 100%
cpp_xloper NumCoupons(1.0); // 年优待券
cpp_xloper RateBasis(1.0); // Act/Act
int xl_ret_val;

if(gExcelVersion12plus)
{
xl_ret_val = Price.Excel(xlfPrice, 7, &Settlement,
&Maturity, &Coupon, &YTM, &RedepmtionValue,
&NumCoupons, &RateBasis);
}
else
{
cpp_xloper Fn("PRICE");
xl_ret_val = Price.Excel(xlUDF, 8, &Fn, &Settlement,
&Maturity, &Coupon, &YTM, &RedepmtionValue,
&NumCoupons, &RateBasis);
}
if(xl_ret_val != xlretSuccess || !Price.IsNum())
return -1.0; // 错误值
return (double)Price;
}

如果尝试在较早版本中调用新的 C API 工作表函数,则会得到一个 xlretInvXlfn 错误。

编写线程安全 XLL 和工作表函数

Excel 的先前版本是将单个线程用于所有的工作表计算。在多处理器计算机或用户已明确配置为使用多线程的单处理器计算机上,Excel 2007 尝试在主线程与一个或多个附加线程(操作系统对所有处理器分配的线程)之间平衡负载。在双处理器(或双核)计算机上,这最多可将重计算时间加速两倍,具体 要视工作簿内依存关系树的拓扑和所涉及函数中线程安全函数的数量而定。

Excel 2007 使用一个线程(其主线程)调用所有命令、非线程安全函数、xlAuto 函数(除 xlAutoFree 之外),以及 COM 和 VBA 函数。

只要 XLL 开发人员遵守以下几条简单规则,就可创建出线程安全函数:

  • 不在其他可能非线程安全的 DLL 中调用资源。

  • 不通过 C API 或 COM 执行任何非线程安全调用。

  • 使用临界区对可能被多个线程同时使用的资源进行保护。

  • 将线程本地内存用于线程特定存储区,并用线程本地变量替换函数中的静态变量。

当 设置了 xlbitDllFree 的 XLOPER 或 XLOPER12 返回到 Excel 后,Excel 在调用该线程上的其他任何函数之前会调用同一线程上的 xlAutoFree。这对所有函数(无论是线程安全还是非线程安全)都是如此,并且避免了线程本地 XLOPER 在其所关联内存释放之前被重用的风险。

从线程安全函数调用 C API 函数

VBA 和 COM 加载项函数不被视为线程安全函数。除 C API 命令(例如,任何工作表函数都不允许调用的 xlcDefineName)外,线程安全函数不能访问 XLM 信息函数。也不能通过在类型字符串后面附加磅符号 (#) 来将线程安全 XLL 函数注册为宏表等效项。这样做的结果是,线程安全函数无法执行以下操作:

  • 读取未计算单元格(包括调用单元格)的值。

  • 调用 xlfGetCell、xlfGetWindow、xlfGetWorkbook 和 xlfGetWorkspace 之类的函数以及其他信息函数。

  • 使用 xlfSetName 定义或删除 XLL 内部名称。

XLM 的一个例外就是 xlfCaller,它是线程安全函数。如果调用方是一个工作表单元格或范围,则 xlfCaller 会返回一个引用。但是,您不能使用某线程安全函数中的 xlCoerce 以安全方式将所得到的引用强制为一个值,因为这将返回 xlretUncalced。注册带有 # 的该函数可以解决此问题,但在此例中,该函数是宏表等效项,因此不将其视为线程安全函数。这就防止了返回先前值(例如在某一错误条件存在时)的函数成为线 程安全函数。

应注意的是,纯粹的 C API 函数都是线程安全函数:

  • xlCoerce(尽管对未计算单元格引用的强制失败)

  • xlFree

  • xlStack

  • xlSheetId

  • xlSheetNm

  • xlAbort(只不过它不能用于清除中断条件)

  • xlGetInst

  • xlGetHwnd

  • xlGetBinaryName

  • xlDefineBinaryName

作为命令等效项的 xlSet 是一个例外,因此不能从任何工作表函数对其进行调用。

除了以下几项外,Excel 2007 所有的内置工作表函数及其 C API 等效项都是线程安全函数:

  • PHONETIC

  • CELL(当使用“format”或“address”参数时)

  • INDIRECT

  • GETPIVOTDATA

  • CUBEMEMBER

  • CUBEVALUE

  • CUBEMEMBERPROPERTY

  • CUBESET

  • CUBERANKEDMEMBER

  • CUBEKPIMEMBER

  • CUBESETCOUNT

  • ADDRESS(其中给出第五个参数 (sheet_name))

  • 引用 Excel 数据透视表的任何数据库函数(例如 DSUM 或 DAVERAGE)

  • ERROR.TYPE

  • HYPERLINK

多线程使用的资源

应 使用临界区对可被多个线程访问的读/写内存进行保护。对于每个要保护的内存块,都需要一个命名的临界区。您可以在调用 xlAutoOpen 时对其进行初始化,并在调用 xlAutoClose 时将其释放并设置为空值。应通过调用 EnterCriticalSection 和 LeaveCriticalSection 来包含对受保护块的每个访问。任何时候都只允许在临界区内存在一个线程。以下是对一个名为 g_csSharedTable 的临界区的初始化、取消初始化及使用示例:

C++

CRITICAL_SECTION g_csSharedTable; // 全局范围(如果需要)
bool xll_initialised = false; // 模块范围

int __stdcall xlAutoOpen(void)
{
if(xll_initialised)
return 1;
// 省略其他初始化操作
InitializeCriticalSection(&g_csSharedTable);
xll_initialised = true;
return 1;
}

int __stdcall xlAutoClose(void)
{
if(!xll_initialised)
return 1;
// 省略其他清理操作
DeleteCriticalSection(&g_csSharedTable);
xll_initialised = false;
return 1;
}

bool read_shared_table_element(unsigned int index, double &d)
{
if(index >= SHARED_TABLE_SIZE) return false;
EnterCriticalSection(&g_csSharedTable);
d = shared_table[index];
LeaveCriticalSection(&g_csSharedTable);
return true;
}
bool set_shared_table_element(unsigned int index, double d)
{
if(index >= SHARED_TABLE_SIZE) return false;
EnterCriticalSection(&g_csSharedTable);
shared_table[index] = d;
LeaveCriticalSection(&g_csSharedTable);
return true;
}

另一个保护内存块的可能更为安全的方法就是创建一个包含其自身 CRITICAL_SECTION 的类,由该类的构造函数、析构函数和存取器方法来管理其使用情况。此方法的另外一个好处是,保护可能在 xlAutoOpen 运行之前进行初始化或在调用 xlAutoClose 之后仍然存在的对象,但应注意,不要创建过多临界区,无谓地降低操作系统运行速度。

在使用需要同时访问多个受保护内存块的代码时,应特别注意各临界区的输入和退出顺序。例如,以下两个函数可能会创建一个死锁:

C++

bool copy_shared_table_element_A_to_B(unsigned int index)
{
if(index >= SHARED_TABLE_SIZE) return false;
EnterCriticalSection(&g_csSharedTableA);
EnterCriticalSection(&g_csSharedTableB);
shared_table_B[index] = shared_table_A[index];
LeaveCriticalSection(&g_csSharedTableA);
LeaveCriticalSection(&g_csSharedTableB);
return true;
}
bool copy_shared_table_element_B_to_A(unsigned int index)
{
if(index >= SHARED_TABLE_SIZE) return false;
EnterCriticalSection(&g_csSharedTableB);
EnterCriticalSection(&g_csSharedTableA);
shared_table_A[index] = shared_table_B[index];
LeaveCriticalSection(&g_csSharedTableA);
LeaveCriticalSection(&g_csSharedTableB);
return true;
}

如果某一线程上的第一个函数输入 g_csSharedTableA,而另一个线程上的第二个函数输入 g_csSharedTableB,则两个线程都将挂起。正确的方法是按一致顺序输入,再按相反顺序退出,如下所示:

C++

    EnterCriticalSection(&g_csSharedTableA);
EnterCriticalSection(&g_csSharedTableB);
// 访问两个块的代码
LeaveCriticalSection(&g_csSharedTableB);
LeaveCriticalSection(&g_csSharedTableA);

在可能的情况下,最好从线程合作的角度出发来隔离对不同块的访问,代码如下所示:

C++

bool copy_shared_table_element_A_to_B(unsigned int index)
{
if(index >= SHARED_TABLE_SIZE) return false;
EnterCriticalSection(&g_csSharedTableA);
double d = shared_table_A[index];
LeaveCriticalSection(&g_csSharedTableA);
EnterCriticalSection(&g_csSharedTableB);
shared_table_B[index] = d;
LeaveCriticalSection(&g_csSharedTableB);
return true;
}

当存在对共享资源的大量争用(例如,频繁的短期访问请求)时,应考虑利用临界区的旋转能力。此方法可减轻资源等待的处理器密集度。为 此,可在临界区初始化时使用 InitializeCriticalSectionAndSpinCount 或在临界区初始化后使用 SetCriticalSectionSpinCount 来设置线程在等到资源可用之前所循环的次数。等待操作的成本比较昂贵,如果资源在此期间被释放,则旋转可以避免等待该资源。在单处理器系统上,旋转计数实 际上是忽略不计的,但也可以指定它且不会带来任何不利影响。内存堆管理器使用的旋转计数为 4000。有关使用临界区的更多信息,可参考“平台 SDK”文档中的 Critical Sections Object(英文)主题。

声明和使用线程本地内存

例如,设想一个返回指向 XLOPER 的指针的函数:

C++

xloper * __stdcall mtr_unsafe_example(xloper *arg)
{
static xloper ret_val; // 所有线程共享内存!!!
// 代码将 ret_val 设置为 arg 的函数...
return &ret_val;
}

此函数不是线程安全函数,因为可能出现一个线程返回静态 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);

xloper * __stdcall mtr_safe_example_2(xloper *arg)
{
xloper *p_ret_val = get_thread_local_xloper();
// 代码将 ret_val 设置为 arg 的函数,根据需要设置 xlbitDLLFree
// 或 xlbitXLFree
return p_ret_val; // xlAutoFree 不能释放此指针!
}

下一个问题是如何建立和检索线程本地内存。这可利用线程本地存储 (TLS) API 来实现。第一步是使用 TlsAlloc 获得一个 TLS 索引,该索引最终必须使用 TlsFree 得以释放。这两者都可从 DllMain 成功完成:

C++

// 此实现只是调用一个函数来设置线程局部存储
BOOL TLS_Action(DWORD Reason);

__declspec(dllexport) BOOL __stdcall DllMain(HINSTANCE hDll, DWORD Reason, void *Reserved)
{
return TLS_Action(Reason);
}
DWORD TlsIndex; // 如果所有 TLS 都在此模块中进行访问,则仅需要模块范围

BOOL TLS_Action(DWORD DllMainCallReason)
{
switch (DllMainCallReason)
{
case DLL_PROCESS_ATTACH: // 加载 DLL
if((TlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
return FALSE;
break;

case DLL_PROCESS_DETACH: // 卸载 DLL
TlsFree(TlsIndex); // 释放 TLS 索引。
break;
}
return TRUE;
}

获得索引后,下一步是为每个线程分配一个内存块。“动态链接库参考”中的 DllMain 建议,在每次随 DLL_THREAD_ATTACH 事件调用 DllMain 时都执行该操作,并在每个 DLL_THREAD_DETACH 发生时释放内存,但是遵照此建议将导致 DLL 为 Excel 未用于重计算的那些线程执行不必要的分配。最好改为使用“在第一次使用时分配”的策略。首先,需要定义一个要分配给每个线程的结构。对于上述简单示例,使 用以下代码就足够了:

C++

struct TLS_data
{
xloper xloper_shared_ret_val;
// 在此添加其他必需的线程局部数据...
};

下面的函数获得一个指向线程本地实例的指针,或在第一次调用时分配一个指针:

C++

TLS_data *get_TLS_data(void)
{
// 取得指向此线程静态内存的指针
void *pTLS = TlsGetValue(TlsIndex);
if(!pTLS) // 此线程尚无 TLS 内存
{
if((pTLS = calloc(1, sizeof(TLS_data))) == NULL)
// 显示一些错误消息(省略)
return NULL;
TlsSetValue(TlsIndex, pTLS); // 将此与此线程关联
}
return (TLS_data *)pTLS;
}

现在我们可以看到线程本地 XLOPER 内存是如何获得的:首先,获得一个指向 TLS_data 线程实例的指针,然后返回一个指向内含于其中的 XLOPER 的指针:

C++

xloper *get_thread_local_xloper(void)
{
TLS_data *pTLS = get_TLS_data();
if(pTLS)
return &(pTLS->xloper_shared_ret_val);
return NULL;
}

通过将 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 版权所有。保留所有权利。使用规定。


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多