消息映射的實現

来源:互联网 发布:cctv发现之旅骗术知乎 编辑:程序博客网 时间:2024/05/21 17:24

保存以防原文看不见

http://lipingke.blog.hexun.com.tw/37176683_d.html


消息映射的實現

  1. Windows消息概述

    Windows應用程序的輸入由Windows系統以消息的形式發送給應用程序的窗口。這些窗口通過窗口過程來接收和處理消息,然後把控制返還給Windows。

    1. 消息的分類

  1. 隊列消息和非隊列消息

    從消息的發送途徑上看,消息分兩種:隊列消息和非隊列消息。隊列消息送到系統消息隊列,然後到線程消息隊列;非隊列消息直接送給目的窗口過程。

    這裏,對消息隊列闡述如下:

    Windows維護一個系統消息隊列(System message queue),每個GUI線程有一個線程消息隊列(Thread message queue)。

    鼠標、鍵盤事件由鼠標或鍵盤驅動程序轉換成輸入消息並把消息放進系統消息隊列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次從系統消息隊列移走一個消息,確定它是送給哪個窗口的和這個窗口是由哪個線程創建的,然後,把它放進窗口創建線程的線程消息隊列。線程消息隊列接收送給該線程所創建窗口的消息。線程從消息隊列取出消息,通過Windows把它送給適當的窗口過程來處理。

    除了鍵盤、鼠標消息以外,隊列消息還有WM_PAINT、WM_TIMER和WM_QUIT。

    這些隊列消息以外的絕大多數消息是非隊列消息。

  2. 系統消息和應用程序消息

從消息的來源來看,可以分為:系統定義的消息和應用程序定義的消息。

系統消息ID的範圍是從0到WM_USER-1,或0X80000到0XBFFFF;應用程序消息從WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF範圍的消息由應用程序自己使用;0XC000到0XFFFF範圍的消息用來和其他應用程序通信,為了ID的唯一性,使用::RegisterWindowMessage來得到該範圍的消息ID。

      1. 消息結構和消息處理

  1. 消息的結構

    為了從消息隊列獲取消息信息,需要使用MSG結構。例如,::GetMessage函數(從消息隊列得到消息並從隊列中移走)和::PeekMessage函數(從消息隊列得到消息但是可以不移走)都使用了該結構來保存獲得的消息信息。

    MSG結構的定義如下:

    typedef struct tagMSG { // msg

    HWND hwnd;

    UINT message;

    WPARAM wParam;

    LPARAM lParam;

    DWORD time;

    POINT pt;

    } MSG;

    該結構包括了六個成員,用來描述消息的有關屬性:

    接收消息的窗口句柄、消息標識(ID)、第一個消息參數、第二個消息參數、消息產生的時間、消息產生時鼠標的位置。

  2. 應用程序通過窗口過程來處理消息

    如前所述,每個“窗口類”都要登記一個如下形式的窗口過程:

    LRESULT CALLBACK MainWndProc (

    HWND hwnd,// 窗口句柄

    UINT msg,// 消息標識

    WPARAM wParam,//消息參數1

    LPARAM lParam//消息參數2

    )

    應用程序通過窗口過程來處理消息:非隊列消息由Windows直接送給目的窗口的窗口過程,隊列消息由::DispatchMessage等派發給目的窗口的窗口過程。窗口過程被調用時,接受四個參數:

    a window handle(窗口句柄);

    a message identifier(消息標識);

    two 32-bit values called message parameters(兩個32位的消息參數);

    需要的話,窗口過程用::GetMessageTime獲取消息產生的時間,用::GetMessagePos獲取消息產生時鼠標光標所在的位置。

    在窗口過程裏,用switch/case分支處理語句來識別和處理消息。

  3. 應用程序通過消息循環來獲得對消息的處理

    每個GDI應用程序在主窗口創建之後,都會進入消息循環,接受用戶輸入、解釋和處理消息。

    消息循環的結構如下:

    while (GetMessage(&msg, (HWND) NULL, 0, 0)) {//從消息隊列得到消息

    if (hwndDlgModeless == (HWND) NULL ||

    !IsDialogMessage(hwndDlgModeless, &msg) &&

    !TranslateAccelerator(hwndMain, haccel, &msg)) {

    TranslateMessage(&msg);

    DispatchMessage(&msg); //發送消息

    }

    }

    消息循環從消息隊列中得到消息,如果不是快捷鍵消息或者對話框消息,就進行消息轉換和派發,讓目的窗口的窗口過程來處理。

    當得到消息WM_QUIT,或者::GetMessage出錯時,退出消息循環。

  4. MFC消息處理

使用MFC框架編程時,消息發送和處理的本質也如上所述。但是,有一點需要強調的是,所有的MFC窗口都使用同一窗口過程,程序員不必去設計和實現自己的窗口過程,而是通過MFC提供的一套消息映射機制來處理消息。因此,MFC簡化了程序員編程時處理消息的復雜性。

所謂消息映射,簡單地講,就是讓程序員指定要某個MFC類(有消息處理能力的類)處理某個消息。MFC提供了工具ClassWizard來幫助實現消息映射,在處理消息的類中添加一些有關消息映射的內容和處理消息的成員函數。程序員將完成消息處理函數,實現所希望的消息處理能力。

如果派生類要覆蓋基類的消息處理函數,就用ClassWizard在派生類中添加一個消息映射條目,用同樣的原型定義一個函數,然後實現該函數。這個函數覆蓋派生類的任何基類的同名處理函數。

下面幾節將分析MFC的消息機制的實現原理和消息處理的過程。為此,首先要分析ClassWizard實現消息映射的內幕,然後討論MFC的窗口過程,分析MFC窗口過程是如何實現消息處理的。

    1. 消息映射的定義和實現

      1. MFC處理的三類消息

根據處理函數和處理過程的不同,MFC主要處理三類消息:

  • Windows消息,前綴以“WM_”打頭,WM_COMMAND例外。Windows消息直接送給MFC窗口過程處理,窗口過程調用對應的消息處理函數。一般,由窗口對象來處理這類消息,也就是說,這類消息處理函數一般是MFC窗口類的成員函數。
  • 控制通知消息,是控制子窗口送給父窗口的WM_COMMAND通知消息。窗口過程調用對應的消息處理函數。一般,由窗口對象來處理這類消息,也就是說,這類消息處理函數一般是MFC窗口類的成員函數。

需要指出的是,Win32使用新的WM_NOFITY來處理復雜的通知消息。WM_COMMAND類型的通知消息僅僅能傳遞一個控制窗口句柄(lparam)、控制窗ID和通知代碼(wparam)。WM_NOTIFY能傳遞任意復雜的信息。

  • 命令消息,這是來自菜單、工具條按鈕、加速鍵等用戶接口對象的WM_COMMAND通知消息,屬於應用程序自己定義的消息。通過消息映射機制,MFC框架把命令按一定的路徑分發給多種類型的對象(具備消息處理能力)處理,如文檔、窗口、應用程序、文檔模板等對象。能處理消息映射的類必須從CCmdTarget類派生。

在討論了消息的分類之後,應該是討論各類消息如何處理的時候了。但是,要知道怎麽處理消息,首先要知道如何映射消息。

      1. MFC消息映射的實現方法

        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。

        為什麽這樣做之後就完成了一個消息映射?這些聲明和實現到底作了些什麽呢?接著,將討論這些問題。

      2. 在聲明與實現的內部

  1. 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的情形。

  2. 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相對簡單,就只有一種定義。

  3. ON_COMMAND宏

最後,看ON_COMMAND宏的內容:

#define ON_COMMAND(id, memberFxn) \

{\

WM_COMMAND,\

CN_COMMAND,\

(WORD)id,\

(WORD)id,\

AfxSig_vv,\

(AFX_PMSG)memberFxn\

};

        1. 消息映射聲明的解釋

在清楚了有關宏的定義之後,現在來分析它們的作用和功能。

消息映射聲明的實質是給所在類添加幾個靜態成員變量和靜態或虛擬函數,當然它們是與消息映射相關的變量和函數。

  1. 成員變量

有兩個成員變量被添加,第一個是_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時,是基類消息映射數組的地址)。

  1. 成員函數

  • _GetBaseMessageMap()

用來得到基類消息映射的函數。

  • GetMessageMap()

用來得到自身消息映射的函數。

        1. 消息映射實現的解釋

消息映射實現的實質是初始化聲明中定義的靜態成員函數_messageEntries和messageMap,實現所聲明的靜態或虛擬函數GetMessageMap、_GetBaseMessageMap。

這樣,在進入WinMain函數之前,每個可以響應消息的MFC類都生成了一個消息映射表,程序運行時通過查詢該表判斷是否需要響應某條消息。

  1. 對消息映射入口表(消息映射數組)的初始化

    如前所述,消息映射數組的元素是消息映射條目,條目的格式符合結構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的內容,它標識消息處理類的消息映射條目的終止。

  2. 對messageMap的初始化

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

    經過初始化,域lpEntries保存了消息映射數組_messageEntries的地址;如果動態鏈接到MFC DLL,則pfnGetBaseMap保存了_GetBaseMessageMap成員函數的地址;否則pBaseMap保存了基類的消息映射數組的地址。

  3. 對函數的實現

_GetBaseMessageMap()

它返回基類的成員變量messagMap(當使用MFC DLL時),使用該函數得到基類消息映射入口表。

GetMessageMap():

它返回成員變量messageMap,使用該函數得到自身消息映射入口表。

順便說一下,消息映射類的基類CCmdTarget也實現了上述和消息映射相關的函數,不過,它的消息映射數組是空的。

既然消息映射宏方便了消息映射的實現,那麽有必要詳細的討論消息映射宏。下一節,介紹消息映射宏的分類、用法和用途。

      1. 消息映射宏的種類

為了簡化程序員的工作,MFC定義了一系列的消息映射宏和像AfxSig_vv這樣的枚舉變量,以及標準消息處理函數,並且具體地實現這些函數。這裏主要討論消息映射宏,常用的分為以下幾類。

  1. 用於Windows消息的宏,前綴為“ON_WM_”。

    這樣的宏不帶參數,因為它對應的消息和消息處理函數的函數名稱、函數原型是確定的。MFC提供了這類消息處理函數的定義和缺省實現。每個這樣的宏處理不同的Windows消息。

    例如:宏ON_WM_CREATE()把消息WM_CREATE映射到OnCreate函數,消息映射條目的第一個成員nMessage指定為要處理的Windows消息的ID,第二個成員nCode指定為0。

  2. 用於命令消息的宏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節先對其作一個大略的介紹。

    1. CcmdTarget類

除了CObject類外,還有一個非常重要的類CCmdTarget。所有響應消息或事件的類都從它派生。例如,CWinapp,CWnd,CDocument,CView,CDocTemplate,CFrameWnd,等等。

CCmdTarget類是MFC處理命令消息的基礎、核心。MFC為該類設計了許多成員函數和一些成員數據,基本上是為了解決消息映射問題的,而且,很大一部分是針對OLE設計的。在OLE應用中,CCmdTarget是MFC處理模塊狀態的重要環節,它起到了傳遞模塊狀態的作用:其構造函數獲取當前模塊狀態,並保存在成員變量m_pModuleState裏頭。關於模塊狀態,在後面章節講述。

CCmdTarget有兩個與消息映射有密切關系的成員函數:DispatchCmdMsg和OnCmdMsg。

  1. 靜態成員函數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章節命令消息的處理中作更詳細的描述。

  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默認對象;在新的非默認對象中或在可能要傳出命令的命令目標中。

本節對CCmdTarget的兩個成員函數作一些討論,是為了對MFC的消息處理有一個大致印象。後面4.4.3.2節和4.4.3.3節將作進一步的討論。

    1. MFC窗口過程

      前文曾經提到,所有的消息都送給窗口過程處理,MFC的所有窗口都使用同一窗口過程,消息或者直接由窗口過程調用相應的消息處理函數處理,或者按MFC命令消息派發路徑送給指定的命令目標處理。

      那麽,MFC的窗口過程是什麽?怎麽處理標準Windows消息?怎麽實現命令消息的派發?這些都將是下文要回答的問題。

      1. MFC窗口過程的指定

        從前面的討論可知,每一個“窗口類”都有自己的窗口過程。正常情況下使用該“窗口類”創建的窗口都使用它的窗口過程。

        MFC的窗口對象在創建HWND窗口時,也使用了已經註冊的“窗口類”,這些“窗口類”或者使用應用程序提供的窗口過程,或者使用Windows提供的窗口過程(例如Windows控制窗口、對話框等)。那麽,為什麽說MFC創建的所有HWND窗口使用同一個窗口過程呢?

        在MFC中,的確所有的窗口都使用同一個窗口過程:AfxWndProc或AfxWndProcBase(如果定義了_AFXDLL)。它們的原型如下:

        LRESULT CALLBACK

        AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

        LRESULT CALLBACK

        AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam)

        這兩個函數的原型都如4.1.1節描述的窗口過程一樣。

        如果動態鏈接到MFC DLL(定義了_AFXDLL),則AfxWndProcBase被用作窗口過程,否則AfxWndProc被用作窗口過程。AfxWndProcBase首先使用宏AFX_MANAGE_STATE設置正確的模塊狀態,然後調用AfxWndProc。

        下面,假設不使用MFC DLL,討論MFC如何使用AfxWndProc取代各個窗口的原窗口過程。

        窗口過程的取代發生在窗口創建的過程時,使用了子類化(Subclass)的方法。所以,從窗口的創建過程來考察取代過程。從前面可以知道,窗口創建最終是通過調用CWnd::CreateEx函數完成的,分析該函數的流程,如圖4-1所示。

        消息映射的實現

        圖4-1中的CREATESTRUCT結構類型的變量cs包含了傳遞給窗口過程的初始化參數。CREATESTRUCT結構描述了創建窗口所需要的信息,定義如下:

        typedef struct tagCREATESTRUCT {

        LPVOID lpCreateParams; //用來創建窗口的數據

        HANDLE hInstance; //創建窗口的實例

        HMENU hMenu; //窗口菜單

        HWND hwndParent; //父窗口

        int cy; //高度

        int cx; //寬度

        int y; //原點Y坐標

        int x;//原點X坐標

        LONG style; //窗口風格

        LPCSTR lpszName; //窗口名

        LPCSTR lpszClass; //窗口類

        DWORD dwExStyle; //窗口擴展風格

        } CREATESTRUCT;

        cs表示的創建參數可以在創建窗口之前被程序員修改,程序員可以覆蓋當前窗口類的虛擬成員函數PreCreateWindow,通過該函數來修改cs的style域,改變窗口風格。這裏cs的主要作用是保存創建窗口的各種信息,::CreateWindowEx函數使用cs的各個域作為參數來創建窗口,關於該函數見2.2.2節。

        在創建窗口之前,創建了一個WH_CBT類型的鉤子(Hook)。這樣,創建窗口時所有的消息都會被鉤子過程函數_AfxCbtFilterHook截獲。

        AfxCbtFilterHook函數首先檢查是不是希望處理的Hook──HCBT_CREATEWND。如果是,則先把MFC窗口對象(該對象必須已經創建了)和剛剛創建的Windows窗口對象捆綁在一起,建立它們之間的映射(見後面模塊-線程狀態);然後,調用::SetWindowLong設置窗口過程為AfxWndProc,並保存原窗口過程在窗口類成員變量m_pfnSuper中,這樣形成一個窗口過程鏈。需要的時候,原窗口過程地址可以通過窗口類成員函數GetSuperWndProcAddr得到。

        這樣,AfxWndProc就成為CWnd或其派生類的窗口過程。不論隊列消息,還是非隊列消息,都送到AfxWndProc窗口過程來處理(如果使用MFC DLL,則AfxWndProcBase被調用,然後是AfxWndProc)。經過消息分發之後沒有被處理的消息,將送給原窗口過程處理。

        最後,有一點可能需要解釋:為什麽不直接指定窗口過程為AfxWndProc,而要這麽大費周折呢?這是因為原窗口過程(“窗口類”指定的窗口過程)常常是必要的,是不可缺少的。

        接下來,討論AfxWndProc窗口過程如何使用消息映射數據實現消息映射。Windows消息和命令消息的處理不一樣,前者沒有消息分發的過程。

      2. 對Windows消息的接收和處理

        Windows消息送給AfxWndProc窗口過程之後,AfxWndProc得到HWND窗口對應的MFC窗口對象,然後,搜索該MFC窗口對象和其基類的消息映射數組,判定它們是否處理當前消息,如果是則調用對應的消息處理函數,否則,進行缺省處理。

        下面,以一個應用程序的視窗口創建時,對WM_CREATE消息的處理為例,詳細地討論Windows消息的分發過程。

        用第一章的例子,類CTview要處理WM_CREATE消息,使用ClassWizard加入消息處理函數CTview::OnCreate。下面,看這個函數怎麽被調用:

        視窗口最終調用::CreateEx函數來創建。由Windows系統發送WM_CREATE消息給視的窗口過程AfxWndProc,參數1是創建的視窗口的句柄,參數2是消息ID(WM_CREATE),參數3、4是消息參數。圖4-2描述了其余的處理過程。圖中函數的類屬限制並非源碼中所具有的,而是根據處理過程得出的判斷。例如,“CWnd::WindowProc”表示CWnd類的虛擬函數WindowProc被調用,並不一定當前對象是CWnd類的實例,事實上,它是CWnd派生類CTview類的實例;而“CTview::OnCreate”表示CTview的消息處理函數OnCreate被調用。下面描述每一步的詳細處理。

        消息映射的實現

        1. 從窗口過程到消息映射

首先,分析AfxWndProc窗口過程函數。

  • AfxWndProc的原型如下:

LRESULT AfxWndProc(HWND hWnd,

UINT nMsg, WPARAM wParam, LPARAM lParam)

如果收到的消息nMsg不是WM_QUERYAFXWNDPROC(該消息被MFC內部用來確認窗口過程是否使用AfxWndProc),則從hWnd得到對應的MFC Windows對象(該對象必須已存在,是永久性<Permanent>對象)指針pWnd。pWnd所指的MFC窗口對象將負責完成消息的處理。這裏,pWnd所指示的對象是MFC視窗口對象,即CTview對象。

然後,把pWnd和AfxWndProc接受的四個參數傳遞給函數AfxCallWndProc執行。

  • AfxCallWndProc原型如下:

LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd,

UINT nMsg, WPARAM wParam = 0, LPARAM lParam = 0)

MFC使用AfxCallWndProc函數把消息送給CWnd類或其派生類的對象。該函數主要是把消息和消息參數(nMsg、wParam、lParam)傳遞給MFC窗口對象的成員函數WindowProc(pWnd->WindowProc)作進一步處理。如果是WM_INITDIALOG消息,則在調用WindowProc前後要作一些處理。

WindowProc的函數原型如下:

LRESULT CWnd::WindowProc(UINT message,

WPARAM wParam, LPARAM lParam)

這是一個虛擬函數,程序員可以在CWnd的派生類中覆蓋它,改變MFC分發消息的方式。例如,MFC的CControlBar就覆蓋了WindowProc,對某些消息作了自己的特別處理,其他消息處理由基類的WindowProc函數完成。

但是在當前例子中,當前對象的類CTview沒有覆蓋該函數,所以CWnd的WindowProc被調用。

這個函數把下一步的工作交給OnWndMsg函數來處理。如果OnWndMsg沒有處理,則交給DefWindowProc來處理。

OnWndMsg和DefWindowProc都是CWnd類的虛擬函數。

  • OnWndMsg的原型如下:

BOOL CWnd::OnWndMsg( UINT message,

WPARAM wParam, LPARAM lParam,RESULT*pResult );

該函數是虛擬函數。

和WindowProc一樣,由於當前對象的類CTview沒有覆蓋該函數,所以CWnd的OnWndMsg被調用。

在CWnd中,MFC使用OnWndMsg來分別處理各類消息:

如果是WM_COMMAND消息,交給OnCommand處理;然後返回。

如果是WM_NOTIFY消息,交給OnNotify處理;然後返回。

如果是WM_ACTIVATE消息,先交給_AfxHandleActivate處理(後面5.3.3.7節會解釋它的處理),再繼續下面的處理。

如果是WM_SETCURSOR消息,先交給_AfxHandleSetCursor處理;然後返回。

如果是其他的Windows消息(包括WM_ACTIVATE),則

首先在消息緩衝池進行消息匹配,

若匹配成功,則調用相應的消息處理函數;

若不成功,則在消息目標的消息映射數組中進行查找匹配,看它是否處理當前消息。這裏,消息目標即CTview對象。

如果消息目標處理了該消息,則會匹配到消息處理函數,調用它進行處理;

否則,該消息沒有被應用程序處理,OnWndMsg返回FALSE。

關於Windows消息和消息處理函數的匹配,見下一節。

缺省處理函數DefWindowProc將在討論對話框等的實現時具體分析。

        1. Windows消息的查找和匹配

          CWnd或者派生類的對象調用OnWndMsg搜索本對象或者基類的消息映射數組,尋找當前消息的消息處理函數。如果當前對象或者基類處理了當前消息,則必定在其中一個類的消息映射數組中匹配到當前消息的處理函數。

          消息匹配是一個比較耗時的任務,為了提高效率,MFC設計了一個消息緩衝池,把要處理的消息和匹配到的消息映射條目(條目包含了消息處理函數的地址)以及進行消息處理的當前類等信息構成一條緩衝信息,放到緩衝池中。如果以後又有同樣的消息需要同一個類處理,則直接從緩衝池查找到對應的消息映射條目就可以了。

          MFC用哈希查找來查詢消息映射緩衝池。消息緩衝池相當於一個哈希表,它是應用程序的一個全局變量,可以放512條最新用到的消息映射條目的緩衝信息,每一條緩衝信息是哈希表的一個入口。

          采用AFX_MSG_CACHE結構描述每條緩衝信息,其定義如下:

          struct AFX_MSG_CACHE

          {

          UINT nMsg;

          const AFX_MSGMAP_ENTRY* lpEntry;

          const AFX_MSGMAP* pMessageMap;

          };

          nMsg存放消息ID,每個哈希表入口有不同的nMsg。

          lpEnty存放和消息ID匹配的消息映射條目的地址,它可能是this所指對象的類的映射條目,也可能是這個類的某個基類的映射條目,也可能是空。

          pMessageMap存放消息處理函數匹配成功時進行消息處理的當前類(this所指對象的類)的靜態成員變量messageMap的地址,它唯一的標識了一個類(每個類的messageMap變量都不一樣)。

          this所指對象是一個CWnd或其派生類的實例,是正在處理消息的MFC窗口對象。

          哈希查找:使用消息ID的值作為關鍵值進行哈希查找,如果成功,即可從lpEntry獲得消息映射條目的地址,從而得到消息處理函數及其原型。

          如何判斷是否成功匹配呢?有兩條標準:

          第一,當前要處理的消息message在哈希表(緩衝池)中有入口;第二,當前窗口對象(this所指對象)的類的靜態變量messageMap的地址應該等於本條緩衝信息的pMessagMap。MFC通過虛擬函數GetMessagMap得到messageMap的地址。

          如果在消息緩衝池中沒有找到匹配,則搜索當前對象的消息映射數組,看是否有合適的消息處理函數。

          如果匹配到一個消息處理函數,則把匹配結果加入到消息緩衝池中,即填寫該條消息對應的哈希表入口:

          nMsg=message;

          pMessageMap=this->GetMessageMap;

          lpEntry=查找結果

          然後,調用匹配到的消息處理函數。否則(沒有找到),使用_GetBaseMessageMap得到基類的消息映射數組,查找和匹配;直到匹配成功或搜尋了所有的基類(到CCmdTarget)為止。

          如果最後沒有找到,則也把該條消息的匹配結果加入到緩衝池中。和匹配成功不同的是:指定lpEntry為空。這樣OnWndMsg返回,把控制權返還給AfxCallWndProc函數,AfxCallWndProc將繼續調用DefWndProc進行缺省處理。

          消息映射數組的搜索在CCmdTarget::OnCmdMsg函數中也用到了,而且算法相同。為了提高速度,MFC把和消息映射數組條目逐一比較、匹配的函數AfxFindMessageEntry用匯編書寫。

          const AFX_MSGMAP_ENTRY* AFXAPI

          AfxFindMessageEntry(const AFX_MSGMAP_ENTRY* lpEntry,

          UINT nMsg, UINT nCode, UINT nID)

          第一個參數是要搜索的映射數組的入口;第二個參數是Windows消息標識;第三個參數是控制通知消息標識;第四個參數是命令消息標識。

          對Windows消息來說,nMsg是每條消息不同的,nID和nCode為0。

          對命令消息來說,nMsg固定為WM_COMMAND,nID是每條消息不同,nCode都是CN_COMMAND(定義為0)。

          對控制通知消息來說,nMsg固定為WM_COMMAND或者WM_NOTIFY,nID和nCode是每條消息不同。

          對於Register消息,nMsg指定為0XC000,nID和nCode為0。在使用函數AfxFindMessageEntry得到匹配結果之後,還必須判斷nSig是否等於message,只有相等才調用對應的消息處理函數。

        2. Windows消息處理函數的調用

          對一個Windows消息,匹配到了一個消息映射條目之後,將調用映射條目所指示的消息處理函數。

          調用處理函數的過程就是轉換映射條目的pfn指針為適當的函數類型並執行它:MFC定義了一個成員函數指針mmf,首先把消息處理函數的地址賦值給該函數指針,然後根據消息映射條目的nSig值轉換指針的類型。但是,要給函數指針mmf賦值,必須使該指針可以指向所有的消息處理函數,為此則該指針的類型是所有類型的消息處理函數指針的聯合體。

          對上述過程,MFC的實現大略如下:

          union MessageMapFunctions mmf;

          mmf.pfn = lpEntry->pfn;

          swithc (value_of_nsig){

          case AfxSig_is: //OnCreate就是該類型

          lResult = (this->*mmf.pfn_is)((LPTSTR)lParam);

          break;

          default:

          ASSERT(FALSE); break;

          }

          LDispatchRegistered: // 處理registered windows messages

          ASSERT(message >= 0xC000);

          mmf.pfn = lpEntry->pfn;

          lResult = (this->*mmf.pfn_lwl)(wParam, lParam);

          如果消息處理函數有返回值,則返回該結果,否則,返回TRUE。

          對於圖4-1所示的例子,nSig等於AfxSig_is,所以將執行語句

          (this->*mmf.pfn_is)((LPTSTR)lParam)

          也就是對CTview::OnCreate的調用。

          順便指出,對於Registered窗口消息,消息處理函數都是同一原型,所以都被轉換成lwl型(關於Registered窗口消息的映射,見4.4.2節)。

          綜上所述,標準Windwos消息和應用程序消息中的Registered消息,由窗口過程直接調用相應的處理函數處理:

          如果某個類型的窗口(C++類)處理了某條消息(覆蓋了CWnd或直接基類的處理函數),則對應的HWND窗口(Winodws window)收到該消息時就調用該覆蓋函數來處理;如果該類窗口沒有處理該消息,則調用實現該處理函數最直接的基類(在C++的類層次上接近該類)來處理,上述例子中如果CTview不處理WM_CREATE消息,則調用上一層的CWnd::OnCreate處理;

          如果基類都不處理該消息,則調用DefWndProc來處理。

        3. 消息映射機制完成虛擬函數功能的原理

綜合對Windows消息的處理來看,MFC使用消息映射機制完成了C++虛擬函數的功能。這主要基於以下幾點:

  • 所有處理消息的類從CCmdTarget派生。
  • 使用靜態成員變量_messageEntries數組存放消息映射條目,使用靜態成員變量messageMap來唯一地區別和得到類的消息映射。
  • 通過GetMessage虛擬函數來獲取當前對象的類的messageMap變量,進而得到消息映射入口。
  • 按照先底層,後基層的順序在類的消息映射數組中搜索消息處理函數。基於這樣的機制,一般在覆蓋基類的消息處理函數時,應該調用基類的同名函數。

以上論斷適合於MFC其他消息處理機制,如對命令消息的處理等。不同的是其他消息處理有一個命令派發/分發的過程。

下一節,討論命令消息的接受和處理。

      1. 對命令消息的接收和處理

        1. MFC標準命令消息的發送

在SDI或者MDI應用程序中,命令消息由用戶界面對象(如菜單、工具條等)產生,然後送給主邊框窗口。主邊框窗口使用標準MFC窗口過程處理命令消息。窗口過程把命令傳遞給MFC主邊框窗口對象,開始命令消息的分發。MFC邊框窗口類CFrameWnd提供了消息分發的能力。

下面,還是通過一個例子來說明命令消息的處理過程。

使用AppWizard產生一個單文檔應用程序t。從help菜單選擇“About”,就會彈出一個ABOUT對話框。下面,討論從命令消息的發出到對話框彈出的過程。

首先,選擇“ About”菜單項的動作導致一個Windows命令消息ID_APP_ABOUT的產生。Windows系統發送該命令消息到邊框窗口,導致它的窗口過程AfxWndProc被調用,參數1是邊框窗口的句柄,參數2是消息ID(即WM_COMMAND),參數3、4是消息參數,參數3的值是ID_APP_ABOUT。接著的系列調用如圖4-3所示。

消息映射的實現

下面分別講述每一層所調用的函數。

前4步同對Windows消息的處理。這裏接受消息的HWND窗口是主邊框窗口,因此,AfxWndProc根據HWND句柄得到的MFC窗口對象是MFC邊框窗口對象。

在4.2.2節談到,如果CWnd::OnWndMsg判斷要處理的消息是命令消息(WM_COMMAND),就調用OnCommand進一步處理。由於OnCommand是虛擬函數,當前MFC窗口對象是邊框窗口對象,它的類從CFrameWnd類導出,沒有覆蓋CWnd的虛擬函數OnCommand,而CFrameWnd覆蓋了CWnd的OnCommand,所以,CFrameWnd的OnCommand被調用。換句話說,CFrameWnd的OnCommand被調用是動態約束的結果。接著介紹的本例子的有關調用,也是通過動態約束而實際發生的函數調用。

接著的有關調用,將不進行為什麽調用某個類的虛擬或者消息處理函數的分析。

(1)CFrameWnd的OnCommand函數

BOOL CFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)

參數wParam的低階word存放了菜單命令nID或控制子窗口ID;如果消息來自控制窗口,高階word存放了控制通知消息;如果消息來自加速鍵,高階word值為1;如果消息來自菜單,高階word值為0。

如果是通知消息,參數lParam存放了控制窗口的句柄hWndCtrl,其他情況下lParam是0。

在這個例子裏,低階word是ID_APP_ABOUT,高階word是1;lParam是0。

MFC對CFrameWnd的缺省實現主要是獲得一個機會來檢查程序是否運行在HELP狀態,需要執行上下文幫助,如果不需要,則調用基類的CWnd::OnCommand實現正常的命令消息發送。

(2)CWnd的OnCommand函數

BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam)

它按一定的順序處理命令或者通知消息,如果發送成功,返回TRUE,否則,FALSE。處理順序如下:

如果是命令消息,則調用OnCmdMsg(nID, CN_UPDATE_COMMAND_UI, &state, NULL)測試nID命令是否已經被禁止,如果這樣,返回FALSE;否則,調用OnCmdMsg進行命令發送。關於CN_UPDATE_COMMAND_UI通知消息,見後面用戶界面狀態的更新處理。

如果是控制通知消息,則先用ReflectLastMsg反射通知消息到子窗口。如果子窗口處理了該消息,則返回TRUE;否則,調用OnCmdMsg進行命令發送。關於通知消息的反射見後面4.4.4.3節。OnCommand給OnCmdMsg傳遞四個參數:nID,即命令消息ID;nCode,如果是通知消息則為通知代碼,如果是命令消息則為NC_COMMAND(即0);其余兩個參數為空。

(3)CFrameWnd的OnCmdMsg函數

BOOL CFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

參數1是命令ID;如果是通知消息(WM_COMMAND或者WM_NOTIFY),則參數2表示通知代碼,如果是命令消息,參數2是0;如果是WM_NOTIFY,參數3包含了一些額外的信息;參數4在正常消息處理中應該是空。

在這個例子裏,參數1是命令ID,參數2為0,參數3空。

OnCmdMsg是虛擬函數,CFrameWnd覆蓋了該函數,當前對象(this所指)是MFC單文檔的邊框窗口對象。故CFrameWnd的OnCmdMsg被調用。CFrameWnd::OnCmdMsg在MFC消息發送中占有非常重要的地位,MFC對該函數的缺省實現確定了MFC的標準命令發送路徑:

  1. 送給活動(Active)視處理,調用活動視的OnCmdMsg。由於當前對象是MFC視對象,所以,OnCmdMsg將搜索CTview及其基類的消息映射數組,試圖得到相應的處理函數。
  2. 如果視對象自己不處理,則視得到和它關聯的文檔,調用關聯文檔的OnCmdMsg。由於當前對象是MFC視對象,所以,OnCmdMsg將搜索CTdoc及其基類的消息映射數組,試圖得到相應的處理函數。
  3. 如果文檔對象不處理,則它得到管理文檔的文檔模板對象,調用文檔模板的OnCmdMsg。由於當前對象是MFC文檔模板對象,所以,OnCmdMsg將搜索文檔模板類及其基類的消息映射數組,試圖得到相應的處理函數。
  4. 如果文檔模板不處理,則把沒有處理的信息逐級返回:文檔模板告訴文檔對象,文檔對象告訴視對象,視對象告訴邊框窗口對象。最後,邊框窗口得知,視、文檔、文檔模板都沒有處理消息。
  5. CFrameWnd的OnCmdMsg繼續調用CWnd::OnCmdMsg(斜體表示有類屬限制)來處理消息。由於CWnd沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由於當前對象是MFC邊框窗口對象,所以OnCmdMsg函數將搜索CMainFrame類及其所有基類的消息映射數組,試圖得到相應的處理函數。CWnd沒有實現OnCmdMsg卻指定要執行其OnCmdMsg函數,可能是為了以後MFC給CWnd實現了OnCmdMsg之後其他代碼不用改變。

    這一步是邊框窗口自己嘗試處理消息。

  6. 如果邊框窗口對象不處理,則送給應用程序對象處理。調用CTApp的OnCmdMsg,由於實際上CTApp及其基類CWinApp沒有覆蓋OnCmdMsg,故實際上調用了函數CCmdTarget::OnCmdMsg。由於當前對象是MFC應用程序對象,所以OnCmdMsg函數將搜索CTApp類及其所有基類的的消息映射入口數組,試圖得到相應的處理函數
  7. 如果應用程序對象不處理,則返回FALSE,表明沒有命令目標處理當前的命令消息。這樣,函數逐級別返回,OnCmdMsg告訴OnCommand消息沒有被處理,OnCommand告訴OnWndMsg消息沒有被處理,OnWndMsg告訴WindowProc消息沒有被處理,於是WindowProc調用DefWindowProc進行缺省處理。

本例子在第六步中,應用程序對ID_APP_ABOUT消息作了處理。它找到處理函數CTApp::OnAbout,使用DispatchCmdMsg派發消息給該函數處理。

如果是MDI邊框窗口,標準發送路徑還有一個環節,該環節和第二、三、四步所涉及的OnCmdMsg函數,將在下兩節再次具體分析。

        1. 命令消息的派發和消息的多次處理

  1. 命令消息的派發

    如前3.1所述,CCmdTarget的靜態成員函數DispatchCmdMsg用來派發命令消息給指定的命令目標的消息處理函數。

    static BOOL DispatchCmdMsg(CCmdTarget* pTarget,

    UINT nID, int nCode,

    AFX_PMSG pfn, void* pExtra, UINT nSig,

    AFX_CMDHANDLERINFO* pHandlerInfo)

    前面在講CCmdTarget時,提到了該函數。這裏講述它的實現:

    第一個參數指向處理消息的對象;第二個參數是命令ID;第三個是通知消息等;第四個是消息處理函數地址;第五個參數用於存放一些有用的信息,根據nCode的值表示不同的意義,例如當消息是WM_NOFITY,指向一個NMHDR結構(關於WM_NOTIFY,參見4.4.4.2節通知消息的處理);第六個參數標識消息處理函數原型;第七個參數是一個指針,指向AFX_CMDHANDLERINFO結構。前六個參數(除了第五個外)都是向函數傳遞信息,第五個和第七個參數是雙向的,既向函數傳遞信息,也可以向調用者返回信息。

    關於AFX_CMDHANDLERINFO結構:

    struct AFX_CMDHANDLERINFO

    {

    CCmdTarget* pTarget;

    void (AFX_MSG_CALL CCmdTarget::*pmf)(void);

    };

    第一個成員是一個指向命令目標對象的指針,第二個成員是一個指向CCmdTarget成員函數的指針。

    該函數的實現流程可以如下描述:

    首先,它檢查參數pHandlerInfo是否空,如果不空,則用pTarget和pfn填寫其指向的結構,返回TRUE;通常消息處理時傳遞來的pHandlerInfo空,而在使用OnCmdMsg來測試某個對象是否處理某條命令時,傳遞一個非空的pHandlerInfo指針。若返回TRUE,則表示可以處理那條消息。

    如果pHandlerInfo空,則進行消息處理函數的調用。它根據參數nSig的值,把參數pfn的類型轉換為要調用的消息處理函數的類型。這種指針轉換技術和前面講述的Windows消息的處理是一樣的。

  2. 消息的多次處理

如果消息處理函數不返回值,則DispatchCmdMsg返回TRUE;否則,DispatchCmdMsg返回消息處理函數的返回值。這個返回值沿著消息發送相反的路徑逐級向上傳遞,使得各個環節的OnCmdMsg和OnCommand得到返回的處理結果:TRUE或者FALSE,即成功或者失敗。

這樣就產生了一個問題,如果消息處理函數有意返回一個FALSE,那麽不就傳遞了一個錯誤的信息?例如,OnCmdMsg函數得到FALSE返回值,就認為消息沒有被處理,它將繼續發送消息到下一環節。的確是這樣的,但是這不是MFC的漏洞,而是有意這麽設計的,用來處理一些特別的消息映射宏,實現同一個消息的多次處理。

通常的命令或者通知消息是沒有返回值的(見4.4.2節的消息映射宏),僅僅一些特殊的消息處理函數具有返回值,這類消息的消息處理函數是使用擴展消息映射宏映射的,例如:

ON_COMMAND對應的ON_COMMAND_EX

擴展映射宏和對應的普通映射宏的參數個數相同,含義一樣。但是擴展映射宏的消息處理函數的原型和對應的普通映射宏相比,有兩個不同之處:一是多了一個UINT類型的參數,另外就是有返回值(返回BOOL類型)。回顧4.4.2章節,範圍映射宏ON_COMMAND_RANGE的消息處理函數也有一個這樣的參數,該參數在兩處的含義是一樣的,例如:命令消息擴展映射宏ON_COMMAND_EX定義的消息處理函數解釋該參數是當前要處理的命令消息ID。有返回值的意義在於:如果擴展映射宏的消息處理函數返回FALSE,則導致當前消息被發送給消息路徑上的下一個消息目標處理。

綜合來看,ON_COMMAND_EX宏有兩個功能:

一是可以把多個命令消息指定給一個消息處理函數處理。這類似於ON_COMMAND_RANGE宏的作用。不過,這裏的多條消息的命令ID或者控制子窗口ID可以不連續,每條消息都需要一個ON_COMMAND_EX宏。

二是可以讓幾個消息目標處理同一個命令或者通知或者反射消息。如果消息發送路徑上較前的命令目標不處理消息或者處理消息後返回FALSE,則下一個命令目標將繼續處理該消息。

對於通知消息、反射消息,它們也有擴展映射宏,而且上述論斷也適合於它們。例如:

ON_NOTIFY對應的ON_NOTIFY_EX

ON_CONTROL對應的ON_CONTROL_EX

ON_CONTROL_REFLECT對應的ON_CONTROL_REFLECT_EX

等等。

範圍消息映射宏也有對應的擴展映射宏,例如:

ON_NOTIFY_RANGE對應的ON_NOTIFY_EX_RANGE

ON_COMMAND_RANGE對應的ON_COMMAND_EX_RANGE

使用這些宏的目的在於利用擴展宏的第二個功能:實現消息的多次處理。

關於擴展消息映射宏的例子,參見13.2..4.4節和13.2.4.6節。

        1. 一些消息處理類的OnCmdMsg的實現

從以上論述知道,OnCmdMsg虛擬函數在MFC命令消息的發送中扮演了重要的角色,CFrameWnd的OnCmdMsg實現了MFC的標準命令消息發送路徑。

那麽,就產生一個問題:如果命令消息不送給邊框窗口對象,那麽就不會有按標準命令發送路徑發送消息的過程?答案是肯定的。例如一個菜單被一個對話框窗口所擁有,那麽,菜單命令將送給MFC對話框窗口對象處理,而不是MFC邊框窗口處理,當然不會和CFrameWnd的處理流程相同。

但是,有一點需要指出,一般標準的SDI和MDI應用程序,只有主邊框窗口擁有菜單和工具條等用戶接口對象,只有在用戶與用戶接口對象進行交互時,才產生命令,產生的命令必然是送給SDI或者MDI程序的主邊框窗口對象處理。

下面,討論幾個MFC類覆蓋OnCmdMsg虛擬函數時的實現。這些類的OnCmdMsg或者可能是標準MFC命令消息路徑的一個環節,或者可能是一個獨立的處理過程(對於其中的MFC窗口類)。

從分析CView的OnCmdMsg實現開始。

  • CView的OnCmdMsg

CView::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

首先,調用CWnd::OnCmdMsg,結果是搜索當前視的類和基類的消息映射數組,搜索順序是從下層到上層。若某一層實現了對命令消息nID的處理,則調用它的實現函數;否則,調用m_pDocument->OnCmdMsg,把命令消息送給文檔類處理。m_pDocument是和當前視關聯的文檔對象指針。如果文檔對象類實現了OnCmdMsg,則調用它的覆蓋函數;否則,調用基類(例如CDocument)的OnCmdMsg。

接著,討論CDocument的實現。

  • CDocument的 OnCmdMsg

BOOL CDocument::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

首先,調用CCmdTarget::OnCmdMsg,導致當前對象(this)的類和基類的消息映射數組被搜索,看是否有對應的消息處理函數可用。如果有,就調用它;如果沒有,則調用文檔模板的OnCmdMsg函數(m_pTemplate->OnCmdMsg)把消息送給文檔模板處理。

MFC文檔模板沒有覆蓋OnCmdMsg,導致基類CCmdTarget的OnCmdMsg被調用,看是否有文檔模板類或基類實現了對消息的處理。是的話,調用對應的消息處理函數,否則,返回FALSE。從前面的分析知道,CCmdTarget類的消息映射數組是空的,所以這裏返回FALSE。

  • CDialog的OnCmdMsg

BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

  1. 調用CWnd::OnCmdMsg,讓對話框或其基類處理消息。
  2. 如果還沒有處理,而且是控制消息或系統命令或非命令按鈕,則返回FALSE,不作進一步處理。否則,調用父窗口的OnCmdmsg(GetParent()->OnCmdmsg)把消息送給父窗口處理。
  3. 如果仍然沒有處理,則調用當前線程的OnCmdMsg(GetThread()->OnCmdMsg)把消息送給線程對象處理。
  4. 如果最後沒有處理,返回FALSE。

  • CMDIFrameWnd的OnCmdMsg

對於MDI應用程序,MDI主邊框窗口首先是把命令消息發送給活動的MDI文檔邊框窗口進行處理。MDI主邊框窗口對OnCmdMsg的實現函數的原型如下:

BOOL CMDIFrameWnd::OnCmdMsg(UINT nID, int nCode, void* pExtra,

AFX_CMDHANDLERINFO* pHandlerInfo)

  1. 如果有激活的文檔邊框窗口,則調用它的OnCmdMsg(MDIGetActive()->OnCmdMsg)把消息交給它進行處理。MFC的文檔邊框窗口類並沒有覆蓋OnCmdMsg函數,所以基類CFrameWnd的函數被調用,導致文檔邊框窗口的活動視、文檔邊框窗口本身、應用程序對象依次來進行消息處理。
  2. 如果文檔邊框窗口沒有處理,調用CFrameWnd::OnCmdMsg把消息按標準路徑發送,重復第一次的步驟,不過對於MDI邊框窗口來說不存在活動視,所以省卻了讓視處理消息的必要;接著讓MDI邊框窗口本身來處理消息,如果它還沒有處理,則讓應用程序對象進行消息處理──雖然這是一個無用的重復。

除了CView、CDocument和CMDIFrameWnd類,還有幾個OLE相關的類覆蓋了OnCmdMsg函數。OLE的處理本書暫不涉及,CDialog::OnCmdMsg將在對話框章節專項討論其具體實現。

        1. 一些消息處理類的OnCommand的實現

除了虛擬函數OnCmdMsg,還有一個虛擬函數OnCommand在命令消息的發送中占有重要地位。在處理命令或者通知消息時,OnCommand被MFC窗口過程調用,然後它調用OnCmdMsg按一定路徑傳送消息。除了CWnd類和一些OLE相關類外,MFC裏主要還有MDI邊框窗口實現了OnCommand。

BOOL CMDIFrameWnd::OnCommand(WPARAM wParam, LPARAM lParam)

第一,如果存在活動的文檔邊框窗口,則使用AfxCallWndProc調用它的窗口過程,把消息送給文檔邊框窗口來處理。這將導致文檔邊框窗口的OnCmdMsg作如下的處理:

活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理

任何一個環節如果處理消息,則不再向下發送消息,處理終止。如果消息仍然沒有被處理,就只有交給主邊框窗口了。

第二,第一步沒有處理命令,繼續調用CFrameWnd::OnCommand,將導致CMDIFrameWnd的OnCmdMsg被調用。從前面的分析知道,將再次把消息送給MDI邊框窗口的活動文檔邊框窗口,第一步的過程除了文檔邊框窗口缺省處理外都將被重復。具體的處理過程見前文的CMDIFrameWnd::OnCmdMsg函數。

  1. 對於MDI消息,如果主邊框窗口還不處理的話,交給CMDIFrameWnd的DefWindowProc作缺省處理。
  2. 消息沒有處理,返回FALSE。

上述分析綜合了OnCommand和OnCmdMsg的處理,它們是在MFC內部MDI邊框窗口處理命令消息的完整的流程和標準的步驟。整個處理過程再次表明了邊框窗口在處理命令消息時的中心作用。從程序員的角度來看,可以認為整個標準處理路徑如下:

活動視處理消息→與視關聯的文檔處理消息→本文檔邊框窗口處理消息→應用程序對象處理消息→文檔邊框窗口缺省處理→MDI邊框窗口處理消息→MDI邊框窗口缺省處理

任何一個環節如果處理消息,不再向下發送消息,急處理終止。

      1. 對控制通知消息的接收和處理

        1. WM_COMMAND控制通知消息的處理

WM_COMMAND控制通知消息的處理和WM_COMMAND命令消息的處理類似,但是也有不同之處。

首先,分析處理WM_COMMAND控制通知消息和命令消息的相似處。如前所述,命令消息和控制通知消息都是由窗口過程給OnCommand處理(參見CWnd::OnWndMsg的實現),OnCommand通過wParam和lParam參數區分是命令消息或通知消息,然後送給OnCmdMsg處理(參見CWnd::OnCommnd的實現)。

其次,兩者的不同之處是:

  • 命令消息一般是送給主邊框窗口的,這時,邊框窗口的OnCmdMsg被調用;而控制通知消息送給控制子窗口的父窗口,這時,父窗口的OnCmdMsg被調用。
  • OnCmdMsg處理命令消息時,通過命令分發可以由多種命令目標處理,包括非窗口對象如文檔對象等;而處理控制通知消息時,不會有消息分發的過程,控制通知消息最終肯定是由窗口對象處理的。

不過,在某種程度上可以說,控制通知消息由窗口對象處理是一種習慣和約定。當使用ClassWizard進行消息映射時,它不提供把控制通知消息映射到非窗口對象的機會。但是,手工地添加消息映射,讓非窗口對象處理控制通知消息的可能是存在的。例如,對於CFormView,一方面它具備接受WM_COMMAND通知消息的條件,另一方面,具備把WM_COMMAND消息派發給關聯文檔對象處理的能力,所以給CFormView的通知消息是可以讓文檔對象處理的。

事實上,BN_CLICKED控制通知消息的處理和命令消息的處理完全一樣,因為該消息的通知代碼是0,ON_BN_CLICKED(id,memberfunction)和ON_COMMAND(id,memberfunction)是等同的。

此外,MFC的狀態更新處理機制就是建立在通知消息可以發送給各種命令目標的基礎之上的。關於MFC的狀態更新處理機制,見後面4.4.4.4節的討論。

  • 控制通知消息可以反射給子窗口處理。OnCommand判定當前消息是WM_COMAND通知消息之後,首先它把消息反射給控制子窗口處理,如果子窗口處理了反射消息,OnCommand不會繼續調用OnCmdMsg讓父窗口對象來處理通知消息。

        1. WM_NOTIFY消息及其處理:

(1)WM_NOTIFY消息

還有一種通知消息WM_NOTIFY,在Win32中用來傳遞信息復雜的通知消息。WM_NOTIFY消息怎麽來傳遞復雜的信息呢?WM_NOTIFY的消息參數wParam包含了發送通知消息的控制窗口ID,另一個參數lParam包含了一個指針。該指針指向一個NMHDR結構,或者更大的結構,只要它的第一個結構成員是NMHDR結構。

NMHDR結構:

typedef struct tagNMHDR {

HWND hwndFrom;

UINT idFrom;

UINT code;

} NMHDR;

上述結構有三個成員,分別是發送通知消息的控制窗口的句柄、ID和通知消息代碼。

舉一個更大、更復雜的結構例子:列表控制窗發送LVN_KEYDOWN控制通知消息,則lParam包含了一個指向LV_KEYDOWN結構的指針。其結構如下:

typedef struct tagLV_KEYDOWN {

NMHDR hdr;

WORD wVKey;

UINT flags;

}LV_KEYDOWN;

它的第一個結構成員hdr就是NMHDR類型。其他成員包含了更多的信息:哪個鍵被按下,哪些輔助鍵(SHIFT、CTRL、ALT等)被按下。

(2)WM_NOTIFY消息的處理

在分析CWnd::OnWndMsg函數時,曾指出當消息是WM_NOTIFY時,它把消息傳遞給OnNotify虛擬函數處理。這是一個虛擬函數,類似於OnCommand,CWnd和派生類都可以覆蓋該函數。OnNotify的函數原型如下:

BOOL CWnd::OnNotify(WPARAM, LPARAM lParam, LRESULT* pResult)

參數1是發送通知消息的控制窗口ID,沒有被使用;參數2是一個指針;參數3指向一個long類型的數據,用來返回處理結果。

WM_NOTIFY消息的處理過程如下:

  1. 反射消息給控制子窗口處理。
  2. 如果子窗口不處理反射消息,則交給OnCmdMsg處理。給OnCmdMsg的四個參數分別如下:第一個是命令消息ID,第四個為空;第二個高階word是WM_NOTIFY,低階word是通知消息;第三個參數是指向AFX_NOTIFY結構的指針。第二、三個參數有別於OnCommand送給OnCmdMsg的參數。

AFX_NOTIFY結構:

struct AFX_NOTIFY

{

LRESULT* pResult;

NMHDR* pNMHDR;

};

pNMHDR的值來源於參數2 lParam,該結構的域pResult用來保存處理結果,域pNMHDR用來傳遞信息。

OnCmdMsg後續的處理和WM_COMMAND通知消息基本相同,只是在派發消息給消息處理函數時,DispatchMsdMsg的第五個參數pExtra指向OnCmdMsg傳遞給它的AFX_NOTIFY類型的參數,而不是空指針。這樣,處理函數就得到了復雜的通知消息信息。

        1. 消息反射

(1)消息反射的概念

前面討論控制通知消息時,曾經多次提到了消息反射。MFC提供了兩種消息反射機制,一種用於OLE控件,一種用於Windows控制窗口。這裏只討論後一種消息反射。

Windows控制常常發送通知消息給它們的父窗口,通常控制消息由父窗口處理。但是在MFC裏頭,父窗口在收到這些消息後,或者自己處理,或者反射這些消息給控制窗口自己處理,或者兩者都進行處理。如果程序員在父窗口類覆蓋了通知消息的處理(假定不調用基類的實現),消息將不會反射給控制子窗口。這種反射機制是MFC實現的,便於程序員創建可重用的控制窗口類。

MFC的CWnd類處理以下控制通知消息時,必要或者可能的話,把它們反射給子窗口處理:

WM_CTLCOLOR,

WM_VSCROLL,WM_HSCROLL,

WM_DRAWITEM,WM_MEASUREITEM,

WM_COMPAREITEM,WM_DELETEITEM,

WM_CHARTOITEM,WM_VKEYTOITEM,

WM_COMMAND、WM_NOTIFY。

例如,對WM_VSCROLL、WM_HSCROLL消息的處理,其消息處理函數如下:

void CWnd::OnHScroll(UINT, UINT, CScrollBar* pScrollBar)

{

//如果是一個滾動條控制,首先反射消息給它處理

if (pScrollBar != NULL && pScrollBar->SendChildNotifyLastMsg())

return; //控制窗口成功處理了該消息

Default();

}

又如:在討論OnCommand和OnNofity函數處理通知消息時,都曾經指出,它們首先調用ReflectLastMsg把消息反射給控制窗口處理。

為了利用消息反射的功能,首先需要從適當的MFC窗口派生出一個控制窗口類,然後使用ClassWizard給它添加消息映射條目,指定它處理感興趣的反射消息。下面,討論反射消息映射宏。

上述消息的反射消息映射宏的命名遵循以下格式:“ON”前綴+消息名+“REFLECT”後綴,例如:消息WM_VSCROLL的反射消息映射宏是ON_WM_VSCROLL_REFECT。但是通知消息WM_COMMAND和WM_NOTIFY是例外,分別為ON_CONTROL_REFLECT和ON_NOFITY_REFLECT。狀態更新通知消息的反射消息映射宏是ON_UPDATE_COMMAND_UI_REFLECT。

消息處理函數的名字和去掉“WM_”前綴的消息名相同 ,例如WM_HSCROLL反射消息處理函數是Hscroll。

消息處理函數的原型這裏不一一列舉了。

這些消息映射宏和消息處理函數的原型可以借助於ClassWizard自動地添加到程序中。ClassWizard添加消息處理函數時,可以處理的反射消息前面有一個等號,例如處理WM_HSCROLL的反射消息,選擇映射消息“=EN_HSC ROLL”。ClassWizard自動的添加消息映射宏和處理函數到框架文件。

(2)消息反射的處理過程

如果不考慮有OLE控件的情況,消息反射的處理流程如下圖所示:

消息映射的實現

首先,調用CWnd的成員函數SendChildNotifyLastMsg,它從線程狀態得到本線程最近一次獲取的消息(關於線程狀態,後面第9章會詳細介紹)和消息參數,並且把這些參數傳遞給函數OnChildNotify。註意,當前的CWnd對象就是MFC控制子窗口對象。

OnChlidNofify是CWnd定義的虛擬函數,不考慮OLE控制的話,它僅僅只調用ReflectChildNotify。OnChlidNofify可以被覆蓋,所以如果程序員希望處理某個控制的通知消息,除了采用消息映射的方法處理通知反射消息以外,還可以覆蓋OnChlidNotify虛擬函數,如果成功地處理了通知消息,則返回TRUE。

ReflectChildNotify是CWnd的成員函數,完成反射消息的派發。對於WM_COMMAND,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_COMMAND;對於WM_NOTIFY,它直接調用CWnd::OnCmdMsg派發反射消息WM_REFLECT_BASE+WM_NOFITY;對於其他消息,則直接調用CWnd::OnWndMsg(即CmdTarge::OnWndMsg)派發相應的反射消息,例如WM_REFLECT_BASE+WM_HSCROLL。

註意:ReflectChildNotify直接調用了CWnd的OnCmdMsg或OnWndMsg,這樣反射消息被直接派發給控制子窗口,省卻了消息發送的過程。

接著,控制子窗口如果處理了當前的反射消息,則返回反射消息被成員處理的信息。

(3)一個示例

如果要創建一個編輯框控制,要求它背景使用黃色,其他特性不變,則可以從CEdit派生一個類CYellowEdit,處理通知消息WM_CTLCOLOR的反射消息。CYellowEdit有三個屬性,定義如下:

CYellowEdit::CYellowEdit()

{

m_clrText = RGB( 0, 0, 0 );

m_clrBkgnd = RGB( 255, 255, 0 );

m_brBkgnd.CreateSolidBrush( m_clrBkgnd );

}

使用ClassWizard添加反射消息處理函數:

函數原型:

afx_msg void HScroll();

消息映射宏:

ON_WM_CTLCOLOR_REFLECT()

函數的框架

HBRUSH CYellowEdit::CtlColor(CDC* pDC, UINT nCtlColor)

{

// TODO:添加代碼改變設備描述表的屬性

// TODO: 如果不再調用父窗口的處理,則返回一個非空的刷子句柄

return NULL;

}

添加一些處理到函數CtlColor中,如下:

pDC->SetTextColor( m_clrText );//設置文本顏色

pDC->SetBkColor( m_clrBkgnd );//設置背景顏色

return m_brBkgnd; //返回背景刷

這樣,如果某個地方需要使用黃色背景的編輯框,則可以使用CYellowEdit控制。

      1. 對更新命令的接收和處理

        用戶接口對象如菜單、工具條有多種狀態,例如:禁止,可用,選中,未選中,等等。這些狀態隨著運行條件的變化,由程序來進行更新。雖然程序員可以自己來完成更新,但是MFC框架為自動更新用戶接口對象提供了一個方便的接口,使用它對程序員來說可能是一個好的選擇。

        1. 實現方法

          每一個用戶接口對象,如菜單、工具條、控制窗口的子窗口,都由唯一的ID號標識,用戶和它們交互時,產生相應ID號的命令消息。在MFC裏,一個用戶接口對象還可以響應CN_UPDATE_COMMAND_UI通知消息。因此,對每個標號ID的接口對象,可以有兩個處理函數:一個消息處理函數用來處理該對象產生的命令消息ID,另一個狀態更新函數用來處理給該對象的CN_UPDATE_COMMAND_UID的通知消息。

          使用ClassWizard可把狀態更新函數加入到某個消息處理類,其結果是:

          在類的定義中聲明一個狀態函數;

          在消息映射中使用ON_UPDATE_COMMAND_UI宏添加一個映射條目;

          在類的實現文件中實現狀態更新函數的定義。

          ON_UPDATE_COMMAND_UI給指定ID的用戶對象指定狀態更新函數,例如:

          ON_UPDATE_COMMAND_UI(ID_EDIT_COPY, OnUpdateEditCopy)

          映射標識號ID為ID_EDIT_COPY菜單的通知消息CN_UPDATE_COMMAND_UI到函數OnUpdateEditCopy。用於給EDIT(編輯菜單)的菜單項ID_EDIT_COPY(復制)添加一個狀態處理函數OnUpdateEditCopy,通過處理通知消息CN_UPDATE_COMMAND_UI實現該菜單項的狀態更新。

          狀態處理函數的原型如下:

          afxmsg void ClassName::OnUpdateEditPaste(CCmdUI* pCmdUI)

          CCmdUI對象由MFC自動地構造。在完善函數的實現時,使用pCmdUI對象和CmdUI的成員函數實現菜單項ID_EDIT_COPY的狀態更新,讓它變灰或者變亮,也就是禁止或者允許用戶使用該菜單項。

        2. 狀態更新命令消息

          要討論MFC的狀態更新處理,先得了解一條特殊的消息。MFC的消息映射機制除了處理各種Windows消息、控制通知消息、命令消息、反射消息外,還處理一種特別的“通知命令消息”,並通過它來更新菜單、工具欄(包括對話框工具欄)等命令目標的狀態。

          這種“通知命令消息”是MFC內部定義的,消息ID是WM_COMMAND,通知代碼是CN_UPDATE_COMMAND_UI(0XFFFFFFFF)。

          它不是一個真正意義上的通知消息,因為沒有控制窗口產生這樣的通知消息,而是MFC自己主動產生,用於送給工具條窗口或者主邊框窗口,通知它們更新用戶接口對象的狀態。

          它和標準WM_COMMAND命令消息也不相同,因為它有特定的通知代碼,而命令消息通知代碼是0。

          但是,從消息的處理角度,可以把它看作是一條通知消息。如果是工具條窗口接收該消息,則在發送機制上它和WM_COMMAND控制通知消息是相同的,相當於讓工具條窗口處理一條通知消息。如果是邊框窗口接收該消息,則在消息的發送機制上它和WM_COMMAND命令消息是相同的,可以讓任意命令目標處理該消息,也就是說邊框窗口可以把該條通知消息發送給任意命令目標處理。

          從程序員的角度,可以把它看作一條“狀態更新命令消息”,像處理命令消息那樣處理該消息。每條命令消息都可以對應有一條“狀態更新命令消息”。ClassWizard也支持讓任意消息目標處理“狀態更新命令消息”(包括非窗口命令目標),實現用戶接口狀態的更新。

          在這條消息發送時,通過OnCmdMsg的第三個參數pExtra傳遞一些信息,表示要更新的用戶接口對象。pExtra指向一個CCmdUI對象。這些信息將傳遞給狀態更新命令消息的處理函數。

          下面討論用於更新用戶接口對象狀態的類CCmdUI。

        3. 類CCmdUI

CCmdUI不是從CObject派生,沒有基類。

  1. 成員變量

    m_nID 用戶接口對象的ID

    m_nIndex 用戶接口對象的index

    m_pMenu 指向CCmdUI對象表示的菜單

    m_pSubMenu 指向CCmdUI對象表示的子菜單

    m_pOther 指向其他發送通知消息的窗口對象

    m_pParentMenu 指向CCmdUI對象表示的子菜單

  2. 成員函數

Enable(BOOL bOn = TRUE ) 禁止用戶接口對象或者使之可用

SetCheck( int nCheck = 1) 標記用戶接口對象選中或未選中

SetRadio(BOOL bOn = TRUE)

SetText(LPCTSTR lpszText)

ContinueRouting()

還有一個MFC內部使用的成員函數:

DoUpdate(CCmdTarget* pTarget, BOOL bDisableIfNoHndler)

其中,參數1指向處理接收更新通知的命令目標,一般是邊框窗口;參數2指示如果沒有提供處理函數(例如某個菜單沒有對應的命令處理函數),是否禁止用戶對象。

DoUpdate作以下事情:

首先,發送狀態更新命令消息給參數1表示的命令目標:調用pTarget->OnCmdMsg(m_nID, CN_UPDATE_COMMAND_UI, this, NULL)發送m_nID對象的通知消息CN_UPDATE_COMMAND_UI。OnCmdMsg的參數3取值this,包含了當前要更新的用戶接口對象的信息。

然後,如果參數2為TRUE,調用pTarget->OnCmdMsg(m_nID, CN_COMMAND, this, &info)測試命令消息m_nID是否被處理。這時,OnCmdMsg的第四個參數非空,表示僅僅是測試,不是真的要派發消息。如果沒有提供命令消息m_nID的處理函數,則禁止用戶對象m_nID,否則使之可用。

從上面的討論可以知道:通過其結構,一個CCmdUI對象標識它表示了哪一個用戶接口對象,如果是菜單接口對象,pMenu表示了要更新的菜單對象;如果是工具條,pOther表示了要更新的工具條窗口對象,nID表示了工具條按鈕ID。

所以,由參數上狀態更新消息的消息處理函數就知道要更新什麽接口對象的狀態。例如,第1節的函數OnUpdateEditPaste,函數參數pCmdUI表示一個菜單對象,需要更新該菜單對象的狀態。

通過其成員函數,一個CCmdUI可以更新、改變用戶接口對象的狀態。例如,CCmdUI可以管理菜單和對話框控制的狀態,調用Enable禁止或者允許菜單或者控制子窗口,等等。

所以,函數OnUpdateEditPaste可以直接調用參數的成員函數(如pCmdUI->Enable)實現菜單對象的狀態更新。

由於接口對象的多樣性,其他接口對象將從CCmdUI派生出管理自己的類來,覆蓋基類的有關成員函數如Enable等,提供對自身狀態更新的功能。例如管理狀態條和工具欄更新的CStatusCmdUI類和CToolCmdUI類。

        1. 自動更新用戶接口對象狀態的機制

MFC提供了分別用於更新菜單和工具條的兩種途徑。

  1. 更新菜單狀態

    當用戶對菜單如File單擊鼠標時,就產生一條WM_INITMENUPOPUP消息,邊框窗口在菜單下拉之前響應該消息,從而更新該菜單所有項的狀態。

    在應用程序開始運行時,邊框也會收到WM_INITMENUPOPUP消息。

  2. 更新工具條等狀態

    當應用程序進入空閑處理狀態時,將發送WM_IDLEUPDATECMDUI消息,導致所有的工具條用戶對象的狀態處理函數被調用,從而改變其狀態。WM_IDLEUPDATECMDUI是MFC自己定義和使用的消息。

    在窗口初始化時,工具條也會收到WM_IDLEUPDATECMDUI消息。

  3. 菜單狀態更新的實現

    MFC讓邊框窗口來響應WM_INITMENUPOPUP消息,消息處理函數是OnInitMenuPopup,其原型如下:

    afx_msg void CFrameWnd::OnInitMenuPopup( CMenu* pPopupMenu,

    UINT nIndex, BOOL bSysMenu );

    第一個參數指向一個CMenu對象,是當前按擊的菜單;第二個參數是菜單索引;第三個參數表示子菜單是否是系統控制菜單。

    函數的處理:

    如果是系統控制菜單,不作處理;否則,創建CCmdUI對象state,給它的各個成員如m_pMenu,m_pParentMenu,m_pOther等賦值。

    對該菜單的各個菜單項,調函數state.DoUpdate,用CCmdUI的DoUpdate來更新狀態。DoUpdate的第一個參數是this,表示命令目標是邊框窗口;在CFrameWnd的成員變量m_bAutoMenuEnable為TRUE時(表示如果菜單m_nID沒有對應的消息處理函數或狀態更新函數,則禁止它),把DoUpdate的第二個參數bDisableIfNoHndler置為TRUE。

    順便指出,m_bAutoMenuEnable缺省時為TRUE,所以,應用程序啟動時菜單經過初始化處理,沒有提供消息處理函數或狀態更新函數的菜單項被禁止。

  4. 工具條等狀態更新的實現

圖4-5表示了消息空閑時MFC更新用戶對象狀態的流程:

MFC提供的缺省空閑處理向頂層窗口(框架窗口)的所有子窗口發送消息WM_IDLEUPDATECMDUI;MFC的控制窗口(工具條、狀態欄等)實現了對該消息的處理,導致用戶對象狀態處理函數的調用。

雖然兩種途徑調用了同一狀態處理函數,但是傳遞的 CCmdUI參數從內部構成上是不一樣的:第一種傳遞的CCmdUI對象表示了一菜單對象,(pMenu域被賦值);第二種傳遞了一個窗口對象(pOther域被賦值)。同樣的狀態改變動作,如禁止、允許狀態的改變,前者調用了CMenu的成員函數EnableMenuItem,後者使用了CWnd的成員函數EnabelWindow。但是,這些不同由CCmdUI對象內部區分、處理,對用戶是透明的:不論菜單還是對應的工具條,用戶都用同一個狀態處理函數使用同樣的形式來處理。

消息映射的實現

 

這一節分析了用戶界面更新的原理和機制。在後面第13章討論工具條和狀態欄時,將詳細的分析這種機制的具體實現。

    1. 消息的預處理

      到現在為止,詳細的討論了MFC的消息映射機制。但是,為了提高效率和簡化處理,MFC提供了一種消息預處理機制,如果一條消息在預處理時被過濾掉了(被處理),則不會被派發給目的窗口的窗口過程,更不會進入消息循環了。

      顯然,能夠進行預處理的消息只可能是隊列消息,而且必須在消息派發之前進行預處理。因此,MFC在實現消息循環時,對於得到的每一條消息,首先送給目的窗口、其父窗口、其祖父窗口乃至最頂層父窗口,依次進行預處理,如果沒有被處理,則進行消息轉換和消息派發,如果某個窗口實現了預處理,則終止。有關實現見後面關於CWinThread線程類的章節,CWinThread的Run函數和PreTranslateMessage函數以及CWnd的函數WalkPreTranslateTree實現了上述要求和功能。這裏要討論的是MFC窗口類如何進行消息預處理。

      CWnd提供了虛擬函數PreTranslateMessage來進行消息預處理。CWnd的派生類可以覆蓋該函數,實現自己的預處理。下面,討論幾個典型的預處理。

      首先,是CWnd的預處理:

      預處理函數的原型為:

      BOOL CWnd::PreTranslateMessage(MSG* pMsg)

      CWnd類主要是處理和過濾Tooltips消息。關於該函數的實現和Tooltips消息,見後面第13章關於工具欄的討論。

      然後,是CFrameWnd的預處理:

      CFrameWnd除了調用基類CWnd的實現過濾Tooltips消息之外,還要判斷當前消息是否是鍵盤快捷鍵被按下,如果是,則調用函數::TranslateAccelerator(m_hWnd, hAccel, pMsg)處理快捷鍵。

      接著,是CMDIChildWnd的預處理:

      CMDIChildWnd的預處理過程和CFrameWnd的一樣,但是不能依靠基類CFrameWnd的實現,必須覆蓋它。因為MDI子窗口沒有菜單,所以它必須在MDI邊框窗口的上下文中來處理快捷鍵,它調用了函數::TranslateAccelerator(GetMDIFrame()->m_hWnd, hAccel, pMsg)。

      討論了MDI子窗口的預處理後,還要討論MDI邊框窗口:

      CMDIFrameWnd的實現除了CFrameWnd的實現的功能外,它還要處理MDI快捷鍵(標準MDI界面統一使用的系統快捷鍵)。

      在後面,還會討論CDialog、CFormView、CToolBar等的消息預處理及其實現。

      至於CWnd::WalkPreTranslateTree函數,它從接受消息的窗口開始,逐級向父窗回溯,逐一對各層窗口調用PreTranslateMessage函數,直到消息被處理或者到最頂層窗口為止。

    2. MFC消息映射的回顧

從處理命令消息的過程可以看出,Windows消息和控制消息的處理要比命令消息的處理簡單,因為查找消息處理函數時,後者只要搜索當前窗口對象(this所指)的類或其基類的消息映射入口表。但是,命令消息就要復雜多了,它沿一定的順序鏈查找鏈上的各個命令目標,每一個被查找的命令目標都要搜索它的類或基類的消息映射入口表。

MFC通過消息映射的手段,以一種類似C++虛擬函數的概念向程序員提供了一種處理消息的方式。但是,若使用C++虛擬函數實現眾多的消息,將導致虛擬函數表極其龐大;而使用消息映射,則僅僅感興趣的消息才加入映射表,這樣就要節省資源、提高效率。這套消息映射機制的基礎包括以下幾個方面:

  1. 消息映射入口表的實現:采用了C++靜態成員和虛擬函數的方法來表示和得到一個消息映射類(CCmdTarget或派生類)的映射表。
  2. 消息查找的實現:從低層到高層搜索消息映射入口表,直至根類CCmdTarget。
  3. 消息發送的實現:主要以幾個虛擬函數為基礎來實現標準MFC消息發送路徑:OnComamnd、OnNotify、OnWndMsg和OnCmdMsg。、

OnWndMsg是CWnd類或其派生類的成員函數,由窗口過程調用。它處理標準的Windows消息。

OnCommand是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_COMMAND消息,實現命令消息或者控制通知消息的發送。如果派生類覆蓋該函數,則必須調用基類的實現,否則將不能自動的處理命令消息映射,而且必須使用該函數接受的參數(不是程序員給定值)調用基類的OnCommand。

OnNotify是CWnd類或其派生類的成員函數,由OnWndMsg調用來處理WM_NOTIFY消息,實現控制通知消息的發送。

OnCmdMsg是CCmdTarget類或其派生類的成員函數。被OnCommand調用,用來實現命令消息發送和派發命令消息到命令消息處理函數。

自動更新用戶對象狀態是通過MFC的命令消息發送機制實現的。

控制消息可以反射給控制窗口處理。

隊列消息在發送給窗口過程之前可以進行消息預處理,如果消息被MFC窗口對象預處理了,則不會進入消息發送過程。

0 0
原创粉丝点击