Win32基於事件驅動的消息機制

消息機制和繪圖機制是微軟Windows及其周邊其它產品與生俱來的,是Win 系列OS作爲一個操作系統進行微機內部實現的二大支柱和特徵,消息系統是Windows下一切應用程序間,包括Windows自身,進行交互和通訊的渠道,是Windows實現對運行在其下的所有應用程序進行控制及應用程序對Windows進行響應的解決手段,因此對Windows的編程,無論是在哪種 語言規範和IDE 下,都不可避免地要涉及到消息處理,雖然有些編程語言如 VB 用事件驅動編程機制在很大程度上封裝了消息的複雜性,但若要深入Win32編程,就必須學習Windows的消息系統,正如遊戲編程要掌握Win的繪圖機制一樣,而只要你一旦深韻了這二大支柱和基本,你就掌握了Win32編程的根本。。
       

消息的產生來源於系統事情(包括計時器事件)和用戶事件,Windows用消息來調入和關閉(還有其它處理,如繪製一個窗口等)應用程序,一個典型表現是在關機操作中,Windows發一個關機的消息給所有正在運行的應用程序,告知它們退出內存,此時,應用程序用迴應消息的方法來響應OS,因此,消息是應用程序與WinOS交互的手段..
      

消息的主體是應用程序之間和應用程序與 OS 之間,(這是通俗的說法,其實在一個應用程序的內部,各窗口組件之間也存在着消息的流動,窗口組件與它們的父窗口和上層窗口之間當然也有消息的傳遞過程("命令傳遞",後面在跟蹤一個消息的路徑中將會詳談)Windows內部即時流動的消息數量是如此的寵大,程序實現之外的手工分析是一種很自不量力的事情)消息的最終主體卻是窗口與窗口之間,窗口與OS之間 - 因爲在MFC的技術規範裏,只有窗口進程才能發送和接收一個消息並處理它,當然一些非界面窗口類如文檔類也能處理一個消息,消息的最終歸宿是某個窗口類的成員函數,也就是進入消息處理函數被處理,或被某個非界面類也就是內部處理類如文檔類處理,系統中默認的窗口類和用戶註冊的窗口類都有進程,都能在內存中創建實在的窗口對象,窗口對象和窗口類接收和處理(千萬注意:接收一個消息和處理一個消息是相差甚大的二個過程,後面將在討論重定向一個消息技術時將談到)發往它或由它主動發往別的窗口進程或OS的消息,修改窗口進程干涉窗口進程對消息的處理過程(而不是接收過程,這個區別的詳細解釋請參見後面從"注意消息泵並不是一個.."起的文字)是可能的(窗口進程只是一段函數),但是如果這個窗口進程屬於別人,如系統的窗口類,你將沒有源程序進行修改,但卻可以用消息重定向的技術加以干涉,比如用戶自定義的窗口類,用戶完全可以自定義它的窗口進程,編寫自己的消息泵,實現對消息的重定向,編寫用戶自己的消息泵屬於Win32編程中重定向一個消息的七大技術之一。      

不要覺得奇怪,雖然我們擁有衆多“所見即所得”的編程方式來開發衆多界面精美的應用程序,這些可視化的編程環境提供了大量的類庫和控件,但是在開發者享受方便的同時,他們的手腳已經不知不覺的受到了限制,有很多深入到Windows內部的操作它們無法完成,爲什麼?因爲所用的類庫不支持。

  事實上這些類庫與控件都是架構在Window API的基礎上面API即 Application Programming Interface - 應用編程接口的縮寫,它不僅爲應用程序所調用,同時也是Windows的一部分,Windows自身的運行也調用這些API函數。要了解如何使用API就必須瞭解一些Windows的運行機制。

  簡單地說,Windows是由事件驅動的搶佔式多任務操作系統。事件驅動是相對於過程驅動而言的,它改變了原來文件的順序執行方式;Windows既然是多任務系統,就必須能同時處理多個事件,系統爲應用程序生成一個消息隊列,消息在上面被張貼和發送,應用程序只要從其消息隊列中取出消息,然後一一執行就可以了。

  現在,我將使用最最基本的範例程序 HelloWin 來說明WIN32 API的運行機制。首先,一個程序一定要有進入點,Win32 App的進入點函數的名稱是WinMain,它的原型如下:

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)

hInstance是所謂的“實例句柄”,它是一個數值,當程序在Windows下運行的時候,它被用來唯一的標示這個程序,雖然用戶可能同時運行多個同一個程序,即運行多個“實例”,我們可以看到,每一個實例都有不同的hInstance值。

hPrevInstance,簡單地說就是沒用...它是存在於16Windows程序中的,在編寫Windows 9x/NT/2000 程序的時候,總應該是NULL

szCmdLine是一個指針,指向一個以0爲終結的字串,裏面包含傳給該程序的命令行參數,如果想要讓程序處理命令行,那麼這個參數就有用了。

iCmdShow參數是一個數值,指示窗口將如何被顯示,這個數值由在Windows下運行該程序的程序所決定,通常是SW_SHOWNORMAL

接下來是註冊一個窗口類,窗口總是從窗口類的基礎上創建的,窗口類用以標示處理窗口消息的窗口過程,註冊窗口類時使用 RegisterClassEx() 函數,它只需要一個參數,一個指向類型爲 WNDCLASSEX 的結構指針。

具體註冊初始是這樣的:
WNDCLASSEX wcex;

wcex.cbSize = sizeof(WNDCLASSEX);     //結構的大小

wcex.style = CS_HREDRAW | CS_VREDRAW; //類風格
wcex.lpfnWndProc = (WNDPROC)WndProc; 
 //窗口類的窗口過程
wcex.cbClsExtra = 0;
            //在類結構中預留的空間
wcex.cbWndExtra = 0;
        //Windows內部保存的窗口結構中預留的空間
wcex.hInstance = hInstance;
        //程序的實例句柄
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_EXAMPLE);
  //程序圖標
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
         //結構的大小
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
  //指定窗口的背景顏色
wcex.lpszMenuName = (LPCSTR)IDC_EXAMPLE;
         //菜單
wcex.lpszClassName = szWindowClass;
           //類名,和程序名相同
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL); //
也是程序圖標

return RegisterClassEx(&wcex);

接下來,定義一個HWND,然後使用 CreateWindow() 函數,原型如下:
HWND CreateWindow(
  LPCTSTR lpClassName,    // 窗口類名
  LPCTSTR lpWindowName,  // 窗口標題
  DWORD dwStyle,       // 窗口風格
  int x,             // 初始
  int y,             // 初始y
  int nWidth,          // 窗口寬 
  int nHeight,         // 窗口高 
  HWND hWndParent,     // 父窗口句柄 
  HMENU hMenu,       // 菜單句柄
  HINSTANCE hInstance,   // 實例句柄
  LPVOID lpParam       // 創建參數
);

在 CreateWindow() 調用返回之後,Windows內部已經創建了這窗口。但是窗口併爲顯示,還需要兩個調用,一個是 ShowWindow(hwnd, iCmdShow):第一個參數是剛剛創建的窗口句柄,第二個參數是傳遞給WinMainnCmdShow;另一個是 UpdateWindow(hwnd) ,導致客戶區域被繪製。

接下來,程序通過執行一塊被稱爲“消息循環”的代碼從消息隊列中取出消息

while (GetMessage(&msg, NULL, 0, 0)) 
{
  if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)) 
  {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
  }
}

消息循環以 GetMessage 開始,它從消息隊列中取出一條消息,只要從消息隊列中取出消息的 Message 域不爲 WM_QUITGetMessage 就返回一個非零值,否則將導致程序退出消息循環,然後程序中止,返回 msg 結構的 wParam 參數。在循環中,TranslateMessage將 msg 結構的內容進行修改,而DispatchMessage 找出準備調用的窗口過程。

上面進行的僅僅是準備性工作:註冊窗口類、創建窗口、顯示窗口、進入消息循環取出消息而實際的動作都發生在窗口過程中。

LRESULT CALLBACK WndProc(HWND hWnd, //剛剛創建的窗口句柄
  UINT message,           //得到的消息
  WPARAM wParam, 
  LPARAM lParam           //消息的進一步詳細的參數
)

在程序中窗口過程通常是命名爲 WndProc 的函數,其實窗口過程可以任意的命名,一個Windows程序可以包含多個窗口過程,一個窗口過程總是與調用了 RegisterClassEx 註冊的窗口類相關聯,CreateWindow 函數根據窗口類來創建窗口,但是一個窗口類可以被用來創建多個窗口。

消息收到之後,接下來應該根據消息的不同來進行處理
switch(message)
{

 case ...:
 ...
 ...

HelloWin程序只需要處理兩條消息,即 WM_PAINT 和 WM_DESTROY

WM_PAINT 消息在Windows程序中的地位極其重要,當窗口客戶區的一部分或者全部變爲“無效”,必須進行刷新的時候,將由這條消息通知程序。

爲什麼客戶區域會變得無效呢?在創建窗口的時候,整個客戶區都是無效的,因爲還沒有畫任何的東西。第一條 WM_PAINT 消息指示窗口過程在窗口上面畫一些東西;還有在用戶改變了窗口的大小之後,客戶區域重新變得無效,除此之外最小化窗口之後再還原、窗口的一部分被覆蓋,都會引發這條消息。

WM_DESTROY消息則是當用戶按下“關閉”按鈕的時候被觸發,標準的處理方法是調用PostQuitMessage 將一條 WM_QUIT 消息插入消息隊列,這將使得 GetMessage 函數調用返回0,從而退出消息循環,結束整個程序。

其實,從上面可以看出,Windows程序的這種運行機制並不是很難理解,真正困難的是不知道調用什麼函數去完成想要的操作,以及怎樣調用那些函數,從而靈活的進行底層API程序開發,這是一個循序漸進的積累過程,沒有捷徑可走的。請各位一定要記住。 

附註

MFC中有七種技術可以用來重定向一個消息,它們是:1,子分類2,超分類3,OnCmdMsg(),4,SetCapture5,編寫自己的消息泵,6SetWindowsHookEx(),人們常說的鉤子函數,便是其中之一.
     
在談完消息泵的概念後,我們將一步一步追蹤一個消息在系統中的路徑,然後才能討論對它的重定向。
     
消息泵並不是一個窗口類的窗口進程,雖然它們都是函數,同樣都對注入到這個窗口進程的消息進行工作,而並不最終處理消息本身(上面已經說到原因),消息泵是一個通俗的說法,它只與消息被髮往窗口進程後的接收工作有關而不與處理過程有關(上面也已經說到消息的接收和處理是二不同過程),而窗口進程恰恰相反它只與處理有關不與接收有關下面開始詳述。
    
消息泵被包含 CWinApp 的成員函數Run()..

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章