VC++/MFC訊息對映機制(3):MFC訊息路由的原始碼分析
VC++/MFC訊息對映機制(3):MFC訊息路由的原始碼分析
若對C++語法不熟悉,建議參閱《C++語法詳解》一書,電子工業出版社出版,該書語法示例短小精悍,對查閱C++知識點相當方便,並對語法原理進行了透徹、深入詳細的講解。
注意:本文最好結合本人所作前兩篇與訊息對映機制有關的文章一起閱讀。
注意:訊息路由包含了訊息對映功能
1、MFC原始碼的訊息對映巨集,把模仿程式中的訊息對映陣列ss和訊息對映表msgmp封裝在了名為GetThisMessageMap()的函式內部。但其基本原理是相同的,請讀者自行理解。
2、基本原理:在MFC原始碼中,訊息路由的功能是在重新設定的過程函式::AfxWndProc()內部間接完成的,因此在MFC原始碼中的第一步就是使用鉤子攔截訊息,並重新設定過程函式。
3、首先使用鉤子函式攔截訊息,以實現訊息對映。在MFC原始碼中是使用AfxHookWindowCreate()函式間接呼叫::SetWindowsHookEx(…)函式,設定鉤子函式的。AfxHookWindowCreate是在CWnd::CreateEx函式中的PreCreateWindow函式(完成視窗註冊)和::CreateWindowEx函式(完成視窗建立)之間呼叫的。
4、然後通過鉤子函式間接的使用SetWindowLongPtr()函式,重新設定程式的過程函式為::AfxWndProc()。
5、過程函式AfxWndProc並不直接處理訊息,而是通過呼叫CWnd::WindowProc函式處理由鉤子攔截到的訊息。訊息路由的功能就是在該函式內通過呼叫OnWndMsg()間接完成的。
6、CWnd::WindowProc根據訊息的不同情形,分別使用CWnd::OnWndMsg()函式和CWnd::DefWindowProc處理訊息。其中CWnd::OnWndMsg()是主要的處理訊息的函式,即訊息對映和訊息路由的功能都在該函式內直接或間接的實現。
7、CWnd::OnWndMsg()函式,根據接收到的訊息型別的不同,而對訊息進行不同的路由,若訊息是命令訊息WM_COMMAND,則呼叫OnCommand()虛擬函式處理。若訊息是通知訊息WM_NOTIFY則呼叫OnNotify虛擬函式進行處理,若訊息是一般訊息,則在CWnd::OnWndMsg()函式內部直接進行處理,對於一般訊息,使用的是直線路由模式,因此CWnd::OnWndMsg內部實現了直線路由。
8、對於OnCommand虛擬函式,因為處理的是命令訊息,而命令訊息可能會拐彎,因此在OnCommand函式內部實現了拐彎路由。
一、使用鉤子函式重新設定過程函式(以下程式全部在wincore.cpp檔案內實現)
注:鉤子函式的原理見後文附註講解
以下程式全部來自wincore.cpp檔案內部的MFC原始碼程式
BOOL CWnd::CreateEx(......){ //節選原始碼 ...... if (!PreCreateWindow(cs)){PostNcDestroy();return FALSE;} AfxHookWindowCreate(this); //❶、由該函式間接設定鉤子函式。 HWND hWnd = CreateWindowEx(...); } void AFXAPI AfxHookWindowCreate(CWnd* pWnd) //完整原始碼 { _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); if (pThreadState->m_pWndInit == pWnd) return; if (pThreadState->m_hHookOldCbtFilter == NULL) { pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, //設定鉤子函式 _AfxCbtFilterHook, NULL, ::GetCurrentThreadId()); //❷、粗體為設定的鉤子函式 if (pThreadState->m_hHookOldCbtFilter == NULL) AfxThrowMemoryException(); } //以下為一些發生錯誤時的處理程式碼。 ASSERT(pThreadState->m_hHookOldCbtFilter != NULL); ASSERT(pWnd != NULL); ASSERT(pWnd->m_hWnd == NULL); // only do once ASSERT(pThreadState->m_pWndInit == NULL); // hook not already in progress pThreadState->m_pWndInit = pWnd;} LRESULT CALLBACK _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam){//節選原始碼 ...... if (pWndInit != NULL || (!(lpcs->style & WS_CHILD) && !bContextIsDLL)){ ...... if (pWndInit != NULL){ ...... WNDPROC afxWndProc = AfxGetAfxWndProc(); //❸、粗體為重新設定的過程函式 oldWndProc = (WNDPROC)SetWindowLongPtr( //重新設定過程函式為❸的反回值。 hWnd, GWLP_WNDPROC,(DWORD_PTR)afxWndProc); ......} WNDPROC AFXAPI AfxGetAfxWndProc(){//完整原始碼 #ifdef _AFXDLL return AfxGetModuleState()->m_pfnAfxWndProc; #else return &AfxWndProc; //❹、粗體為重新設定的過程函式,至此過程函式重新設定完畢。 #endif } LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)//完整原始碼 { // special message which identifies the window as using AfxWndProc if (nMsg == WM_QUERYAFXWNDPROC) return 1; // all other messages route through message map CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //表示獲取視窗控制代碼hWnd所關聯的類物件。 ASSERT(pWnd != NULL); ASSERT(pWnd==NULL || pWnd->m_hWnd == hWnd); if (pWnd==NULL||pWnd->m_hWnd!=hWnd) return ::DefWindowProc(hWnd,nMsg,wParam,lParam); return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);}//❺、呼叫此函式完成過程函式的功能 LRESULT AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,WPARAM wParam = 0, LPARAM lParam = 0) ...... if (nMsg == WM_INITDIALOG) _AfxPreInitDialog(pWnd, &rectOld, &dwStyle); lResult=pWnd->WindowProc(nMsg,wParam,lParam); //❻、呼叫父類的該函式完成過程函式的功能。 ...... } 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; }
二、MFC原始碼實現直線路由(CWnd::OnWndMsg原始碼解析wincore.cpp)
1、基本原理:直線路由的實現原理請參閱模仿直線路由的講解(示例3.10)。在MFC原始碼中是使用CWnd::OnWndMsg實現直線路由和拐彎路由的,其中直線路由由該函式內部直接實現,而拐彎路由通過呼叫另一個虛擬成員函式CWnd::OnCommand間接實現,另外OnWndMsg還對不同型別的訊息,進行了不同的處理,詳見原始碼。
2、存放<訊息,處理函式>對的結構體(afxwin.h)
struct AFX_MSGMAP_ENTRY { UINT nMessage; UINT nCode; UINT nID; UINT nLastID; UINT_PTR nSig; AFX_PMSG pfn; };
3、訊息對映表(afxwin.h)
struct AFX_MSGMAP
{const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)(); const AFX_MSGMAP_ENTRY* lpEntries;};
4、用於遍歷訊息對映陣列的函式
const AFX_MSGMAP_ENTRY* AFXAPI
AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,UINT nMsg, UINT nCode, UINT nID){
#if defined(_M_IX86)
...... //在這之中是一系列的彙編程式碼,用於加快處理速度。
#else
while (lpEntry->nSig != AfxSig_end) /*⓬、此while用於遍歷訊息對映陣列_messageEntries(該陣列在巨集BEGIN_MESSAGE_MAP之中宣告和定義中的所有<訊息,處理函式>對。其原理與模仿程式示例3.10中的while語句相同。*/
{if (lpEntry->nMessage == nMsg /*⓭、比對從視窗接收到的訊息是否與訊息對映陣列_messageEntries中<訊息,處理函式>對裡的訊息相等*/
&& lpEntry->nCode == nCode &&nID >= lpEntry->nID && nID <= lpEntry->nLastID)
{ return lpEntry; }
lpEntry++;} //while結束
return NULL; // not found
#endif }
5、實現訊息路由的共用體
union MessageMapFunctions{
BOOL (AFX_MSG_CALL CWnd::*pfn_b_D_v)(CDC*);
BOOL (AFX_MSG_CALL CWnd::*pfn_b_b_v)(BOOL);
BOOL (AFX_MSG_CALL CWnd::*pfn_b_W_uu)(CWnd*,UNIT,UNIT);
BOOL (AFX_MSG_CALL CWnd::*pfn_l_w_l)(WPARAM,LPARAM);
...... } //大約有一百多行類似的程式碼
6、需要用到的列舉
enum AfxSig //位於檔案afxmsg_.h
{ AfxSig_end = 0, // [marks end of message map]
AfxSig_b_D_v, // BOOL (CDC*)
AfxSig_b_b_v, // BOOL (BOOL)
AfxSig_b_u_v, // BOOL (UINT)
AfxSig_b_h_v, // BOOL (HANDLE)
AfxSig_b_W_uu, // BOOL (CWnd*, UINT, UINT)
...... } //大約有一百多行類似的程式碼
7、Cwnd::OnWndMsg原始碼(wincore.cpp)
Cwnd::OnWndMsg原始碼中會用到由訊息對映巨集DECLARE_MESSAGE_MAP()、BEGIN_MESSAGE_MAP、END_MESSAGE_MAP、ON_MESSAGE展開後的程式碼,比如其中的GetMessageMap()函式。
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{//絕大部分原始碼
LRESULT lResult = 0;
union MessageMapFunctions mmf; //❶、共用體的使用(原理見示例3.5),其定義見後文。
mmf.pfn = 0;
CInternalGlobalLock winMsgLock;
// special case for commands
if (message == WM_COMMAND) //❷、判斷訊息是否是命令訊息,若是,則按以下方式處理。
{ if (OnCommand(wParam, lParam)) //❸、使用OnCommand實現拐彎路由
{lResult = 1; goto LReturnTrue;}
return FALSE; }
……
if (message == WM_NOTIFY) //判斷訊息是否是通知訊息WM_NOTIFY
{NMHDR* pNMHDR = (NMHDR*)lParam;
if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult))goto LReturnTrue;
return FALSE; }
if (message == WM_ACTIVATE) //判斷是否是WM_ACTIVATE訊息
_AfxHandleActivate(this, wParam, CWnd::FromHandle((HWND)lParam));
if (message == WM_SETCURSOR &&
_AfxHandleSetCursor(this, (short)LOWORD(lParam), HIWORD(lParam)))
{lResult = 1;
goto LReturnTrue;}
// special case for windows that contain windowless ActiveX controls
BOOL bHandled;
bHandled = FALSE;
if ((m_pCtrlCont != NULL) && (m_pCtrlCont->m_nWindowlessControls > 0))
{ if (((message >= WM_MOUSEFIRST) && (message <= AFX_WM_MOUSELAST)) ||
((message >= WM_KEYFIRST) && (message <= WM_IME_KEYLAST)) ||
((message >= WM_IME_SETCONTEXT) && (message <= WM_IME_KEYUP)))
{ bHandled = m_pCtrlCont->HandleWindowlessMessage(message, wParam, lParam, &lResult); }
}
if (bHandled) { goto LReturnTrue; }
……
const AFX_MSGMAP* pMessageMap;
pMessageMap = GetMessageMap();
const AFX_MSGMAP_ENTRY* lpEntry;
UINT iHash; iHash = (LOWORD((DWORD_PTR)pMessageMap) ^ message) & (iHashMax-1);
winMsgLock.Lock(CRIT_WINMSGCACHE);
AFX_MSG_CACHE* pMsgCache; pMsgCache = &_afxMsgCache[iHash];
if (message == pMsgCache->nMsg && //檢查訊息是否處於cache之中
pMessageMap == pMsgCache->pMessageMap)
{ // cache hit
lpEntry = pMsgCache->lpEntry;
winMsgLock.Unlock();
if (lpEntry == NULL) return FALSE;
// cache hit, and it needs to be handled
if (message < 0xC000) goto LDispatch;
else goto LDispatchRegistered; }
else //❺、從此處開始實現訊息的直線路由
{ pMsgCache->nMsg = message;
pMsgCache->pMessageMap = pMessageMap;
/*❻、for迴圈的原理與直線路由模仿程式示例3.10相同。但要注意的是在MFC之中,把訊息對映(模仿示例中的ss,在MFC中為_messageEntries)和訊息對映表(模仿示例中的msgmp,在MFC中為messageMap)封裝在了一個名為GetThisMessageMap的函式之中,但其基本原理是一樣的。*/
for (;pMessageMap->pfnGetBaseMap != NULL;
pMessageMap = (*pMessageMap->pfnGetBaseMap)() )
{ ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
if (message < 0xC000) /*小於0xC000的訊息為一般訊息,否則為使用者註冊訊息。*/
{
/*❼、函式AfxFindMessageEntry的主要作用是用於迴圈查詢訊息對映(即_messageEntries)中的所有<訊息,處理函式>對,該函式類似於模仿程式示例3.10中的while語句除去其中的swtich後的樣子。其定義見後文。*/
if ((lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries,
message, 0, 0)) != NULL)
{ pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatch; /*❽、LDispatch後的語句,主要用於比對應該呼叫共用體中的哪一個函式。類似於模仿程式示例3.10中的swtich語句。*/
} }
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);
if (*pnID == message)
{ pMsgCache->lpEntry = lpEntry;
winMsgLock.Unlock();
goto LDispatchRegistered;
}
lpEntry++; // keep looking past this one
}} } //for結束
pMsgCache->lpEntry = NULL;
winMsgLock.Unlock();
return FALSE; } //if結束
LDispatch:
ASSERT(message < 0xC000);
mmf.pfn = lpEntry->pfn; //❾、初始化共用體
switch (lpEntry->nSig) /*❿、根據訊息處理函式原型的不同,而使用共用體中不同的成員間接呼叫消處理函式,以下呼叫的原則是,共用體mmf中指向函式的指標成員的原型必須與訊息處理函式的原型相同。*/
{ default: ASSERT(FALSE); break;
case AfxSig_l_p: /*⓫、AfxSig_XXXX是列舉型別AfxSig中定義的列舉值,其定義見後文,該列舉是與MessageMapFunctions共用體相對應的。*/
{CPoint point(lParam);lResult=(this->*mmf.pfn_l_p)(point); break;}
case AfxSig_b_D_v:
lResult = (this->*mmf.pfn_b_D)(CDC::FromHandle(reinterpret_cast<HDC>(wParam)));break;
...... //此處大約有100多個類似的語句。
}
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; } //OnWndMsg結束
三、MFC原始碼實現拐彎路由(CWnd::OnCommand原始碼解析wincore.cpp)
1、拐彎路由牽扯到幾個問題,第一是訊息是從哪個類傳入進去的,第二個是拐彎路由時的函式呼叫,第三個是繼承結構問題,因此在講解原始碼的訊息拐彎路由時需作一些假設。
2、因為在CWnd::OnWndMsg中呼叫的OnCommand是個虛擬函式,因此究竟是呼叫的哪個類中的OnCommand虛擬函式,需要視this指標指向的型別而定。以下類都重寫了該虛擬函式CWnd,CFrameWnd,CSplitterWnd,CPropertySheet,CMDIFrameWnd,COlePropertyPage
3、OnCommand函式並不會執行訊息的匹配和查詢工作,該函式的主要作用是實現訊息的拐彎,訊息的匹配和查詢工作由另一個虛擬函式OnCmdMsg函式完成,該虛擬函式主要用於實現訊息在繼承子樹中的直線路由,因此程式最終都會呼叫OnCmdMsg函式,以實現該類所在繼承子樹的直線路由,而該函式的最終實現程式碼(即直線路由的程式碼)位於CCmdTarget::OnCmdMsg之中,因此原始碼中的其他子類,最終都會呼叫CCmdTarget::OnCmdMsg函式。
4、以下類重寫了OnCmdMsg虛擬函式:CCmdTarget, CFrameWnd, CView, CDocument, CMDIFrameWnd, CPropertySheet, CDialog, COleDocument,注意CWnd類沒有重寫該虛擬函式。
5、原始碼中的OnCommand相當於模仿程式示例3.11中的g1函式,而OncmdMsg相當於是g函式。
6、假設訊息從CFrameWnd進入,即假設CWnd::OnWndMsg函式中的this指標指向的型別是CFrameWnd,則在其中呼叫的OnCommand,就是CFrameWnd::OnCommand。現在從此處開始列出所有有關的原始碼。
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult)
{ ......
if (message == WM_COMMAND)
{ if (OnCommand(wParam, lParam)) //❶、假設this指標指向的型別是CFrameWnd
......}
......}
BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam) //winfrm.cpp
{ ......
return CWnd::OnCommand(wParam, lParam); }
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam) (wincore.cpp)
{ ......
return OnCmdMsg(nID,nCode,NULL,NULL);} //❷、此處再次假設this指標指向的型別是CFrameWnd
/*❸、OnCmdMsg用於處理訊息在繼承子樹中的直線路由,該函式的具體程式碼(此處指用於實現直線路由的程式碼)位於頂級父類CCmdTarget之中,因此其他各子類最終都會呼叫CCmdTarget::OnCmdMsg函式。*/
BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra, //winfrm.cpp
AFX_CMDHANDLERINFO* pHandlerInfo)
{ CPushRoutingFrame push(this);
/*❹、 CFrameWnd類中,訊息橫向流動的三條路徑,這三條路徑最終都會呼叫CCmdTarget::OnCmdMsg函式,以便處理訊息在各自的繼承子樹中的直線路由,因為分路徑比較多,本示例只追蹤部分分路徑原始碼,其餘分路徑原理都是類似的,請讀者自行追蹤,但最終都會呼叫CCmdTarget::OnCmdMsg。
注意:CWnd未重寫OnCmdMsg函式,因此CWnd::OnCmdMsg就是直接呼叫CCmdTarget::OnCmdMsg。*/
CView* pView = GetActiveView();
if (pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //路徑1
return TRUE;
if (CWnd::OnCmdMsg(nID,nCode,pExtra,pHandlerInfo))//路徑2,直接呼叫CCmdTarget::OnCmdMsg
return TRUE;
CWinApp* pApp = AfxGetApp();
if (pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //路徑3
return TRUE;
return FALSE; }
//❺、CFrameWnd路徑1:CView::OnCmdMsg原始碼(viewcore.cpp),本路徑又產生兩個分路徑1---1和1---2
BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo)
{ //分路徑1---1,直接呼叫CCmdTarget::OnCmdMsg
if(CWnd::OnCmdMsg(nID,nCode,pExtra,pHandlerInfo))return TRUE;
if (m_pDocument != NULL){ CPushRoutingView push(this);
return m_pDocument->OnCmdMsg(nID,nCode,pExtra, pHandlerInfo);} //分路徑1----2
return FALSE; }
//❻、CFrameWnd分路徑1----2:CDocument::OnCmdMsg原始碼(doccore.cpp)
//本路徑再次產生兩個分路徑1----2-----1和1----2------2
BOOL CDocument::OnCmdMsg(UINT nID,int nCode,void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo)
{ if (CCmdTarget::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //分路徑1----2-----1
return TRUE;
if (m_pDocTemplate != NULL &&
m_pDocTemplate->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))//分路徑1----2-----2
return TRUE;
return FALSE;}
//❼、CCmdTarget::OnCmdMsg原始碼,用於處理訊息在各自的繼承子樹中的直線路由,該程式中的執行原理與CWnd::OnWndMsg是類似的,詳見直線路由模仿程式示例3.10。
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra, //cmdtarg.cpp
AFX_CMDHANDLERINFO* pHandlerInfo)
{ ......
const AFX_MSGMAP* pMessageMap;
const AFX_MSGMAP_ENTRY* lpEntry;
......
if (nCode != CN_UPDATE_COMMAND_UI){ nMsg = HIWORD(nCode); nCode = LOWORD(nCode); }
// for backward compatibility HIWORD(nCode)==0 is WM_COMMAND
if (nMsg == 0) nMsg = WM_COMMAND;
// look through message map to see if it applies to us
//❽、以下for語句與MFC原始碼的直線路由程式碼的原理是相同的,詳見直線路由模仿程式示例3.10。
for (pMessageMap = GetMessageMap(); pMessageMap->pfnGetBaseMap != NULL;
pMessageMap = (*pMessageMap->pfnGetBaseMap)())
{ // Note: catches BEGIN_MESSAGE_MAP(CMyClass, CMyClass)!
ASSERT(pMessageMap != (*pMessageMap->pfnGetBaseMap)());
/*❾、AfxFindMessageEntry函式的原始碼詳見MFC原始碼直線路由,該函式的功能是用於查詢訊息對映中的所有<訊息,處理函式>對。類似於模仿程式的while迴圈。*/
lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID);
if (lpEntry != NULL)
{ ......
return _AfxDispatchCmdMsg( /*❿、此函式的功能就是MFC原始碼直線路由goto語句跳轉到的類似swtich的功能,主要用於對比應該使用哪一個函式原型呼叫訊息處理函式。*/
this, nID, nCode, lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
} }
return FALSE; } // not handled
AFX_STATIC BOOL AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget,UINT nID, int nCode, //cmdtarg.cpp
AFX_PMSG pfn, void* pExtra, UINT_PTR nSig, AFX_CMDHANDLERINFO* pHandlerInfo)
{ ENSURE_VALID(pTarget);
UNUSED(nCode); // unused in release builds
union MessageMapFunctions mmf;
mmf.pfn = pfn;
BOOL bResult = TRUE; // default is ok
if (pHandlerInfo != NULL)
{ pHandlerInfo->pTarget = pTarget; pHandlerInfo->pmf = mmf.pfn; return TRUE; }
switch (nSig)
{default: // illegal
ASSERT(FALSE);
return 0;
break;
case AfxSigCmd_v:
// normal command or control notification
ASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKED
ASSERT(pExtra == NULL);
(pTarget->*mmf.pfnCmd_v_v)();
break;
......} //一系列類似的case子句。
本文作者:黃邦勇帥(原名:黃勇)