1. 添加一个工程,添加一个模块,粘贴以下内容:Option Explicit
Private Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long, ByVal hwnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Public lpPrevWndProc As Long
Public Const GWL_WNDPROC As Long = (-4)
Private bPass As Boolean
Public Sub Hook(ByVal gHW As Long)
让程序介入Windows消息传递:
lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, AddressOf WindowProc)
第三个参数实际为指针参数,VB中用AddressOf运算符得到回调函数WindowProc的指针(首地址)
End Sub
Public Sub Unhook(ByVal gHW As Long)
取消:
Call SetWindowLong(gHW, GWL_WNDPROC, lpPrevWndProc)
End Sub
Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
WindowProc,自定义的事件驱动
不是来自frmVolume的事件,按Windows默认处理:
按键0-9 wParam48-57
按键A-F wParam97-102
bPass = False
If uMsg = 258 Then uMsg 258,键盘按下消息
Select Case wParam
Case 97 To 102 小写a-f
bPass = True
wParam = wParam - 32 小写强行显示为大写
Case 48 To 57 按键0-9
bPass = True
Case 65 To 70 按键大写A-F
bPass = True
Case 8 按键BackSpace
bPass = True
End Select
Else
bPass = True
End If
If bPass = True Then WindowProc = CallWindowProc(lpPrevWndProc, hwnd, uMsg, wParam, lParam)
End Function
----------------------------------------------
2. 添加一个Form,添加一个TextBox,并粘贴以下代码:
Private Sub Form_Load()
Call Hook(Text1.hwnd) 开始调用回调函数
End Sub
Private Sub Form_Unload(Cancel As Integer)
Call Unhook(Text1.hwnd) 释放回调函数 ps: 如不释放将引起程序崩溃
End Sub
程序解析:
功能:吞噬掉一些按键信息,当我们按下不是0-9,A-F,a-f这些键时,文本框不显示任何内容。
首先是SetWindowLong函数,以下该函数的官方描述(描述比较多而且很复杂,我只摘录一些和本例相关的):
SetWindowLong 函数功能:
该函数改变指定窗口的属性。函数也将在指定偏移地址的一个32位值存入窗口的额外窗口内存。
函数原型:LONG SetWindowLong(HWN hWnd,int nlndex.LONG dwNewLong);
返回值:如果函数成功,返回值为给定的32位整数的原来的值。如果函数失败,返回值为 0。
参数:
1.
hWnd:窗口句柄,及间接声明的该窗口所属的类。
2.
GWL_WNDPROC:为窗口过程设置一个新的地址。
3.
dWNewLong:指定替换值。
如果使用带GWL_WNDPROC索引值的SetWindowLong函数替换窗口过程,则该窗日过程必须和WindowProc回调函数说明部分指定的指导行一致。
可以使用带GWL_WNDPROC索引值的SetWindowLong函数创建一个窗口类的子类,该窗口类是用于创建该窗口的类。一个应用程序可以一个系统类为子类,但是不能以一个其他进程产生的窗口类为于类。 SetWindowLong函数通过改变与一个特殊的窗口类相联系的窗口过程来创建窗口子类。从而使系统调用新的窗口过程而不是以前定义的窗口过程。应用程序必须通过调用 CallWindowProc函数向前窗口传递未被新窗口处理的消息。允许应用程序创建一个窗口过程链。
再回头来看看我们的程序:
lpPrevWndProc = SetWindowLong(gHW, GWL_WNDPROC, AddressOf WindowProc)
gHW:指定窗口的句柄,在Windows,并不是我们所谓看到的“窗口”才能称为窗口,事实上系统认为所有控件都是窗口,比如文本框,也是按照“窗口”来操作的。 本例的窗口为Text1,gHW参数将Text1的句柄带入函数。
GWL_WNDPROC: 该参数为常量-4, 表示为窗口过程设置一个新的地址(见官方描述)。
AddressOf WindowProc: 回调函数的函数入口(首地址指针),在VB里,是没有指针这种东西的,但是从VB5.0开始,微软提供了一个AddressOf功能, 间接地使VB支持了指针。
这一行程序,究竟做了什么?
用最通俗的语言来讲,就是改变了文本输入框Text1的首地址,我们先来看看在常规模式下,Windows做了些什么,首先,程序启动,分配一个内存地址给Text1,然后对Text1的所有操作均从那个空间的首地址开始执行。而现在我们改变了Text1的首地址,这个地址是什么?是我们自定义的函数WindowsProc的首地址。也就是说,对Text1的所有操作,入口均在我们这里,相当于我们有了一个处理消息的机会。
lpPrevWndProc 返回了什么?
看官方描述,如果函数成功,返回值为给定的32位整数的原来的值。如果函数失败,返回值为 0。怎么理解呢?也就是说,一旦SetWindowLong成功,那么它返回你给他设置的值。我们给它的是我们自定义的函数WindowsProc的首地址。
OK,说完 SetWindowLong,接着说我们的自定义回调函数WindowProc,
关于回调函数WindowProc的函数声明:
Function WindowProc(ByVal hwnd As Long, ByVal uMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
形式参数由hWND,uMSG,wParam,lParam组成,并且,古怪的的事情出现了,你必须要这样去声明!这跟一般的VB函数定义有很大区别,因为我们这个函数实际的作用是改变Text1的消息操作。那么,我们的函数必须与Text1(文本框)的函数参数定义相吻合。有关这些参数的描述可以在SDK帮助,或者拜访MSDN, 在官方描述里对此也有规定:“如果使用带GWL_WNDPROC索引值的SetWindowLong函数替换窗口过程,则该窗日过程必须和WindowProc回调函数说明部分指定的指导行一致。”
你可以试着修改这个参数,比如,我们把lParam这个参数取消,然后运行程序,结果很恐怖,连VB的编译器环境一起崩溃,而且还不带任何提示,呵呵,好玩吧!
以下是各参数的具体说明:
hWnd: 操作句柄,也就是Text1的句柄。
uMsg: 消息代码
wParam: 消息参数1
lParam: 消息参数2
什么是消息?
消息是Windows与应用程序(窗口)之间的桥梁,是Windows的基本控制手段。消息通过句柄来区分归属,区分到底是哪个窗口发生了变化,是窗口1?还是窗口2? 比如当一个文本框(前面说过了,也认为它是一个窗口),用户按下了某个按键后,这个时候,Windows产生一条消息,并将这条消息传递给应用程序。用过VB都知道,文本框有一个KeyPress事件,你可以在这个事件里加入自己的代码,一旦按下按键,程序直接跳转到KeyPress里去执行你自定义的程序。如果你没有为KeyPress事件写代码,也就是说如果你不理睬KeyPress事件的话,则Windows按照默认的方法去处理这些消息。
注意,事实上,VB的“事件”,就是Windows的消息,并且也是一种回调函数。按理来说,Windows是操作系统,它提供了无数个函数以供应用程序调用,原则上应该是我们去调用系统函数。比如发生按键这个事情后,我们应该主动去调用Windows有关按键的那些程序,比如显示字符等等之类(比如说DOS时代的编程)。在Windows时代,应用程序发生按键之后,Windows反过来主动回调应用程序的函数,也就是各种“事件”的代码,如果事件代码里有代码,则执行,如没有,Windows按默认方式处理。
事件是一种很好的简化Windows编程的好东西!它不仅屏蔽了消息的地址,而且将消息归类化,程序员在处理消息的时候显得很“舒服”,很合理。但是,这种傻瓜化的方式在处理一般事务的时候没有问题,但当有一些特别应用的时候就显得不足了。比如本例,我们需要它只能显示16进制的字符,也就是0到9,A到F,那么VB的事件就无能为力了,因为VB为用户提供的默认事件KeyPress并不包含按键吞噬的功能,也就是说在KeyPress里,你可以知道用户按下了什么键,但你没办法阻止这个键的显示,因为这是Windows默认处理的结果。
所以,我们必须编写我们自己的消息处理函数,自己生长一个回调函数,让消息还没有抵达窗口之前将它拦截住并进行筛选和处理。
继续回到程序,我们来到自定义消息函数WindowProc里面:
If uMsg = 258 Then uMsg 258,键盘按下消息
Select Case wParam
Case 97 To 102 小写a-f
bPass = True
wParam = wParam - 32 小写强行显示为大写
Case 48 To 57 按键0-9
bPass = True
Case 65 To 70 按键大写A-F
bPass = True
Case 8 按键BackSpace
bPass = True
End Select
Else
bPass = True
End If
Form1.Caption = lpPrevWndProc
If bPass = True Then WindowProc1 = CallWindowProc(lpPrevWndProc, hwnd, uMsg, wParam, lParam)
这里面就没有包含什么特殊的东西了,uMsg是消息代码,按下按键,系统产生一条258的消息,并且通过wParam来传递是按下了什么键。一个简单的Select判断,布尔变量bPass决定最终什么时候我们放行,很简单的从以上代码看出,当uMsg不等于258(也就是非按下键盘的时候),或者uMsg=258但参数为:0-9、 A-F、 a-f的按键我们放行。其中a-f我们还强制显示为A-F(ASCII码减32)。
最后一行,CallWindowProc(lpPrevWndProc, hwnd, uMsg, wParam, lParam),在理解了上面的信息后,这行很容易理解,那就是我们只对我们感兴趣的消息进行过滤,关于Text1的大部分消息,即bPass我们放过他们,交由CallWindowsProc去处理(按Windows默认处理)。参数lpPrevWndProc,又见面了,它就是最开始我们定义的那个地址,即我们自定义回调函数的地址。
总结:
1. 回调函数是一种被调用者回头调用调用者的函数。
2. Windows是一种以消息机制为基础的OS。
3. Windows的消息机制以回调函数方式表现的,窗口程序先向Windows进行函数注册,当发生一些情况的时候(比如按键),Windows系统反过来调用窗口的函数。
4. 我们可以通过SetWindowLong来构造自己的消息回调函数。
5. 在自定义的消息回调函数里,我们可以拦截、筛选、处理或恢复系统消息。
我晕,总算写完了,回调函数是一种很有用的东西,以前一直在用,但在一些细节上一直有点晕,这篇东西搞不好还有错漏之处,还望指出,谢谢!
这个太深了
Being a devil is the same as being a human. We should be kind. If we are kind, we are not a devil, we are a hybrid.
举一个例子,
写一个大文件到硬盘,你的主模块调用存盘这个子模块,
现在开始调用,你就要一直等子模块运行完毕(也就是存盘完毕)之后你才能接着干别的事情,这种调用称为同步调用,显然很费时间。
现在我们这样做,我调用我的存盘子模块的同时,将主模块的地址同时告诉子模块,
现在开始调用,你无需等待子模块运行完毕之后才可以干别的事情,你现在就可以去执行别的操作了,当存盘完毕后,因为子模块事先就知道你主程序小样的地址,所以他可以自动呼叫主程序“我存完了!”,感觉一下这个过程,很像VB里的“事件”对吧??这种反过来的调用其实就是递归调用的一种形式,跟同步调用不同,这种调用称为异步调用,而回调函数是完成异步调用的基础。
Windows作为多任务操作系统,是一定要采用异步调用的,否则许多程序一起运行,系统就会卡在某个程序里出不来(就像DOS一样,某一时刻只能运行一个程序),当然Windows的多任务系统并不是这么简单,其中还有优先级等等概念,但大体上而言:Windows是一种以异步调用为基础的,以回调函数构造并以消息方式表达的多任务操作系统。
不知道这个现在还能不能拦住QQ的键盘记录(输入密码那一刻),以前是可以的,不过好想很久以前QQ就有反拦截的版本了。不知道你们注意没有,以前的QQ,输入完帐号之后,用TAB可以切换到密码,现在的需要按鼠标指定到密码框之后才能输。