深入剖析WTL
1、 wtl对窗口的封装
2、 消息封装
3、 消息路由
一、Wtl对窗口的封装
一般我们使用WTL创建窗口的时候继承CWindowImpl和CDialogImpl两个基类来分别创建自己的窗口,关于不同的基类,创建窗口的方式不同
CWindowImpl 创建窗口
创建一个窗口分为以下几个步骤:
a) 注册一个窗口类
b) 根据窗口类创建窗口
c) 显示创建的窗口
在wtl中窗口类的设置以及注册是通过以下方式进行的:
设置
以下是一个宏定义,在CWindowImpl的开始部分会有这个宏定义,自己的窗口也可以使用这个来设置窗口的相关属性。
DECLARE_WND_CLASS(WndClassName)
DECLARE_WND_CLASS_EX(WndClassName,style, bkgnd)
#define DECLARE_WND_CLASS_EX(WndClassName,style, bkgnd) \
static ATL::CWndClassInfo&GetWndClassInfo() \
{ \
staticATL::CWndClassInfo wc = \
{\
{ sizeof(WNDCLASSEX), style, StartWindowProc, \
0, 0,NULL, NULL, NULL, (HBRUSH)(bkgnd + 1), NULL, WndClassName, NULL }, \
NULL,NULL, IDC_ARROW, TRUE, 0, _T("") \
};\
returnwc; \
}
其中,返回的是CWndClassInfo,是struct_ATL_WNDCLASSINFOW结构体的typedef定义,
_ATL_WNDCLASSINFOW的定义如下:
struct _ATL_WNDCLASSINFOW
{
WNDCLASSEXWm_wc;
LPCWSTRm_lpszOrigName;
WNDPROCpWndProc;
LPCWSTRm_lpszCursorID;
BOOLm_bSystemCursor;
ATOMm_atom;
WCHARm_szAutoName[5+sizeof(void*)*CHAR_BIT];
ATOMRegister(WNDPROC* p)
{
returnAtlWinModuleRegisterWndClassInfoW(&_AtlWinModule, &_AtlBaseModule,this, p);
}
};
第一个成员WNDCLASSEXW是注册窗口时使用的参数,上述红色部分是对这个结构体进行初始化。由于这个是静态函数,当自己的窗口定义了这个宏之后会使用自己的宏定义来注册窗口。
{ sizeof(WNDCLASSEX), style, StartWindowProc, \
0, 0, NULL, NULL, NULL, (HBRUSH)(bkgnd + 1),NULL, WndClassName, NULL },
可以看出最开始设置的窗口处理函数是StartWindowProc。在CWindowImplBase(这是CWindowImpl的基类)中定义了两个静态函数:
static LRESULTCALLBACK StartWindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
static LRESULT CALLBACK WindowProc(HWNDhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
说明:在StartWindowProc会使用thunk技术来设置保存当前窗口类对象的this指针。并且修改当前窗口的回调函数为WindowProc:
static LRESULT CALLBACKStartWindowProc(HWND hWnd, UINT uMsg,
WPARAMwParam, LPARAM lParam)
{
CContainedWindowT<TBase >* pThis = (CContainedWindowT< TBase>*)_AtlWinModule.ExtractCreateWndData();
ATLASSERT(pThis!= NULL);
if(!pThis)
{
return0;
}
pThis->m_hWnd= hWnd;
//Initialize the thunk. This was allocatedin CContainedWindowT::Create,
//so failure is unexpected here.
pThis->m_thunk.Init(WindowProc, pThis);
WNDPROCpProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd,GWLP_WNDPROC, (LONG_PTR)pProc);
#ifdef _DEBUG
//check if somebody has subclassed us already since we discard it
if(pOldProc!= StartWindowProc)
ATLTRACE(atlTraceWindowing,0, _T("Subclassing through a hook discarded.\n"));
#else
pOldProc; // avoid unused warning
#endif
returnpProc(hWnd, uMsg, wParam, lParam);
}
创建
HWND Create(HWND hWndParent, _U_RECT rect =NULL, LPCTSTR szWindowName = NULL,
DWORDdwStyle = 0, DWORD dwExStyle = 0,
_U_MENUorIDMenuOrID = 0U, LPVOID lpCreateParam = NULL)
{
if (T::GetWndClassInfo().m_lpszOrigName == NULL)
T::GetWndClassInfo().m_lpszOrigName =GetWndClassName();
ATOMatom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
dwStyle= T::GetWndStyle(dwStyle);
dwExStyle= T::GetWndExStyle(dwExStyle);
//set caption
if(szWindowName == NULL)
szWindowName =T::GetWndCaption();
returnCWindowImplBaseT< TBase, TWinTraits >::Create(hWndParent, rect,szWindowName,
dwStyle,dwExStyle, MenuOrID, atom, lpCreateParam);
}
红色部分正是调用使用DECLARE_WND_CLASS设置的窗口类信息,其调用方式是T::的方式,CWindowImpl声明如下:
template <class T, class TBase /* =CWindow */, class TWinTraits /* = CControlWinTraits */>
class ATL_NO_VTABLE CWindowImpl : publicCWindowImplBaseT< TBase, TWinTraits >
{
…
}
其中的T是在继承CWindowImpl实现窗口的时候传递的自己的窗口类。从前面可以知道,GetWndClassInfo是一个静态函数,并且子类的同名函数会隐藏父类的同名函数。
紫色部分内部使用DECLARE_WND_CLASS中CWndClassInfo中的WNDCLASS结构体进行注册窗口。紫色部分最后掉用到如下注释:
ATLINLINE ATLAPIINL_(ATOM)AtlWinModuleRegisterClassExA(_ATL_WIN_MODULE* pWinModule, const WNDCLASSEXA*lpwc)
{
if(pWinModule == NULL || lpwc == NULL)
return0;
ATOM atom = ::RegisterClassExW(lpwc);
BOOLbRet = pWinModule->m_rgWindowClassAtoms.Add(atom);
ATLASSERT(bRet);
(bRet);
returnatom;
}
其中的lpwc就是上述的WNDCLASS
创建窗口
窗口的创建最终要调用系统的API来创建窗口,CreateWIndowEx。在CWindowImpl中使用Create来创建窗口,需要传递创建窗口的一些参数。
CWindowImpl
HWND Create(HWND hWndParent, _U_RECT rect =NULL, LPCTSTR szWindowName = NULL,
DWORDdwStyle = 0, DWORD dwExStyle = 0,
_U_MENUorIDMenuOrID = 0U, LPVOID lpCreateParam = NULL)
{
if(T::GetWndClassInfo().m_lpszOrigName == NULL)
T::GetWndClassInfo().m_lpszOrigName= GetWndClassName();
ATOMatom = T::GetWndClassInfo().Register(&m_pfnSuperWindowProc);
dwStyle= T::GetWndStyle(dwStyle);
dwExStyle= T::GetWndExStyle(dwExStyle);
//set caption
if(szWindowName == NULL)
szWindowName= T::GetWndCaption();
return CWindowImplBaseT< TBase, TWinTraits>::Create(hWndParent, rect, szWindowName,
dwStyle, dwExStyle, MenuOrID, atom,lpCreateParam);
}
创建成功之后返回一个窗口句柄,最后调用到CWindowImplBaseT的Create函数。
template <class TBase, classTWinTraits>
HWND CWindowImplBaseT< TBase, TWinTraits>::Create(HWND hWndParent, _U_RECT rect, LPCTSTR szWindowName,
DWORD dwStyle, DWORD dwExStyle, _U_MENUorIDMenuOrID, ATOM atom, LPVOID lpCreateParam)
{
BOOL result;
ATLASSUME(m_hWnd== NULL);
// Allocate the thunk structure here, where we can fail gracefully.
result= m_thunk.Init(NULL,NULL);
if(result == FALSE) {
SetLastError(ERROR_OUTOFMEMORY);
returnNULL;
}
if(atom== 0)
returnNULL;
_AtlWinModule.AddCreateWndData(&m_thunk.cd,this);
if(MenuOrID.m_hMenu== NULL && (dwStyle & WS_CHILD))
MenuOrID.m_hMenu= (HMENU)(UINT_PTR)this;
if(rect.m_lpRect== NULL)
rect.m_lpRect= &TBase::rcDefault;
HWND hWnd = ::CreateWindowEx(dwExStyle, MAKEINTATOM(atom),szWindowName,
dwStyle, rect.m_lpRect->left,rect.m_lpRect->top, rect.m_lpRect->right - rect.m_lpRect->left,
rect.m_lpRect->bottom - rect.m_lpRect->top,hWndParent, MenuOrID.m_hMenu,
_AtlBaseModule.GetModuleInstance(),lpCreateParam);
ATLASSUME(m_hWnd== hWnd);
returnhWnd;
}
红色部分表示创建一个窗口。第二个参数使用的是RegisterClassEx的返回值,在MSDN上有一句话:
RegisterClass Function
If the functionsucceeds, the return value is a class atom that uniquely identifies the classbeing registered. This atom can only be used by the CreateWindow,CreateWindowEx
所以使用MAKEINTATOM(atom),因为在CreateWindowEx的介绍中有一句话The atom must be in the low-order word of lpClassName,所以使用MAKEINTAUTO这个宏,MAKEINTAUTO的相关信息如下MSDN介绍:
TheMAKEINTATOM macro converts the specified atom into a string, so it can bepassed to functions which accept either atoms or strings.
#defineMAKEINTATOM(i) (LPTSTR)((ULONG_PTR)((WORD)(i)))
到为止,注册了一个窗口类并且创建窗口成功。
二、消息封装
在Wtl中消息封装在一个atlapp.h中的CMessageLoop这个类中,其中最核心的成员函数是Run。执行流程是:首先看是不是空闲消息,是的话进去处理,从消息队列中取消息,哦按段是否成功,成功之后判断是不是WM_QUIT消息,因为WM_QUIT消息在MSDN的解释是:If the function retrieves the WM_QUIT message, the return value iszero。否则就处理
PreTranslateMessage,之后根据这个函数的返回值来判断是否需要进入窗口的消息处理。
int Run()
{
BOOLbDoIdle = TRUE;
intnIdleCount = 0;
BOOLbRet;
for(;;)
{
while(bDoIdle && !::PeekMessage(&m_msg, NULL, 0,0, PM_NOREMOVE))
{
if(!OnIdle(nIdleCount++))
bDoIdle = FALSE;
}
bRet = ::GetMessage(&m_msg, NULL, 0, 0);
if(bRet== -1)
{
ATLTRACE2(atlTraceUI,0, _T("::GetMessage returned -1 (error)\n"));
continue; // error, don't process
}
elseif(!bRet)
{
ATLTRACE2(atlTraceUI,0, _T("CMessageLoop::Run - exiting\n"));
break; // WM_QUIT, exit message loop
}
if(!PreTranslateMessage(&m_msg))
{
::TranslateMessage(&m_msg);
::DispatchMessage(&m_msg);
}
if(IsIdleMessage(&m_msg))
{
bDoIdle = TRUE;
nIdleCount = 0;
}
}
return(int)m_msg.wParam;
}
说明:
a) 进来之后首先PeekMessage,判断当前消息队列中是否有消息,有消息的话如果当前是空闲模式,则处理空闲消息,空闲消息是一个虚函数,所以会调用到真正实现这个函数的类方法中。
class CIdleHandler
{
public:
virtualBOOL OnIdle() = 0;
};
b) bRet = ::GetMessage(&m_msg, NULL, 0, 0);从消息队列中拿出消息,与PeekMessage不同的是GetMessage会从消息队列中将消息拿走。
GetMessage第个参数是用来获取MSG结构指针第 2个参数是个窗口句柄(HWND)用来获取指定窗口消息填NULL表示获取当前线程所有窗口消息或者线程消息(Thread message)最后两个参数是wMsgFilterMin和wMsgFilterMax用来获取指定消息当都填0则表示获取所有消息
c) 接下来的消息处理
if(!PreTranslateMessage(&m_msg))
{
::TranslateMessage(&m_msg);
::DispatchMessage(&m_msg);
}
PreTranslateMessage是一个虚函数, 类似于OnIdle一样,也是一个纯虚函数。接下来就是翻译消息与派发消息。
DispatchMessage将窗口消息交给注册窗口类时指定的窗口过程(WindowProc)来处理这个消息。
三、消息路由
消息路由就是窗口的消息处理过程。在每个窗口创建的时候都会实现虚函数ProcessWindowMessage,因为由最开始的图可以知道CWindowImpl的父类继承了CMessageMap,这个类的声明如下:
class ATL_NO_VTABLE CMessageMap
{
public:
virtual BOOL ProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAMwParam, LPARAM lParam,
LRESULT& lResult, DWORD dwMsgMapID) = 0;
};
可以看到ProcessWindowMessage是一个纯虚函数,并且在继承类中并没有去实现这个函数,所以我们自己定义的窗口必须去实现这个函数。待会介绍实现方法。
在设置窗口类的时候指定了窗口的默认处理函数,是StartWindowProc,当窗口创建成功之后发送的第一个消息是WM_NCCREATE,此时进来之后StartWindowProc会修改窗口处理函数为WindowProc。具体实现代码如下:
static LRESULT CALLBACK StartWindowProc(HWNDhWnd, UINT uMsg,
WPARAMwParam, LPARAM lParam)
{
CContainedWindowT<TBase >* pThis = (CContainedWindowT< TBase>*)_AtlWinModule.ExtractCreateWndData();
ATLASSERT(pThis!= NULL);
if(!pThis)
{
return0;
}
pThis->m_hWnd= hWnd;
//Initialize the thunk. This was allocatedin CContainedWindowT::Create,
//so failure is unexpected here.
pThis->m_thunk.Init(WindowProc,pThis);
WNDPROCpProc = pThis->m_thunk.GetWNDPROC();
WNDPROC pOldProc = (WNDPROC)::SetWindowLongPtr(hWnd, GWLP_WNDPROC,(LONG_PTR)pProc);
#ifdef _DEBUG
//check if somebody has subclassed us already since we discard it
if(pOldProc!= StartWindowProc)
ATLTRACE(atlTraceWindowing,0, _T("Subclassing through a hook discarded.\n"));
#else
pOldProc; // avoid unused warning
#endif
returnpProc(hWnd, uMsg, wParam, lParam);
}
红色部分就是修改窗口处理函数为WindowProc。之后的消息都由这个窗口函数去处理。
在WindowProc中的实现就是调用具体类的ProcessWindowMessage。
static LRESULT CALLBACK WindowProc(HWNDhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
CContainedWindowT<TBase >* pThis = (CContainedWindowT< TBase >*)hWnd;
......
BOOL bRet =pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd, uMsg, wParam,lParam, lRes, pThis->m_dwMsgMapID);
......
lRes = pThis->DefWindowProc(uMsg, wParam, lParam);
......
returnlRes;
}
根据ProcessWindowMessage的返回值,这个窗口过程会来判断是否需要调用窗口默认的处理函数,
LRESULTDefWindowProc(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
#ifdef STRICT
return ::CallWindowProc(m_pfnSuperWindowProc, m_hWnd, uMsg,wParam, lParam);
#else
return::CallWindowProc((FARPROC)m_pfnSuperWindowProc, m_hWnd, uMsg, wParam, lParam);
#endif
}
其中m_pfnSuperWindowProc在CWindowImplBaseT中初始化的时候指定为:
CWindowImplBaseT() : m_pfnSuperWindowProc(::DefWindowProc);
DefWindowProc是系统的默认消息处理函数。创建普通的窗口时不会修改指针所指向的内容,只有超类化和子类化的情况下才会去更改m_pfnSuperWindowProc中窗口函数。
CContainedWindowT<TBase >* pThis = (CContainedWindowT< TBase >*)hWnd;将窗口句柄转换成类的this指针,这个适用的是thunk技术,即同辉汇编代码来修改当前函数的第一个参数,将其存放当前类的this指针。
在自己创建的窗口在继承CWindowImpl之后,WTL中使用消息映射的方式来实现之前提到的纯虚函数。
BEGIN_MSG_MAP(KSimple)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
END_MSG_MAP()
查看相应的宏定义之后会发现:
#define BEGIN_MSG_MAP(theClass) \
public: \
BOOLProcessWindowMessage(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,LRESULT& lResult, DWORD dwMsgMapID = 0) \
{\
BOOL bHandled = TRUE; \
(hWnd);\
(uMsg);\
(wParam);\
(lParam);\
(lResult);\
(bHandled);\
switch(dwMsgMapID)\
{\
case0:
BEGIN_MSG_MAP正是实现了ProcessWidnowsMessage这个纯虚函数。其中有一个局部变量bHandled,这个标志用来表示当前消息是否需要传递下去,
TRUE:表示这个消息我已经处理了,不需要上一级或者默认消息处理函数来处理这个消息。
FALSE:表示这个消息还需要上一级或者默认消息处理函数来处理。
#define MESSAGE_HANDLER(msg, func) \
if(uMsg== msg) \
{\
bHandled= TRUE; \
lResult= func(uMsg, wParam, lParam, bHandled); \
if(bHandled) \
return TRUE; \
}
MESSAGE_HANDLER是其中的一个类型的消息,可以发现有一个bHandled = TRUE默认是TRUE,表示这个消息不需要路由到上一级或者默认消息处理中。红色部分表示为TRUE则直接return TRUE,否则,由END_MSG_MAP的定义可知:return FALSE,这样再拿出WindowProc的处理:
static LRESULT CALLBACK WindowProc(HWNDhWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
......
BOOL bRet = pThis->m_pObject->ProcessWindowMessage(pThis->m_hWnd,uMsg, wParam, lParam, lRes, pThis->m_dwMsgMapID);
//restore saved value for the current message
ATLASSERT(pThis->m_pCurrentMsg== &msg);
pThis->m_pCurrentMsg= pOldMsg;
//do the default processing if message was not handled
if(!bRet)
{
if(uMsg != WM_NCDESTROY)
lRes =pThis->DefWindowProc(uMsg, wParam, lParam);
else
{
// unsubclass, if needed
LONG_PTR pfnWndProc = ::GetWindowLongPtr(pThis->m_hWnd, GWLP_WNDPROC);
lRes= pThis->DefWindowProc(uMsg, wParam, lParam);
if(pThis->m_pfnSuperWindowProc!= ::DefWindowProc && ::GetWindowLongPtr(pThis->m_hWnd,GWLP_WNDPROC) == pfnWndProc)
::SetWindowLongPtr(pThis->m_hWnd,GWLP_WNDPROC, (LONG_PTR)pThis->m_pfnSuperWindowProc);
//clear out window handle
pThis->m_hWnd= NULL;
}
}
returnlRes;
}
如果bRet为TRUE,则什么也不做,否则进入DefWindowProc进而走系统的默认消息处理函数。这就是没有处理的消息都由系统统一去处理。
#define END_MSG_MAP() \
break;\
default:\
ATLTRACE(ATL::atlTraceWindowing,0, _T("Invalid message map ID (%i)\n"), dwMsgMapID); \
ATLASSERT(FALSE);\
break;\
}\
returnFALSE; \
}
这个是END_MSG_MAP的宏定义。
关于消息映射,在WTL中还有BEIGN_MSG_MAP_EX,定义在atlcrack.h文件中。其中将标准消息的处理可以使用两种宏来支持:
方式一:MESSAGE_HANDLER_EX
方式二:MSG_WM_XXX
CDialogImpl 的处理流程跟CWidnowImpl一样。