天天看点

《Windows 程序设计(第3版)》——6.3 创建窗口

本节书摘来自异步社区《windows 程序设计(第3版)》一书中的第6章,第6.3节,作者:王艳平 , 张铮著,更多章节内容可以访问云栖社区“异步社区”公众号查看

6.3.1 窗口函数

windows为每个窗口都提供了默认的消息处理函数,自定义类的窗口的默认消息处理函数是defwindowproc,各子窗口控件(见7.1节)的类名是windows预定义的,其窗口函数自然由windows提供。

我们的框架也提供了一个通用的消息处理函数afxwndproc。为了响应窗口消息,必须让windows把窗口的消息处理函数的地址全设为afxwndproc,在处理消息时由我们自己决定是否调用默认的消息处理函数。改变窗口消息处理函数地址最简单的办法是使用setwindowlong函数。比如,下面代码会将句柄为hwnd的窗口的窗口函数地址设为afxwndproc,并将原来的地址保存在oldwndproc变量中。

gwl_wndproc标记指示了此次调用的目的是设置窗口函数的地址,新的地址由第3个参数afxwndproc指明。调用成功后,setwindowlong返回原来窗口函数的地址。以这个地址为参数调用callwindowproc函数就相当于对消息做了默认处理。

<code>::callwindowproc(oldwndproc, hwnd, message, wparam, lparam);</code>

以前在注册窗口类的时候,系统都将一个自定义的函数wndproc的地址传给wndclass或wndclassex结构,然后在wndproc函数里处理windows发来的消息。但是,以这种方式创建出来的窗口和标准的子窗口控件有一个明显的区别,就是其窗口函数不是由windows系统提供的。为了消除这种区别,在注册窗口类时可以直接让api函数defwindowproc作为窗口函数响应windows消息,如下面代码所示。

这样一来,消息都会被直接发送到默认的消息处理函数,各种窗口处理消息的方式都相同了,我们的框架程序可以使用setwindowlong和callwindowproc两个函数对待所有的窗口。

6.3.2 注册窗口类

根据窗口的不同用途,框架程序要为它们注册不同的窗口类,为了进行试验,这里只把它们分成两类(虽然还可以分得更细),子窗口使用的窗口类和框架或视图窗口使用的窗口类。这两种类的类名分别是wnd和frameorview,其类型标志被定义为afx_wnd_reg和afx_wnd frameorview_reg。

上面的代码定义了两种类型的窗口类使用的标志和类名,如果想添加新的类型,按这种方式继续添加代码就行了。

自定义函数afxenddeferregisterclass实现了为框架程序注册窗口类的功能,函数唯一的参数是类型标志,指明要注册什么类型的窗口类,具体的代码如下。

由afxenddeferregisterclass函数注册的窗口类使用的窗口函数都是默认的消息处理函数defwindowproc,两种不同类型的窗口类使用的类的风格、类名或背景刷子等参数不完全相同。最终的注册工作由afxregisterclass函数来完成。afxenddeferregisterclass函数的用法十分简单,比如下面语句为创建框架窗口注册了窗口类。

afxregisterclass函数是对api函数registerclass的扩展。它先调用getclassinfo函数试图查看要注册的类的信息,如果查看成功就不注册了,仅返回true。

afxenddeferregisterclass函数只能作为类库内部调用的一个函数来使用,下面再提供一个更通用的注册窗口类的函数。

_afx_thread_state结构的成员m_sztempclassname[96]的作用是保存当前线程注册的窗口类。后面的例子程序基本都要使用这个函数注册窗口类。在_afxwin.h文件中有如下这样几个函数的声明。

6.3.3 消息钩子

现在,框架程序创建的窗口的窗口函数都是windows提供的默认的消息处理函数,不管在创建的过程中使用的是自定义的窗口类,还是使用系统预定义的窗口类,为了使框架程序提供的函数afxwndproc获得消息的处理权,必须调用setwindowlong将窗口函数的地址设为afxwndproc函数的地址。可是应该在什么时候调用此函数呢?

这个问题并不像想象的那么简单。调用createwindowex的时候,窗口函数就开始接受消息。也就是说,在createwindowex返回窗口句柄之前窗口函数已经开始处理消息了,这些消息有wm_getminmaxinfo、wm_nccreate和wm_create等。所以等到createwindowex返回的时候再调用setwindowlong函数就已经晚了,漏掉了许多的消息。

那么,有没有办法让系统在正要创建窗口的时候通知应用程序呢?这样的话,就可以在窗口函数接受到任何消息之前有机会改变窗口函数的地址。使用钩子函数能够实现这一设想。

在windows的消息处理机制中,应用程序可以通过安装钩子函数监视系统中消息的传输。在特定的消息到达目的窗口之前,钩子函数就可以将它们截获。这种机制的实现原理第9章有专门介绍。但钩子函数的使用方法是比较简单的,例如,下面一条语句就给当前线程安装了一个类型为wh_cbt的钩子,其钩子函数的地址为hookproc。

系统在发生下列事件之前激活wh_cbt类型的钩子,调用自定义钩子函数hookproc通知应用程序:

创建、销毁、激活、最大化、最小化、移动或者改变窗口的大小;

完成系统命令;

将鼠标或键盘消息移出消息队列;

设置输入输出焦点;

同步系统消息队列。

hookproc是一个自定义的回调函数,和窗口函数wndproc一样,其函数名称可以是任意的。

<code>lresult callback cbtproc(int ncode, wparam wparam, lparam lparam);</code>

ncode参数指示了钩子函数应该如何处理这条消息,如果它的值小于0,钩子函数必须将消息传给callnexthookex函数。此参数的取值可以是hcbt_createwnd、hcbt_activate等,从字面也可以看出,它们分别对应着窗口的创建、窗口的激活等消息。当窗口将要被创建的时候,ncode的取值是hcbt_createwnd,此时,wparam参数指定了新建窗口的句柄,lparam参数是cbt_createwnd类型的指针,包含了新建窗口的坐标位置和大小等信息。

setwindowshookex函数的返回值是钩子句柄hhook。callnexthookex函数的第一个参数就是此钩子句柄。此函数的作用是调用钩子队列中的下一个钩子。

`lresult callnexthookex(hhook hhook, int ncode, wparam wparam, lparam lparam);

在不使用钩子的时候还应该以此句柄为参数,调用unhookwindowshookex函数将钩子释放掉。

有了这些知识,我们很容易会想到,在创建窗口之前先安装一个wh_cbt类型的钩子就有机会改变窗口函数的地址了。下面介绍这一过程的具体实现。

在改变窗口函数地址的时候,必须将此窗口原来的窗口函数的地址保存下来以便对消息做默认处理。窗口函数的地址是窗口的一个属性,所以再在cwnd类中添加一个wndproc类型的成员变量m_pfnsuper,并添加一个虚函数getsuperwndprocaddr返回默认的消息处理函数的地址。默认处理时,只要以m_pfnsuper或getsuperwndprocaddr函数返回的指针所指向的函数为参数调用callwindowproc函数即可,下面是相关的代码。

函数的实现代码在wincore.cpp文件中。

在类的构造函数中应该把成员m_pfnsuper的值初始化为null。cwnd的派生类有可能重载虚函数getsuperwndprocaddr,所以成员函数defwindowproc发现m_pfnsuper是null后还会去检查getsuperwndprocaddr的返回值,如果能够得到一个有效的函数地址就将消息传给此函数,否则调用api函数defwindowproc。

类的友元函数_afxcbtfilterhook就是要安装的过滤消息的钩子函数。框架程序要在这个函数里改变窗口函数的地址。它将原来窗口函数的地址保存在cwnd类的m_pfnsuper成员中。而getsuperwndprocaddr成员的保护类型是“protected”,所以要将_afxcbtfilterhook声明为cwnd类的友元函数。

假设cwnd类提供的创建窗口的函数的名称为createex,现在模拟用户创建窗口的过程。创建窗口的代码如下。

createex函数先安装wh_cbt类型的钩子,然后调用api函数createwindowex创建窗口。

但是,在写_afxcbtfilterhook函数的实现代码的时候会遇到如下两个问题:

(1)如何获得调用callnexthookex函数时所需的钩子句柄。

(2)在改变窗口函数的地址之前,必须首先让此窗口的窗口句柄hwnd与mywnd对象关联起来,即执行代码“mywnd. attach(hwnd)”。只有这样,框架程序的窗口函数afxwndproc才能将接收到的消息传给正确的cwnd对象。可是,在_afxcbtfilterhook函数中,如何知道正在创建的窗口的cwnd对象的地址呢?

这都是关于传递变量的值的问题,一个是钩子句柄的值,另一个是正在初始化的cwnd对象的指针的值。因为这些变量是线程局部有效的,所以只要在表示线程状态的类中添加相关变量即可。

安装钩子的时候设置这两个成员的值,在钩子函数中再访问它们就行了。下面是框架程序为创建窗口提供的安装钩子和卸载钩子的函数。

因为钩子函数在改变窗口函数的地址以后会将pthreadstate-&gt;m_pwndinit的值初始化为null,所以通过检查此成员的值就可以知道钩子是否被正确安装。下面是实现钩子函数_afxcbtfilterhook所需的代码。

下面是createex函数最基本的实现代码。

因为程序为当前线程安装了wh_cbt类型的钩子,所以在有任何windows消息发送到窗口函数前,钩子函数会首先接收到hcbt_createwnd通知。在这个时候将窗口函数的地址设为afxwndproc最合适了。在保存原来的窗口函数的过程中,程序没有直接访问m_pfnsuper变量,而是通过语句“pwndinit-&gt;getsuperwndprocaddr”得到此变量的地址,然后将原来的窗口函数的地址保存到此变量中。

<code>*poldwndproc = oldwndproc;</code>

getsuperwndprocaddr返回m_pfnsuper变量的地址仅仅是cwnd类的默认实现,如果cwnd类的派生类重载了虚函数getsuperwndprocaddr,结果就可能不一样了。

6.3.4 最终实现

至此,完全可以写出createex函数完整的实现代码了。注册窗口类、安装钩子、创建窗口、子类化窗口等全都会出现在这个函数里,下面是在_afxwin.h文件中添加的代码。

这些函数的实现代码如下。

cwnd类提供了create和createex两个创建窗口的函数。前一个是虚函数,这说明cwnd类的派生类可以重载此函数以创建不同的窗口;后一个函数createex实现了实际创建窗口的代码。cwnd类默认的行为是创建不具有ws_popup风格的子窗口。

在创建窗口前,框架程序首先调用虚函数precreatewindow,给用户修改创建参数的机会。此函数默认的实现仅仅对窗口类的类名cs.lpszclass感兴趣,发现这个值为null后会调用函数afxenddeferregisterclass进行注册。cwnd类的派生类往往重载此函数注册合适自己的窗口类,也可以改变cs对象中其他成员的值,比如窗口风格等。

createex在不能完成创建任务的时候会调用虚函数postncdestroy。另外在窗口销毁的时候,框架程序会再次调用此函数,所以用户可以重载这个函数做一些清理工作,如销毁cwnd对象等。

总之,创建窗口的时候只要先实例化一个cwnd类(或其派生类)的对象,然后调用成员函数create或createex即可。一般从cwnd派生的类都会重载虚函数create以创建特定类型的窗口,比如以后要讲述的cedit类、cdialog类等。

6.3.5 创建窗口的例子

本小节将把上述知识放在一起,使用框架程序创建第一个窗口。例子代码在配套光盘的06createexample工程下。

新建一个win32 application类型的工程06createexample,应用程序的种类选择an empty project。工程创建完毕以后,将common目录下所有的文件都添加到工程中。新建两个文件example.h和example.cpp,其中example.h文件包含了两个派生类的定义,example.cpp文件包含了这两个类的实现代码。

运行上面的代码,一个典型的窗口出现了,如图6.3所示。

《Windows 程序设计(第3版)》——6.3 创建窗口

cmywnd是cwnd的派生类,它重载了虚函数windowproc以处理afxwndproc发送给本cmywnd对象的消息。在窗口的整个生命周期,必须保证cmywnd对象没有被销毁,否则有关该窗口的消息谁来处理?所以,直到接收到最后一个消息wm_ncdestroy才可以删除cmywnd对象。

写这个小例子仅仅是为演示框架程序创建窗口的过程。创建cmywnd对象时发生的事情有:注册窗口类、安装钩子、创建窗口、子类化窗口、卸载钩子。这些事件完成以后,初始化窗口的工作也就完成了,接着initinstance函数调用showwindow和updatewindow函数显示更新窗口。

继续阅读