我等你等你回来的消息:在VBA中使用Windows API

来源:百度文库 编辑:中财网 时间:2024/04/28 15:06:57

VBA是一种强大的编程语言,可用于自定义Microsoft Office解决方案。通过使用VBA处理一个或多个Office应用程序对象模型,可以容易地修改Office应用程序的功能或者能够使两个或多个Office应用程序协同工作以完成单个应用程序无法完成的任务。然而,使用VBA仅能控制操作系统的一小部分。Windows API提供了控制操作系统绝大多数方面的功能。下面,介绍在VBA中使用Windows API的一些知识。
理解APIs
API只是一组函数,可用于处理组件、应用程序或操作系统。通常,API由一个或多个提供某种特定功能的DLLs组成。
DLLs是包含函数的文件,能够从任何运行的Windows应用程序中调用DLLs。在运行时,DLL中的函数被动态链接到调用它的应用程序里。无论多少应用程序调用DLL中的函数,该函数仅存在于磁盘的单个文件中,并且DLL在内存中仅被创建一次。
您可能最经常听说的API是Windows API,它包括组成Windows操作系统的DLLs。每个Windows应用程序都直接或间接地与Windows API相交互,Windows API确保运行在Windows下的所有应用程序都按一致的方式工作。
除了Windows API外,还有其它发布的APIs可用。例如,邮件应用程序编程接口(MAPI)是一组用于编写电子邮件应用程序的DLLs。
APIs通常是由创建Windows应用程序的C和C++程序员编写,但能够使用VBA调用DLL中的函数。因为大多数DLLs最初都是由C/C++程序员编写和文档规范,所以调用DLL函数与调用VBA函数不同。为了使用API,必需理解如何传递参数到DLL函数。
为了调用Windows API中的函数,需要描述这些可用的函数的文档规范,如何在VBA中声明这些函数,以及如何调用它们。下面是两个有用的资源:
1、Win32API.txt文件,包含Windows API中大多数函数的VBA Declare(声明)语句。可以使用API Viewer加载宏查找和复制需要的Declare语句。可以在下面的站点下载API声明查看器:
http://www.activevb.de/rubriken/apiviewer/index-apiviewereng.html
也可以在此下载:

win32api.txt文件下载:

2、Microsoft Platform SDK,包含复杂的Windows API文档。可以在下面的地址中查看:http://msdn.microsoft.com/en-us/library/aa383750(VS.85).aspx
此外,很多程序员还开发了一些声明并与大家共享,下面就是一个关于API声明的资源网站:http://www.xcelfiles.com/
使用Declare语句
在从VBA中调用DLL里的函数之前,必须为VBA提供在哪里找到函数以及如何调用该函数的信息,有两种方法:
1、设置对DLL类型库的引用。
2、在模块中使用Declare语句。
设置对DLL类型库的引用是使用DLL中的函数的最容易的方法。一旦设置引用,就可以将其当作工程里的一部分一样调用DLL函数。然而,也要注意一些事项。首先,设置对多个类型库的引用会影响应用程序的性能;其次,不是所有的DLLs都提供类型库,虽然可以对没有提供类型库的DLL设置引用,但不能调用该DLL中的函数。
注意,组成Windows API的DLLs没有提供类型库,因此不能设置对它们的引用并调用其中的函数。要调用Windows API中的函数,必须在工程里模块的声明部分包括Declare语句。
Declare语句是一个定义,告诉VBA在哪里找到特定的DLL函数以及如何调用该函数。在代码中添加Declare语句最简单的办法是使用API Viewer加载宏,其中包含Windows API中大多数函数的Declare语句,也包含一些函数所需要的常量和类型定义。
Declare语句声明的形式如下:

[Public|Private]Declare Sub name Lib "libname" [Alias "aliasname"][([arglist])][Public|Private]Declare Function name Lib "libname" [Alias "aliasname"] [([arglist])] [As type]

下面是GetTempPath函数的Declare语句的示例,该函数返回Windows临时文件夹的路径(默认为C:\Windows\Temp):

Private Declare Function GetTempPath Lib "kernel32" _Alias "GetTempPathA" (ByVal nBufferLength As Long, _ByVal lpBuffer As String) As Long

关键字Declare告诉VBA在工程中要包含的DLL函数的定义。在标准模块中的Declare语句可以是公共的或私有的,取决于你希望API函数仅用于单个模块还是整个工程。在类模块中,Declare语句必须是私有的。
在关键字Function之后是函数的名字,具体地说,是从VBA中调用该函数时使用的名字。这个名字可以与API函数本身的名字相同,也可以在Declare语句中使用关键字Alias指定打算在VBA中通过不同的名字(别名)调用该函数。
在上面的示例中,在DLL中API函数的名字是GetTempPathA,从VBA中调用该函数时使用的名字是GetTempPath。注意,DLL函数的实际名字出现在关键字Alias之后,同时也注意到GetTempPath是Win32API.txt文件用于该函数的别名,但你可以将其改变为任何你想要的名字。
下面是为什么要在Declare语句中使用别名的一些理由:

  • 一些API函数的名字以下划线(_)开始,在VBA中是不合乎语法的。为了从VBA中调用该函数,需要使用别名。
  • 因为别名允许将DLL函数命名为你所希望的名字,所以可以使函数名字遵循你自已在VBA中的命名标准。
  • 因为API函数是区分大小写的,而VBA函数则不,所以可以使用别名来改变函数名的大小写。
  • 一些DLL函数带有接受不同数据类型的参数,这些函数的VBA声明语句定义这些参数为类型Any,调用带有声明为Any的参数的DLL函数是危险的,因为VBA不会执行任何数据类型检查。如果想避免传递类型为Any的参数的危险,可以声明相同的DLL函数的多个版本,每一个都具有不同的名字和不同的数据类型。
  • Windows API为所有接受字符串参数的函数都包含两个版本:ANSI版和Unicode版。ANSI版带有“A”后缀,正如上例所示,而Unicode版带有“W”后缀。虽然VBA使用Unicode,但在调用DLL中的函数之前,它将所有的字符串转换为ANSI字符串,因此在从VBA中调用Windows API函数时通常使用ANSI版。API Viewer加载宏自动为所有接受字符串参数的函数命名别名,因此可以不必包含“A”后缀而调用该函数。

关键字Lib指定包含函数的DLL。注意,在声明语句里以字符串形式包含DLL的名字。如果在系统中没有找到关键字Lib之后指定的DLL,对该函数的调用将失败,导致运行时错误:48,装载DLL错误。因为可以在VBA代码中处理这种错误,所以可以编写健壮的代码得体地处理错误。
下面列出了Windows API中最常使用的DLLs:

  • Kernel32.dll:低级别的操作系统函数,例如内存管理和资源处理。
  • User32.dll:Windows管理函数,例如消息处理、计时器、菜单和通讯。
  • GDI32.dll:图像设备接口(GDI)库,包含设置输出的函数,例如绘图、显示上下文和字体管理。

大多数DLLs,包括Windows API中的DLLs,都采用C/C++编写,因此,传递参数到DLL函数需要参数的理解以及C/C++接受的数据类型,而这些不同于VBA函数。
同时,DLL函数的许多参数按值传递。默认情况下,VBA中的参数按引用传递。因此,当DLL函数需要按值传递的参数时,在函数定义中包括关键字ByVal是必要的。在函数定义中忽略ByVal关键字可能会在应用程序中导致无效的页错误。有时,可能会发生VBA运行时错误:49,坏的DLL调用协议。
按引用传递参数传递该参数的内存位置到被调用的过程,如果该过程修改了参数的值,那么会修改该参数的唯一的副本,因此,当返回到调用过程时,参数包含的是修改后的值。
按值传递参数到DLL函数,将传递该参数的副本,函数操作该参数的副本,避免了修改实际参数的内容。当返回到调用过程时,该参数包含与调用其它过程前相同的值。
因为按引用传递允许在内存中修改参数值,如果不恰当地按引用传递参数,DLL函数可能会覆盖它不应该覆盖的内存,导致错误或者不可预料的结果。Windows维护许多值不应该被覆盖,例如,Windows为每个窗口赋惟一的32位标识符,称作句柄(handle)。句柄总是按值传递给API函数,因为如果Windows修改了某窗口的句柄,那么不再能够追踪到该窗口。(虽然关键字ByVal出现在String类型的一些参数前面,但是字符串总是按引用被传递到Windows API函数)
上述声明语句接受两个参数,一个为Long型,另一个为String型,并返回一个Long型值。
使用常量
除了DLL函数的声明语句外,一些函数还需要定义常量以及在函数中使用的类型。在模块的声明部分包括常量和用户定义类型。
如何知道函数需要的常量和用户定义类型呢?需要查看该函数的文档。Win32API.txt文件包含函数的常量和用户定义类型的定义。可以使用API Viewer加载宏找出这些常量和用户定义类型,并将它们复制到代码中。不巧的是,常量和用户定义类型不会以任何方式与需要它们的声明语句相联系,因此,仍然需要检查DLL函数的文档,决定哪个常量和类型与哪个声明语句匹配。
函数可能需要传递常量来指明想要函数返回的信息。例如,GetSystemMetrics函数接受75个常量,每一个都指定操作系统的不同方面,该函数返回的信息取决于传递给它的常量。要调用GetSystemMetrics,不需要包括所有的75个常量,只需包括要使用的就可以了。
建议定义常量而不是简单地传递它们代表的值。Microsoft确保在将来的版本中仍然会保留相同的常量,但不保证常量的值相同。
DLL函数需要的常量通常是隐含的,因此需要查阅函数的文档来确定传递的常量,以返回特定的值。
在《Professional Excel Development》中介绍了如何查找常量的值的方法。即在Microsoft的站点下载并安装核心SDK软件包,其中有一个名为“include”的子目录,所有用于创建动态链接库(DLL)的C++头文件都存放在这个目录中。通过搜索就能找到常量所在的文件,例如查找SM_CXSCREEN,会返回文件“winuser.h”,打开该文件查询就可找到相关的常量。
下面的示例是包括GetSystemMetrics函数的声明语句,接受两个常量,然后展示如何从属性过程中调用GetSystemMetrics,以像素为单位返回屏幕的高度。

Declare Function GetSystemMetrics Lib "User32" (ByVal nIndex As Long) As LongConst SM_CXSCREEN As Long = 0 '屏幕宽度Const SM_CYSCREEN As Long = 1 '屏幕高度Public Property Get ScreenHeight() As Long'以像素为单位返回屏幕的高度    ScreenHeight = GetSystemMetrics(SM_CYSCREEN)End Property Public Property Get ScreenWidth() As Long'以像素为单位返回屏幕的宽度    ScreenWidth = GetSystemMetrics(SM_CXSCREEN)End Property

使用用户定义类型
用户定义类型是一种数据结构,可以存储多个相关的不同类型的变量,与C/C++中的结构一致。有时,传递空的用户定义类型到DLL函数,函数填充值;有时,从VBA填充用户定义类型,并将其传递给DLL函数。
可以将用户定义类型作为一箱抽屉,每个抽屉可以包含不同类型的项目,但将它们组合在一起可以当作相关项目的单个箱子。可以从任何抽屉获得项目而不必担心存储在任何其它抽屉中的项目。
要创建用户定义类型,使用Type … End Type语句。在Type…End Type语句里,列出了每个项目,包含值和数据类型。用户定义类型的元素可以是数组。
下面的代码段展示如何定义RECT用户定义类型,和管理屏幕矩形块的几个Windows API函数一起使用。例如,GetWindowRect函数接受RECT类型的数据结构,使用关于窗口的左侧、顶部、右侧和底部位置的信息填充。

Type RECTLeft As LongTop As LongRight As LongBottom As LongEnd Type

要传递用户定义类型到DLL函数,必须创建该类型的变量。例如,如果打算传递RECT类型的用户定义类型到DLL函数,那么就要包括变量声明,如下所示:

Private rectWindow As RECT

可以引用用户定义类型里的单个元素,如下所示:

Debug.Print rectWindow.Left

使用句柄
调用DLLs中的函数之前需要理解的另一个重要的概念是句柄(handle)。简单地说,句柄是32位正整数,Windows用于识别窗口或另一个对象,例如字体或位图。
在Windows中,窗口有许多不同的表现形式。事实上,在屏幕中看到的几乎所有事情都在窗口里,并且不能看到的大多数事情也在窗口里。窗口能够是一个绑定的屏幕矩形区域,就像您习惯看到的应用程序窗口一样。窗体中的控件,例如列表框或滚动条,也都是窗口,虽然不是所有类型的控件都是窗口。在桌面上显示的图标以及桌面本身,都是窗口。
因为所有这些类型的对象都是窗口,所以Windows能够相同地对待它们。Windows提供给每个窗口一个唯一的句柄,并使用该句柄去处理窗口。许多API函数返回句柄或者接受句柄作为其参数。
当窗口创建时Windows赋句柄给该窗口,当窗口销毁时Windows释放该句柄。虽然句柄保留的时间与窗口存在的时间相同,但不保证一个窗口在销毁并重新创建后有相同的句柄。因此,如果在变量中存储句柄,那么记住该窗口销毁后,该句柄不再有效。
GetActiveWindow函数是返回窗口句柄的函数示例,此时,应用程序窗口是当前活动的窗口。GetWindowText函数接受某窗口的句柄,并且如果窗口有标题的话返回该窗口的标题。下面的程序使用GetActiveWindow返回活动窗口的句柄,GetWindowText返回其标题:

Declare Function GetActiveWindow Lib "user32" () As LongDeclare Function GetWindowText Lib "user32" _Alias "GetWindowTextA" (ByVal Hwnd As Long, _ByVal lpString As String, ByVal cch As Long) As Long Function ActiveWindowCaption() As StringDim strCaption As StringDim lngLen As Long'创建使用空字符填充的字符串    strCaption = String$(255, vbNullChar)'返回字符串的长度    lngLen = Len(strCaption)'调用GetActiveWindow来返回活动窗口的句柄    '与字符串和其长度一起,传递句柄到GetWindowText    If (GetWindowText(GetActiveWindow, strCaption, lngLen) > 0) Then'返回Windows已写入的值给字符串        ActiveWindowCaption = strCaptionEnd IfEnd Function

GetWindowText函数接受三个参数:窗口的句柄、将返回窗口标题里的空结尾的字符串、以及字符串的长度。
下面列出了Excel中常用的窗口类名称:

  • Excel主窗口:XLMAIN
  • Excel桌面:XLDESK
  • Excel工作表:EXCEL7
  • Excel用户窗体:ThunderDFrame(Excel 2000以后版本)、ThunderRT6DFrame(Excel 2000以后版本,用于作为COM加载项时)、ThunderXFrame(Excel 97)
  • Excel状态栏:EXCEL4
  • Excel图表窗口:EXCELE(Excel2007以前版本)

FindWindow函数使用类名和窗口标题查找窗口。下面的代码以像素为单位查找Excel主窗口的位置和大小:

'包含窗口大小的用户定义类型Type RECTLeft As LongTop As LongRight As LongBottom As LongEnd Type '查找窗口的API函数Declare Function FindWindow Lib "user32" _Alias "FindWindowA" ( _ByVal lpClassName As String, _ByVal lpWindowName As String) As Long '获取窗口大小的API函数Declare Function GetWindowRect Lib "user32" ( _ByVal hWnd As Long, _lpRect As RECT) As Long Sub ShowExcelWindowSize()Dim hWnd As Long, uRect As RECT'获取Excel主窗口的句柄    'Excel 2002及以后版本也可使用hWnd=Application.Hwnd    hWnd = FindWindow("XLMAIN", Application.Caption)'将窗口大小信息存入到RECT结构中    GetWindowRect hWnd, uRect'显示结果    MsgBox "这个Excel窗口的尺寸为:" & _vbCrLf & "左侧:" & uRect.Left & _vbCrLf & "右侧:" & uRect.Right & _vbCrLf & "顶部:" & uRect.Top & _vbCrLf & "底部:" & uRect.Bottom & _vbCrLf & "宽度:" & (uRect.Right - uRect.Left) & _vbCrLf & "高度:" & (uRect.Bottom - uRect.Top)End Sub

调用函数
虽然调用DLL函数的许多方式与调用VBA函数相似,但是开始时有一些不同可能会使DLL函数混淆。下面将介绍如何输入DLL函数中的参数并加前缀、如何返回字符串、如何传递数据结构、能够接受什么返回值、以及如何获取错误信息。
参数数据类型
在C/C++中使用的数据类型、用于描述它们的标记都不同于在VBA中的用法,下面描述了DLL函数中常用的数据类型以及它们在VBA中的等效表示。


C/C++数据类型 匈牙利前缀 描述 等效的VBA表示 BOOL b 8位布尔值。0表示False;非0表示True Boolean或Long BYTE ch 8位无符号整数 Byte HANDLE h 32位无符号整数,代表Windows对象的句柄 Long int n 16位符号整数 Integer long l 32位符号整数 Long LP lp 32位对内存中C/C++结构、字符串、函数或其它数据的长指针 Long LPZSTR lpsz 32位对C类型空结尾字符串的长指针 Long


 

 

虽然您应该熟悉这些数据类型和前缀,但前面提到的Win32API.txt文件包含了准备在VBA中使用的声明语句。如果在代码中使用这些声明语句,那么函数参数已经定义了正确的VBA数据类型。
在《Excel 2007 VBA参考大全》的第27章,详细介绍了如何将C-样式声明转换为VBA声明语句。
只要已经定义并传递了正确的数据类型,调用DLL函数与调用VBA函数采取相同的方法。当然也有例外,这将在下面的内容中介绍。
从DLL函数中返回字符串
DLL函数不会以VBA函数相同的方法返回字符串。因为字符串总是按引用传递到DLL函数,DLL函数能够修改字符串参数的值。宁可返回字符串作为函数的返回值,就像可能在VBA中做的那样,DLL函数返回字符串到传递给该函数的String类型的参数。函数的实际返回值经常是一个长整型值,指定写入到字符串参数的字节数量。
接受字符串参数的DLL函数获得指针,指向内存中该字符串的位置。指针只是内存地址,表明在哪里存储字符串。因此,当从VBA中传递字符串到DLL函数时,传递给DLL函数一个指针,指向内存中的字符串。接着,这个DLL函数修改存储在那个地址的字符串。
要调用写到String变量的DLL函数,需要采取额外的步骤合适地格式字符串。首先,String变量必须是空结尾字符串。一个空结尾字符串以特定的空字符结束,空字符通过VBA常量vbNullChar来指定。
其次,DLL函数不能修改已经创建的字符串的大小。因此,需要确保传递给函数的字符串足够大以容纳整个返回值。当传递字符串到DLL函数中时,通常需要指定在另一个传递的参数中字符串的大小。Windows追踪字符串的长度,以确保不会覆盖掉字符串已使用过的内存。
传递字符串到DLL函数中的一个好方法是创建String变量,并使用String$函数在其中填充空字符,使其足够大以容纳函数返回的字符串。例如,下面的代码创建一个144字节长的字符串,并使用空字符串填充:

Dim strTempPath As StringstrTempPath = String$(144, vbNullChar)

当传递字符串到DLL函数中时,如果不知道字符串的长度,那么可以使用Len函数确定其长度。
获取Windows临时文件夹的GetTempPath函数,就是返回String值的DLL函数的例子。该函数接受两个参数,一个空结尾的字符串变量和一个包含字符串长度的数值变量。修改该字符串以便包含路径,例如C:\Temp\。(Windows需要一个临时文件夹存在,于是该函数应该总是返回该文件夹的路径。如果由于某种原因不存在临时文件夹,GetTempPath返回0)。
下面的程序调用GetTempPath函数获取Windows临时文件夹的路径:

Declare Function GetTempPath Lib "kernel32" Alias "GetTempPathA" _(ByVal nBufferLength As Long, ByVal lpBuffer As String) As Long Property Get GetTempFolder() As String'返回用户临时文件夹的路径.    '对于根目录,Windows需要一个临时文件夹存在    '因此应该总是返回其路径    '以防万一,检查GetTempPath的返回值    Dim strTempPath As StringDim lngTempPath As Long'使用空字符填充字符串    strTempPath = String(144, vbNullChar)'获得字符串的长度    lngTempPath = Len(strTempPath)'调用GetTempPath,传递字符串长度和字符串    If (GetTempPath(lngTempPath, strTempPath) > 0) Then'GetTempPath返回路径到字符串中.        '截去字符串开始的空字符        GetTempFolder = Left(strTempPath, InStr(1, strTempPath, vbNullChar) - 1)ElseGetTempFolder = ""End IfEnd Property

注意,当传递字符串到函数中时,使用空字符填充该字符串。函数写入返回的字符串值“C:\Temp”到字符串变量的第一部分中,并且剩下的保留空字符填充,接着使用Left函数截取字符串。
GetTempPath函数的实际返回值是已经被写到字符串变量中的字符数。如果返回的字符串是“C:\Temp\”,那么GetTempPath函数返回8。
注意,这仅对从函数返回字符串时传递空结尾字符串及其大小是必需的。如果函数不返回字符串到字符串参数中,而是接受对函数指定信息的字符串,那么只需传递正常的VBA字符串变量。
传递用户定义类型到DLL函数
许多DLL函数需要通过使用预定义的格式传递数据结构。当从VBA中调用DLL函数时,根据函数的需求传递已经定义的用户定义类型。
通过查看函数的声明语句,您能够理解什么时候需要传递用户定义类型以及需要在代码中包括哪种类型定义。需要数据结构的参数总是被声明为长指针:指向内存中数据结构的32位数字值。为长指针参数约定的前缀是“lp”。此外,参数的数据类型是数据结构的名称。
例如,看看GetLocalTime函数和SetLocalTime函数的声明语句:

Private Declare Sub GetLocalTime Lib "kernel32" _(lpSystem As SYSTEMTIME)Private Declare Function SetLocalTime Lib "kernel32" _(lpSystem As SYSTEMTIME) As Long

两个函数都接受SYSTEMTIME类型的参数,即包含日期和时间信息的数据结构。下面是SYSTEMTIME类型的定义:

Private Type SYSTEMTIMEwYear As IntegerwMonth As IntegerwDayOfWeek As IntegerwDay As IntegerwHour As IntegerwMinute As IntegerwSecond As IntegerwMilliseconds As IntegerEnd Type

要将数据结构传递给函数,必须声明SYSTEMTIME类型的变量,如下所示:

Private sysLocalTime As SYSTEMTIME

当调用GetLocalTime时,传递SYSTEMTIME类型的变量到该函数,并且使用表示当前本地的年、月、日、星期几、小时、分、秒、毫秒的数字值填充该数据结构。例如,下面的Property Get程序调用GetLocalTime返回表明当前小时的值:

Public Property Get Hour() As Integer'返回当前时间,然后返回小时    GetLocalTime sysLocalTimeHour = sysLocalTime.wHourEnd Property

当调用SetLocalTime时,也传递了SYSTEMTIME类型的变量,但首先提供数据结构的一个或多个元素的值。例如,下面的Property Let程序设置本地系统时间的小时值。首先,调用GetLocalTime函数获取本地时间的当前值到数据结构中,然后使用传递给属性过程的值更新数据结构的sysLocalTime.wHour的值。最后,调用SetLocalTime函数,传递相同的数据结构,包含通过GetLocalTime加新小时值而取得的值。

Public Property Let Hour(intHour As Integer)'获取当前时间以便所有值都是当前的    '然后设计本地时间的小时部分    GetLocalTime sysLocalTimesysLocalTime.wHour = intHourSetLocalTime sysLocalTimeEnd Property

GetLocalTime函数和SetLocalTime函数与GetSystemTime函数和SetSystemTime函数相似。主要的不同在于,GetSystemTime函数和SetSystemTime函数表达的时间为格林威治标准时间。例如,如果本地时间是午夜12时,而您居住在西海岸,那么格林威治标准时间就是上午8时,有8小时的时差。GetSystemTime函数返回当前时间即8:00 A.M,而GetLocalTime返回午夜12:00。
理解Any数据类型
一些带有一个参数的DLL函数可以接受多个数据类型。在DLL函数的声明语句中,这样的参数被声明为类型Any。VBA允许传递任何数据类型到这个参数。然而,DLL函数可能被设计为接受仅仅两个或三个不同的数据类型,因此传递错误的数据类型可能会导致应用程序错误。
通常,当在VBA工程中编译代码时,VBA对传递给每个参数的值执行类型检查。也就是说,确保传递的值的数据类型与函数定义中的参数的数据类型相匹配。例如,如果参数定义为Long型,而试图传递String型的数值,则会发生编译时错误。这适用于调用内置的VBA函数、用户定义函数、或者DLL函数。当将参数声明为类型Any时,不会进行类型检查,因此当传递值到这种类型的参数时应该谨慎。
一些具有一个参数的DLL函数可以接受字符串或者指向字符串的空指针。指向字符串的空指针是一个特别的指针,指令Windows忽略所给的参数。它与零长度字符串(“”)不同。在VBA的早期版本中,程序员必须声明参数为类型Any,或者声明DLL函数的两个版本,即一个版本定义参数类型为String,一个版本定义参数类型为Long。现在VBA包括vbNullString常量,代表指向字符串的空指针,这样可以声明参数为String类型,并且在需要传递空指针的情形下传递vbNullString常量。
获取错误信息
DLL函数中发生的运行时错误的行为不同于VBA中的运行时错误,即没有错误消息框显示。当运行时错误发生时,DLL函数返回某值表时发生了错误,而且错误不会中断VBA代码的执行。
Windows API中的一些函数存储运行时错误的错误信息。如果使用C/C++编程,可以使用GetLastError函数获取关于发生的最后一次错误的信息。然而,从VBA中,GetLastError函数可能返回不确切的结果。要从VBA获得关于DLL错误的信息,可以使用VBA的Err对象的LastDLLError属性。LastDLLError属性返回发生的错误号。
为了使用LastDLLError属性,需要知道与错误相对应的错误号。在Win32API.txt文件没有这方面的可用信息,而Microsoft Platform SDK中可以找到。
下面的示例展示在已经调用了Windows API中的函数后如何使用LastDLLError属性。PrintWindowCoordinates程序接受窗口句柄,并调用GetWindowRect函数。GetWindowRect使用组成窗口的矩形的边的长度填充RECT数据结构。如果传递了无效的句柄,将发生错误,并且可以通过LastDLLError属性获得错误号。

Declare Function GetWindowRect Lib "user32" (ByVal hwnd As Long, _lpRect As RECT) As Long Type RECTLeft As LongTop As LongRight As LongBottom As LongEnd Type Const ERROR_INVALID_WINDOW_HANDLE As Long = 1400Const ERROR_INVALID_WINDOW_HANDLE_DESCR As String = "无效的窗口句柄." Sub PrintWindowCoordinates(hwnd As Long)'以像素为单位打印窗口左侧,右侧,顶部和底部位置    Dim rectWindow As RECT'传递窗口句柄和空的数据结构    '如果函数返回0,那么错误就发生了    If GetWindowRect(hwnd, rectWindow) = 0 Then'因为传递了无效的句柄        '所以如果发生错误则检查LastDLLError并显示对话框        If Err.LastDllError = ERROR_INVALID_WINDOW_HANDLE ThenMsgBox ERROR_INVALID_WINDOW_HANDLE_DESCR, _Title:="错误!"End IfElseDebug.Print rectWindow.BottomDebug.Print rectWindow.LeftDebug.Print rectWindow.RightDebug.Print rectWindow.TopEnd IfEnd Sub

要获得活动窗口的坐标,可以通过使用GetActiveWindow函数返回活动窗口的句柄,并将结果传递到前面示例定义的过程中。要使用GetActiveWindow函数,包括下面的声明语句:

Declare Function GetActiveWindow Lib "user32" () As Long

输入下面的过程后运行:

Sub test()PrintWindowCoordinates (GetActiveWindow)End Sub
要生成一条错误消息,随便使用一个长整型数值调用这个过程。  我们都知道,Visual Basic? for Applications (VBA) 是一种功能强大的编程语言,可用来开发自定义 Microsoft Office 解决方案。将 VBA 和一个或多个 Office 应用程序对象模块配合使用,您可以轻松地修改 Office 应用程序的功能,或者使两个或多个应用程序协同工作,从而完成单个应用程序无法完成的任务。VBA 只能控制操作系统的一小部分,即直接向 VBA 公开的那些函数和对象。Windows? 应用程序编程接口 (API) 提供了众多函数,可让您深入控制操作系统的绝大部分内容。您可以从 VBA 中调用 Windows API 函数,扩展和优化自定义 Office 解决方案。

在这个月的专栏中,我将为您简单介绍如何在 VBA 中使用 Windows API,并提供一些有用的示例。您可以复制这些示例,在自己的自定义解决方案中直接使用它们。

警告:调用 Windows API 和其他 DLL 函数可能会影响您的应用程序的可靠性。当您从自己的代码中直接调用 DLL 函数时,会跳过 VBA 在正常情况下提供的一些安全机制。如果您错误地定义或调用 DLL 函数(任何程序员都难免犯这类错误),就可能会产生应用程序错误,也称为常规保护错误,即 GPF。如果使用了 API,那么在运行代码前一定要保存项目,并确保已理解调用 DLL 函数的原理。

理解 API

简单地说,API 就是您用来控制组件、应用程序或操作系统的一组函数。API 通常包含一个或多个可以提供某些特定功能的 DLL。

DLL 是一些包含任何 Windows 应用程序都可以调用的函数的文件。在运行过程中,DLL 中的函数“动态地链接”到调用它的应用程序中。不管有多少个应用程序调用 DLL 中的函数,该函数只存在于驱动器上的一个单一的文件中,并且该 DLL 在内存中只创建一次。

您最常听说的 API 可能是 Windows API,它包含组成 Windows 操作系统的所有 DLL。每个 Windows 应用程序都直接或间接地与 Windows API 交互作用。Windows API 可以保证所有在 Windows 中运行的应用程序都按照统一的方式运行。

除了 Windows API 外,还有其他一些公开的 API。例如,邮件应用程序编程接口 (MAPI) 是用于编写电子邮件应用程序的一组 DLL。

传统上 API 是为那些创建 Windows 应用程序的 C 和 C++ 程序员编写的,但使用 VBA 也可以调用 DLL 中的函数。由于大部分 DLL 及其文档最初是为 C/C++ 程序员编写的,所以调用 DLL 函数和调用 VBA 函数可能会有所不同。为了使用 API,您需要了解如何向 DLL 函数传递参数。

为了调用 Windows API 中的函数,您需要参考有关文档,了解有哪些函数可供使用以及如何在 VBA 中声明和调用这些函数。以下是两个很有用的资源:

文件 Win32API.txt,包含在 Microsoft Office 2000 Developer 和 Microsoft Visual Basic 中。文件 Win32API.txt 包含了大部分 Windows API 函数所使用的 VBA Declare 语句。您可以使用 API Viewer 外接程序(同样包含在 Office 2000 Developer 中)来查找和复制您需要的 Declare 语句。有关安装和使用 API Viewer 外接程序的信息,请参阅 Office 2000 Developer 中的 apiload.txt 文件。Microsoft Visual Basic 附带的 API Viewer 应用程序具有相同的功能,只不过它是一个独立的应用程序。

初次运行 API Viewer 应用程序时,它将加载 Win32API.txt 文件。该文本文件可以导出到一个 Microsoft Access 数据库(.mdb 文件)中,这样能加速加载和浏览 API 数据的过程。

Microsoft Platform SDK,包含完整的 Windows API 文档。它可以在 Microsoft Developer Network 站点上免费获得:http://msdn.microsoft.com/library/default.asp?URL=/library/psdk/portals/win32start_1n6t.htm(英文)。

使用 Declare 语句

在从 VBA 调用 DLL 中的函数前,您必须先告诉 VBA 该函数位于何处以及如何调用它。有两种方法可以实现这一目的:


  • 设置 DLL 类型库的引用。
  • 在模块中使用 Declare 语句。


设置 DLL 类型库的引用是使用 DLL 函数的最简单的方法。一旦设置了引用,您便可以调用 DLL 函数,就好象它是项目中的一部分。然而,这里有一些需要注意的地方。首先,设置引用多个类型库会影响应用程序的性能。其次,并非所有 DLL 都提供类型库。尽管可以设置引用一个不提供类型库的 DLL,但却不能象将其作为项目中的一部分那样调用该 DLL 中的函数。

请注意,组成 Windows API 的 DLL 不提供类型库,因而不能通过设置引用这些 DLL 来调用其中的函数。要调用 Windows API 中的函数,您必须在项目中的模块声明部分插入一条 Declare 语句。

Declare 语句是一条定义语句,它告诉 VBA 从何处获得特定的 DLL 函数以及如何调用该函数。在代码中添加 Declare 语句的最简单方法是使用 API Viewer 外接程序,该程序包含了大部分 Windows API 函数所使用的 Declare 语句,以及一些函数需要的常量和类型定义。

下例所示为 GetTempPath 函数的 Declare 语句,该函数将返回 Windows 临时文件夹的路径(默认情况下为 C:\Windows\Temp):

复制内容到剪贴板 程序代码Private Declare Function GetTempPath Lib "kernel32" _
   Alias "GetTempPathA" (ByVal nBufferLength As Long, _
   ByVal lpBuffer As String) As Long

关键字 Declare 告诉 VBA 您要在项目中包含一个 DLL 函数的定义。标准模块中的 Declare 语句可以是公共的或是私有的,这取决于您想使该 API 函数只在单个模块中可用还是在整个项目中都可用。在类模块中,Declare 语句必须为私有的。

紧随关键字 Function 的函数名是您从 VBA 中调用该函数时要使用的名称。该名称可以与 API 函数本身的名称相同,也可以使用 Declare 语句中的 Alias 关键字,表示您要在 VBA 中使用不同的名称(“别名”)调用该函数。

在上面的示例中,在 DLL 内 API 函数的名称为 GetTempPathA,而从 VBA 中调用它时使用的名称就是 GetTempPath。请注意,DLL 函数的真实名称应出现在 Alias 关键字的后面。另外,GetTempPath 是 Win32API.txt 文件中使用的函数别名,您可以把它更改为任何您需要的名称。

以下是您可能要在 Declare 语句中使用别名的几个原因:


  • 有些 API 函数的名称以下划线字符 (_) 开头,这在 VBA 中是非法的。为了从 VBA 中调用这类函数,您需要使用别名。
  • 由于可以任意定义 DLL 函数的别名,因此您可以在 VBA 中按照自己的标准来命名函数。
  • 由于 API 函数区分大小写,而 VBA 函数不区分,您可以使用别名来改变函数名称的大小写。
  • 有些 DLL 函数具有可使用多种数据类型的参数。VBA 的 Declare 语句可将这些函数的参数定义为 Any 类型。将 DLL 函数的参数定义为 Any,调用时可能会有危险,因为 VBA 不会为您检查数据类型。如果想避免以 Any 类型传递参数带来的危害,您可以为同一个 DLL 函数声明多个不同版本,每一个版本都使用不同的名称和不同的数据类型。


Windows API 中所有使用字符串参数的函数都具有两个版本:ANSI 版和 Unicode 版。ANSI 版以字母“A”开头,如上例所示,而 Unicode 版以字母“W”开头。尽管 VBA 内部使用 Unicode,但在调用 DLL 函数前,它会将所有字符串都转换成 ANSI 字符串。因此当您从 VBA 中调用 Windows API 函数时,通常还是使用 ANSI 版。API Viewer 外接程序将自动为所有使用字符串参数的函数命名,这样您可以不用包含首字母“A”而直接调用函数。

关键字 Lib 指明包含该函数的 DLL。请注意 DLL 的名称包含在 Declare 语句的一个字符串中。如果在 Lib 关键字后指定了一个用户系统上不存在的 DLL,那么调用该函数时将导致运行时错误 48,“加载 DLL 时出错”。在 VBA 代码中可以处理这些错误,您可以编写具有适当错误处理的代码,使之运行稳定可靠。(如果您调用基本 Windows DLL 中的函数,这就不成为问题,因为当您加载应用程序时,就必然要用到这些 DLL。)

下表列出了 Windows API 中最常用的 DLL。