Windows消息的封装之:对话框与控件(二)

    好了,继续讨论Frameworks 对控件消息的处理。继续拿可怜的MFC作例子,好吧,这家伙土是土了一点,以后我们会提到点稍微时髦点的东西,现在我们还是继续解剖这个古董Framework 吧(拜托,在Windows 下面用C++ 写桌面应用这个行当里面,还有啥时髦东西?连恐龙们都去玩.Net 了)。

    MFC 继承了Windows 程序对控件的处理方式,基本的控件消息是通过WM_COMMAND和WM_NOTIFY 来传递的。这个消息就是从上一篇提到的user32.dll!_ButtonWndProcWorker()发出来的。控件窗体得到Windows 消息之后,经过处理,最终通过系统默认的消息处理函数调用到的user.dll 里面,然后user.dll 就开始负责把这个消息转告给与控件有关的其他窗体,比如控件的父窗口。这个当然是消息系统应该做的,不然你想,一个按钮被点击了,它的父窗体是怎么知道的呢^_^

    好了,user32.dll!_ButtonWndProcWorker()就这么把来自控件的消息转达给了父窗口。但是,明显不能原样转达,因为原样转达的话,父窗口怎么来处理这个消息呢?你想,转来了一个点击消息,接收者是我,可是我明明没有被点过啊?于是父窗口就疯掉了。怎么办呢?Windows 消息系统采用的方法是WM_COMMAND和WM_NOTIFY,告诉父窗口:这不是你自己的消息,这是来自你的子窗体的消息,这样父窗口就可以针对这个消息来处理了。

    了解了这个,所谓的ON_BN_XXX()宏就没有什么秘密可言了,无非是WM_COMMAND 消息的映射,把那几个宏那么小小的展开一下,就是那么回事了。

    但是,故事到这里还没有结束,因为这里有一个很好玩的OwnerDraw问题。我们知道,一个控件要自绘,在MFC里面一般是重载了控件类的DrawItem 虚方法实现的。那么请问,这个DrawItem 方法,处理的是哪里来的WM_DRAWITEM 消息呢?

    答案是父窗体“反射”来的。这个“反射”的行为可以在下面的代码里面看到(wincore.cpp)

void CWnd::OnDrawItem(int /*nIDCtl*/, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
if (lpDrawItemStruct->CtlType == ODT_MENU)
{
   CMenu* pMenu = CMenu::FromHandlePermanent(
    (HMENU)lpDrawItemStruct->hwndItem);
   if (pMenu != NULL)
   {
    pMenu->DrawItem(lpDrawItemStruct);
    return; // eat it
   }
}

// reflect notification to child window control
if (ReflectLastMsg(lpDrawItemStruct->hwndItem))
   return;     // eat it

// not handled - do default
Default();
}

    这个ReflectLastMsg()就把消息“反射”给了子窗体。为什么叫“反射”呢?前面我们提到了,点击消息是从控件开始,经过user.DLL 的传递,交到父窗口手里的,现在父窗口要处理这个问题,看起来又要发个消息回控件去,让控件干活,岂不是消息又被“反射”了回去么?

    但是!但是!事情只是“看起来”是这样!我们继续跟着代码走,看看这个ReflectLastMsg():

BOOL PASCAL CWnd::ReflectLastMsg(HWND hWndChild, LRESULT* pResult)
{
// get the map, and if no map, then this message does not need reflection
CHandleMap* pMap = afxMapHWND();
if (pMap == NULL)
   return FALSE;

// check if in permanent map, if it is reflect it (could be OLE control)
CWnd* pWnd = (CWnd*)pMap->LookupPermanent(hWndChild);
ASSERT(pWnd == NULL || pWnd->m_hWnd == hWndChild);
if (pWnd == NULL)
{
    ...
   return FALSE;
}

// only OLE controls and permanent windows will get reflected msgs
ASSERT(pWnd != NULL);
return pWnd->SendChildNotifyLastMsg(pResult);
}

    我们看到,MFC 先是找到了作为反射目标的子窗体,然后调用了它的SendChildNotifyLastMsg(),继续:

BOOL CWnd::SendChildNotifyLastMsg(LRESULT* pResult)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
return OnChildNotify(pThreadState->m_lastSentMsg.message,
   pThreadState->m_lastSentMsg.wParam, pThreadState->m_lastSentMsg.lParam, pResult);
}

把消息投递给子窗体,注意这里已经走到控件的代码里面来了。


BOOL CButton::OnChildNotify(UINT message, WPARAM wParam, LPARAM lParam,
LRESULT* pResult)
{
if (message != WM_DRAWITEM)
   return CWnd::OnChildNotify(message, wParam, lParam, pResult);

ASSERT(pResult == NULL);       // no return value expected
UNUSED(pResult); // unused in release builds
DrawItem((LPDRAWITEMSTRUCT)lParam);
return TRUE;
}

    我们看到,对于WM_DRAWITEM 消息,控件在这里就在调用被重载的DrawItem()函数了。对于其他消息呢?CWnd::OnChildNotify()会把原先的消息加一个WM_REFLECT_BASE 转回控件,同样直接调用到负责处理这些消息虚函数重载OnWndMsg()和OnCmdMsg()。现在我们可以长出一口气了,MFC的“反射”尽管实现了将消息通知给控件的功能,但是并没有反弹一个消息回去,而是直接调用对应的消息处理函数,因此效率还是可以保证的。

   咦?没说散场,怎么人都开始跑没了?听评书要讲职业道德的说……说的就是你,坐沙发那个!你觉得这个主题我说完了么?当然没有!要是就这么点料就不敢来茶馆混了!我问你,前面提到的那个反射消息给控件的函数ReflectLastMsg(),你可看出什么问题来?没有?再看看?

    难道你没有注意到我们的老熟人pMap->LookupPermanent() 么?请问,我们前面第一节提到了,afxMapHWND()专门管理窗口对象CWnd *与窗口句柄HWND 的关联,帮助MFC将每一个消息投递到对应的窗口对象那里。我们也提到了,窗口和对话框在创建的时候,通过Attach()方法将自己加入这个列表,并且在Dettach()的时候将自己从这个Map 里面去掉,这样,每个窗口对象才可以接收到MFC 框架投递过来的消息。

    但是!你何尝让每一个控件在这个Map 里面注册过自己?既然没有注册,afxMapHWND()又何以能通过LookupPermanent 找到每一个控件的对象呢?难道有什么神秘法术?

    嘿嘿,这个问题,且听下回分解,今天先收工先!

你可能感兴趣的:(开发随想)