MFC框架机制详细论述
1.1 Windows消息機制要點
1.1.1 窗口過程
? 每個窗口會有一個稱為窗口過程的回調函數(WndProc),它帶有四個參數,分別為:窗口句柄(Window Handle), 消息ID(Message ID), 和兩個消息參數(wParam, lParam), 當窗口收到消息時系統就會調用此窗口過程來處理消息。(所以叫回調函數)
1.1.2 消息類型
-
1. 系統定義消息(System-Defined Messages)
在SDK中事先定義好的消息,非用戶定義的,其范圍在[0x0000, 0x03ff]之間 (1024),
可以分為以下三類:
窗口消息(Windows Message)
與窗口的內部運作有關,如創建窗口,繪制窗口,銷毀窗口等??梢允且话愕拇翱?#xff0c;也可以是Dialog,控件等。所有派生自CWnd 的類才有資格接收標準消息。
第一個消息是:WM_NULL = $0000;
最后一個消息是:WM_DDE_LAST = WM_DDE_FIRST (03E8=1000;
中間有幾個地方并不連續;
如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR, WM_HSCROLL…
-
命令消息(Command Message)
-
與處理用戶請求有關, 如單擊菜單項或工具欄或控件時, 就會產生命令消息。WM_COMMAND=$0111, LOWORD(wParam)表示菜單項,工具欄按鈕或控件的ID。如果是控件, HIWORD(wParam)表示控件消息類型。
所有派生自CCmdTarget 的類都有能力接收WM_COMMAND消息。
-
控件通知(Notify Message)
-
控件通知消息, 這是最靈活的消息格式, 其Message, wParam, lParam分別為:WM_NOTIFY= $004E, 控件ID,指向NMHDR的指針。NMHDR包含控件通知的內容, 可以任意擴展。
-
2. 用戶程序定義消息(Application-Defined Messages)
-
用戶自定義的消息, 對于其范圍有如下規定:
WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
RegisterWindowMessage: 0xC000-0xFFFF -
3. 消息隊列(Message Queues)
-
Windows中有兩種類型的消息隊列
-
系統消息隊列(System Message Queue)
-
線程消息隊列(Thread-specific Message Queue)
-
每一個GUI線程都會維護這樣一個線程消息隊列。(這個隊列只有在線程調用GDI函數時才會創建,默認不創建)。然后線程消息隊列中的消息會被送到相應的窗口過程(WndProc)處理。
注意:?線程消息隊列中WM_PAINT,WM_TIMER只有在Queue中沒有其他消息的時候才會被處理,WM_PAINT消息還會被合并以提高效率。其他所有消息以先進先出(FIFO)的方式被處理。
-
4. 隊列消息(Queued Messages)和非隊列消息(Non-Queued Messages)
-
隊列消息(Queued Messages)
-
非隊列消息(NonQueued Messages)
-
消息會繞過系統消息隊列和線程消息隊列直接發送到窗口過程被處理
如:WM_ACTIVATE, WM_SETFOCUS,
WM_SETCURSOR, WM_WINDOWPOSCHANGED注意:?postMessage發送的消息是隊列消息,它會把消息Post到消息隊列中; SendMessage發送的消息是非隊列消息, 被直接送到窗口過程處理
-
5. PostMessage(PostThreadMessage), SendMessage
-
PostMessage:把消息放到指定窗口所在的線程消息隊列中后立即返回。 PostThreadMessage:把消息放到指定線程的消息隊列中后立即返回。
SendMessage:直接把消息送到窗口過程處理, 處理完了才返回。 -
6. GetMessage, PeekMessage
-
PeekMessage會立即返回 可以保留消息
GetMessage在有消息時返回 會刪除消息
-
7. TranslateMessage, TranslateAccelerator
-
TranslateMessage: 把一個virtual-key消息轉化成字符消息(character message),并放到當前線程的消息隊列中,消息循環下一次取出處理。
TranslateAccelerator: 將快捷鍵對應到相應的菜單命令。它會把WM_KEYDOWN 或 WM_SYSKEYDOWN轉化成快捷鍵表中相應的WM_COMMAND 或WM_SYSCOMMAND消息, 然后把轉化后的 WM_COMMAND或WM_SYSCOMMAND直接發送到窗口過程處理, 處理完后才會返回。
-
8. 消息死鎖( Message Deadlocks)
-
假設有線程A和B, 現在有以下下步驟
1) 線程A SendMessage給線程B, A等待消息在線程B中處理后返回
2) 線程B收到了線程A發來的消息,并進行處理,在處理過程中,B也向線程A SendMessgae,然后等待從A返回。
因為此時, 線程A正等待從線程B返回, 無法處理B發來的消息, 從而導致了線程A,B相互等待, 形成死鎖。多個線程也可以形成環形死鎖。
可以使用 SendNotifyMessage或SendMessageTimeout來避免出現死鎖。 -
9. BroadcastSystemMessage
-
我們一般所接觸到的消息都是發送給窗口的, 其實, 消息的接收者可以是多種多樣的,它可以是應用程序(applications), 可安裝驅動(installable drivers), 網絡設備(network drivers), 系統級設備驅動(system-level device drivers)等,
BroadcastSystemMessage這個API可以對以上系統組件發送消息。1.2 MessageMAP的形成
? MFC也定義了豐富的宏來簡化消息響應的代碼,要想真正了解MFC的消息機制,必需弄清楚這些宏。
-
第一個宏:DECLARE_MESSAGE_MAP()
-
第二個重要的宏:BEGIN_MESSAGE_MAP
-
作用:定義DECLARE_MESSAGE_MAP宏聲明的靜態變量。
BEGIN_MESSAGE_MAP定義的源代碼如下:
//以下這一段是重點,理解了這一段,就全部理解了MFC框架中的消息映射機制。
#define BEGIN_MESSAGE_MAP(theClass, baseClass)const AFX_MSGMAP* theClass::GetMessageMap() const { return &theClass::messageMap; } /// AFX_COMDAT const AFX_MSGMAP theClass::messageMap =//定義Message { &baseClass::messageMap, &theClass::_messageEntries[0] };//傳入消息入口地址 /// AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] =//消息條目內容 {BEGIN_MESSAGE_MAP宏有兩個參數,theClass表示為當前類,bassClass為當前類的父類。
BEGIN_MESSAGE_MAP宏首先定義了函數GetMessageMap的函數體,如前文所述,直接返回當前類的成員變量messageMap的地址。
const AFX_MSGMAP* theClass::GetMessageMap() const { return &theClass::messageMap; }然后初始化了當前類的成員變量messageMap。messageMap的pBaseMap指針指向其父類(基類)的messageMap成員,lpEntries指針指向當前類的_messageEntries數組的首地址。
AFX_COMDAT const AFX_MSGMAP theClass::messageMap = { &baseClass::messageMap, &theClass::_messageEntries[0] };最后,定義了_messageEntries數組初始化代碼的開始部分。
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = { -
第二個重要的宏END_MESSAGE_MAP()
-
作用:定義_messageEntries數組初始化代碼的結束部分。
#define END_MESSAGE_MAP(){0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } };在DECLARE_MESSAGE_MAP和END_MESSAGE_MAP之間還有一些宏,如ON_COMMAND、ON_WM_CREATE 等,這些宏最終都會被生成一條AFX_MSGMAP_ENTRY結構體數據,并成為_messageEntries消息映射表數據的一個元素。我們以常見的ON_COMMAND宏為例。ON_COMMAND宏的源代碼為:
#define ON_COMMAND(id, memberFxn){ WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSigCmd_v, static_cast (memberFxn) },通過以上分析,我們可以得到一個鏈表式的數據結構,子類的messageMap成員為鏈表的頭節點。鏈表的每個節點都包含一個消息入口表。MFC的消息處理函數CCmdTarget::OnCmdMsg正是通過這樣一個鏈表查找到消息的響應函數,并調用該函數來響應消息。
1.3 整體過程分析
-
鼠標點擊,產生單擊事件,鼠標設備驅動程序根據用戶事件,轉換成消息,并放置于WINDOWS的系統隊列中
-
WINDOWS將系統隊列中的消息取出,并投擲于消息對應的應用程序所屬的線程隊列。
-
每個應用程序在創建時,系統都會為其創建一個消息隊列,發送給應用程序的消息都存放在該消息隊列中,等待被處理。 而應用程序的消息引擎
MSG msg;
while (GetMessage(msg, NULL, NULL, NULL))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
} - Sent Message Queue 發送消息隊列
- Posted Message Queue 登記消息隊列
- Visualized Input Queue 輸入消息隊列
- Reply Message Queue 響應消息隊列ml>
-
Win32 SDK程序的入口點 WinMain()
-
(1) 定義應用程序要用的變量;
-
(2) 定義窗口類(WNDCLAS)變量wndclass;
- WNDCLAS wndclass;
-
(3) 根據窗口類結構填寫各條款,形成初始化的窗口類;
- wndclass.style = CS_HREDRAW | CS_VREDRAW; ...
-
(4) 利用RegisterClass()函數注冊初始化好的窗口類wndclass,注冊失敗則輸出信息并返回操作系統,成功則跳過if繼續執行;
- if(!RegisterClass(&wndclass)) { //注冊失敗處理 }
-
(5) 根據窗口類建立窗口:
- hwnd = CreateWindow(...);
-
(6) 在屏幕上顯示窗口;
- ShowWindow(...); UpdateWindow(...); // 發出WM_PAINT消息
-
(7) 進入消息循環;
- while(GetMessage(&msg, NULL, 0, 0)) // 從調用線程的消息隊列里取得一個消息并將其放于指定的結構msg { TranslateMessage(&msg); // 將虛擬鍵消息轉換為字符消息 DispatchMessage(&msg); // 調度一個消息給窗口程序 }
-
(8) return (msg.wParam);
- 其中窗口函數: LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { .... switch (message) { case WM_CREATE: // 響應WM_CREATE消息的處理過程 case WM_PAINT: //... default: return (DefWindowProc(hWnd, message, wParam, lParam)); // 不愿處理的消息交給默認處理函數解決 }
-
MFC編寫的Windows應用程序的運行機制:
-
創建應用程序類(CWinApp的派生類)全局對象 theAppe
-
調用應用程序類的構造函數初始化對象 theApp
- 配置內存,并設定成員初值
-
調用WinMain()主函數
-
獲取theApp的指針;
pApp = AfxGetApp();
pThread = AfxGetThread(); -
全局初始化();
- AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow)
- 設置錯誤處理模式:
-
設置模塊資源句柄和填寫應用程序初始化狀態數據;
-
初始化主線程特定數據,并把消息隊列盡量加大;
-
應用程序初始化;
- pApp->InitApplication(); // 這個函數在MFC中已作廢,最好不要重載它 pThread->InitInstance(); // 注冊窗口類,產生窗口,顯示窗口,須改寫(注:下面是MDI程序中InitInstance()中LoadFrame()函數引發的窗口注冊,創建等一系列過程,只寫出了主要的一些函數調用過程,具體函數作用查查MFC類手冊或MSDN,我就不羅嗦了: CFrameWnd::LoadFrame(), CFrameWnd::Create(), CWnd::CreateEx(), CFrameWnd::PreCreateWindow(), AfxEndDeferRegisterClass(), _AfxRegisterWithIcon(), AfxRegisterClass(), CreateWindowEx();) m_pMainWnd->ShowWindow(SW_SHOW); m_pMainWnd->UpdateWindow();
-
運行程序, 進入消息處理循環;
- pThread->Run();
- 如果消息隊列中沒有消息: OnIdle();
- 當消息隊列中有消息且不是WM_QUIT,分發給消息處理函數:PumpMessage();
- 當收到WM_QUIT消息:ExitInstance();
-
調用析構函數,退出應用程序,控制權交還操作系統;
-
其中Run()中的消息映射過程如下(大致的函數調用過程):
wndcls.lpfnWndProc = DefWindowProc; // 窗口類注冊的窗口函數 CWnd::DefWindowProc(); ::CallWindowProc(pfnWndProc, m_hWnd, nMsg, wParam, lParam); AfxWndProc(); AfxCallWndProc(); CWnd::WindowProc(); CWnd::OnWndMsg(); 如果是WM_COMMAND: OnCommand() OnCmdMsg(); 如果是WM_NOTIFY: OnNotify() OnCmdMsg();其中重點在CWnd::WindowProc()及其后的消息分派過程:
當一個消息抵達時,框架調用了CMainFrame從CWnd繼承下來的虛擬WindowProc()函數;
WindowProc函數調用OnWndMsg函數;
而OnWndMsg又調用GetMessageMap來獲取一個指向CMainFrame::messageMap的指針,并搜索CMainFrame::_messageEntries來獲取一個其消息ID與當前正等待處理的消息ID相匹配的條目,如果找到了該條目,對應的CMainFrame函數(其地址與該消息ID一同存儲在_messageEntries數組中)就被調用。否則,OnWndMsg參考CMainFrame::messageMap獲得一個指向CFrameWnd::messageMap的指針并為基類重復該過程。如果基類沒有該消息的處理程序,則框架將上升一個級別,參考基類的基類,相當系統地沿著繼承鏈向上走,直到它找到一個消息處理程序或者該消息傳遞Windows進行默認處理為止;”
這是一個系統唯一的Queue,設備驅動(mouse, keyboard)會把操作輸入轉化成消息存在系統隊列中,然后系統會把此消息放到目標窗口所在的線程的消息隊列(thread-specific message queue)中等待處理
消息會先保存在消息隊列中,消息循環會從此隊列中取消息并分發到各窗口處理
如鼠標,鍵盤消息。
作用:為一個消息響應類聲明必需的成員變量和成員函數。
我們在窗口類、應用程序類、文檔類、視圖類、以及這些類的子類的定義中,都能看到DECLARE_MESSAGE_MAP()宏,通常被自動化工具聲明在類的最后部分,如:
// 生成的消息映射函數 protected: DECLARE_MESSAGE_MAP() };DECLARE_MESSAGE_MAP()宏定義如下(在DLL類型和WINDOWS程序類型下,定義會有不同,本文只分析非DLL類型,下同):
#define DECLARE_MESSAGE_MAP()private: static const AFX_MSGMAP_ENTRY _messageEntries[]; protected: static const AFX_MSGMAP messageMap; virtual const AFX_MSGMAP* GetMessageMap() const;可以看到,宏DECLARE_MESSAGE_MAP()定義了兩個靜態成員變量,并重載了一個虛函數。下面分析一下這三個成員:_messageEntries被定義為一個AFX_MSGMAP_ENTRY類型的數組。
結構體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) };通過查看源代碼中的注釋,可以看出AFX_MSGMAP_ENTRY定義了一個消息入口,或者說定義了一個消息到函數的映射關系。 nMessage和nCode確定一條消息的內容,nID和nLastID確定了一條消息的來源,而nSig和pfn確定了消息的響應函數和調用方式。
通過對消息響應過程的源碼分析可知,nSig事實上是一系列編碼,每一種編碼代表一種響應函數的類型,包括返回值、參數信息等。在響應消息的時候,將把pfn指向的函數指針強制類型轉換為nSig代表的函數類型,然后再調用。
pfn的類型AFX_PMSG定義如下,意為CCmdTarget的成員函數:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);由此我們可以得出:靜態成員_messageEntries是一個消息到函數的映射表,或叫消息入口表。通過查找此表,可以找到消息的響應函數。
?
DECLARE_MESSAGE_MAP()宏聲明的另一個靜態成員變量messageMap被定義為AFX_MSGMAP類型。AFX_MSGMAP定義如下:
struct AFX_MSGMAP {#ifdef _AFXDLLconst AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();#elseconst AFX_MSGMAP* pBaseMap;#endifconst AFX_MSGMAP_ENTRY* lpEntries; };過濾掉_AFXDLL(當MFC工程是以動態鏈接庫為目標代碼編譯時,使用_AFXDLL宏)的影響,可以簡化為如下:
struct AFX_MSGMAP { const AFX_MSGMAP* pBaseMap; const AFX_MSGMAP_ENTRY* lpEntries; };可見結構體AFX_MSGMAP中定義了兩個指針,pBaseMap指向另一個AFX_MSGMAP,lpEntries指向一個消息入口表。可以推想,在響應消息時,一定是在lpEntries指向的消息入口表中尋找響應函數,也可能會在pBaseMap指向的結構體中做同樣的響應函數尋找操作。其圖示如下:
至于DECLARE_MESSAGE_MAP()宏重載的虛函數GetMessageMap,可以猜測只是用來返回成員messageMap 的地址而已。因為GetMessageMap是虛函數,所以系統只要通過調用消息響應類的基類CCmdTarget類的GetMessageMap函數,便可以找到最后一級子類的消息映射信息。我們將在接下來對其它幾個宏的分析中得到相同的結論。
會不停的從自己的專屬消息隊列中獲取消息,并進行消息的翻譯和轉發(TranslateMessage和DiapatchMessage)
GetMessage:從線程隊列中取消息,取出后對應的消息會從隊列中刪除;若無消息,則阻塞
TranslateMessage:把鍵盤消息轉換成對應的ASCII字符內存,并重新放置于隊列中,等待取出 DispatchMessage:在注冊窗口對象時,有如下代碼:及設置對口的消息處理回調函數
WNDCLASS wc; ...... wc.lpfnWndProc = (WNDPROC)WndProc; ......在窗口類的定義中,有如下代碼:
DECLARE_MESSAGE_MAP() ...... BEGIN_MESSAGE_MAP(CMsgTestDlg, CDialog) ...... ON_MESSAGE(WM_USER_SEND_MSG, &CMsgTestDlg::HandleSendMsg) ON_MESSAGE(WM_USER_POST_MSG, &CMsgTestDlg::HandlePostMsg) ...... END_MESSAGE_MAP()該段代碼,是的所有該類型的對話框對象共享一個消息MAP表,DispatchMessage會根據消息所屬的窗口,調用回調函數WndProc,而WndProc則getMessageMap,根據消息類型,在map中進行匹配查找,找到對應的處理函數,并調用,則消息處理完畢。
1. 4 系統消息隊列
當操作系統啟動并初始化時,線程Raw Input Thread(RIT)就會啟動,并創系統硬件輸入隊列(System Hardware Input Queue)(SHIQ). 對于外部的硬件事件(鼠標或者鍵盤),硬件驅動會將事件轉換成消息,并存放到SHIQ中,而RIT線程就專門負責處理SHIQ中的消息,把消息分發到對應線程的消息隊列里面。
1.5 線程消息隊列
對于每個用MFC開發的GUI程序,他們都有一個CMy***App,該類繼承自CWinApp,而CWinApp繼承自CWinThread, CWinApp是一個GUI線程,系統會為其維護一個THREADINFO結構,
消息隊列包含在一個叫THREADINFO的結構中,有四個隊列:
Sent Message Queue: 該隊列保存其他程序通過SendMessage給該線程發送的消息
Posted Message Queue: 該隊列保存其他隊列通過PostMessage給該線程發送的消息
Visualized Input Queue: 保存系統隊列分發過來的消息,比如鼠標或者鍵盤的消息
Reply Message Queue: 保存向窗體發送消息后的結果,比如sendMessage操作結束后,接收消息方會發送一個Reply消息給發送方的Reply隊列中,以喚醒發送隊列。
這些隊列如何產生的?線程是內核對象,我們看看線程信息是如何定義的,包含哪些內容,分析如下THREADINFO結構體定義,可以得出一些信息:
2.0 MFC技術內幕:執行期類型識別與動態創建
2.1 WIN32運行機制
總結
以上是生活随笔為你收集整理的MFC框架机制详细论述的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vue 2 使用 Bus.js 实现兄
- 下一篇: 解决:idea - maven proj