ATL对WINDOWS窗口的封装
ATL包装了WINAPI中与创建和操作“窗口、对话框以及WINDOWS控制”有关的部分。ATL窗口类还包含了诸如子类化和超类化这样的高级特性。
一、Windows应用程序的结构
入口点——_tWinMain,它提供应用程序的HINSTANCE、命令行参数和指示如何显示主窗口的标志。
调用RegisterClass注册主窗口类。
调用CreateWindow创建主窗口。
调用ShowWindow和UpdateWindow来显示主窗口。
一个分发消息的消息循环。
一个处理主窗口消息的过程。
一组消息处理函数,用来处理窗口感兴趣的消息。
调用DefWindowProc让Windows处理我们不感兴趣的消息。
一旦主窗口被销毁,调用PostQuitMessage。
ATL 把以上的过程调用封装成一个对象。它包含窗口类(用WNDCLASSEX结构来表示)、窗口对象(用HWND来表示)和成员函数调用(通过对 WndProc的调用表示)。ATL提供了一组窗口类,CWindow、CWindowImpl、CWinTraits、CWinTraitsOR、 CDialogImpl、CSimpleDialog和CContainedWindowT。以及CWindowImplRoot、 CWindowImplBaseT和CDialogImplBaseT等辅助类。
二、CWindow类
1、封装HWND及相关的一些API函数
(1)Cwindow类包含了公共成员HWND hWnd。
(2)CWindow类封装了User32 API函数。
2、CWindow类的使用
//Needed to use ATL windowing classes
#include <atlbase.h>
extern CComModule _Module;
#include <atlwin.h>
ATL窗口类定义在atlwin.h中,依赖于atlbase.h。
_Module实例保存了应用的HINSTANCE实例以及ATL模块的初始化和终结。
_Module.Init(0,hinst); //初始化ATL模块
CWindow win; //对象构造
win.Create( “button”, NULL, CWindow::rcDefault, “Click me”,WS_CHILD ); //窗口创建
_Module.Term(); //终结ATL模块
CWindows在窗口创建时就决定了窗口收到消息后的处理方法,无法改变。从CWindows类派生出来的CWindowsImpl类提供指定窗口的新行为。通过给窗口类添加消息映射达到了添加新行为的方法。扩展或改变一个窗口实例的行为的方法:
(1)写一个新的ATL窗口类,并子类化这个已经存在的实例。
(2)使这个实例成为被包含的窗口,所有到这个实例的消息都会通过容器窗口。
(3)采用消息反映射,当一个窗口收到消息后不做处理,而反射给发送这个消息的窗口自己处理,这种技术可用于创建自包含的控件。
三、CWindowImpl类
CWindowImpl类从CWindow派生出来的,并且提供了窗口类注册和消息处理两个特性。
1、窗口类注册
在Create成员函数里注册了窗口类。
(1)窗口信息
CWndClassInfo结构:
struct _ATL_WNDCLASSINFOA
{
WNDCLASSEXA m_wc;
LPCSTR m_lpszOrigName;
WNDPROC pWndProc;
LPCSTR m_lpszCursorID;
BOOL m_bSystemCursor;
ATOM m_atom;
CHAR m_szAutoName[32];
ATOM Register(WNDPROC* p)
{
return AtlModuleRegisterWndClassInfoA
(&_Module, this, p);
}
};
m_wc表示窗口类的结构,在手工注册类时,用它来进行注册。
m_atom用于确定类是否被注册。
DECLARE_WND_CLASS宏定义函数GetWndClassInfo()获得一个CWndClassInfo实例。
DECLARE_WND_CLASS_EX宏提供了但对DECLARE_WND_CLASS宏的扩展。
这两个宏对窗口类的信息都提供了默认值,要修改其他的值,可以用函数GetWndClassInfo函数修改CWndClassInfo结构。
(2)窗口特性(Window Traits)
CWinTraits类保存了一个风格(style)和扩展风格(extended style)
(3)窗口过程(Window Procedure)
每个窗口都有一个窗口过程处理窗口消息。这个窗口过程是在窗口注册期间设置 WNDCLASSEX结构的lpfnWndProc成员。DECLARE_WND_CLASS和DECLARE_WND_CLASS_EX宏中用 StartWindowProc函数设置lpnWndProc成员。StartWindowProc函数完成了HWND和对象的this指针之间的映射。
在CreateWindow[Ex]时之前通过_Module.AddCreateWndData(&m_thunk.cd, this);将对象的this指针存在_Module维护的列表中。在调用窗口过程的时候在StartWindowProc中调用 _Module.ExtractCreateWndData()获得this指针。
ATL采用一组动态创建的汇编指令thunk实现了对this指针的存储和查找。每个CWindowImpl对象有自己的thunk,thunk就是窗口的过程。thunk保存在_WndProcThunk结果里面。
struct _WndProcThunk
{
DWORD m_mov; // mov dword ptr
[esp+0x4], pThis (esp+0×4 is hWnd)
DWORD m_this; //
BYTE m_jmp; // jmp WndProc
DWORD m_relproc; // relative jmp
};
在StartWindowProc函数里,通过pThis->m_thunk.Init(pThis->GetWindowProc(), pThis);将窗口的this指针和窗口消息处理函数WindowProc初始化到thunk静态结构里。
Windows消息路由路径:
Windows — WM_XXX –> CWindowImpl派生类的对象的thunk — 函数调用 –> CWindowImpl的静态成员函数WindowProc — 成员函数调用 –> CWindowImpl派生类的ProcessWindowMessage
(4)windows超类化
声明一个窗口类并且创建这个类的一个实例。超类化是“复制已有窗口类的WNDCLASSEX结构并且赋予它自己的名字和自己的WndProc”。窗口收到一条消息后,消息被路由到新的WndProc,如果新的WndProc不处理该消息,消息将路由到原来的WndProc。
2、消息处理
ATL的消息处理通过CWindowImpl派生类提供的ProcessWindowMessage函数分发处理各个类型的消息。ATL定义了一些宏便于组合处理消息链。
#define BEGIN_MSG_MAP(theClass) \
public: \
BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam, LRESULT& lResult, DWORD
dwMsgMapID = 0) \
{ \
BOOL bHandled = TRUE; \
hWnd; \
uMsg; \
wParam; \
lParam; \
lResult; \
bHandled; \
switch(dwMsgMapID) \
{ \
case 0:
#define END_MSG_MAP() \
break; \
default: \
ATLTRACE2(atlTraceWindowing, 0,
_T(“Invalid message map ID (%i)\n”), dwMsgMapID); \
ATLASSERT(FALSE); \
break; \
} \
return FALSE; \
}
通过BEGIN_MSG_MAP与END_MSG_MAP定义了消息链的框架。BOOL bHandled实现了一个是否这条消息还需要后续处理的标记。作为消息处理函数的参数,如果需要后续处理将该值设为false。
BEGIN_MSG_MAP( CBase )
MESSAGE_HANDLER( WM_CREATE, OnCreate1 )
MESSAGE_HANDLER( WM_PAINT, OnPaint1 )
ALT_MSG_MAP( 100 )
MESSAGE_HANDLER( WM_CREATE, OnCreate2 )
MESSAGE_HANDLER( WM_PAINT, OnPaint2 )
ALT_MSG_MAP( 101)
MESSAGE_HANDLER( WM_CREATE, OnCreate3 )
MESSAGE_HANDLER( WM_PAINT, OnPaint3 )
END_MSG_MAP()
如上,基类的消息映射表由3节组成:一个默认的消息映射表(隐含的标识为0)和两个可选的消息映射表(标识为100和101)。
如果我们在多个类中都要响应WM_CREATE消息,但是不同的类需要基类提供不同的处理,怎么办呢?为了解决这个问题,ATL使用了可选的消息映射:将消息映射表分成很多节,每一节用不同的数字标识,每一节都是一个可选的消息映射表。
当你链接消息映射表时,采用以下的方案的标识,如下:
class CDerived: public CBase {
BEGIN_MSG_MAP( CDerived )
CHAIN_MSG_MAP_ALT( CBase, 100 )
END_MSG_MAP()
…
}
CDerived的消息传入CBase后会被100的消息表内的函数处理。
CHAIN_MSG_MAP( CBase ) 可以把没被处理的消息从被派生的类传入基类CBase。
CHAIN_MSG_MAP_ALT( CBase, 100 ) 可以把没被处理的消息从被派生的类传入基类CBase,由基类内消息标识为100的处理函数处理。
四、CDialogImpl类
1、 模式
对话框有两种模式:有模式和无模式。
有模式:当对话框可见时,父窗口不可访问。
无模式:父窗口在对话框可见期间仍然可以被访问。
2、交换数据
有模式对话框的交换数据的操作:
(1)应用程序创建一个CDialogImpl派生类的实例。
(2)应用程序向对话框对象的数据成员复制一些数据。
(3)应用程序调用DoModal。
(4)对话框处理WM_INITDIALOG,把数据成员复制到子控制中。
(5)对话框处理OK按钮时会确认子控制保存的数据的有效性。如果数据无效,那么对话框向用户报错并且让用户继续尝试,直至使之正确或者用户点击Cancel取消。
(6)如果数据是有效的,那么数据被复制回到对话框的数据成员,并且终止对话框。
(7)如果应用程序从DoModal得到了IDOK,应用把对话框的数据成员复制到自己的拷贝中。
无模式对话框的交互数据操作:
(1)应用程序创建一个CDialogImpl派生类的实例。
(2)应用程序向对话框对象的数据成员复制一些数据。
(3)应用程序调用Create。
(4)对话框处理WM_INITDIALOG,把数据成员复制到子控制中。
(5)对话框处理Apply按钮时会确认子控制保存的数据的有效性。如果数据是无效,那么对话框向用户报错并且让用户继续尝试,直至使之正确或者用户点击Cancel取消。
(6)如果数据是有效的,那么数据被复制回到对话框的数据成员,并通知应用从对话框读取更新后的数据。
(7)应用程序接到通知时,应用把对话框的数据成员复制到自己的拷贝中。
五、CContainedWindow类
CContainedWindow 的对象让它的父窗口处理消息,让一个已有的窗口类处理父窗口传递的消息。父窗口可以创建一个子窗口类的实例,然后把它子类化,子窗口的消息会通过父窗口的 消息映射表路由。父窗口通过替代消息映射表辨别来自子窗口和自己的消息,每个CContainedWindow分配一个消息映射ID,并且它的消息被路由 到父窗口消息映射表的相应替代部分。
(1)创建被包含的窗口
调用CContainedWindow的Create成员创建被包含的窗口。
(2)子类化被包含的窗口
子类化不创建新的窗口,它仅仅是截取单个窗口的消息。我们创建某个类的一个窗口,并且使用SetWindowLong(GWL_INITDIALOG)把原来的窗口过程替换成我们自己的窗口过程。