65.9K
CodeProject 正在变化。 阅读更多。
Home

Microsoft Office 2011 - Mac 上的 VBA

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.85/5 (4投票s)

2014年8月22日

CPOL

12分钟阅读

viewsIcon

22967

downloadIcon

417

在 Mac 上的 Microsoft Office 2011 中开发自定义功能, 使用 C 和 VBA

引言

随着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模板。

Sample Image - maximum width is 600 pixels

确保库类型指定为Dynamic,如下图所示。

Sample Image - maximum width is 600 pixels

项目创建后,请确保“**Architectures**”设置指定为“**32-bit Intel (i386)**”,如下图所示。

Sample Image - maximum width is 600 pixels

基本输入/输出

任何输入和输出的基础都围绕着布尔值、数字和字符串。在本文项目的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 stringdata)中。为了帮助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实际上返回了一个名为utsnamestruct。该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输入——我们要检查的stringregexString),以及我们要对照检查的正则表达式模式(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 GETPOST操作的自定义函数。dylib项目将需要对libcurl库的引用——在此示例中,使用libcurl.4.dylib

有关libcurl的更多详细信息,请参阅http://curl.haxx.se/libcurl

为简化起见,HTTP GETPOST在示例代码库中实现为两个独立的函数。有关更多详细信息,请参阅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调用目标网址的stringurl),一个包含字段数据的stringfields),以及一个已初始化的stringhttpResponse),其大小足以容纳预期的响应。

这两个HTTP函数都返回HTTP响应的长度作为整数(VBA long),从而允许VBA进行错误处理。在发生错误的情况下,HTTP函数将返回0,响应stringhttpResponse)将包含错误消息。

Using the Code

本文的下载内容除了Xcode 5.1项目文件外,还包含一个Excel 2011 XLSM文件,其中包含本文提及的所有函数的示例,以及一个已编译的dylib文件libvba2themacs.dylib。Excel VBA代码中使用的declare语句不引用dylib的完整路径名,因此为了运行这些函数,必须将libvba2themacs.dylib文件复制到/usr/libusr/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日:初始版本
© . All rights reserved.