Windows消息概述
Windows 應用程序的輸入由Windows系統以消息的形式發送給應用程序的窗口。這些窗口通過窗口過程來接收和處理消息,然後把控制返還給Windows。
消息的分類
隊列消息和非隊列消息
從消息的發送途徑上看,消息分兩種:隊列消息和非隊列消息。隊列消息送到系統消息隊列,然後到線程消息隊列;非隊列消息直接送給目的窗口過程。
這裏,對消息隊列闡述如下:
Windows 維護一個系統消息隊列(System message queue),每個GUI線程有一個線程消息隊列(Thread message queue)。
鼠 標、鍵盤事件由鼠標或鍵盤驅動程序轉換成輸入消息並把消息放進系統消息隊列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次從系統消息隊列移走一個消息,確定它是送給哪個窗口的和這個窗口是由哪個線程創建的,然後,把它放進窗口創建線程的線程消息隊列。線程消息隊列接收送給該線程所創建窗口的消息。線程從消息隊列取出消息,通過Windows把它送給適當的窗口過程來處理。
除了鍵盤、鼠標消息以外,隊列消息還有 WM_PAINT、WM_TIMER和WM_QUIT。
這些隊列消息以外的絕大多數消息是非隊列消息。
系統消息和應用程序消息
從消息的來源來看,可以分爲:系統定義的消息和應用程序定義的消息。
系統消息ID的範圍是從0到WM_USER-1,或0X80000到0XBFFFF;應用程序消息從WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF範圍的消息由應用程序自己使用;0XC000到0XFFFF範圍的消息用來和其他應用程序通信,爲了ID的唯一性,使用::RegisterWindowMessage來得到該範圍的消息ID。
消息的結構
爲了從消息隊列獲取消息信息,需要使用MSG結構。例如,::GetMessage函數(從消息隊列得到消息並從隊列中移走)和::PeekMessage函數(從消息隊列得到消息但是可以不移走)都使用了該結構來保存獲得的消息信息。
MSG 結構的定義如下:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
該結構包括了六個成員,用來描述消息的有關屬性:
接收消息的窗口句柄、消息標識( ID)、第一個消息參數、第二個消息參數、消息產生的時間、消息產生時鼠標的位置。
應用程序通過窗口過程來處理消息
如前所述,每個“窗口類”都要登記一個如下形式的窗口過程:
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分支處理語句來識別和處理消息。
應用程序通過消息循環來獲得對消息的處理
每個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出錯時,退出消息循環。
MFC消息處理
使用MFC框架編程時,消息發送和處理的本質也如上所述。但是,有一點需要強調的是,所有的MFC窗口都使用同一窗口過程,程序員不必去設計和實現自己的窗口過程,而是通過MFC提供的一套消息映射機制來處理消息。因此,MFC簡化了程序員編程時處理消息的複雜性。
所謂消息映射,簡單地講,就是讓程序員指定要某個MFC類(有消息處理能力的類)處理某個消息。MFC提供了工具ClassWizard來幫助實現消息映射,在處理消息的類中添加一些有關消息映射的內容和處理消息的成員函數。程序員將完成消息處理函數,實現所希望的消息處理能力。
如果派生類要覆蓋基類的消息處理函數,就用ClassWizard在派生類中添加一個消息映射條目,用同樣的原型定義一個函數,然後實現該函數。這個函數覆蓋派生類的任何基類的同名處理函數。
下面幾節將分析MFC的消息機制的實現原理和消息處理的過程。爲此,首先要分析ClassWizard實現消息映射的內幕,然後討論MFC的窗口過程,分析MFC窗口過程是如何實現消息處理的。
-
- 消息映射的定義和實現
- 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類派生。
在討論了消息的分類之後,應該是討論各類消息如何處理的時候了。但是,要知道怎麼處理消息,首先要知道如何映射消息。
-
-
- 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。
爲什麼這樣做之後就完成了一個消息映射?這些聲明和實現到底作了些什麼呢?接着,將討論這些問題。
- 在聲明與實現的內部
-
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。
|