1. 程式人生 > >Windows的訊息機制要點

Windows的訊息機制要點

  • 執行緒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可以對以上系統元件傳送訊息。
    一、引言
    隨著Windows作業系統的不斷推廣,眾多軟體開發包都提供有開發基於Windows平臺應用軟體的功能。雖然這些開發包不盡相同,流行的有Visual C++、Visual Basic、Delphi、C++ Builder 等多種,但由這些不同語言開發的軟體有一點卻是相同的–都是運行於Windows 操作平臺,都必須接受Windows 的執行機制。作為Windows 作業系統靈魂的訊息機制也就必然為眾多用不同語言開發的Windows作業系統下執行的應用程式所接受。因此,要編寫深入的Windows程式,就必須對 Windows的執行機制有很好的認識和理解。本文下面將對Windows作業系統下的訊息執行機制做較為深入的剖析。
    二、Windows事件驅動機制
    我們當中不少使用VC、Delphi等作為開發語言的程式設計師是一步步從DOS下的Basic、C++中走過來的,而且大多在剛開始學習程式設計時也是先從 DOS下的程式設計環境入手的,因此在習慣了DOS下的過程驅動形式的順序程式設計方法後,往往在向Windows下的開發環境轉型的過程中會對 Windows所採取的事件驅動方式感到無法適應。因為DOS和Windows這兩種作業系統的執行機制是截然不同的,DOS下的任何程式都是使用順序的、過程驅動的程式設計方法。這種程式都有一個明顯的開始、明顯的過程以及一個明顯的結束,因此通過程式就能直接控制程式事件或過程的全部順序。即使是在處理異常時,處理過程也仍然是順序的、過程驅動的結構。而Windows的驅動方式則是事件驅動的,即程式的流程不是由事件的順序來控制,而是由事件的發生來控制,所有的事件是無序的,所為一個程式設計師,在編寫程式時,並不知道使用者會先按下哪個按紐,也就不知道程式先觸發哪個訊息。因此我們的主要任務就是對正在開發的應用程式要發出的或要接收的訊息進行排序和管理。事件驅動程式設計是密切圍繞訊息的產生與處理而展開的,一條訊息是關於發生的事件的訊息。
    三、Windows的訊息迴圈
    Windows作業系統為每一個正在執行的應用程式保持有一個訊息佇列。當有事件發生後,Windows並不是將這個激發事件直接送給應用程式,而是先將其翻譯成一個Windows訊息,然後再把這個訊息加入到這個應用程式的訊息佇列中去。應用程式需要通過訊息迴圈來接收這些訊息。在MFC中使用了對 WinAPI進行了很好封裝的類庫,雖然可以為程式設計提供一個面向物件的介面,使Windows程式設計師能夠以面象物件的方式進行程式設計,把那些進行SDK程式設計時最繁瑣的部分提供給程式設計師,使之專注於功能的實現,但是由於引入了很好的封裝特性,使我們不能直接操縱部分核心程式碼。對於訊息的迴圈和接收也只是通過類似於下面的訊息對映予以很簡單的表示:
    BEGIN_MESSAGE_MAP(CTEMMSView, CFormView)
    //{ { AFX_MSG_MAP(CTEMMSView)
    ON_WM_LBUTTONDOWN()
    ON_COMMAND(ID_OPENDATA, OnOpenData)
    ON_WM_TIMER()
    ON_WM_PAINT()
    //} } AFX_MSG_MAP
    END_MESSAGE_MAP()
    雖然上述訊息對映在程式設計過程中處理訊息非常簡練方便,但顯然是難於理解訊息是如何參與迴圈和分發的。因此有必要通過SDK(Software Developers Kit,軟體開發工具箱)程式碼深入到被MFC封裝的Windows程式設計的核心中來研究其具體是如何工作的。在SDK程式設計中,一般是在Windows應用程式的入口點WinMain函式中新增處理訊息迴圈的程式碼以檢索Windows送來的訊息,然後WinMain再把這些訊息分配給相應的視窗函式並處理它們:
    ……
    MSG msg; //定義訊息名
    while (GetMessage (& msg, NULL, 0, 0))
    {
    TranslateMessage (& msg) ; //翻譯訊息
    DispatchMessage (& msg) ; //撤去訊息
    }
    return msg.wParam ;
    上述幾句雖然簡單但卻是所有Windows程式的關鍵程式碼,擔負著獲取、解釋和分發訊息的任務,下面就重點對其功能和作用進行分析:
    MSG結構在標頭檔案中定義如下:
    typedef struct tagMSG
    {
    HWND hwnd;
    UINT message;
    WPARAM wParam;
    LPARAM lParam;
    DWORD time;
    POINT pt;
    } MSG, *PMSG;
    其資料成員的具體意義如下:
    hwnd:訊息將要傳送到的那個視窗的控制代碼,用這個引數可以決定讓哪個視窗接收訊息。
    message:訊息號,它唯一標識了一種訊息型別。每種訊息型別都在Windows檔案進行了預定義。
    wParam:一個32位的訊息引數,這個值的確切意義取決於訊息本身。
    lParam:同上。
    time:訊息放入訊息佇列中的時間,在這個域中寫入的並非當時日期,而是從Windows啟動後所測量的時間值。Windows用
    這個域來使用訊息保持正確的順序。
    pt:訊息放入訊息佇列時的滑鼠座標。
    訊息迴圈以GetMessage呼叫開始,它從訊息佇列中取出一個訊息。該函式的四個引數可以有控制地獲取訊息,第一個引數指定要接收訊息的MSG結構的地址,第二個引數表示視窗控制代碼,一般將其設定為空,表示要獲取該應用程式建立的所有視窗的訊息;第三、四引數用於指定訊息範圍。後面三個引數被設定為預設值,用於接收發送到屬於這個應用程式的任何一個視窗的所有訊息。在接收到除WM_QUIT之外的任何一個訊息後,GetMessage()返回 TRUE;如果GetMessage收到一個WM_QUIT訊息,則返回FALSE以退出訊息迴圈,終止程式執行。因此,在接收到WM_QUIT之前,帶有GetMessage()的訊息迴圈可以一直迴圈下去。當除WM_QUIT的訊息用GetMessage讀入後,首先要經過函式 TranslateMessage()對其進行解釋,但對大多數訊息來說並不起什麼作用。這裡起關鍵作用的是DispatchMessage()函式,把由GetMessage獲取的Windows訊息傳送給在MSG結構中為視窗所指定的視窗過程。在訊息處理函式處理完訊息之後,程式碼又迴圈到開始去接收另一個訊息,這樣就完成了一個完整的訊息迴圈。
    由於Windows作業系統是一種非剝奪式多工作業系統。只有在應用程式主動交出CPU控制權後,Windows才能把控制權交給其他應用程式。在訊息迴圈中,一定要有能交出控制的系統函式才能實現協同式多工操作。能完成該功能的只有GetMessage、PeekMessage和 WaitMessage這三個函式,如果在應用程式中長期不去呼叫這三個函式之一其他任務則無法執行。GetMessage函式在找不到等待應用程式處理的訊息時,會自動交出控制權,由Windows把CPU的控制權交給其他等待獲取控制權的應用程式。由於任何Windows應用程式都含有一個訊息迴圈,這種隱式交出控制權的方式可以保證合併各個應用程式共享控制權。一旦發往該應用程式的訊息到達應用程式佇列,即開始執行GetMessage語句的下一條語句。使用GetMessage函式的訊息迴圈在訊息佇列中沒有訊息時將等待,如果需要,可以利用這段時間進行I/O埠操作等耗時操作,不過需要在訊息迴圈中使用PeekMessage函式來代替GetMessage。使用PeekMessage的方法同GetMessage類似,下面是一段使用 PeekMessage函式的訊息迴圈的典型例子:
    MSG msg;
    BOOL bDone=FALSE;
    do{
    if(PeekMessage(& msg,NULL,0,0,PM_REMOVE)){
    if(msg.message==WM_QUIT)
    bDone=TRUE;
    else{
    TranslateMessage(& msg);
    DispatchMessage(& msg);
    }
    }
    //無訊息處理,進行長時間操作
    else{
    ……//長時間操作
    }
    } while(!bDone)
    ……
    無論應用程式訊息佇列中是否有訊息,PeekMessage函式都立即返回,如果希望等待新訊息入隊,可以利用無返回值的函式WaitMessage配合PeekMessage進行訊息迴圈。
    四、對Windowds訊息的處理
    視窗過程處理訊息通常以switch語句開始,對於它要處理的每一條訊息ID都跟有一條case語句,這在功能上同MFC的訊息對映有些類似:
    switch(uMsgId)
    {
    case WM_TIMER:
    //對WM_TIMER定時器訊息的處理過程
    return 0;
    case WM_LBUTTONDOWN:
    //對WM_ LBUTTONDOWN滑鼠左鍵單擊訊息的處理過程
    ruturn 0;
    ……
    default:
    //其他訊息由這個預設處理函式來處理
    return DefWindowProc(hwnd,uMsgId,wParam,lParam);
    }
    在處理完訊息後必須返回0,這很重要,否則Windows將要不停地重試下去。對於那些在程式中不準備處理的訊息,視窗過程會把它們都扔給 DefWindowProc進行預設處理,而且還要返回那個函式的返回值。在訊息傳遞層次中,可以認為DefWindowProc函式是最頂層的函式。該函式發出WM_SYSCOMMAND訊息,由系統執行Windows環境中多數視窗所公用的各種通用操作,如更新視窗的正文標題等等。在MFC下可以用下述部分程式碼實現與上述SDK程式碼相同的功能:
    BEGIN_MESSAGE_MAP(CTEMMSView, CFormView)
    //{ { AFX_MSG_MAP(CTEMMSView)
    ON_WM_LBUTTONDOWN()
    ON_WM_TIMER()
    //} } AFX_MSG_MAP
    END_MESSAGE_MAP()
    小結:Windows環境提供有非常豐富的系統資源,在這個基礎上可以編制出能滿足各種各樣目標功能的應用系統。要深入Windows程式設計就必須首先對Windows系統的執行機理有很好的認識,本文僅針對Windows的一種重要執行機制–訊息機制作了較深入的剖析和闡述。對培養在Windows 下的程式設計思想有一定的幫助。對某些相關問題的詳細論述可以參考MSDN線上幫助的" SDK Reference" 部分。