基础知识:Windows的消息驱动机制
子类化,对于各位高手来说easy到不得了了,不用我说,基本可以绕路了。但是对于一些新手来说确实很陌生的,基本上在网上也没有什么系统的资料。那么,我就趁着有空余的时间,将自己掌握的一些子类化的基本思路,概念介绍给大家。
要说子类化,就必须先简单介绍一下操作系统Windows的工作方式。打个比方吧:
“你和我需要交流,那么一种古老的办法就是写信。我将一封信件投递到你的邮箱,信件中包含这我要传达的信息,你打开邮箱,取出信,看了,再作出反应,或者是回信,这样我们就完成了一次交流。”
是不是觉得我说的很废话,“打开邮箱,取出信,看了”,需要说的那么仔细吗,鬼都知道。然而,将以上的工作方式转换到电脑上来,就成了一种高效的工作方式,Windows系统正是以这种方式工作的,我们称这种工作方式为“消息驱动”。上面写信的比喻落实到电脑上来,就变成了如下的工作方式:
“Windows系统发出一条消息,当然,当中可能附带着其他一些信息,投递到应用程序的消息处理函数,程序根据消息的不同,作出相应的反应。”
再举个例子吧,windows发了一条窗体大小更改消息,程序收到了,就可以调整相应的界面,保持协调。以上的这些,如果你学习过VC++,那么你就会很快反应过来,因为在VC++里,消息是要自己映射的和消息处理函数也是要自己定义的。
那么,回到VB的编程环境,不知你是否会感到奇怪,窗体的大小改变了,在VB里不是有ReSize事件吗,为什么要搞子类化呢?而且,前面说了Windows系统是消息驱动的,为什么到了VB里就变成了事件驱动呢?
其实,VB作为一门容易学习的语言,自然不会搞到那么复杂,消息处理函数不需要我们去写,VB已经提供了,但是VB提供的消息处理函数往往不能满足我们的需求。如果说,在VB6里,你需要在窗体移动后做出某些反应,你怎么知道窗体是否移动了呢?还有,我们经常使用飞轮(鼠标中键),但是VB6提供的事件里没有这个,你怎么办?
子类化原理:
那么好,我们就以上面的两个问题,作为我们探讨的方向,从中了解子类化的内容。
有了上面的介绍,你应该很容易看懂下面的子类化原题图:
上图中,红色的线条是程序一开始时的子类化设定步骤,黑色的线是设置了子类化之后的程序执行步骤,最后蓝色的线是程序的结束。实线是消息的传递过程,虚线是程序命令的传递过程。
那么,说完原理后,我们就开始说子类化的实现。首先介绍两个API函数:
Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long,ByVal nIndex As Long,ByVal dwNewLong As Long) As Long
Public 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
第一个函数,SetWindowLong是用来重设程序消息处理函数用的,尽管它的用途还很多,在这里只介绍他这个功能。第一个参数hwnd是窗体的句柄,因为不同的窗体就有不同的消息处理函数,尽管可以多个窗体的消息共用一个函数来处理,这将在后面提到。第二个参数nindex我们将它设置为GWL_WNDPROC=(-4&),这是个常数,尽管第二个参数可以是别的内容,但是在这个函数用作重设消息处理函数时,第二个参数就应该传入这个常数。第三个参数是新的消息处理函数的地址(如何在VB6中获取函数的地址,将在后面说,很简单的),而这个函数的返回值是旧的消息处理函数的地址(就是VB提供的那个),这个值一定要保存下来,以后会用到。
而第二个函数是将所有收到的消息重新发回给VB默认的消息处理函数的API函数,将所有收到的的消息重新发回给VB默认的消息处理函数,这是必须的,不然VB将失去对程序的控制,调试,终止都不能使用,VB的开发环境也将陷入崩溃。在CallWindowProc函数中,也有三个参数;第一个是需要接受当前发送消息的消息处理函数的地址,也就是VB提供的消息处理函数,这时候第一个函数返回的值就有用了;第二个参数是对象句柄,指明当前的消息是发给那个对象的,这在多个窗体共用一个消息处理函数时会用到;第三个是消息本身,是一个16进制常数;第四第五个参数是这个消息的附加内容。
这两个API函数说完了,剩下来的就要我们自己去编写一个消息处理函数。一个标准的消息处理函数如下:
Public Function SubWndProc(ByVal hwnd As Long,ByVal lParam As Long) As Long ' 向以前所有窗口过程发送信息(这是必须的) ' PrevWndProc是SetWindowLong的返回值,即VB提供的消息处理函数的地址 SubWndProc = CallWindowProc(PrevWndProc,hwnd,MSG,wParam,lParam) End Function
这个函数是我们自己定义的,放在一个模块里就行了。虽然上面函数名称是SubWndProc,但消息处理函数的名称并不固定使用这个名称,可以是别的,只是在下面的API函数调用的时候需要用回真实的消息处理函数的名称。这个函数有四个参数,第一个是hwnd,即对象句柄,用来指明当前的消息应用于那个对象;第二个参数Msg即消息本身,是一个16进制数,不同的数有不同的含义,在后面将给出一份Windows消息表的下载地址,大家可以借此参考;最后两个参数即是当前消息的附加内容,例如当前的消息是鼠标飞轮(鼠标中键)滚动消息,那么飞轮是向前还是向后滚动呢,就有另外的值放到这两个参数里(这些值虽然也是常数,但是目前没有人将其整理起来,所以要知道就只能发帖问或者百度了,很无奈-_-),以更精地描述飞轮滚动这个消息。
这个函数是在我们重设了消息处理函数之后由Windows系统调用的。函数的地址被作为某个API函数(甚至以后我们自己编写的函数)的参数传进去,然后再由其他程序来调用,这样的一种工作形式的函数称为“回调函数”。函数名称可以改,但是后面的参数就不要改了。
调用API函数重设程序的消息处理函数(请根据注释自行理解代码的内容):
现在,我们以对Form1进行子类化,实现检测窗口移动为例,说明如何调用API函数实现子类化。
【1】启动VB6.0中文企业版,选择“标准EXE”工程。
【2】选择“工程”菜单中的“添加模块”菜单项(看清楚了,不是添加“类模块”),将上面的API函数声明复制到类模块里,内容如下:
Public PrevWndProc As Long '存储旧的消息处理函数的地址 Public Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hwnd As Long,ByVal dwNewLong As Long) As Long ’声明设置子类化的API的函数 Public Declare Function CallWindowProc Lib "user32" Alias "CallWindowProcA" (ByVal lpPrevWndFunc As Long,ByVal lParam As Long) As Long '声明将消息送回VB默认的消息处理函数的API函数 Public Const WM_MOVE = &H3'移动消息的常数 Public Const GWL_WNDPROC = (-4&) '重设消息处理函数的常 Public Function SubWndProc(ByVal hwnd As Long,ByVal lParam As Long) As Long '消息的处理代码---------------------------------- If Msg = WM_MOVE Then MsgBox "窗口移动了" End If '--------------------------------------------- ' 向以前所有窗口过程发送信息(这是必须的) ' PrevWndProc是旧的消息处理函数的地址 SubWndProc = CallWindowProc(PrevWndProc,lParam) End Function
【3】打开Form1的代码窗口,在Form_Load()事件里输入如下代码:
PrevWndProc = SetWindowLong(Me.hwnd,GWL_WNDPROC,AddressOf SubWndProc) 'AddressOf运算符用于获取函数的地址
注:鉴于一些网友说无法理解AddressOf SubWndProc到底是怎么回事。那么这里简要说一下(当然,本人知识浅薄,这些内在的原理要是说错了,还请高手指出,各位见谅)。当一段代码被系统的PE加载器载入之后,系统会为其分配一段内存,以供这段代码使用。供代码使用包括了储存代码所产生的数据,也包括了执行这些代码所需要使用的内存资源。同样道理,当我们声明一个函数的时候,系统也会为它创建一个内存区域。这个内存区域里的内存不仅是我们声明的函数局部变量的变量值存储的地方,而且也供给函数代码运行所需的内存。也就是说,一个函数的所有东西都在这个内存块里了。而内存块有他的地址,换句话来说,这个内存的地址就可以代表这个函数,可以通过地址来调用函数。而AddressOf 就是取回函数的地址。因此,AddressOf SubWndProc是传入了SubWndProc函数的地址,而不是调用SubWndProc函数之后的返回值!!操作系统能够使用我们传入的地址,在有新的消息进入程序的消息队列的时候,就可以将这些消息作为参数,通过我们传进去地址调用我们的SubWndProc函数,让我们能够收到这些消息,这也就是子类化根本的原理。
【4】保存,运行。刚运行的时候就会出现一次“窗口移动了”的消息框,这是为什么呢?这只因为在窗口在创建之后会执行一次窗口移动,以使窗口的位置符合窗口的Left和Top属性的值。知道这个之后,我们单击“确定”关闭消息框,然后用鼠标再次去拖动窗体,那该消息框就会再次出现。运行效果及全部代码如下图所示:
注意:
1.在使用了子类化的程序中,如果不是调试子类化程序的那个部分,最好将设置子类化的那个API函数调用注释掉,以避免子类化后程序出错,导致整个编程环境崩溃。这也是我为什么在上面第四步强调要保存的原因。
2.如果在设置了子类化后,使用断点来调试,发现一直按F8都是在消息处理函数里面执行,想去按VB菜单栏上的“结束”按钮或是“运行”菜单的“结束”菜单项,而VB却没有任何反应,这时候应该按F5让程序自动执行下去,这时候就能按VB工具栏上的“结束”按钮了。
如果还遇到其他问题,可以加此Q:1838805008,本人可以在有时间的时候尝试为你解决问题。