Thursday, March 03, 2005

 

Some notes on MFC Message

1.
The author and source are unknown

用于封装消息的结构AFX_MSGMAP_ENTRY
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; // windows message
UINT nCode; // control code or WM_NOTIFY code
UINT nID; // control ID (or 0 for windows messages)
UINT nLastID; // used for entries specifying a range of control id's
UINT_PTR nSig; // signature type (action) or pointer to message #
AFX_PMSG pfn; // routine to call (or special value)
};
其中typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
构成消息映射标的结构AFX_MSGMAP
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap;
const AFX_MSGMAP_ENTRY* lpEntries;
};
这样一个AFX_MSGMAP对象就成了构建消息映射表的关键人物,它一只手拉着基类的AFX_MSGMAP对象,另一只手拉着类本身的消息映射表,这样只要正确地在每一个类中都安插一个AFX_MSGMAP对象,那么整个消息映射表就建立起来了。
那么,何为正确呢?含义有2:一是正确的设置pBaseMap,令它指向基类,二是正确的建立类自身的消息映射表。这两个工作是由4个宏完成的,
它们是:DECLARE_MEMSSAGE_MAP() / BEGIN_MESSAGE_MAP() / ON_COMMAND()(ON_COMMAND宏只是为了处理命令消息,对于其它的消息还有对应的宏,但是原理是相同的) / MESSAGE_MAP()

#define DECLARE_MESSAGE_MAP() private: static const AFX_MSGMAP_ENTRY _messageEntries[]; protected: static const AFX_MSGMAP messageMap; virtual const AFX_MSGMAP* GetMessageMap() const; 这个宏的作用有3:
在类中插入一个静态成员_messageEntries,这是用来存放类要处理的消息的数组(即类本身的消息映射表)
另一个静态成员massageMap用来指向基类的消息映射表
安插一个虚函数,其内容有待实现

接下来,_messageEntries的初始化,messageMap的正确指向,GetMessageMap函数的实现这些工作还都没做,那正是后三个宏的责任,它们要顺序使用,方能工作正常。

#define BEGIN_MESSAGE_MAP(theClass, baseClass) const AFX_MSGMAP* theClass::GetMessageMap() const { return &theClass::messageMap; } AFX_COMDAT const AFX_MSGMAP theClass::messageMap = { &baseClass::messageMap, &theClass::_messageEntries[0] }; AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = { 这个宏的作用有3:
定义了安插在类中的虚函数GetMessageMap(),只是简单的返回messageMap对象的地址
初始化messageMap,把派生类和基类联系起来构成一个大的消息映射表
为类本身的消息映射表的初始化做语法准备

ON_COMMAND这个宏的作用就是向_messageEntries数组中添加类本身要处理的命令消息,其实在MFC中还有很多更方便的宏可以向类中添加消息,例如OM_WM_PAINT等,这里,我们主要讨论ON_COMMAND,毕竟原理都是相同的。
#define ON_COMMAND(id, memberFxn) { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, static_cast (memberFxn) },
无非是对AFX_MSG_ENTRY结构的初始化,这样在类中为每一个想要处理的消息都是用一个ON_COMMAND宏,就自动的初始化了类本身的消息映射表。

最后,当全部的信息添加完毕后,使用END_MESSAGE_MAP()宏通知MFC一个类消息映射表结束了。
#define END_MESSAGE_MAP() {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } }; 实现手法单纯得很,无非是一个全0的AFX_MESSAGE_MAP对象。

2.
The author and source are unknown

MFC中消息分为3类:
WM_COMMAND:所有的UI组件和加速键都会产生这种消息,所有派生于CCmdTarget的类都有能力处理该消息
标准消息:除WM_COMMAND之外的WM_xx消息都是标准消息,派生于CWnd的类都有能力处理该消息
控件通知消息:用于子窗口控件向父窗口发送的消息

在MFC的消息映射表的建立中,通过一组宏,你就可以让自己的类先于父类处理某些windows消息,这种行为很像虚函数,只是我们重载的内容不是虚函数,而是消息。

推动消息的泵
第一阶段 窗口过程
在产生一个窗口的时候,会调用CFrameWnd::Create,所有的故事也都从这里展开。
下面的代码为了简洁,去掉了不相关的代码
BOOL CFrameWnd::Create(…) {
// …
if (!CreateEx(…)) {
// …
}
// …
}
BOOL CWnd::CreateEx(…) {
// …
AfxHookWindowCreate(this);
HWND hWnd = ::CreateWindowEx(…);
// …
}
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
// …
if (pThreadState->m_hHookOldCbtFilter == NULL)
{
pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT,
_AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
// …
}
// …
pThreadState->m_pWndInit = pWnd;
}
这样,通过AfxHookWindowCreate,在当前线程中安装了一个钩子,用来拦截和窗口相关的事件,每当:
另一个窗口成为active;
产生或摧毁一个窗口
Minimize或maximize一个窗口;
移动或缩放一个窗口;
完成一个来自系统菜单的命令;
从系统队列中取出一个消息;
时,都会先调用_AfxCbtFilterHook,接下来:
LRESULT CALLBACK
_AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{
// …
WNDPROC afxWndProc = AfxGetAfxWndProc();
oldWndProc = (WNDPROC)SetWindowLongPtr(hWnd, GWLP_WNDPROC,(DWORD_PTR)afxWndProc);
// …
}
WNDPROC AFXAPI AfxGetAfxWndProc()
{
// …
return &AfxWndProc;
}
这样,_AfxCbtFilterHook的工作总结起来就是通过窗口子类化,把新建的窗口的窗口过程设置成AfxWndProc。
到这里,我们终于找到了窗口过程,总结一下
CFrameWnd::Create创建窗口调用CWnd::CreateEx
CWnd::CreateEx调用AfxHookWindowCreate准备为窗口设置钩子
AfxHookWindowCreate调用::SetWindowHookEx为窗口设置了一个WH_CBT类型的钩子来过滤消息,并把过滤函数设置成_AfxCbtFilterHook
_AfxCbtFilterHook通过窗口子类化设置窗口的窗口过程为AfxWndProc
这样,通过::DispatchMessage发送给窗口的消息就会源源不断地送到AfxWndProc中来,可以想到,AfxWndProc利用MFC的消息映射表,分门别类的对消息进行分流。

消息的流动
直线上溯的消息
LRESULT CALLBACK
AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)
{
// …

// all other messages route through message map
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
// …
return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam);
}

LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,
WPARAM wParam = 0, LPARAM lParam = 0)
{
// …
// Catch exceptions thrown outside the scope of a callback
// in debug builds and warn the user.
LRESULT lResult;
// …
// delegate to object's WindowProc
lResult = pWnd->WindowProc(nMsg, wParam, lParam);
// …
return lResult;
}
最后,消息被传到了WindowProc中,这是一个CWnd类中的虚函数,在MFC中,重载次函数的类有很多,但是由于一般的窗口都派生于CWnd(例如CFrameWnd),所以,来看CWnd::WndProc
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
// OnWndMsg does most of the work, except for DefWindowProc call
LRESULT lResult = 0;
if (!OnWndMsg(message, wParam, lParam, &lResult))
lResult = DefWindowProc(message, wParam, lParam);
return lResult;
}
首先利用OnWndMsg处理消息,如果OnWndMsg没能处理消息,调用DefWindowProc。先来看OnWndMsg,这是一个CWnd类的虚函数,这个函数的逻辑很简单,如果消息是WM_COMMAND或WM_NOTIFY,则把消息分别交给OnCommand和OnNotify处理,否则首先在MFC内建的消息缓存中查找消息,如果命中但没有相应的处理函数,则返回FALSE(这样的话会交由CWnd::DefWindowProc处理),如果命中,则进一步判断是用户自己注册的消息还是标准Windows消息,如果是前者,就跳到标签LDispatchRegistered处理,调用相应的消息处理函数,否则就跳到LDispatch处理,调用正确的消息处理函数。
如果消息不在缓存中,那么就沿着某个类的继承路线,由AfxFindMessageEntry在每一个类的消息映射表中查找,如果找到匹配项,同样按照是否是用户注册的消息的逻辑对消息进行处理,如果始终没有找到匹配项,则返回FALSE,交由CWnd::DefWindowProc处理。
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{
LRESULT lResult = 0;
union MessageMapFunctions mmf;
mmf.pfn = 0;

// special case for commands
if (message == WM_COMMAND)
{
if (OnCommand(wParam, lParam)) // 对WM_COMMAND交给OnCommand处理
{
lResult = 1;
goto LReturnTrue;
}
return FALSE;
}

// special case for notifies
if (message == WM_NOTIFY)
{
NMHDR* pNMHDR = (NMHDR*)lParam;
if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))
goto LReturnTrue;
return FALSE;
}
// … …
const AFX_MSGMAP* pMessageMap; pMessageMap = GetMessageMap(); // 获取类的消息映射表
UINT iHash;
iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax-1);
AfxLockGlobals(CRIT_WINMSGCACHE);
AFX_MSG_CACHE* pMsgCache;
pMsgCache = &_afxMsgCache[iHash];
const AFX_MSGMAP_ENTRY* lpEntry;
// 判断消息是否在缓存中
if (message == pMsgCache->nMsg && pMessageMap == pMsgCache->pMessageMap)
{
// cache hit
lpEntry = pMsgCache->lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
if (lpEntry == NULL) // 在缓存中,但是没有相应的表项,返回FALSE
return FALSE;

// cache hit, and it needs to be handled
if (message < 0xC000) // 否则,按照是否是用户自定义的消息, 跳转到相应的位置
goto LDispatch; // 标准Windows消息
else
goto LDispatchRegistered; // 用户自定义消息
}
else // 如果消息不在缓存中,就只好挨家挨户检查一番
{
// not in cache, look for it
pMsgCache->nMsg = message;
pMsgCache->pMessageMap = pMessageMap;
// 下面这个for循环从派生类到基类检查每一个类的消息映射表
for (/* pMessageMap already init'ed */; pMessageMap != NULL;
pMessageMap = pMessageMap->pBaseMap)
{
if (message < 0xC000) // 如果消息是Windows标准消息
{
// constant window message
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries,
message, 0, 0)) != NULL)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto LDispatch; // 如果找到对应项,就转去处理
}
}
else // 如果是用户自定义消息
{
// registered windows message
lpEntry = pMessageMap->lpEntries;
while ((lpEntry = AfxFindMessageEntry(lpEntry, 0xC000, 0, 0)) != NULL)
{
UINT* pnID = (UINT*)(lpEntry->nSig);
ASSERT(*pnID >= 0xC000 || *pnID == 0);
// must be successfully registered
if (*pnID == message)
{
pMsgCache->lpEntry = lpEntry;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
goto LDispatchRegistered; // 如果找到就转去处理
}
lpEntry++; // keep looking past this one
}
}
}
// 即不在缓存中,所有的类也都对此消息置之不理,那么就返回FALSE,交由CWnd::DefWndProc
// 处理
pMsgCache->lpEntry = NULL;
AfxUnlockGlobals(CRIT_WINMSGCACHE);
return FALSE;
}
// 下面是对Windows标准消息和用户自定义消息的处理
LDispatch:
ASSERT(message < 0xC000);

mmf.pfn = lpEntry->pfn;

switch (lpEntry->nSig)
{
default:
ASSERT(FALSE);
break;

case AfxSig_b_D_v:
lResult = (this->*mmf.pfn_b_D)(CDC::FromHandle(reinterpret_cast(wParam)));
break;

case AfxSig_b_b_v:
lResult = (this->*mmf.pfn_b_b)(static_cast(wParam));
break;

case AfxSig_b_u_v:
lResult = (this->*mmf.pfn_b_u)(static_cast(wParam));
break;

case AfxSig_b_h_v:
lResult = (this->*mmf.pfn_b_h)(reinterpret_cast(wParam));
break;
// … …
}
goto LReturnTrue;
LDispatchRegistered: // for registered windows messages
ASSERT(message >= 0xC000);
ASSERT(sizeof(mmf) == sizeof(mmf.pfn));
mmf.pfn = lpEntry->pfn;
lResult = (this->*mmf.pfn_l_w_l)(wParam, lParam);

LReturnTrue:
if (pResult != NULL)
*pResult = lResult;
return TRUE;
}
写到这里,对于直线上溯的消息的处理过程,应该是很清楚了,概括一下,就是先准备好两个处理过程,一个用来处理标准Windows消息,一个用来处理用户自定义消息,之后,根据消息是不是在缓存中,进行不同的查找,如果找到,根据消息的类型转到不同的处理过程中去,如果处理过了,就返回TRUE,否则返回FALSE,交由CWnd::DefWindowProc处理。
最后要补充的就是用来查找消息的函数AfxFindMessageEntry
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,
UINT nMsg, UINT nCode, UINT nID)
{
_asm
{
MOV EBX,lpEntry
MOV EAX,nMsg
MOV EDX,nCode
MOV ECX,nID
__loop:
CMP DWORD PTR [EBX+16],0 ; nSig (0 => end)
JZ __failed
CMP EAX,DWORD PTR [EBX] ; nMessage
JE __found_message
__next:
ADD EBX,SIZE AFX_MSGMAP_ENTRY
JMP short __loop
__found_message:
CMP EDX,DWORD PTR [EBX+4] ; nCode
JNE __next
// message and code good so far
// check the ID
CMP ECX,DWORD PTR [EBX+8] ; nID
JB __next
CMP ECX,DWORD PTR [EBX+12] ; nLastID
JA __next
// found a match
MOV lpEntry,EBX ; return EBX
JMP short __end
__failed:
XOR EAX,EAX ; return NULL
MOV lpEntry,EAX
__end:
}
return lpEntry;
// …
}
到此,关于直线上溯消息的处理就结束了,很简单,无非就是从派生类到基类的一个比较操作,真正复杂些的是MFC对于WM_COMMAND消息的处理,这个消息可以被Document/View处理。

3.
MFC消息映射的实现方法

http://www.vczx.com/tutorial/mfc/mfc4.php

MFC使用ClassWizard帮助实现消息映射,它在源码中添加一些消息映射的内容,并声明和实现消息处理函数。现在来分析这些被添加的内容。

在类的定义(头文件)里,它增加了消息处理函数声明,并添加一行声明消息映射的宏DECLARE_MESSAGE_MAP。

在类的实现(实现文件)里,实现消息处理函数,并使用IMPLEMENT_MESSAGE_MAP宏实现消息映射。一般情况下,这些声明和实现是由MFC的ClassWizard自动来维护的。看一个例子:

在AppWizard产生的应用程序类的源码中,应用程序类的定义(头文件)包含了类似如下的代码:

//{{AFX_MSG(CTttApp)

afx_msg void OnAppAbout();

//}}AFX_MSG

DECLARE_MESSAGE_MAP()


应用程序类的实现文件中包含了类似如下的代码:

BEGIN_MESSAGE_MAP(CTApp, CWinApp)

//{{AFX_MSG_MAP(CTttApp)

ON_COMMAND(ID_APP_ABOUT, OnAppAbout)

//}}AFX_MSG_MAP

END_MESSAGE_MAP()


头文件里是消息映射和消息处理函数的声明,实现文件里是消息映射的实现和消息处理函数的实现。它表示让应用程序对象处理命令消息ID_APP_ABOUT,消息处理函数是OnAppAbout。

为什么这样做之后就完成了一个消息映射?这些声明和实现到底作了些什么呢?接着,将讨论这些问题。


在声明与实现的内部


DECLARE_MESSAGE_MAP宏:

首先,看DECLARE_MESSAGE_MAP宏的内容:

#ifdef _AFXDLL

#define DECLARE_MESSAGE_MAP()
private:
static const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static AFX_DATA const AFX_MSGMAP messageMap;
static const AFX_MSGMAP* PASCAL _GetBaseMessageMap();
virtual const AFX_MSGMAP* GetMessageMap() const;

#else

#define DECLARE_MESSAGE_MAP()
private:
static const AFX_MSGMAP_ENTRY _messageEntries[];
protected:
static AFX_DATA const AFX_MSGMAP messageMap;
virtual const AFX_MSGMAP* GetMessageMap() const;

#endif

DECLARE_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。


BEGIN_MESSAE_MAP宏

然后,看BEGIN_MESSAE_MAP宏的内容:

#ifdef _AFXDLL

#define BEGIN_MESSAGE_MAP(theClass, baseClass)
const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap()
{ return &baseClass::messageMap; }
const AFX_MSGMAP* theClass::GetMessageMap() const
{ return &theClass::messageMap; }
AFX_DATADEF const AFX_MSGMAP theClass::messageMap =
{ &theClass::_GetBaseMessageMap, &theClass::_messageEntries[0] };
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
{

#else

#define BEGIN_MESSAGE_MAP(theClass, baseClass)
const AFX_MSGMAP* theClass::GetMessageMap() const
{ return &theClass::messageMap; }
AFX_DATADEF const AFX_MSGMAP theClass::messageMap =
{ &baseClass::messageMap, &theClass::_messageEntries[0] };
const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =
{

#endif


#define END_MESSAGE_MAP()
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 }
};
对应地,BEGIN_MESSAGE_MAP定义了两个版本,分别用于静态或者动态链接到MFC DLL的情形。END_MESSAGE_MAP相对简单,就只有一种定义。


ON_COMMAND宏

最后,看ON_COMMAND宏的内容:

#define ON_COMMAND(id, memberFxn)
{
WM_COMMAND,
CN_COMMAND,
(WORD)id,
(WORD)id,
AfxSig_vv,
(AFX_PMSG)memberFxn
};


消息映射声明的解释

在清楚了有关宏的定义之后,现在来分析它们的作用和功能。

消息映射声明的实质是给所在类添加几个静态成员变量和静态或虚拟函数,当然它们是与消息映射相关的变量和函数。


成员变量

有两个成员变量被添加,第一个是_messageEntries,第二个是messageMap。


第一个成员变量的声明:

AFX_MSGMAP_ENTRY _messageEntries[]

这是一个AFX_MSGMAP_ENTRY 类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用AFX_MSGMAP_ENTRY结构来描述。

AFX_MSGMAP_ENTRY结构的定义如下:

struct AFX_MSGMAP_ENTRY

{

//Windows消息ID

UINT nMessage;

//控制消息的通知码

UINT nCode;

//Windows Control的ID

UINT nID;

//如果是一定范围的消息被映射,则nLastID指定其范围

UINT nLastID;


UINT nSig;//消息的动作标识

//响应消息时应执行的函数(routine to call (or special value))

AFX_PMSG pfn;

};

从上述结构可以看出,每条映射有两部分的内容:第一部分是关于消息ID的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。

在上述结构的六个域中,pfn是一个指向CCmdTarger成员函数的指针。函数指针的类型定义如下:

typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);

当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从CCmdTarge派生的,所以可以实现这样的转换。

nSig是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个不同的nSig。在消息分发时,MFC内部根据nSig把消息派发给对应的成员函数处理,实际上,就是根据nSig的值把pfn还原成相应类型的消息处理函数并执行它。



第二个成员变量的声明

AFX_MSGMAP messageMap;

这是一个AFX_MSGMAP类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP结构的定义如下:

struct AFX_MSGMAP

{

//得到基类的消息映射入口地址的数据或者函数

#ifdef _AFXDLL

//pfnGetBaseMap指向_GetBaseMessageMap函数

const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();

#else

//pBaseMap保存基类消息映射入口_messageEntries的地址

const AFX_MSGMAP* pBaseMap;

#endif

//lpEntries保存消息映射入口_messageEntries的地址

const AFX_MSGMAP_ENTRY* lpEntries;

};

从上面的定义可以看出,通过messageMap可以得到类的消息映射数组_messageEntries和函数_GetBaseMessageMap的地址(不使用MFC DLL时,是基类消息映射数组的地址)。



成员函数


_GetBaseMessageMap()

用来得到基类消息映射的函数。



GetMessageMap()

用来得到自身消息映射的函数。


消息映射实现的解释

消息映射实现的实质是初始化声明中定义的静态成员函数_messageEntries和messageMap,实现所声明的静态或虚拟函数GetMessageMap、_GetBaseMessageMap。

这样,在进入WinMain函数之前,每个可以响应消息的MFC类都生成了一个消息映射表,程序运行时通过查询该表判断是否需要响应某条消息。


对消息映射入口表(消息映射数组)的初始化

如前所述,消息映射数组的元素是消息映射条目,条目的格式符合结构AFX_MESSAGE_ENTRY的描述。所以,要初始化消息映射数组,就必须使用符合该格式的数据来填充:如果指定当前类处理某个消息,则把和该消息有关的信息(四个)和消息处理函数的地址及原型组合成为一个消息映射条目,加入到消息映射数组中。

显然,这是一个繁琐的工作。为了简化操作,MFC根据消息的不同和消息处理方式的不同,把消息映射划分成若干类别,每一类的消息映射至少有一个共性:消息处理函数的原型相同。对每一类消息映射,MFC定义了一个宏来简化初始化消息数组的工作。例如,前文提到的ON_COMMAND宏用来映射命令消息,只要指定命令ID和消息处理函数即可,因为对这类命令消息映射条目,其他四个属性都是固定的。ON_COMMAND宏的初始化内容如下:

{WM_COMMAND,

CN_COMMAND,

(WORD)ID_APP_ABOUT,

(WORD)ID_APP_ABOUT,

AfxSig_vv,

(AFX_PMSG)OnAppAbout

}

这个消息映射条目的含义是:消息ID是ID_APP_ABOUT,OnAppAbout被转换成AFX_PMSG指针类型,AfxSig_vv是MFC预定义的枚举变量,用来标识OnAppAbout的函数类型为参数空(Void)、返回空(Void)。

在消息映射数组的最后,是宏END_MESSAGE_MAP的内容,它标识消息处理类的消息映射条目的终止。


对messageMap的初始化

如前所述,messageMap的类型是AFX_MESSMAP。

经过初始化,域lpEntries保存了消息映射数组_messageEntries的地址;如果动态链接到MFC DLL,则pfnGetBaseMap保存了_GetBaseMessageMap成员函数的地址;否则pBaseMap保存了基类的消息映射数组的地址。


对函数的实现

_GetBaseMessageMap()

它返回基类的成员变量messagMap(当使用MFC DLL时),使用该函数得到基类消息映射入口表。


GetMessageMap():

它返回成员变量messageMap,使用该函数得到自身消息映射入口表。


顺便说一下,消息映射类的基类CCmdTarget也实现了上述和消息映射相关的函数,不过,它的消息映射数组是空的。


既然消息映射宏方便了消息映射的实现,那么有必要详细的讨论消息映射宏。下一节,介绍消息映射宏的分类、用法和用途。


消息映射宏的种类

为了简化程序员的工作,MFC定义了一系列的消息映射宏和像AfxSig_vv这样的枚举变量,以及标准消息处理函数,并且具体地实现这些函数。这里主要讨论消息映射宏,常用的分为以下几类。


用于Windows消息的宏,前缀为“ON_WM_”。

这样的宏不带参数,因为它对应的消息和消息处理函数的函数名称、函数原型是确定的。MFC提供了这类消息处理函数的定义和缺省实现。每个这样的宏处理不同的Windows消息。

例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函数,消息映射条目的第一个成员nMessage指定为要处理的Windows消息的ID,第二个成员nCode指定为0。


用于命令消息的宏ON_COMMAND

这类宏带有参数,需要通过参数指定命令ID和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0)。消息处理函数的原型是void (void),不带参数,不返回值。

除了单条命令消息的映射,还有把一定范围的命令消息映射到一个消息处理函数的映射宏ON_COMMAND_RANGE。这类宏带有参数,需要指定命令ID的范围和消息处理函数。这些消息都映射到WM_COMMAND上,也就是将消息映射条目的第一个成员nMessage指定为WM_COMMAND,第二个成员nCode指定为CN_COMMAND(即0),第三个成员nID和第四个成员nLastID指定了映射消息的起止范围。消息处理函数的原型是void (UINT),有一个UINT类型的参数,表示要处理的命令消息ID,不返回值。

(3)用于控制通知消息的宏

这类宏可能带有三个参数,如ON_CONTROL,就需要指定控制窗口ID,通知码和消息处理函数;也可能带有两个参数,如具体处理特定通知消息的宏ON_BN_CLICKED、ON_LBN_DBLCLK、ON_CBN_EDITCHANGE等,需要指定控制窗口ID和消息处理函数。

控制通知消息也被映射到WM_COMMAND上,也就是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (void),没有参数,不返回值。

还有一类宏处理通知消息ON_NOTIFY,它类似于ON_CONTROL,但是控制通知消息被映射到WM_NOTIFY。消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID是控制子窗口的ID,第四个成员nLastID等于第三个成员的值。消息处理函数的原型是void (NMHDR*, LRESULT*),参数1是NMHDR指针,参数2是LRESULT指针,用于返回结果,但函数不返回值。

对应地,还有把一定范围的控制子窗口的某个通知消息映射到一个消息处理函数的映射宏,这类宏包括ON__CONTROL_RANGE和ON_NOTIFY_RANGE。这类宏带有参数,需要指定控制子窗口ID的范围和通知消息,以及消息处理函数。

对于ON__CONTROL_RANGE,是将消息映射条目的第一个成员的nMessage指定为WM_COMMAND,但是第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID等于指定了控制窗口ID的范围。消息处理函数的原型是void (UINT),参数表示要处理的通知消息是哪个ID的控制子窗口发送的,函数不返回值。

对于ON__NOTIFY_RANGE,消息映射条目的第一个成员的nMessage被指定为WM_NOTIFY,第二个成员nCode是特定的通知码,第三个成员nID和第四个成员nLastID指定了控制窗口ID的范围。消息处理函数的原型是void (UINT, NMHDR*, LRESULT*),参数1表示要处理的通知消息是哪个ID的控制子窗口发送的,参数2是NMHDR指针,参数3是LRESULT指针,用于返回结果,但函数不返回值。

(4)用于用户界面接口状态更新的ON_UPDATE_COMMAND_UI宏

这类宏被映射到消息WM_COMMND上,带有两个参数,需要指定用户接口对象ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID都指定为用户接口对象ID。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,不返回值。

对应地,有更新一定ID范围的用户接口对象的宏ON_UPDATE_COMMAND_UI_RANGE,此宏带有三个参数,用于指定用户接口对象ID的范围和消息处理函数。消息映射条目的第一个成员nMessage被指定为WM_COMMAND,第二个成员nCode被指定为-1,第三个成员nID和第四个成员nLastID用于指定用户接口对象ID的范围。消息处理函数的原型是 void (CCmdUI*),参数指向一个CCmdUI对象,函数不返回值。之所以不用当前用户接口对象ID作为参数,是因为CCmdUI对象包含了有关信息。

(5)用于其他消息的宏

例如用于用户定义消息的ON_MESSAGE。这类宏带有参数,需要指定消息ID和消息处理函数。消息映射条目的第一个成员nMessage被指定为消息ID,第二个成员nCode被指定为0,第三个成员nID和第四个成员也是0。消息处理的原型是LRESULT (WPARAM, LPARAM),参数1和参数2是消息参数wParam和lParam,返回LRESULT类型的值。

(6)扩展消息映射宏

很多普通消息映射宏都有对应的扩展消息映射宏,例如:ON_COMMAND对应的ON_COMMAND_EX,ON_ONTIFY对应的ON_ONTIFY_EX,等等。扩展宏除了具有普通宏的功能,还有特别的用途。关于扩展宏的具体讨论和分析,见4.4.3.2节。

作为一个总结,下表列出了这些常用的消息映射宏。

表4-1 常用的消息映射宏

消息映射宏
用途

ON_COMMAND
把command message映射到相应的函数

ON_CONTROL
把control notification message映射到相应的函数。MFC根据不同的控制消息,在此基础上定义了更具体的宏,这样用户在使用时就不需要指定通知代码ID,如ON_BN_CLICKED。

ON_MESSAGE
把user-defined message.映射到相应的函数

ON_REGISTERED_MESSAGE
把registered user-defined message映射到相应的函数,实际上nMessage等于0x0C000,nSig等于宏的消息参数。nSig的真实值为Afxsig_lwl。

ON_UPDATE_COMMAND_UI
把user interface user update command message映射到相应的函数上。

ON_COMMAND_RANGE
把一定范围内的command IDs 映射到相应的函数上

ON_UPDATE_COMMAND_UI_RANGE
把一定范围内的user interface user update command message映射到相应的函数上

ON_CONTROL_RANGE
把一定范围内的control notification message映射到相应的函数上



在表4-1中,宏ON_REGISTERED_MESSAGE的定义如下:

#define ON_REGISTERED_MESSAGE(nMessageVariable, memberFxn)
{ 0xC000, 0, 0, 0,
(UINT)(UINT*)(&nMessageVariable),
/*implied 'AfxSig_lwl'*/
(AFX_PMSG)(AFX_PMSGW)(LRESULT
(AFX_MSG_CALL CWnd::*)
(WPARAM, LPARAM))&memberFxn }

从上面的定义可以看出,实际上,该消息被映射到WM_COMMAND(0XC000),指定的registered消息ID存放在nSig域内,nSig的值在这样的映射条目下隐含地定为AfxSig_lwl。由于ID和正常的nSig域存放的值范围不同,所以MFC可以判断出是否是registered消息映射条目。如果是,则使用AfxSig_lwl把消息处理函数转换成参数1为Word、参数2为long、返回值为long的类型。

在介绍完了消息映射的内幕之后,应该讨论消息处理过程了。由于CCmdTarge的特殊性和重要性,在4.3节先对其作一个大略的介绍。


CcmdTarget类

除了CObject类外,还有一个非常重要的类CCmdTarget。所有响应消息或事件的类都从它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。

CCmdTarget类是MFC处理命令消息的基础、核心。MFC为该类设计了许多成员函数和一些成员数据,基本上是为了解决消息映射问题的,而且,很大一部分是针对OLE设计的。在OLE应用中,CCmdTarget是MFC处理模块状态的重要环节,它起到了传递模块状态的作用:其构造函数获取当前模块状态,并保存在成员变量m_pModuleState里头。关于模块状态,在后面章节讲述。

CCmdTarget有两个与消息映射有密切关系的成员函数:DispatchCmdMsg和OnCmdMsg。


静态成员函数DispatchCmdMsg

CCmdTarget的静态成员函数DispatchCmdMsg,用来分发Windows消息。此函数是MFC内部使用的,其原型如下:

static BOOL DispatchCmdMsg(

CCmdTarget* pTarget,

UINT nID,

int nCode,

AFX_PMSG pfn,

void* pExtra,

UINT nSig,

AFX_CMDHANDLERINFO* pHandlerInfo)


关于此函数将在4.4.3.2章节命令消息的处理中作更详细的描述。


虚拟函数OnCmdMsg

CCmdTarget的虚拟函数OnCmdMsg,用来传递和发送消息、更新用户界面对象的状态,其原型如下:

OnCmdMsg(

UINT nID,

int nCode,

void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

框架的命令消息传递机制主要是通过该函数来实现的。其参数描述参见4.3.3.2章节DispacthCMdMessage的参数描述。

在本书中,命令目标指希望或者可能处理消息的对象;命令目标类指命令目标的类。

CCmdTarget对OnCmdMsg的默认实现:在当前命令目标(this所指)的类和基类的消息映射数组里搜索指定命令消息的消息处理函数(标准Windows消息不会送到这里处理)。

这里使用虚拟函数GetMessageMap得到命令目标类的消息映射入口数组_messageEntries,然后在数组里匹配指定的消息映射条目。匹配标准:命令消息ID相同,控制通知代码相同。因为GetMessageMap是虚拟函数,所以可以确认当前命令目标的确切类。

如果找到了一个匹配的消息映射条目,则使用DispachCmdMsg调用这个处理函数;

如果没有找到,则使用_GetBaseMessageMap得到基类的消息映射数组,查找,直到找到或搜寻了所有的基类(到CCmdTarget)为止;

如果最后没有找到,则返回FASLE。


每个从CCmdTarget派生的命令目标类都可以覆盖OnCmdMsg,利用它来确定是否可以处理某条命令,如果不能,就通过调用下一命令目标的OnCmdMsg,把该命令送给下一个命令目标处理。通常,派生类覆盖OnCmdMsg时,要调用基类的被覆盖的OnCmdMsg。

在MFC框架中,一些MFC命令目标类覆盖了OnCmdMsg,如框架窗口类覆盖了该函数,实现了MFC的标准命令消息发送路径。具体实现见后续章节。

必要的话,应用程序也可以覆盖OnCmdMsg,改变一个或多个类中的发送规定,实现与标准框架发送规定不同的发送路径。例如,在以下情况可以作这样的处理:在要打断发送顺序的类中把命令传给一个非MFC默认对象;在新的非默认对象中或在可能要传出命令的命令目标中。



<< Home

This page is powered by Blogger. Isn't yours?