查看: 543  |  回复: 0
  VB6 / VBA 的秘密
楼主
发表于 2023年12月18日 22:32

1 介绍:

Visual Basic被称为“快速应用程序开发(RAD)开发工具”,因为它旨在为你处理Windows“基础工作”,从而使你可以专注于重要的东西,如程序的功能和文档。

例如,你打开VB,新建一个项目,然后在项目中添加一个标准的“窗体”,按“F5”来执行程序,该窗体就被显示出来,我们操作起来非常简单,但是,你可能不知道,VB在背后帮我们做了很多事情。 VB需要调用“CreateWindow”来实际创建“窗体”,并为构成它界面的属性赋值。 然后,需要通过调用各种Win32 API来修改窗体上的字体,前景色,背景色,设备上下文(device context)等。 最后,VB需要用子类化的方式,将新创建的窗体挂钩(hook)到Windows消息流中,让窗体捕获到发送给它的Windows消息,最后使用“WindowProc”回调函数,处理每个Windows消息。 窗体的接口越复杂,窗体对象的创建和功能处理代码就越复杂。 而C和C ++程序员就没有这么轻松了,他们需要亲自编写代码,来创建所有的对象,处理消息流以及销毁对象(或者通过模板来生成代码)。

对于会正确使用VB开发工具的程序员来讲,Visual Basic能帮助你做一些类似上述“基础”的事情,是一个非常强大的功能。但是,与此同时,那些不太了解如何编程的人,也拥有了很大的能力。也正是这个原因,Visual Basic被C和C++程序员嘲笑。他们说,“任何人都可以用VB做开发,但只有真正的程序员才能使用C / C++做开发。我认为,聪明的程序员选择Visual Basic,因为VB在对象创建,消息处理,对象销毁过程中,可以帮你消除潜在的Bug。VB能提供更简单更快捷Windows事件处理,VB能为你提供更强大的界面功能,VB能让你更轻松的访问COM对象和第三方控件,VB更容易阅读,因为它非常接近阅读英语,而C/C++看上去却非常神秘。VB允许你轻松访问Win32 API (这使得程序员有驾驭Windows的强大功能的能力)。以及最最重要的是,Visual Basic可以通过组件,库,和其他用C/C++编写的代码来挂钩住(hook)C/C++的强大功能和速度。嘿。。。。。。C/C++程序员,现在怎么不继续吹牛逼了?^_^

事情就是这样。。。。。。即使是工作多年的VB程序员也没有意识到VB的真正力量,因为他们没有掌握(或意识到)VB提供的一些关键概念和功能。这些概念很少被大家知道,或者受重视的程度还远远不够,因为我将它们称之为“VB的秘密”。

2 在VB中使用指针:

我曾经在求职面试中被问到一个问题,现在我意识到这是一个有坑的问题。“Visual Basic 是否有或者使用‘指针’?”任何用过VB的人都会显而易见的回答“No”,在VB中,你看不到任何像C/C++中那样的指针的声明,这就是当时我认为面试官要问的点,而她对我的答案也表示认同。然而,正确的答案应该是“Yes”。

Visual Basic (就像几乎所有其他编程语言一样)确实使用指针,广泛的使用。不同的是,Visual Basic会尽可能的将它们隐藏起来,或者将它们称为不同的东西,以免给你带来负担。

接下来,我们谈谈,如何使用指针直接访问变量中的信息(VarPtr / StrPtr / ObjPtr),通过指针将信息传递给函数(ByRef / ByVal),取回和传递指向函数的指针(AddressOf)。

3 VarPtr, StrPtr 和 ObjPtr:

VB函数“VarPtr”(变量指针),“StrPtr”(字符串指针)和“ObjPtr”(对象指针)是无官方文档(undocumented),无技术支持的(unsupported)的函数,微软在VB5.0和VB6.0中引入。这些函数(跟随很多其他的函数一起)在http://VB.Net中不再使用。这些函数允许你获取VB变量在内存中的地址(指针),以及变量所指向的实际数据在内存中的地址。这些函数为什么作用这么大?因为,如果你知道数据的内存地址,你可以任意操控它,直接复制或者传递出去,而不需要借助VB的帮助。这样做,速度上更快,并且(在某些情况下)你可以做到VB本身做不到的事情。

下面是微软MSDN关于“VarPtr”的说明:

该函数可以用来获取一个变量或数组元素的地址。它使用变量名或数组元素作为参数,返回它的地址。然而,你需要注意的是,未锁定的动态数组可能会被Visual Basic重新分配地址,所以你当你使用VarPtr获取数组元素地址时,必须非常小心。

下面的示例获取变量的地址:

Dim lngVariableAddress As Long
Dim dblMyVariable As Double
lngVariableAddress = VarPtr(dblMyVariable)

下面的示例获取某个数组的第4个元素的地址:

Dim lngElementAddress As Long
Dim lngArrayOfLongs(9) As Long
'下面的代码将获取数组中第4个元素的地址
lngElementAddress = VarPtr(lngArrayOfLongs(3))

限制:VarPtr函数不能用来获取数组的地址。。。。。。

下面是微软MSDN关于“StrPtr”的说明:

在Visual Basic 中,字符串是以BSTR来存储的。如果你对一个字符串变量使用VarPtr,你将得到BSTR的地址,它是该字符串指针的指针。要获取字符串缓冲本身的地址,你需要使用StrPtr函数。该函数返回字符串第一个字符的地址。需要考虑到在Visual Basic中,字符串是以UNICODE来存储的。

要获取一个字符串的第一个字符的地址,请将该字符串变量传递给StrPtr函数。

示例:

Dim lngCharAddress As Long
Dim strMyVariable As String
strMyVariable = "Some String"
LngCharAddress = StrPtr(strMyVariable)

当你需要传递一个指向UNICODE字符串指针给API时,你可以使用StrPtr函数。

下面是微软MSDN关于“ObjPtr”的说明:

ObjPtr函数使用一个对象变量名作为参数,获取该对象变量所引用的接口的地址。

一种试用该函数的情况,是当你需要处理集合对象的时候。相比使用Is操作符遍历集合对象而言,通过使用对象地址作为索引关键字,你可以获取更快的访问速度。在很多情况下,对象的地址是唯一可以信赖的键值。

示例:

objCollection.Add MyObj1, CStr(ObjPtr(MyObj1))
'...
objCollection.Remove CStr(ObjPtr(MyObj1))

注意在“VarPtr”说明的底部的“限制”,它说你不能使用VarPtr获取数组的地址。在某种程度上,说得没错。你不能将变量“MyArray”传递给它(因为VB将数组保存在一个叫做“SafeArray”的OLE对象中),但是,如果你获取了数据的第一个元素“MyArray(0)”的地址,你就有了整个数组的地址,因为数组元素在内存中是连续存储的(按照数字顺序从第一个元素到最后一个元素)。所以,如果某个Win32 API 或者C / C++ 函数需要一个指向某个字节数组的指针,像这样:

Option Explicit
Private Type POINTAPI
  X As Long
  Y As Long
End Type

'BOOL Polyline(
'  HDC          hDC,    // handle of device context
'  CONST POINT *lpPT,   // address of array containing endpoints
'  int          cPoints // number of points in the array
');
Private Declare Function Polyline Lib "GDI32.DLL" (ByVal hDC As Long, _
       ByRef lpPT As Any, ByVal cPoints As Long) As Long

你可以像这样调用它:

Private Sub Form_Load()
 Dim ThePoints() As POINTAPI
 Me.AutoRedraw = True
 Me.Visible = True
 Me.Move 0, 0, Me.Width, Me.Height
 ReDim ThePoints(1 To 5) As POINTAPI
 ThePoints(1).X = 0:   ThePoints(1).Y = 0
 ThePoints(2).X = 100: ThePoints(2).Y = 0
 ThePoints(3).X = 100: ThePoints(3).Y = 100
 ThePoints(4).X = 0:   ThePoints(4).Y = 100
 ThePoints(5).X = 0:   ThePoints(5).Y = 0
 If Polyline(Me.hDC, ByVal VarPtr(ThePoints(1)), 5) = 0 Then Debug.Print "FAILED!"
 Me.Refresh
 Erase ThePoints
End Sub

注意:将坐标点(pointers)保存到动态数组中要小心,因为当动态数组重新分配内存地址后,或者动态数组改变了大小后,或者被重新定义(ReDim)后,实际数据将非常有可能被保存到一个新的内存地址。

关于VarPtr,StrPtr,ObjPtr的更多信息,可以参见以下链接:

http://support.microsoft.com/default.aspx?scid=kb;en-us;Q199824

http://msdn.microsoft.com/library/en-us/dnw32dev/html/ora_apiprog6_topic1.asp

http://msdn.microsoft.com/library/en-us/dnovba00/html/LightningStrings.asp

http://msdn.microsoft.com/library/en-us/dnovba01/html/Lightweight.asp

4 ByRef / ByVal

到目前为止,VB程序员在调用Win32 API(或任何C/C++导出函数)时遇到的最大的问题,就是参数的正确传递。在应该使用“ByVal”的地方使用了“ByRef”(或者反过来也一样),或者当函数期望传入一个指针时,你传入的却是一个值或者一个变量,这些都可能会导致系统崩溃。要想搞懂如何正确的传递参数,就要了解Windows程序如何调用堆栈的(calling stacks),以及主调程序与被调函数之间的内存分配。

首先,我们来讨论一下什么是“调用堆栈”,以及当传递参数给函数时,它是怎样受内存分配影响的。“调用堆栈”说白了,就是内存中的一段空间,那些传递给函数的变量和值,以及函数返回值都存储在这段内存空间中。它被叫做“堆栈”(stack)是因为参数值是一个挨着一个被存储在这段内存空间中,访问这些参数值时,也是按照这种假定来访问。正因为如此,从纵向角度从下到上来看,这些参数一个堆在另一个上面,组成了要给到函数的全部参数信息。当参数被添加到函数的调用堆栈上时,它被称为“压入”(push)到调用堆栈上面。当参数从函数的调用堆栈移除的时候,它被称之为从堆栈“弹出”(pop)。“堆栈”,“压入”,“弹出”这些都是汇编术语(是的,我们现在讨论得非常底层),如果你将程序或者DLL反编译到汇编语言,你会看到一些代码行上有“push”,“pop”等单词。

当Visual Basic调用Win32 API(或者任何导出的C/C++函数)时,它期望被调函数使用“标准调用惯例”(Standard Calling Convention)(_stdcall),这与C/C++默认的调用惯例(_cdecl)刚好不同。这就意味着,当函数被调用的时候,参数被从右到左传入到内存中(或者压入到堆栈上面),被调函数负责清理参数的内存(或者从堆栈弹出)。如果调用一个声明为任何非“标准调用惯例”_stdcall的导出函数,Visual Basic不知道如何处理堆栈和传出传入的参数,VB就会跳出信息说“错误的DLL调用惯例”(Bad DLL Calling Convention)。

调用参数时内存如何分配和收回?调用堆栈是什么?以及它们在Windows中是怎么工作的?想知道更多更深入的解释,我强烈的推荐一本Dan Appleman写的书:“Dan Appleman’s Win32 API Puzzle Book and Tutorial for Visual Basic Programmers”。

现在我们跳出底层的内存运作机制,回到VB上面来。调用函数时,给它传递参数的方式有2种,要么传递给它明确的值,要么传递给它一个指针,该指针指向内存中的某个存储了值的地址。当你传递像数字,大小,标志等简单信息时,你希望用值的方式(ByVal: By Value)传递信息,因为你想让函数获取你所传递的值,而不是这个值当前被存储在内存中的地址。现在当你想要传递更加复杂的数据的时候,比如一个数据类型,一组数组,或者一个对象引用,你需要传递一个引用(或指针)到被调函数,告诉它数据在内存中的地址是多少。这是通过指定ByRef(By Reference)关键字完成的。这样,被调函数到内存中找到这个地址,读取相关的数据。这里有一个例外,当你传递字符串(String)参数给Win32 API函数时(或者任何C/C++导出函数),请使用ByVal方式(除非你传递字符串数组,这种情况下使用ByRef,或者用ByVal传递字符串数组的第一个元素)。

所以到现在为止,你可以说:“我已经知道ByRef/ByVal这两种方式传递参数了”。是的,但是你意识到通过ByRef传递参数时,是在传递指针吗?如果你把这个概念再往前推进一步,你可以让函数接口变得更加通用:把通过引用传递(ByRef)改为通过值传递(ByVal),然后传递一个明文的指针。所以,你可以像这样声明函数:

Option Explicit

Private Type RECT
 Left   As Long
 Top   As Long
 Right   As Long
 Bottom   As Long
End Type

'int FillRect(
'  HDC         hDC,  // handle to device context
'  CONST RECT *lpRC, // pointer to structure with rectangle
'  HBRUSH      hBR   // handle to brush
');
Private Declare Function FillRect Lib "USER32.DLL" (ByVal hDC As Long, _
       ByVal lpRC As Long, ByVal hBR As Long) As Long
Private Declare Function CreateSolidBrush Lib "GDI32.DLL" (ByVal crColor As Long) As Long
Private Declare Function DeleteObject Lib "GDI32.DLL" (ByVal hObject As Long) As Long

Private Sub Form_Load()
 Dim hBrush As Long
 Dim MyRect As RECT
 
 Me.AutoRedraw = True
 Me.Visible = True
 Me.Move 0, 0, Me.Width, Me.Height
 With MyRect
   .Top = 0: .Left = 0: .Right = 100: .Bottom = 100
 End With
 
 hBrush = CreateSolidBrush(vbRed)
 If hBrush = 0 Then Exit Sub
 If FillRect(Me.hDC, VarPtr(MyRect), hBrush) = 0 Then Debug.Print "FAILED!"
 Me.Refresh
 DeleteObject hBrush
End Sub

如果你仔细想想的话,在声明函数和参数的时候,它可以给你各种选择。你不在局限于特定的变量类型。你可以将所有参数都变成“Long”型变量,然后将指针传递给任何函数参数(只要你小心翼翼的话)。比如说在传递自定义类型的时候遇到麻烦了,忘掉它,传递指针就好了。再比如在传递对象的时候遇到麻烦了,忘掉它,传递指针就好了。VB5.0中不允许将变量数组作为函数返回类型,忘掉它,返回一个长整型,指向数组的内存地址,然后用API函数CopyMemory将它拷贝到本地数组中。看出来我要干什么了吗?

联合ByRef和ByVal一起使用VarPtr,StrPtr和ObjPtr,可以让你传递任何格式的数据,如果你知道自己在做什么的话。

5 AddressOf 和回调

操作符“AddressOf”跟回调(call back)息息相关。“但什么是回调?”你问。回调是VB事件在Windows中的等价物。事实上,从底层来讲,当回调函数捕获了以Windows系统消息形式存在的原始事件之后,回调函数会触发VB事件。回调函数常见于Win32 API内部(和其他C/C++代码中),特别是当你的应用程序中需要监测用户和/或Windows的活动的时候。在VB中你不怎么看到回调函数,是因为VB通过“事件”(Events)处理消息和通知,这种方式比起使用回调函数更加简单和安全。

假如说,在你的项目中,Windows发送给窗体的每一个消息你都想收到通知,(很多消息也许你根本不会用到),当然还有一些由于其他API调用,可能发送到你的窗体的个性化消息。要想达到这个目的,你所要做的就是设置一个被Windows认可的回调函数(“WindowProc”),然后告诉Windows(通过“SetWindowLong” API函数)将它的所有与你的窗体有关的消息发送给你的回调函数,这样你就能够检查它们,对消息做出反应,将消息传递下去(通过“CallWindowProc” API函数)。所有这一切,就叫做“子类化”(Sub-Classing),功能非常强大(同时也是非常危险的)的技术,你可以用这个技术来个性化的重画你的窗体,菜单和内容(或你想对你的窗体干的任何事情)。

使用“AddressOf”有2个缺点:

1)你只能获取VB标准模块中的函数或子程序(公有或私有)的地址。没有办法能绕过这个限制。

2)它只能被作为函数或子程序参数列表的一部分被调用。绕过这个限制的办法如下:

Option Explicit

Public Sub Main()
 Dim lngProcAddress As Long
 lngProcAddress = GetProcAddress(AddressOf WindowProc)
End Sub

Public Function GetProcAddress(ByVal lngAddressOf As Long) As Long
 GetProcAddress = lngAddressOf
End Function

Public Function WindowProc(ByVal hWnd As Long, ByVal uMsg As Long, _
                          ByVal wParam As Long, ByVal lParam As Long) As Long
 ' < YOUR CODE GOES HERE >
End Function

你会注意到我们传递“AddressOf”和我们想得到地址(内存指针)的函数名到“GetProcAddress”函数,该函数只是简单的返回地址值,简单又非常有效。函数和子程序的地址不会变,所以你可以将想要调用的函数和子程序的地址保存下来,而不必在每次调用的时候都使用AddressOf。

“那我们来看看实际应用吧!”你说。ok!

如上所述,这里有一个“子类化”的示例:

'标准模块中的代码
Option Explicit

Private Const GWL_WNDPROC = (-4)
Private lngPrevProc As Long
Private lngHWND As Long

Private Declare Function SetWindowLong Lib "USER32.DLL" Alias "SetWindowLongA" _
       (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Declare Function CallWindowProc Lib "USER32.DLL" Alias "CallWindowProcA" _
       (ByVal lpPrevWndFunc As Long, ByVal hWnd As Long, ByVal uMsg As Long, _
       ByVal wParam As Long, ByVal lParam As Long) As Long

'这就是回调函数,接收发送给hWnd的消息
Private Function WindowProc(ByVal hWnd As Long, _
                           ByVal uMsg As Long, _
                           ByVal wParam As Long, _
                           ByVal lParam As Long) As Long
 '在立即窗口中显示消息和参数
 '注意:你可以通过比较“uMsg”的值与Windows消息(WM_*) 常量(在WINUSER.H文件中定义)
 '来找到什么消息被发送了。
 Debug.Print _
 "hWnd=" & hWnd & ", uMsg=" & uMsg & ", wParam=" & wParam & ", lParam=" & lParam
 
 '传送消息到它本应该去的地方。这里,这是必须的,否则窗体将因为停止接收消息而变得无响应
 WindowProc = CallWindowProc(lngPrevProc, hWnd, uMsg, wParam, lParam)
End Function

'该函数启动一个新的子类化
Public Function Subclass_Start(ByVal hWnd As Long) As Boolean
 '停掉任何之前的子类化
 If Subclass_Stop = False Then Exit Function
 '尝试开始一个新的子类化
 lngPrevProc = SetWindowLong(hWnd, GWL_WNDPROC, AddressOf WindowProc)
 If lngPrevProc <> 0 Then
   lngHWND = hWnd
   Subclass_Start = True
 End If
End Function

'该函数停止任何已经存在的子类化
Public Function Subclass_Stop() As Boolean
 '如果之前的子类化未启动,就退出
 If lngPrevProc = 0 Or lngHWND = 0 Then
   Subclass_Stop = True
 Else
   '将消息处理程序设置回原始状态
   If SetWindowLong(lngHWND, GWL_WNDPROC, lngPrevProc) <> 0 Then
     Subclass_Stop = True
   End If
 End If
 '清除使用过的变量
 lngPrevProc = 0
 lngHWND = 0
End Function

'窗体代码

Option Explicit

Private Sub Form_Load()
 Subclass_Start Me.hWnd
End Sub

Private Sub Form_QueryUnload(Cancel As Integer, UnloadMode As Integer)
 Subclass_Stop
End Sub

下面是一个“列举”的示例,它是反馈Windows列表信息非常流行的方法(例如所有窗体列表,窗体上所有对象的列表,已安装的所有字体列表等):

Option Explicit

Private lngWinHandle() As Long
Private lngWinHandleCount As Long

Private Declare Function EnumWindows Lib "USER32.DLL" (ByVal lpEnumFunc As Long, _
       ByVal lParam As Long) As Long

'这就是回调函数,它会遍历当前桌面下所有的窗口
Private Function EnumWindowsProc(ByVal hWnd As Long, ByVal lParam As Long) As Long
 '递增窗口句柄数组的大小
 lngWinHandleCount = lngWinHandleCount + 1
 ReDim Preserve lngWinHandle(1 To lngWinHandleCount) As Long
 '将信息添加到数组
 lngWinHandle(lngWinHandleCount) = hWnd
 '让函数继续执行
 EnumWindowsProc = 1
End Function

Public Function GetAllWindows(ByRef Return_Handles() As Long, _
      Optional ByRef Return_WinCount As Long) As Boolean
 '清除掉之前的信息
 Erase lngWinHandle
 lngWinHandleCount = 0
 '开始遍历窗体
 If EnumWindows(AddressOf EnumWindowsProc, 0) <> 0 Then
   Return_Handles = lngWinHandle
   Return_WinCount = lngWinHandleCount
   GetAllWindows = True
 End If
 Erase lngWinHandle
 lngWinHandleCount = 0
End Function

Option Explicit
Private Sub Form_Load()
 Dim lngWindows() As Long
 Dim lngWindowsCount As Long
 Dim lngCounter As Long
 Dim strWindows As String
 If GetAllWindows(lngWindows, lngWindowsCount) = True Then
   If lngWindowsCount > 0 Then
     For lngCounter = 1 To lngWindowsCount
       strWindows = strWindows & " " & lngWindows(lngCounter) & Chr(13)
     Next
   End If
   Me.AutoRedraw = True
   Me.Print strWindows
 End If
 Erase lngWindows
End Sub

6 访问“隐藏的”API

这部分绝对是这篇文章所述所有秘密中最“秘密”的部分。在Windows中确实有很多隐藏的Win32 API调用……诀窍是找到它们并找出如何调用它们,因为微软肯定不会告诉你。

但是微软为什么要隐藏它们呢?”你可能会问。因为这些隐藏的函数为API添加了额外的功能,只有微软知道如何使用它们。这使得他们自己的产品(在Windows下运行)可以获得优势地位。因为只有他们知道如何访问这些更强大的,更快的隐藏着的API函数,而其他人只能使用官方在MSDN中公开披露的正规的API函数。“不公平的竞争优势”?你的想法的一点没错!但是谁也没有说过微软在公平的展开业务,或者有道德的做生意。这些商业行为正不断地将微软置于法庭和报纸的头条新闻中。

“这些隐藏的API都是什么样的?如何找出它们?如何使用它们?”非常好的问题。网络上有很多网页,它们致力于找到这些隐藏的API,纰漏它们的功能给“平等的竞争环境”,并为像你我这样的开发者提供更酷的功能。这里有一些很好的网页:

http://www.geocities.com/SiliconValley/4942/index.html(无法访问,译者注)

http://www.users.qwest.net/~eballen1/nt.sekrits.html(无法访问,译者注)

http://www.mvps.org/vbnet/code/shell/undocshelldlgs.htm

http://www.mvps.org/vbnet/code/shell/undocformatdlg.htm

http://www.mvps.org/vbnet/code/shell/undocchangeicondlg.htm

http://www.mvps.org/vbnet/code/shell/undocshpaths.htm

http://www.ercb.com/ddj/1992/ddj.9211.html(无法访问,译者注)

你可以在“VB标准模块”下的modCOMDLG32.bas模块中找到一些这些“隐藏的API”。它们看起来像这样:

Public Declare Function DLG_FindFile Lib "shell32.dll" Alias "#90" _
      (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long
Public Declare Function DLG_FindComputer Lib "shell32.dll" Alias "#91" _
      (ByVal pidlRoot As Long, ByVal pidlSavedSearch As Long) As Long

你会注意到它们的别名是数字“#90”,“#91”。这些被称为“序数”,它们是一种通过DLL公开API而不暴露它们的名称的方法。由此可见,如果你编写了一个函数,只想给自己使用,而不想让其他人知道该函数的更多信息,那么你可以通过数字公开它。除了一个数字,其他人对此是一无所知的,但只有你知道如何调用它。

偷偷摸摸的,是吧?:)

好了!这就是我要说的所有内容了。如果我想到了VB中的其他的秘密,或者“隐藏的”和“模糊的”功能(或想起来我还打算放在这里的任何内容),我会把它们加进来。编码愉快!

您需要登录后才可以回帖 登录 | 立即注册
【本版规则】请勿发表违反国家法律的内容,否则会被冻结账号和删贴。
用户名: 立即注册
密码:
2020-2024 MaNongKu.com