天天看點

WIN32GUI消息機制簡單探索

利用現有圖形庫可以做到快速開發一個桌面程式,但當你不滿足于庫産生的控件想自己定義控件或者想了解圖形系統運作時,卻不能随心所欲。圖形庫把底層接口封裝,固然大大簡化了開發的工作量,但其也把具體實作隐與表層之下,WIN32API是作業系統留給我們的接口,直接用API進行GUI的簡單設計無疑能夠極大的貼近圖形庫運轉的底層面貌,去了解消息循環的真正面貌。

以下為MSDN的建立一個主視窗的例子:

#include <windows.h> 
 
// Global variable 
 
HINSTANCE hinst; 
 
// Function prototypes. 
 
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int); 
InitApplication(HINSTANCE); 
InitInstance(HINSTANCE, int); 
LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM); 
 
// Application entry point. 
 
int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, 
    LPSTR lpCmdLine, int nCmdShow) 
{ 
    MSG msg; 
 
    if (!InitApplication(hinstance)) 
        return FALSE; 
 
    if (!InitInstance(hinstance, nCmdShow)) 
        return FALSE; 
 
    BOOL fGotMessage;
    while ((fGotMessage = GetMessage(&msg, (HWND) NULL, 0, 0)) != 0 && fGotMessage != -1) 
    { 
        TranslateMessage(&msg); 
        DispatchMessage(&msg); 
    } 
    return msg.wParam; 
        UNREFERENCED_PARAMETER(lpCmdLine); 
} 
 
BOOL InitApplication(HINSTANCE hinstance) 
{ 
    WNDCLASSEX wcx; 
 
    // Fill in the window class structure with parameters 
    // that describe the main window. 
 
    wcx.cbSize = sizeof(wcx);          // size of structure 
    wcx.style = CS_HREDRAW | 
        CS_VREDRAW;                    // redraw if size changes 
    wcx.lpfnWndProc = MainWndProc;     // points to window procedure 
    wcx.cbClsExtra = 0;                // no extra class memory 
    wcx.cbWndExtra = 0;                // no extra window memory 
    wcx.hInstance = hinstance;         // handle to instance 
    wcx.hIcon = LoadIcon(NULL, 
        IDI_APPLICATION);              // predefined app. icon 
    wcx.hCursor = LoadCursor(NULL, 
        IDC_ARROW);                    // predefined arrow 
    wcx.hbrBackground = GetStockObject( 
        WHITE_BRUSH);                  // white background brush 
    wcx.lpszMenuName =  "MainMenu";    // name of menu resource 
    wcx.lpszClassName = "MainWClass";  // name of window class 
    wcx.hIconSm = LoadImage(hinstance, // small class icon 
        MAKEINTRESOURCE(5),
        IMAGE_ICON, 
        GetSystemMetrics(SM_CXSMICON), 
        GetSystemMetrics(SM_CYSMICON), 
        LR_DEFAULTCOLOR); 
 
    // Register the window class. 
 
    return RegisterClassEx(&wcx); 
} 
 
BOOL InitInstance(HINSTANCE hinstance, int nCmdShow) 
{ 
    HWND hwnd; 
 
    // Save the application-instance handle. 
 
    hinst = hinstance; 
 
    // Create the main window. 
 
    hwnd = CreateWindow( 
        "MainWClass",        // name of window class 
        "Sample",            // title-bar string 
        WS_OVERLAPPEDWINDOW, // top-level window 
        CW_USEDEFAULT,       // default horizontal position 
        CW_USEDEFAULT,       // default vertical position 
        CW_USEDEFAULT,       // default width 
        CW_USEDEFAULT,       // default height 
        (HWND) NULL,         // no owner window 
        (HMENU) NULL,        // use class menu 
        hinstance,           // handle to application instance 
        (LPVOID) NULL);      // no window-creation data 
 
    if (!hwnd) 
        return FALSE; 
 
    // Show the window and send a WM_PAINT message to the window 
    // procedure. 
 
    ShowWindow(hwnd, nCmdShow); 
    UpdateWindow(hwnd); 
    return TRUE; 
 
} 
           

總的來說建立一個主視窗大緻分為三個部分:1.注冊視窗類2.建立主視窗3.建立消息循環。

一。注冊視窗類

這裡有一個結構體WNDCLASSEX:

typedef struct tagWNDCLASSEX {
  UINT      cbSize;
  UINT      style;
  WNDPROC   lpfnWndProc;
  int       cbClsExtra;
  int       cbWndExtra;
  HINSTANCE hInstance;
  HICON     hIcon;
  HCURSOR   hCursor;
  HBRUSH    hbrBackground;
  LPCTSTR   lpszMenuName;
  LPCTSTR   lpszClassName;
  HICON     hIconSm;
} WNDCLASSEX, *PWNDCLASSEX;
           

這個結構體包含了視窗類的資訊,被用于 RegisterClassEx (即注冊視窗類的函數)和GetClassInfoEx這兩個函數。這個結構體比較重要的一個參數是lpfnWndProc,lpfn即long pointer to function,是一個指向函數的長指針,指向視窗的處理函數。

在Window Procedures的使用中,MSDN給出了其例子:

LRESULT CALLBACK MainWndProc(
    HWND hwnd,        // handle to window
    UINT uMsg,        // message identifier
    WPARAM wParam,    // first message parameter
    LPARAM lParam)    // second message parameter
{ 
 
    switch (uMsg) 
    { 
        case WM_CREATE: 
            // Initialize the window. 
            return 0; 
 
        case WM_PAINT: 
            // Paint the window's client area. 
            return 0; 
 
        case WM_SIZE: 
            // Set the size and position of the window. 
            return 0; 
 
        case WM_DESTROY: 
            // Clean up window-specific data objects. 
            return 0; 
 
        // 
        // Process other messages. 
        // 
 
        default: 
            return DefWindowProc(hwnd, uMsg, wParam, lParam); 
    } 
    return 0; 
}
           

注冊視窗類的函數為RegisterClassEx :

ATOM WINAPI RegisterClassEx(
  _In_ const WNDCLASSEX *lpwcx
);
           

它是以指向 結構體WNDCLASSEX的指針為參數的。

二。建立主視窗

建立主視窗用的是CreatWindows函數,傳回的是指向視窗的指針。調用ShowWindow就可以顯示主視窗了

三。建立消息循環

一般消息循環建立如下:

/* Run the message loop. It will run until GetMessage() returns 0 */
    while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Translate virtual-key messages into character messages */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */
        DispatchMessage(&messages);
    }
           

GetMessage不斷抓取消息,轉換後分派給相應的視窗處理函數(視窗過程Window Procedure)。

每當使用者移動滑鼠,單擊滑鼠按鈕或敲擊鍵盤時,滑鼠或鍵盤的裝置驅動程式将輸入轉換為消息并将其放置在系統消息隊列中。系統從系統消息隊列中删除一個消息,檢查它們以确定目标視窗,然後将其釋出到建立目标視窗的線程的消息隊列中。線程的消息隊列接收線程建立的視窗的所有滑鼠和鍵盤消息(GetMessage)。該線程從其隊列中删除消息,并引導系統将其發送到适當的視窗過程(TranslateMessage DispatchMessage)進行處理。

-------------------------------------------------------我是帥氣的分割線--------------------------------------------------------------------------

上面簡單說了建立一個主視窗,但我想要的是消息是如何循環的。現在知道的是程式會将消息隊列中的消息一條條抓取并分派到相應視窗的處理函數讓其對這條消息進行處理,我現在想知道的是消息的産生及分派:消息如何産生,并且還能标記出要發往的視窗,是作業系統完成了這一切嗎?還有就是消息如何分派,是抓取之後直接傳給控件所在主視窗(假設按下了主視窗的一個按鈕),還是說傳給了按鈕,再由按鈕把消息給主視窗?下面進行實驗:

建立按鈕:

hwndButton=CreateWindow(
                    "BUTTON",  // Predefined class; Unicode assumed
                    "OK",      // Button text
                    WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,  // Styles
                    10,         // x position
                    10,         // y position
                    100,        // Button width
                    100,        // Button height
                    hwnd,     // Parent window
                    (HMENU)520,       // No menu.
                    (HINSTANCE)GetWindowLong(hwnd, GWL_HINSTANCE),
                    NULL);      // Pointer not needed.
           

主視窗的Window Procedures:

LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{


    switch (message)                  /* handle the messages */
    {
        case WM_DESTROY:
            PostQuitMessage (0);       /* send a WM_QUIT to the message queue */
            break;
        case WM_COMMAND:
            if (LOWORD(wParam) == 520 && HIWORD(wParam) == BN_CLICKED)
            {
                MessageBox(hwnd, TEXT("22222"), TEXT("11111"), MB_OK);
            }
            break;
        default:                      /* for messages that we don't deal with */
            return DefWindowProc (hwnd, message, wParam, lParam);
    }

    return 0;
}
           

其中WM_COMMAND即為處理按鍵按下消息的處理分支。按鍵能正常工作,如下:

WIN32GUI消息機制簡單探索

下面擷取按鈕的處理函數并設定新的處理函數:

SetWindowLong函數傳回一個指向原始視窗過程的指針; 使用此指針将消息傳遞到原始過程。子類視窗過程必須使用CallWindowProc函數調用原始視窗過程。

LRESULT CALLBACK ButtonWinProc(HWND, UINT, WPARAM, LPARAM);//新的按鍵回調函數
WNDPROC OldButtonwinProc;//儲存舊回調函數的指針
OldButtonwinProc=(WNDPROC)SetWindowLong(hwndButton,GWL_WNDPROC,(LONG)ButtonWinProc);//設定新的回調函數儲存舊的
LRESULT CALLBACK ButtonWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)//新的回調函數
{
    if(message==WM_LBUTTONDOWN) cout<<"down ";
    if(message==WM_LBUTTONUP) cout<<"up ";
    if(message==BM_CLICK) cout<<"click ";

    switch (message)                  /* handle the messages */
    {
        default:
            return CallWindowProc(OldButtonwinProc,hwnd,message,wParam,lParam);//預設調用原有處理函數
    }
}
           

在WinMain裡發送模拟按鍵點選的消息給按鍵:

SendMessage(hwndButton,BM_CLICK,520,0);
           

主視窗回調函數如下:

LRESULT CALLBACK WindowProcedure (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)                  /* handle the messages */
    {
        case WM_DESTROY:
            PostQuitMessage (0);       /* send a WM_QUIT to the message queue */
            break;
        case WM_COMMAND:
            if (LOWORD(wParam) == 520 && HIWORD(wParam) ==  BN_CLICKED)
            {
                cout<<"inmainframe ";
            }
            break;
        default:                      /* for messages that we don't deal with */
            return DefWindowProc (hwnd, message, wParam, lParam);
    }
    return 0;
}
           

結果如下:

WIN32GUI消息機制簡單探索

而如果按鍵的回調函數改為如下:

LRESULT CALLBACK ButtonWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if(message==WM_LBUTTONDOWN) cout<<"down ";
    if(message==WM_LBUTTONUP) cout<<"up ";
    if(message==BM_CLICK) cout<<"click ";

    switch (message)                  /* handle the messages */
    {
        default:;
            //return CallWindowProc(OldButtonwinProc,hwnd,message,wParam,lParam);
    }
}
           

則隻會列印click。

WIN32GUI消息機制簡單探索

這就能夠有一定可信度的說明,當按鍵處理函數接到BM_CLICK後,會給按鍵發送WM_LBUTTONDOWN和WM_LBUTTONUP,然後按鍵給主視窗發送了按鍵點選的消息,也就完成了一次按鍵點選的模拟。另外類似BM_CLICK中的M表示向控件發送的消息,也就是指令控件做事情,而類似BN_CLICKED中的N則表明是控件向外部(父視窗)發出的的通知,表示自己的狀态。

再将WinMian中消息循環改為如下:

while (GetMessage (&messages, NULL, 0, 0))
    {
        /* Translate virtual-key messages into character messages */
        TranslateMessage(&messages);
        /* Send message to WindowProcedure */

        if(messages.hwnd==hwndButton&&messages.message==WM_LBUTTONDOWN) cout<<"111";
        if(messages.hwnd==hwnd&&messages.message==WM_COMMAND) cout<<"222";

        DispatchMessage(&messages);
    }
           

結果為:

WIN32GUI消息機制簡單探索

這說明按鍵按下确實是WinMain循環裡抓取并直接送往按鍵的,而按鍵則是負責按鍵點選消息的發送,且不會再次進入WinMain消息循換了。

由此可總結,作業系統将各種消息放入應用程式隊列,而應用程式抓取消息驅動整個程式一步步響應。一次點選按鍵,WinMain裡面抓取并派送給按鍵滑鼠左鍵按下與擡起這兩個消息,而後按鍵把自身“點選”這個狀态發送給父視窗,進而使父視窗能夠處理滑鼠點選事件。至于消息的産生,如何标記出要發往的視窗?誰标記的?這些還有待探索。。。。

繼續閱讀