|
1. 引言 Windows 在操作系統(tǒng)平臺(tái)占有絕對(duì)統(tǒng)治地位,基于Windows 的編程和開(kāi)發(fā)越來(lái)越廣泛。 Dos 是過(guò)程驅(qū)動(dòng)的,而Windows 是事件驅(qū)動(dòng)的[6],這種差別的存在使得很多Dos 程序員不能 習(xí)慣Windows 的程序開(kāi)發(fā)。而很多Windows 程序開(kāi)發(fā)人員也只是對(duì)消息運(yùn)行機(jī)制一知半解, 想要掌握Windows 編程的核心,必須深刻理解消息機(jī)制。事件驅(qū)動(dòng)圍繞著消息的產(chǎn)生與處 理展開(kāi),事件驅(qū)動(dòng)是靠消息循環(huán)機(jī)制來(lái)實(shí)現(xiàn)的。也可以理解為消息是一種報(bào)告有關(guān)事件發(fā)生 的通知,消息是Windows 操作系統(tǒng)的靈魂,掌握了消息運(yùn)行機(jī)制就掌握了Windows 編程的 神兵利器。本文將首先闡述Windows 的編程原理,繼而對(duì)Windows 的消息運(yùn)行機(jī)制進(jìn)行分 析,并講述對(duì)消息的處理。MFC 是一個(gè)廣為使用的編程類庫(kù),對(duì)Windows 的消息機(jī)制進(jìn)行 了良好的封裝,所以,在第二部分將著重討論MFC 的消息映射,最后結(jié)合編程實(shí)際,通過(guò) 對(duì)MFC 消息映射的分析,非常巧妙的加以應(yīng)用,以幫助解決實(shí)際問(wèn)題
2. Windows 消息運(yùn)行機(jī)制 在介紹Windows 消息運(yùn)行機(jī)制之前,首先介紹一下消息的概念。 2.1 消息的概念和表示 消息(Message)指的就是Windows 操作系統(tǒng)發(fā)給應(yīng)用程序的一個(gè)通告[5],它告訴應(yīng)用 程序某個(gè)特定的事件發(fā)生了。比如,用戶單擊鼠標(biāo)或按鍵都會(huì)引發(fā)Windows 系統(tǒng)發(fā)送相應(yīng) 的消息。最終處理消息的是應(yīng)用程序的窗口函數(shù),如果程序不負(fù)責(zé)處理的話系統(tǒng)將會(huì)作出默 認(rèn)處理。 從數(shù)據(jù)結(jié)構(gòu)[4]的角度來(lái)說(shuō),消息是一個(gè)結(jié)構(gòu)體,它包含了消息的類型標(biāo)識(shí)符以及其他的 一些附加信息。 系統(tǒng)定義的結(jié)構(gòu)體MSG[1]用于表示消息,MSG 具有如下定義形式: typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; }MSG;
其中hwnd 是窗口的句柄,這個(gè)參數(shù)將決定由哪個(gè)窗口過(guò)程函數(shù)對(duì)消息進(jìn)行處理;message 是一個(gè)消息常量,用來(lái)表示消息的類型;wParam 和lParam 都是32 位的附加信息,具體表 示什么內(nèi)容,要視消息的類型而定;time 是消息發(fā)送的時(shí)間;pt 是消息發(fā)送時(shí)鼠標(biāo)所在的位 置。
2.2 Windows 編程原理 Windows 是一消息(Message)驅(qū)動(dòng)式系統(tǒng),Windows 消息提供了應(yīng)用程序與應(yīng)用程序 之間、應(yīng)用程序與Windows 系統(tǒng)之間進(jìn)行通訊的手段。應(yīng)用程序要實(shí)現(xiàn)的功能由消息來(lái)觸 發(fā),并靠對(duì)消息的響應(yīng)和處理來(lái)完成。Windows 系統(tǒng)中有兩種消息隊(duì)列,一種是系統(tǒng)消息隊(duì) 列,另一種是應(yīng)用程序消息隊(duì)列。計(jì)算機(jī)的所有輸入設(shè)備由 Windows 監(jiān)控,當(dāng)一個(gè)事件發(fā) 生時(shí),Windows 先將輸入的消息放入系統(tǒng)消息隊(duì)列中,然后再將輸入的消息拷貝到相應(yīng)的應(yīng) 用程序隊(duì)列中,應(yīng)用程序中的消息循環(huán)從它的消息隊(duì)列中檢索每一個(gè)消息并發(fā)送給相應(yīng)的窗 口函數(shù)中。一個(gè)事件的發(fā)生,到達(dá)處理它的窗口函數(shù)必須經(jīng)歷上述過(guò)程。 所謂消息就是描述事件發(fā)生的信息,Windows 程序是事件驅(qū)動(dòng)的,用這一方法編寫(xiě)程序 避免了死板的操作模式,因?yàn)閃indows 程序的執(zhí)行順序?qū)⑷Q于事件的發(fā)生順序,具有不 可預(yù)知性。Windows 操作系統(tǒng),計(jì)算機(jī)硬件,應(yīng)用程序之間具有如圖1 所示的關(guān)系

箭頭1 說(shuō)明操作系統(tǒng)能夠操縱輸入輸出設(shè)備,例如讓打印機(jī)打印;箭頭2 說(shuō)明操作系統(tǒng) 能夠感知輸入輸出設(shè)備的狀態(tài)變化,如鼠標(biāo)單擊,按鍵按下等,這就是操作系統(tǒng)和計(jì)算機(jī)硬 件之間的交互關(guān)系,應(yīng)用程序開(kāi)發(fā)者并不需要知道他們之間是如何做到的,我們需要了解的 操作系統(tǒng)與應(yīng)用程序之間如何交互。箭頭3 是應(yīng)用程序通知操作系統(tǒng)執(zhí)行某個(gè)具體的操作, 這是通過(guò)調(diào)用操作系統(tǒng)的API 來(lái)實(shí)現(xiàn)的;操作系統(tǒng)能夠感知硬件的狀態(tài)變化,但是并不決 定如何處理,而是把這種變化轉(zhuǎn)交給應(yīng)用程序,由應(yīng)用程序決定如何處理,向上的箭頭4 說(shuō)明了這種轉(zhuǎn)交情況,操作系統(tǒng)通過(guò)把每個(gè)事件都包裝成一個(gè)稱為消息結(jié)構(gòu)體MSG 來(lái)實(shí)現(xiàn) 這個(gè)過(guò)程,也就是消息響應(yīng),要理解消息響應(yīng),首先需要了解消息的概念和表示。
2.3 Windows 消息循環(huán) 消息循環(huán)[1]是Windows 應(yīng)用程序存在的根本,應(yīng)用程序通過(guò)消息循環(huán)獲取各種消息,并 通過(guò)相應(yīng)的窗口過(guò)程函數(shù),對(duì)消息加以處理;正是這個(gè)消息循環(huán)使得一個(gè)應(yīng)用程序能夠響應(yīng) 外部的各種事件,所以消息循環(huán)往往是一個(gè)Windows 應(yīng)用程序的核心部分。 Windows 的消息機(jī)制如圖2 所示:

Windows 操作系統(tǒng)為每個(gè)線程維持一個(gè)消息隊(duì)列,當(dāng)事件產(chǎn)生時(shí),操作系統(tǒng)感知這一事 件的發(fā)生,并包裝成消息發(fā)送到消息隊(duì)列,應(yīng)用程序通過(guò)GetMessage()函數(shù)取得消息并存于 一個(gè)消息結(jié)構(gòu)體中,然后通過(guò)一個(gè)TranslateMessage()和DispatchMessage()解釋和分發(fā)消息, 下面的代碼描述了Windows 的消息循環(huán)。 while(GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } TranslateMessage(&msg)對(duì)于大多數(shù)消息而言不起作用,但是有些消息,比如鍵盤(pán)按鍵按 下和彈起(分別對(duì)于KeyDown 和KeyUp 消息),卻需要通過(guò)它解釋,產(chǎn)生一個(gè)WM_CHAR 消息。DispatchMessage(&msg)負(fù)責(zé)把消息分發(fā)到消息結(jié)構(gòu)體中對(duì)應(yīng)的窗口,交由窗口過(guò)程 函數(shù)處理。GetMessage()在取得WM_QUIT 之前的返回值都為T(mén)RUE,也就是說(shuō)只有獲取到 WM_QUIT 消息才返回FALSE,才能跳出消息循環(huán)。
2.4 消息的處理 取得的消息將交由窗口處理函數(shù)進(jìn)行處理,對(duì)于每個(gè)窗口類Windows 為我們預(yù)備了一個(gè) 默認(rèn)的窗口過(guò)程處理函數(shù)DefWindowProc(),這樣做的好處是,我們可以著眼于我們感興趣 的消息,把其他不感興趣的消息傳遞給默認(rèn)窗口過(guò)程函數(shù)進(jìn)行處理。每一個(gè)窗口類都有一個(gè) 窗口過(guò)程函數(shù),此函數(shù)是一個(gè)回調(diào)函數(shù),它是由Windows 操作系統(tǒng)負(fù)責(zé)調(diào)用的,而應(yīng)用程 序本身不能調(diào)用它。以switch 語(yǔ)句開(kāi)始,對(duì)于每條感興趣的消息都以一個(gè)case 引出。 LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) { … switch(uMsgId) {
case WM_TIMER://對(duì)WM_TIMER 定時(shí)器消息的處理過(guò)程 return 0; case WM_LBUTTONDOWN://對(duì)鼠標(biāo)左鍵單擊消息的處理過(guò)程 reurn 0; . … default: return DefWindowProc(hwnd,uMsgId,wParam,lParam); } } 對(duì)于每條已經(jīng)處理過(guò)的消息都必須返回0,否則消息將不停的重試下去;對(duì)于不感興趣 的消息,交給DefWindowProc()函數(shù)進(jìn)行處理,并需要返回其處理值。
3. MFC 的消息映射 MFC 是Windows 下編程的微軟基礎(chǔ)類庫(kù),封裝了大部分Windows API 和Windows 控件, 提供了一套消息映射和命令響應(yīng)機(jī)制,方便了應(yīng)用程序的開(kāi)發(fā)。MFC 只是通過(guò)對(duì)Windows 消息映射的進(jìn)行封裝,使得添加消息響應(yīng)變得更為簡(jiǎn)單,但深究起來(lái),與Windows 消息機(jī) 制有一樣的底層實(shí)現(xiàn)。
3.1 MFC 消息映射的實(shí)現(xiàn) 在MFC 的框架結(jié)構(gòu)下,“消息映射”是通過(guò)巧妙的宏定義,形成一張消息映射表格來(lái)進(jìn) 行的。這樣一旦消息發(fā)生,F(xiàn)ramework 就可以根據(jù)消息映射表格來(lái)進(jìn)行消息映射和命令傳遞。 首先在需要進(jìn)行消息處理的類的頭文件(.H)里,都會(huì)含有DECLARE_MESSAGE_MAP() 宏,聲明該類擁有消息映射表格。 然后在類應(yīng)用程序文件(.CPP)實(shí)現(xiàn)這一表格 BEGIN_MESSAGE_MAP(CInheritClass, CBaseClass) //{{AFX_MSG_MAP(CInheritClass) ON_COMMAND(ID_EDIT_COPY,OnEditCopy) ……… //}}AFX_MSG_MAP END_MESSAGE_MAP() 這里主要進(jìn)行消息映射的實(shí)現(xiàn),把它和消息處理函數(shù)聯(lián)系在一起。其中出現(xiàn)三個(gè)宏,第 一個(gè)宏是BEGIN_MESSAGE_MAP 有兩個(gè)參數(shù),分別是擁有消息表格的類,及其父類。第 二個(gè)宏是ON_COMMAND , 指定命令消息的處理函數(shù)名稱。第三個(gè)是 END_MESSAGE_MAP()作為結(jié)尾符號(hào)。 DECLARE_MESSAGE_MAP 宏定義里包含了MFC 定義的兩個(gè)新的數(shù)據(jù)結(jié)構(gòu); AFX_MSGMAP_ENTRY 和AFX_MSGMAP;其中AFX_MSGMAP_ENTRY 結(jié)構(gòu)包含了 一個(gè)消息的所有相關(guān)信息,而AFX_MSGMAP 主要作用有兩個(gè),一是用來(lái)得到基類的消息映 射入口地址。二是得到本身的消息映射入口地址。 實(shí)際上,MFC 把所有的消息一條條填入到AFX_MSGMAP_ENTRY 結(jié)構(gòu)中去,形成一 個(gè)數(shù)組,該數(shù)組存放了所有的消息和與它們相關(guān)的參數(shù)。同時(shí)通過(guò)AFX_MSGMAP 能得到 該數(shù)組的首地址,同時(shí)得到基類的消息映射入口地址。當(dāng)本身對(duì)該消息不響應(yīng)的時(shí)候,就可 以上溯到基類的消息映射表尋找對(duì)應(yīng)的消息響應(yīng)。 MFC 通過(guò)鉤子函數(shù)_AfxCbtFilterHook()截獲消息,并在此函數(shù)中把窗口過(guò)程函數(shù)設(shè)置為
AfxWindProc,而原來(lái)的窗口過(guò)程函數(shù)被保存在成員變量m_pfnSuper 中。 在MFC 框架下,通過(guò)下面的步驟來(lái)對(duì)消息進(jìn)行映射[7]。 1 函數(shù)AfxWndProc 接收Windows 操作系統(tǒng)發(fā)送的消息。 2 函數(shù)AfxWndProc 調(diào)用函數(shù)AfxCallWndProc 進(jìn)行消息處理,這里一個(gè)進(jìn)步是把對(duì)句柄的 操作轉(zhuǎn)換成對(duì)CWnd 對(duì)象的操作。 3 函數(shù)AfxCallWndProc 調(diào)用CWnd 類的方法WindowProc 進(jìn)行消息處理。 4 WindowProc 調(diào)用OnWndMsg 進(jìn)行正式的消息處理,即把消息派送到相關(guān)的方法中去處理。 5 如果OnWndMsg 方法沒(méi)有對(duì)消息進(jìn)行處理的話,就調(diào)用DefWindowProc 對(duì)消息進(jìn)行處理。 這就是MFC 對(duì)消息調(diào)用過(guò)程的巧妙封裝。
3.2 MFC 消息分類
1 命令消息(WM_COMMAND) 比如菜單項(xiàng)的選擇,工具欄按鈕點(diǎn)擊等發(fā)出該消息。所有派生自CCmdTarget 的類都有 能力接收WM_COMMAND 消息。
2 標(biāo)準(zhǔn)消息(WM_XXX) 比如窗口創(chuàng)建,窗口銷(xiāo)毀等。所有派生自CWnd 的類才有資格接收標(biāo)準(zhǔn)消息。
3 通告消息(WM_NOTIFY) 這是有控件向父窗口發(fā)送的消息,標(biāo)示控件本身狀態(tài)的變化。比如下拉列表框選項(xiàng)的改 變CBN_SELCHANGE 和樹(shù)形控件的TVN_SELCHANGED 消息都是通告消息。 Window 9x 版及以后的新控件通告消息不再通過(guò)WM_COMMAND 傳送,而是通過(guò) WM_NOTIFY 傳送, 但是老控件的通告消息, 比如CBN_SELCHANGE 還是通過(guò) WM_COMMAND 消息發(fā)送。
4 自定義消息 利用MFC 編程,可以使用自定義消息。使用自定義消息需要遵循一定的步驟[2]并需要 自己編寫(xiě)消息響應(yīng)函數(shù)
4. MFC 消息的靈活運(yùn)用 在此,我們給出一個(gè)示例程序,演示對(duì)MFC 消息的靈活運(yùn)用,通過(guò)此例的剖析,將加 深我們對(duì)MFC 消息的理解。
4.1 示例功能描述 本示例程序?qū)⒀菔具@樣一種效果: 對(duì)話框上有一個(gè)CTabCtrl 控件,一個(gè)CComboBox 控件,兩個(gè)按鈕Button1 和Button2。 CTabCtrl 控件有兩個(gè)標(biāo)簽頁(yè)Tab1 和Tab2;CComboxBox 有兩個(gè)選項(xiàng):選項(xiàng)1 和選項(xiàng)2;通 過(guò)按鈕(Button1 和Button2)單擊,分別發(fā)送CTabCtrl 控件的TCN_SELCHANGE 消息和 下拉列表框的CBN_SELCHANGE 消息,在各自的消息響應(yīng)函數(shù)中只是簡(jiǎn)單的對(duì)控件選項(xiàng)做 切換和給出提示信息。 單擊Button1 將選中標(biāo)簽頁(yè)Tab1 和下拉列表框的選項(xiàng)1,并彈出提示信息;單擊Button2 將選中標(biāo)簽頁(yè)Tab2 和下拉列表框的選項(xiàng)2,并彈出提示信息。
4.2 程序設(shè)計(jì)思路 TCN_SELCHANGE 消息和CBN_SELCHANGE 消息都屬于通告消息,此消息由子控件
發(fā)送給父窗口,在MSDN 中查詢發(fā)現(xiàn)TCN_SELCHANGE 消息是以WM_NOTIFY 消息的形 式發(fā)送,在MSDN 中查詢WM_NOTIFY 消息: idCtrl = (int) wParam; pnmh = (LPNMHDR) lParam; 也就是說(shuō),WPARAM 參數(shù)傳遞發(fā)送此消息的控件標(biāo)識(shí),LAPAM 參數(shù)一個(gè)指向NMHDR 結(jié)構(gòu)體的指針。NMHDR 結(jié)構(gòu)體定義如下: typedef struct tagNMHDR { HWND hwndFrom; UINT idFrom; UINT code; } NMHDR; 其中hwndFrom 標(biāo)識(shí)發(fā)送消息控件的句柄,idFrom 是發(fā)送消息控件的ID,code 則是消息碼,如果要發(fā)送TCN_SELCHANGE 消息,則以TCN_SELCHANGE 填充。 查詢MSDN 發(fā)現(xiàn), 由CComboBox 控件發(fā)送的CBN_SELCHANGE 消息以 WM_COMMAND 消息發(fā)送,WPARAM 的高字節(jié)傳遞CComboBox 控件的ID,低字節(jié)發(fā)送 消息碼CBN_SELCHANGE,而LPARAM 則傳送發(fā)送此消息的控件句柄。 所以我們可以通過(guò)在按鈕控件的單擊響應(yīng)函數(shù)里分別發(fā)送WM_NOTIFY 和 WM_COMMAND 消息來(lái)引起TCN_SELCHANGE 和CBN_SELCHANGE 消息響應(yīng)函數(shù)的調(diào) 用,分別在兩控件消息響應(yīng)函數(shù)中實(shí)現(xiàn)選項(xiàng)改變和消息提示即可,遵照這種思路,我們就可 以實(shí)現(xiàn)我們想要的功能。
4.3 程序?qū)崿F(xiàn)步驟 啟動(dòng)VC++6.0,新建基于對(duì)話框的應(yīng)用程序MsgTest. 在對(duì)話框上添加1 個(gè)CTabCtrl 控件,一個(gè)CComboBox 控件,2 個(gè)按鈕Button1 和Button2; 給IDC_TAB1 和IDC_COMBO1 分別關(guān)聯(lián)控件成員變量m_tab1 和m_cb1;為兩按鈕分 別添加按鈕單擊響應(yīng)函數(shù)。 在對(duì)話框的OnInitDlg()函數(shù)中為CTabCtrl 控件添加兩個(gè)標(biāo)簽頁(yè),Tab1 和Tab2;為 ComboBox 添加選項(xiàng)1 和2;代碼如下: m_tab1.InsertItem(0,"Tab1"); m_tab1.InsertItem(1,"Tab2"); m_cb1.AddString("選項(xiàng)1"); m_cb1.AddString("選項(xiàng)2"); 用ClassWizard 為CTabCtrl 添加消息響應(yīng)TCN_SELCHANGE,為CComboBox 添加消息 響應(yīng)CBN_SELCHANGE。在OnSelchangeTab1()函數(shù)中添加代碼 int nIndex=m_tab1.GetCurSel(); CString str; str.Format("%d",nIndex+1); MessageBox("Tab"+str+" selected!"); 在OnSelchangeCombo1()函數(shù)中添加代碼: int nIndex=m_cb1.GetCurSel(); CString str; str.Format("%d",nIndex+1);
MessageBox("ComboBox 選項(xiàng)"+str+" selected!"); 在按鈕1 的響應(yīng)函數(shù)OnButton1()中添加代碼: m_tab1.SetCurSel(0); NMHDR nmhdr; nmhdr.code=TCN_SELCHANGE; nmhdr.hwndFrom=GetDlgItem(IDC_TAB1)->m_hWnd; nmhdr.idFrom=IDC_TAB1; SendMessage(WM_NOTIFY,(WPARAM)IDC_TAB1,(LPARAM)&nmhdr); m_cb1.SetCurSel(0); WPARAM wParam=0; WPARAM lParam=0; wParam=IDC_COMBO1; wParam= wParam | (CBN_SELCHANGE<<16); lParam=(WPARAM)(GetDlgItem(IDC_COMBO1)->m_hWnd); SendMessage(WM_COMMAND, wParam, lParam); 在按鈕2 的響應(yīng)函數(shù)OnButton2()中添加類似代碼,只需要把m_tab1.SetCurSel(0)和 m_cb1.SetCurSel(0)分別改成m_tab1.SetCurSel(1)和m_cb1.SetCurSel(1)。 通過(guò)SendMessage() 函數(shù)向控件的父窗口也就是對(duì)話框窗口發(fā)送相應(yīng)的消息, TCN_SELCHANGE 是以WM_NOTIFY 消息的形式發(fā)送,參數(shù)WPARAM 標(biāo)識(shí)發(fā)送 TCN_SELCHANGE 消息的控件ID,LPARAM 是一個(gè)NMHDR 結(jié)構(gòu)體的指針,此結(jié)構(gòu)體的 成員code 標(biāo)識(shí)發(fā)送什么通告消息,此處是TCN_SELCHANGE,hwndFrom 是發(fā)送消息的控 件句柄, 程序中用GetDlgItem()->m_hWdn 獲得, idFrom 是發(fā)送消息的控件ID 。 CBN_SELCHANGE 以WM_COMMAND 消息的形式發(fā)送,同樣的,通過(guò)查閱MSDN,可以 對(duì)此消息的兩個(gè)參數(shù)進(jìn)行賦值,以保證消息的正確發(fā)送。 通過(guò)上面的5 個(gè)步驟,我們的程序就編寫(xiě)完成了,單擊Button1,可以發(fā)現(xiàn),CTabCtrl 切換到了Tab1 標(biāo)簽頁(yè),CComboBox 選擇了“選項(xiàng)1”,并彈出消息對(duì)話框。由此可見(jiàn)確實(shí)引 起了消息響應(yīng)函數(shù)的調(diào)用,完成了預(yù)定的功能。 通過(guò)查閱MSDN,可以得到其他消息的發(fā)送和包裝形式,我們可以方便的加以利用,完 成更為復(fù)雜的功能,可以說(shuō),掌握了Windows 的消息機(jī)制,就掌握了Windows 編程的核心。
5. 總結(jié) Windows 消息機(jī)制是Windows 編程的本質(zhì)和核心,對(duì)Windows 消息機(jī)制的理解能提高 我們Windows 程序開(kāi)發(fā)的能力。本文首先闡述Windows 的消息機(jī)制,然后講解了MFC 的 消息映射,消息分類,最后通過(guò)示例程序,講解如何借助MSDN,靈活運(yùn)用消息編程,解 決實(shí)際問(wèn)題。本文對(duì)Windows 下的程序開(kāi)發(fā)具有一定的參考和借鑒意義。
|