子类(Subclassing)是一种允许程序中途截获发往一个窗口的消息的一种技术。我们知道,Windows是基于消息的,因此,我们一旦截获了发往一个窗口的所有消息,就意味着我们能做一些通过常规方法无法做到的事。比如下面要讲到的,在窗口控制菜单上添加菜单项,并能有效地响应单击事件。
在Windows应用程序中,每个窗口(实际上是每类窗口)在创建后都有一个特殊的子程序——窗口程序(window procedure)来处理所有发往该窗口的所有消息。这个子程序如何响应消息也就决定了这个窗口如何动作。
我们要实现子类,要截获发给一个窗口的消息,就是要找到它的窗口程序,然后用我们自己的新窗口程序来替换,这样我们自己的新窗口程序就能接收到发给这个窗口的所有消息了!当然,我们也同时有了正确处理这些消息的责任。
在易语言中是看不到一个叫“_窗口1_窗口程序”的子程序的,易语言替我们实现并隐藏了这个细节。其实找出并替换它并不难,一个API就能搞定,它的声明是这样的:
Dll命令:置窗口特征
返回值类型:子程序指针
在Dll库中的命令名:SetWindowLongA
参数:窗口句柄 数据类型:整数型
参数:特征索引 数据类型:整数型
参数:新特征 数据类型:子程序指针 备注:仅易语言3.0支持
这个函数有3个参数,第一个就是要操作的窗口的句柄,易语言中用“窗口名称.取窗口句柄 ()”来得到。
第二个参数是特征索引,因为这个API除了能替换窗口的“窗口程序”外还能设置指定窗口的其他许多特征,所以就需要这个参数来指明我们要改变的是窗口的哪个特征。在这里用常量 #窗口特征_窗口程序(其值是-4)就可以了。
第三个参数很显然要指明改变后的新窗口程序是哪个,可以用易语言3.0的操作符“&”来取得。易语言3.0以前版本虽然也支持子程序指针,但其值和API要求的值不兼容,所以我们是无法用易语言3.0以前的任何一个版本来实现子类。我用的是《易语言3.0测试版二》估计测《试版一》和以后的版本也应该没问题。
这个API的返回值是原来窗口程序的指针,要保存在一个容器里(假设保存到了一个名为 默认窗口程序 的“子程序指针”类型的容器里),当我们想取消子类化,或要关闭程序时,就应重新调用该API,置回原来的窗口程序。保存这个值的意义还不仅在此……暂时保密!!:)
好了,有了API这把厉刃,加上易语言3.0的强大功能,在概念上应该是没有问题了吧?那就开始吧,打开易语言3.0……慢!先别急,上边所讲的哪个API的第三个参数从哪里获得?取哪个子程序的指针呢?对了,我们还要编写一个新窗口程序。这个窗口程序其实是一个普通的易语言子程序,可以直接用易语言的“插入”菜单来插入一个“新子程序”,只是它的参数个数、类型和返回值的类型都有一定的格式,否则系统是不会让它作为窗口程序来处理窗口消息的(就像易语言中的“__启动窗口_创建完毕”一样,只是这个子程序的名字没有具体规定)。下面是一个例子:
子程序:新窗口程序
返回值类型:整数型
备注:不要修改该子程序的返回值及参数的类型!
参数:窗口句柄 数据类型:整数型
参数:消息 数据类型:整数型
参数:参数1 数据类型:整数型
参数:参数2 数据类型:整数型
我在一开始就说过,“得到与付出是成正比的”,既然系统把所有的发给原窗口程序处理的消息都发给了我们这个“新窗口程序”,那我们也要在这个子程序里正确处理所有接收到的消息。想一想吧,一个窗口从创建开始到被销毁要接收多少消息呢?!@#¥%^&×……“完了,上当了!!”别灰心,我并不是要你写代码来处理所有这些成千上百的烦人的消息,我的建议是这样的:我们在接收到消息后,处理自己感兴趣的那些,然后把其他那些“垃圾”全交给原来的窗口程序来处理好了!怎么样?这个建议还可以接受吧?
好了,再看一个API:
Dll命令:执行窗口程序
返回值类型:整数型
在Dll库中的命令名:CallWindowProcA
参数:窗口程序程序指针 数据类型:子程序指针
参数:窗口句柄 数据类型:整数型
参数:消息 数据类型:整数型
参数:参数1 数据类型:整数型
参数:参数2 数据类型:整数型
和我们的“新窗口程序”的参数差不多!调用也简单得很。在我们刚才添加的“新窗口程序”里添上下面代码:(默认窗口程序 是 置窗口特征()的返回值)
返回 (执行窗口程序 (默认窗口程序, 窗口句柄, 消息, 参数1, 参数2))
*备注:把不需要处理的消息传递给默认窗口处理程序,使窗口能正常响应消息
这样我们把所有的消息原封不动地交给了它原来的“主人”,怎么样?放心了吧?也明白了为什么要保存 置窗口特征()的返回值了吧?现实中的做法通常是在“完璧归赵”之前或之后用“判断”语句来过滤出我们感兴趣的消息,进行处理,毕竟是我们费了这么一番周折才得到的消息,相信你绝对不会那么轻易地交回去的!:)
忠告:调试这个程序要有充分的思想准备,容易死机!最好把易语言的自动保存时间设为1分钟,运行前先点保存按钮不失为明智之举。
讲了那么多乏味的东西,到底学这个有什么用呢?我们来看一个实例,我们要实现在窗口的“控制菜单”的底部加上一个明为“易语言万岁!”的菜单项,怎么样?见没见过别的程序有类似的效果?想不想知道它是怎么实现的?来,打开易语言3.0我们开始喽……
第一步:编写自己的窗口程序
子程序:新窗口程序
返回值类型:整数型
备注:不要修改该子程序的返回值及参数的类型!
参数:窗口句柄 数据类型:整数型
参数:消息 数据类型:整数型
参数:参数1 数据类型:整数型
参数:参数2 数据类型:整数型
如果真 (消息 = 274 且 参数1 = 1982)
*备注:单击了控制菜单,并且菜单项的ID是我们设置的值
信息框 (“易语言万岁!吴涛是我们的民族英雄!!”, 0, “支持国产精品---《易语言》!”)
如果真结束
返回 (执行窗口程序 (默认窗口程序, 窗口句柄, 消息, 参数1, 参数2))
*备注:把不需要处理的消息传递给默认窗口处理程序,使窗口能正常响应消息
第二步:子类化及其恢复
子程序:__启动窗口_创建完毕
窗口句柄 = _启动窗口.取窗口句柄 ()
系统菜单 = 取系统菜单 (窗口句柄, 假)
默认窗口程序 = 置窗口特征 (窗口句柄, #窗口特征_窗口程序, &新窗口程序)
*备注:改变当前窗口的消息处理程序,即常说的“窗口子类化”
子程序:__启动窗口_将被销毁
局部容器:容器 数据类型:整数型
置窗口特征 (窗口句柄, -4, 默认窗口程序)
*备注:窗口销毁前置回原来的消息处理程序
第三步:插入菜单项:
子程序:_添加控制菜单项_被单击
局部容器:结果 数据类型:逻辑型
结果 = 添加菜单项 (系统菜单, 7, 位或 (#菜单函数_按位置, #类型_分隔线), 253, “”)
*备注:添加一条分隔线
结果 = 添加菜单项 (系统菜单, 8, #菜单函数_按位置, 1982, “易语言万岁!(&E)”)
*备注:ID可以自己设,但要注意不要和系统已有的菜单项目ID重复,以免冲突
检查 (结果)
信息框 (“ 只有想不到,没有做不到!” + #换行符 + #换行符 + “点窗口左上角的图标,看控制菜单的底部……”, #信息图标, “添加成功!”)
当然,我这里只是给出了程序的框架,要运行这个程序,有一些API要定义,比如“取系统菜单”、“添加菜单项”还有一些程序集容器要定义。你最好能到www.eyuyan.com的 《易语言用户论坛》上下载我的源程序,里面还有如何实现动态菜单的例子和详细备注。
(本应该写在最前面的)子类化的一条规则:只允许在同一进程内实行子类化,一个应用程序不能对属于其他进程的窗口实行子类化操作。
虽然这不是绝对的,但微软的工程师不推荐我们去子类化一个其他进程的窗口。这是因为在Win32中每一个的进程都拥有独立的地址空间,一个窗口的窗口程序在本进程中有一个特定的地址,但在其他进程中的同一地址里并不包含同样的窗口程序。强制替换的结果只有出错!
子类是一项很有用也很强大的技术,同时它的博大精深也让我肃然起敬,我这里提到的仅仅是子类的冰山一角(实例子类)还有诸如“全局子类”、“超类”等还有待我们去共同研究。建议参看MSDN中一篇题为《Safe Subclassing in Win32》的文章。
联系我:hyzs@sian.com
2003年4月1日