Microsoft Office 2011 - Mac 上的 VBA






4.85/5 (4投票s)
在 Mac 上的 Microsoft Office 2011 中开发自定义功能,
引言
随着Microsoft Office 2011 for Mac的发布,VBA也重新回到了苹果平台。
然而,尽管Microsoft Office 2011中的VBA实现与Windows平台上的实现非常相似,但Windows平台通过COM和ActiveX提供的许多功能却没有被引入。此外,完全没有类似于SDK的东西,而开发者参考文档充其量只能算得上是基础的——这使得开发自定义Office 2011解决方案的前景显得十分艰巨。
在本文中,我将演示如何使用C语言开发动态库(dylib文件)为Office 2011提供自定义功能,通过VBA实现从简单的输入输出操作(使用布尔值、数字、字符串、数组和VBA用户定义类型等基本类型),到简单的RegEx和HTTP POST及GET功能的实现。
背景
在Windows平台,Microsoft Office套件可以通过一系列技术进行扩展,例如ActiveX/COM、特定应用程序组件(如XLL)——甚至在通过Excel的REGISTER
函数注册后直接访问DLL函数,这些都是可行的途径。而在Apple OS X平台,没有SDK,这些选项都不可用。
现在,微软可能没有提供SDK,开发者文档可能很粗糙——但他们确实提供了一个VBA环境,对于接触过Windows版本的人来说,这个环境会很熟悉,并且他们提供了对DECLARE
语句的支持。正是通过DECLARE
的使用,才能在VBA环境中提供新功能。
对于不熟悉Apple OS X平台的VBA开发者来说,可用的文档非常少。在网络上进行了广泛搜索后,我未能找到任何可以作为起点并提供有关如何在OS X上将VBA与外部库接口的基本指导。在本文中,我希望能在一定程度上弥补这一情况,并希望能激励一些开发者重新审视Mac上的VBA。
假定读者熟悉VBA以及与外部函数和库的接口。本文无意教你VBA——而是展示如何将你现有的VBA技能应用到Apple OS X平台。
入门
本文使用的配置如下
- Apple OS X 10.9.2
- Apple Xcode 5.1.1
- Microsoft Office 2011 for Mac 14.4.1
此方法或代码库没有任何内容需要特定的OS X和Xcode版本,您手头的任何OS X版本和相应的Xcode版本都应该可以正常工作。
在开始之前,对于更熟悉Windows世界的读者,有几点需要注意
- 在Apple OS X上,DLL的等价物是
dylib
- Microsoft Office 2011 for Mac是32位应用程序套件
- Office 2011中的VBA版本似乎等同于Windows VBA 6.5版本——也就是说,是Windows环境下Microsoft Office 2007附带的版本。
总而言之,我们为OS X开发的自定义库(dylib
)必须是32位动态库。就VBA语法而言,我们应该能够参考Microsoft Office 2007 for Windows的开发者参考文档来获取指针和示例。
Xcode项目
在Xcode中,创建一个新项目并选择C/C++ Library模板。
确保库类型指定为Dynamic,如下图所示。
项目创建后,请确保“**Architectures**”设置指定为“**32-bit Intel (i386)**”,如下图所示。
基本输入/输出
任何输入和输出的基础都围绕着布尔值、数字和字符串。在本文项目的introduction.c源文件中,您会找到以下C函数,它们对数字和字符串执行基本操作。
double CIntroAddDouble( double a, double b ) { return a + b; }
bool CIntroIntIsOdd (int i) { return i % 2; }
int CIntroStrLen (char *str) { return strlen(str); }
然后是数组。
在这个小示例中,C函数CIntroArraySum
接收两个参数——一个指向长整型(long)的指针,代表传入数组的第一个元素(firstArrayElement
),第二个参数是一个长整型(long),指定传入数组的长度(arrayLength
)。
请注意,与Windows平台一样,无法将包含string
元素的数组传递给dylib
函数和命令,也无法从它们那里返回。在Windows平台,string
数组通常以SAFEARRAY
的形式传递给DLL文件以及从DLL文件返回。
C函数枚举传入的数组,并将各个数组元素的值相加——返回总和。
int CIntroArraySum(long *firstArrayElement, long arrayLength) {
int ret = 0 ;
for (int i = 0; i < arrayLength; i++)
{
ret = ret + firstArrayElement[i];
}
return ret;
}
最后——一个示例命令,它枚举并更新一个由VBA分配、初始化并作为参数传递的数组的元素。
void CIntroArrayUpdate(long *arrayElement, long arrayLength) {
for (int i = 0; i < arrayLength; i++) {
arrayElement[i] = arrayLength - i;
}
return;
}
这些函数在任何方面都不是OS X特有的,它们在Windows平台上功能完全相同,因此对已经开发或通过VBA与DLL文件交互的Windows开发者来说应该很熟悉。
VBA代码
作为declare
语法的一部分,需要提供dylib文件的完整路径,除非该文件放置在dylib文件的默认搜索路径之一。我建议您将libvba2themacs.dylib文件复制到标准搜索路径之一,如/usr/lib、/usr/local/lib或/Users/[Username]/lib。这样做可以确保声明仅限于提供dylib文件名。我在可用的所有OS X版本中都对这三个路径进行了默认搜索,以查找指定的dylib文件。可能还有其他路径——但我还没有找到。
假设dylib已放置在搜索路径之一,则上述基本函数和命令的VBA declare语句将如下所示:
Private Declare Function CIntroAddDouble Lib "libvba2themacs.dylib" _
(ByVal a As Double, ByVal b As Double) As Double
Private Declare Function CIntroIntIsOdd Lib "libvba2themacs.dylib" _
(ByVal a As Integer) As Boolean
Private Declare Function CIntroStrLen Lib "libvba2themacs.dylib" _
(ByVal a As String) As Long
Private Declare Function CIntroArraySum Lib "libvba2themacs.dylib" _
(ByRef firstArrayElement As Long, ByVal arraySize As Long) As Long
Private Declare Sub CIntroArrayUpdate Lib "libvba2themacs.dylib" _
(ByRef firstArrayElement As Long, ByVal arraySize As Long)
总而言之,将基本类型参数从VBA传递到我们的dylib
可以使用ByVal
。对于数组,第一个数组元素必须通过ByRef
传递,而数组长度应通过ByVal
传递。
从dylib获取字符串
传递和返回数值类型,以及传递string
都相对简单,但当涉及到从dylib
返回string
值时,就需要不同的方法。在VBA中,任何string
似乎都是BSTR(OLEstring
,但这只是我的猜测,没有文档可供参考),因此任何直接将C string
返回到VBA的尝试都会导致内存访问错误并导致Office崩溃。解决此问题的一种方法是在VBA中分配并初始化一个string
来存储返回的值,确保其大小足够容纳预期的响应,然后将响应string
作为参数传递给dylib
中的外部函数或子程序,以便在那里进行更新。
例如,考虑检索主机配置信息,如操作系统版本详细信息或硬件平台。在OS X以及大多数Linux发行版上,这可以通过uname
命令完成。在C语言中,一个检索操作系统版本详细信息的示例函数如下所示:
int CUnameRelease(char *data)
{
struct utsname utsnameData;
uname(&utsnameData);
strcpy(data, utsnameData.release);
return strlen(utsnameData.release);
}
在这里,将一个已初始化的string
参数(data
)传递给C函数。该C函数接着执行uname
命令,然后将生成的版本详细信息复制到VBA string
(data
)中。为了帮助VBA端的错误处理,uname
调用返回的版本详细信息string
的大小作为32位整数返回。
在VBA中,上述CUnameRelease
函数的相应declare
语句如下——再次注意,字符串参数声明为ByVal
:
Private Declare Function CUnameRelease Lib "libvba2themacs.dylib" (ByVal Release As String) As Long
string
操作和返回的流程可以总结如下:
VBA函数/子程序分配并初始化字符串并调用 --> dylib函数/命令更新字符串 --> VBA函数/子程序读取更新后的字符串
使用C函数检索uname
版本参数的VBA函数实现如下:
Public Function vbaxUnameRelease() As String
Dim lng As Long
Dim ret As String
ret = String(256, vbNullChar)
lng = CUnameRelease(ret)
If lng <= 256 Then
vbaxUnameRelease = ret
Else
vbaxUnameRelease = "CUnameRelase value in excess of 256 characters"
End If
End Function
在这个VBA函数中,使用vbNullChar
分配并初始化了一个256个字符的string
。然后将此string
作为参数传递给我们的dylib
中的CUnameRelease
函数,该函数用uname
版本参数详细信息填充string
。从CUnameRelease
返回的长整型(long)允许实现错误处理,以防dylib
中的字符串比VBA分配的string
长。
VBA类型和C结构
您可能已经注意到,uname实际上返回了一个名为utsname
的struct
。该struct
定义如下:
#define _SYS_NAMELEN 256
struct utsname {
char sysname[_SYS_NAMELEN]; /* [XSI] Name of OS */
char nodename[_SYS_NAMELEN]; /* [XSI] Name of this network node */
char release[_SYS_NAMELEN]; /* [XSI] Release level */
char version[_SYS_NAMELEN]; /* [XSI] Version level */
char machine[_SYS_NAMELEN]; /* [XSI] Hardware type */
};
所以,在我们的C函数中,一次调用uname
实际上检索了5个不同的string
值,每个字符串的最大长度为256个字符。与其只从我们的dylib
函数返回一个值,不如返回utsname
结果的所有参数,从而在一次操作中将所有数据传递给VBA进行进一步处理,这将更有优势。
一种方法是使用VBA UDT(用户定义类型)。与Windows平台上的VBA一样,VBA UDT可以定义得与相应的C struct
定义相对应。然后,VBA UDT的实例可以作为参数传递给dylib
函数或进程。C struct utsname
的VBA UDT定义如下:
Public Type UTSNAME
Sysname As String * 256
Nodename As String * 256
Release As String * 256
Version As String * 256
Machine As String * 256
End Type
在VBA中,上述CUnameDataStruct
函数的相应declare
语句如下——注意,UTSNAME
类型参数必须声明为ByRef
:
Private Declare Sub CUnameData Lib "libvba2themacs.dylib" (ByRef unameData As UTSNAME)
因此,通过一次VBA调用,我们现在能够检索构成utsname结构的所有五个参数,只需一次调用dylib
即可。在下面的示例Excel VBA函数中,UDT的属性值作为Variant数据类型返回——这是典型的Excel数组函数。
Public Function vbaxUnameData() As Variant
Dim ret(1 To 5, 1 To 1) As Variant
Dim utsnameData As UTSNAME
Call CUnameDataStruct(utsnameData)
ret(1, 1) = utsnameData.Sysname
ret(2, 1) = utsnameData.Nodename
ret(3, 1) = utsnameData.Release
ret(4, 1) = utsnameData.Version
ret(5, 1) = utsnameData.Machine
vbaxUnameData = ret
End Function
总而言之,数值可以以熟悉的Windows平台方式在dylib
和VBA之间传递,string
也可以传递——但只能作为预先分配和初始化的参数,最后,VBA UDT/C struct
可用于在VBA和dylib
之间传递数据。
实现RegEx匹配
您现在知道如何使用VBA将数据进出自定义库。那么我们可以利用这些知识做什么呢?
考虑一个常用于数据录入验证的基本功能——检查输入值是否符合预期格式。例如,这可以用于检查电子邮件地址格式是否有效,或者电话号码是否有效。
仅使用VBA实现这种验证逻辑通常会很困难,并且对有效格式的任何更改都可能导致需要更改VBA代码。在这种用例中,实现一个通用的基于RegEx的验证函数将非常有意义。如果实现了这样的函数,您可以简单地传递输入string
和定义该string
类型有效格式的最新正则表达式,执行RegEx匹配检查操作,并获得简单的匹配/不匹配响应。如果格式将来发生变化,只需更新正则表达式即可,函数本身不受影响。
以下C函数展示了这样一个函数的示例,它接受两个string
输入——我们要检查的string
(regexString
),以及我们要对照检查的正则表达式模式(regexPattern
)。如果正则表达式有效并且与string
匹配,则返回true
,否则返回false
。
#include <regex.h>
bool CRegexMatchBool(char *regexString, char *regexPattern)
{
int status;
regex_t rt;
status = regcomp(&rt, regexPattern, 0);
if (status)
{
return 0;
}
else
{
status = regexec(&rt, regexString, 0, NULL, 0);
regfree(&rt);
return (!status);
}
}
在VBA中,我们函数的相应声明将如下所示:
Private Declare Function CRegexMatchBool Lib "libvba2themacs.dylib" _
(ByVal regexString As String, ByVal regexPattern As String) As Boolean
最后,一个调用CRegexMatchBool
的VBA函数可以实现如下:
Public Function vbaxRegexMatchBool_
(ByVal regexString As String, ByVal regexPattern As String) As Boolean
Dim bret As Boolean
bret = CRegexMatchBool(regexString, regexPattern)
vbaxRegexMatchBool = bret
End Function
如果您考虑创建仅VBA的通用验证函数需要什么,那么实现一个简单的dylib
函数,如上所示,是值得花费时间和精力的。
实现HTTP GET和POST
在OS X中,核心OS提供了许多现成的开源组件,对于HTTP功能,我们可以利用libcurl
来实现可从VBA调用的HTTP GET
和POST
操作的自定义函数。dylib
项目将需要对libcurl
库的引用——在此示例中,使用libcurl.4.dylib。
有关libcurl
的更多详细信息,请参阅http://curl.haxx.se/libcurl。
为简化起见,HTTP GET
和POST
在示例代码库中实现为两个独立的函数。有关更多详细信息,请参阅Xcode项目中的源代码。
#include <curl/curl.h>
int CCurlHttpGet(char *url, char *httpResponse)
{...
CCurlHttpGet
函数采用了CIntroUnameRelease
函数中详细介绍的方法——从VBA向C函数传递两个参数,HTTP调用的目标网址(url
)和一个已初始化的字符串(httpResponse
),其大小足以容纳预期的响应。
int CCurlHttpPost(char *url, char *fields, char *httpResponse)
{...
CCurlHttpPost
也采用了CUnameRelease
函数中详细介绍的方法——在这种情况下,向C函数传递三个参数,一个包含HTTP调用目标网址的string
(url
),一个包含字段数据的string
(fields
),以及一个已初始化的string
(httpResponse
),其大小足以容纳预期的响应。
这两个HTTP函数都返回HTTP响应的长度作为整数(VBA long),从而允许VBA进行错误处理。在发生错误的情况下,HTTP函数将返回0
,响应string
(httpResponse
)将包含错误消息。
Using the Code
本文的下载内容除了Xcode 5.1项目文件外,还包含一个Excel 2011 XLSM文件,其中包含本文提及的所有函数的示例,以及一个已编译的dylib文件libvba2themacs.dylib。Excel VBA代码中使用的declare
语句不引用dylib的完整路径名,因此为了运行这些函数,必须将libvba2themacs.dylib文件复制到/usr/lib、usr/local/lib或/Users/[user name]/lib之一。
关注点
在Apple OS X上为VBA开发自定义库的过程可能会因缺少SDK而受阻,开发者文档也可能很粗糙——但一旦掌握了基本技巧,您就可以专注于查找正确的头文件并正确编写C代码。Apple OS X平台除了拥有许多强大的OS X特定API外,还在标准OS安装中提供了大量现成的开源组件。
我强烈建议加入Apple Developer Network,这是一个免费选项——只有当您想发布到App Store时才需要订阅费用。免费会员将为您提供海量的开发者参考文档和示例代码。
历史
- 2014年8月21日:初始版本