1. 程式人生 > >深入淺出話VC++(1)——Windows程式內部執行機制

深入淺出話VC++(1)——Windows程式內部執行機制

一、引言

要想熟練掌握Windows應用程式的開發,首先需要理解Windows平臺下程式執行的內部機制,然而在.NET平臺下,建立一個Windows桌面程式,只需要簡單地選擇Windows窗體應用程式就可以了,微軟幫我們做了非常好的封裝,以至於對於很多.NET開發人員至今也不清楚Windows 平臺下程式執行的內部機制,所以本專題將深入剖析下Windows 程式的內部執行機制。

二、Windows平臺下幾個基礎概念

有朋友會問,理解了程式執行的內部機制有什麼用,因為在我們實際開發中用得微軟提供的模板來進行程式設計?對於這個疑問,我的回答是——理解了Windows平臺下程式的執行內部機制可以使我們更有自信地寫程式碼,因為我們知道模板後臺幫我們封裝的內容,並且理解這點也是打好了基礎,基礎打好了,學習新的知識也就快了。

2.1 視窗與控制代碼

視窗是Windows應用程式中非常重要的一個元素,一個Windows應用程式至少要有一個視窗,稱為主視窗,視窗是我們看到的一塊矩形區域,它是與使用者進行互動的介面,利用視窗可以接受使用者的輸入以及對使用者輸入的響應。例如我們看到的QQ登陸介面就是一個視窗。視窗又可分為客戶區和非客戶區,如下圖所示,其中,客戶區通常用來顯示控制元件或文字,標題欄、系統選單,選單欄、最小化框和最大化框、可調邊框都稱為視窗的非客戶區,它們主要由Windows系統進行管理,我們建立的應用程式主要負責客戶區的外觀顯示和操作,視窗也可以有一個父視窗,並且,對話方塊和訊息框都是屬於視窗。

在Windows應用程式中,控制代碼是用來唯一標識視窗的,我們想對某個窗體進行操作時,必須首先獲得該視窗的控制代碼,控制代碼還包括圖示控制代碼(如上圖中Form1前小圖示),游標控制代碼(即移到窗體時顯示的游標),和畫刷控制代碼(上圖中客戶區中顏色就是通過指定視窗類的背景畫刷控制代碼進行設定的,關於視窗類的結構會在下面介紹)。對於控制代碼的理解,大家簡單理解為用來標識窗體,它的型別為struct,這點可以通過在VS中通過F12檢視HWND的定義。

2.2 訊息與訊息佇列

Windows 作業系統是基於事件驅動的一種作業系統,所以在Windows平臺下所有應用程式也是基於事件驅動機制,即是基於訊息的。例如,當用戶在視窗中按下滑鼠左鍵時,作業系統會知曉這一事件,於是將事件封裝成一個訊息,傳遞到應用程式的訊息佇列中,,然後應用程式從訊息佇列中取出訊息並進行響應。在這個處理過程中,作業系統會呼叫應用程式中專門負責訊息處理的函式,該函式稱為視窗過程。

1. 訊息

// MSG
typedef struct tagMSG {
  HWND   hwnd;
  UINT   message;
  WPARAM wParam;
  LPARAM lParam;
  DWORD  time;
  POINT  pt;
} MSG, *PMSG, *LPMSG;

該結構體中各引數的含義如下:

  • hwnd——表示訊息所屬的視窗,我們通常開發的視窗應用程式中,一個訊息都是與某個視窗相關聯的。
  • message——指定訊息的識別符號,在Windows中,訊息是由一個數值來表示的,不同訊息對應於不同的數值,但是由於數值不便於記憶,所以Windows將訊息對應的數值定義為巨集,為了使開發人員明白定義的巨集為一個訊息時,把訊息巨集都定義以WM(Windows Message的縮寫)為字首。例如 WM_CHAR表示字元訊息,WM_LBUTTONDOWN表示滑鼠左鍵按下訊息。要想知道每個巨集對應的陣列,可以在VS中用F12 來檢視巨集對應的數值。
  • time——表示訊息被傳送到訊息佇列的時間
  • pt——表示當訊息被傳送時游標在螢幕中的位置

2. 訊息佇列

每一個Windows應用程式開始執行後,系統都會為該程式建立一個訊息佇列(從而得出訊息佇列是由系統建立的)來存放該程式建立過程中的視窗訊息。當用戶在視窗中傳送一個訊息時,系統會將該訊息推送到訊息佇列中,而應用程式的過程函式則通過一個訊息迴圈不斷地從訊息佇列中取出訊息,並進行響應。這種訊息機制,就是Windows程式執行的機制

在Windows程式中,訊息又可分為“進隊訊息”和“不進隊訊息”。進隊的訊息由系統放入到應用程式的訊息佇列中,然後由應用程式取出併發送給視窗過程處理。不進隊的訊息由系統直接呼叫視窗過程進行處理。

三、動手實現第一個Windows桌面程式

下面,讓我們手動要完成一個完整的Win32桌面程式,該程式實現的功能就是簡單地建立一個窗體,並在窗體中響應鍵盤及滑鼠訊息,實現該程式的步驟可分為:

  1. WinMain函式的定義
  2. 建立一個視窗
  3. 進行訊息迴圈,從訊息佇列中獲得一個訊息
  4. 編寫視窗過程函式,用於處理訊息。

3.1 WinMain函式的定義

int WINAPI WinMain(
  _In_  HINSTANCE hInstance,
  _In_  HINSTANCE hPrevInstance,
  _In_  LPSTR lpCmdLine,
  _In_  int nCmdShow
);
上面我列出的定義與MSDN略有不同,MSDN中定義使用的是CALLBACK,而我上面列出的是WINAPI,其實兩者都是一樣的,可以在VS中通過F12檢視巨集的定義可以發現:
#define CALLBACK    __stdcall
#define WINAPI      __stdcall

WinMain函式的4個引數是由系統呼叫WinMain函式時,傳遞給應用程式應用程式的,它們具體的含義為:

  • hInstance——表示該程式當前執行例項的控制代碼,當程式在Windows平臺下執行時,該值唯一標識著執行中的例項,這裡需要朱注意:一個應用程式可以執行多個例項,每執行一個例項,系統都會為該例項分配一個控制代碼值,並通過hInstance引數傳遞給WinMain函式(從這句話可以得出,WinMain函式並不是程式呼叫的第一個函式,你在VS的呼叫堆疊中可以發現,在WinMain函式之前還有:WinMainCRTStartup()和_tmainCRTStartup()函式)。這裡的控制代碼應該與視窗控制代碼區分開來,hInstance引數代表的是應用程式例項的控制代碼,而一個應用程式可以有多個視窗,每個視窗都對應一個控制代碼
  • hPrevInstance——表示當前例項的前一個例項的控制代碼。在Win32環境下,它的值總是為NULL;
  • lpCmdLine——表示一個以空終止的字串,指定傳遞給應用程式的命令列引數,例如:你雙擊一個Word檔案,此時此時將該檔案的路徑作為命令列引數傳遞給Word應用程式,安裝的Word應用程式得到該檔案的路徑後,就在視窗中開啟檔案的內容。我們可以在VS中通過屬性——>除錯——>命令引數來編輯想輸入的命令引數
  • nCmdShow——指定視窗應該如何顯示,如最大化、最小化等。如.NET中Form的WindowState屬性。

3.2 建立一個視窗

建立一個完整的視窗,需要經過下面4個步驟:

  1. 設計一個視窗類
  2. 註冊視窗類
  3. 建立視窗類
  4. 顯示及更新視窗。

上面四個步驟,仔細想想你也知道的,我們想建立一個視窗,首先應該設計下它長什麼樣子吧(第一步),設計完成之後,總要讓系統知道已經設計完了視窗了吧,所以我們要通過註冊視窗類的方式來通知系統(第二步),成功註冊之後,系統已經知道存在這樣一個視窗了,接下來就應該建立視窗類的一個例項了(第三步),最後就是把建立完的視窗顯示和再加修飾下(第四步)。這四步完全來源我們生活,例如,上司找你做一個東西出來,你首先要在腦海中構想它的樣子(設計,第一步),設計完之後,要讓老闆知道你設計完成了就應該告知老闆你設計完了(第二步),老闆知道之後,老闆覺得可以就命令工廠把模型做出來(第三步),最後就是拿給客戶看(第四步)。下面我們按照這4步來完成一個視窗的建立。

3.2.1 設計一個視窗類

typedef struct tagWNDCLASS {
  UINT      style; // 視窗的樣式,如CS_HREDRAW表示當視窗水平向上的寬度發生變化時,將重繪整個視窗
  WNDPROC   lpfnWndProc; // 一個函式指標,指向視窗過程函式,用於對事件進行響應,該函式是回撥函式,可以對照委託回撥進行理解
  int       cbClsExtra; // 類的附加記憶體,用於儲存類的附加資訊,一般把該引數設定為0
  int       cbWndExtra; // 視窗的附加記憶體,一般也設定為0
  HINSTANCE hInstance; // 包含視窗的應用程式例項控制代碼
  HICON     hIcon; // 視窗類的圖示控制代碼,如果設定為NULL,那麼系統會提供一個預設的圖示
  HCURSOR   hCursor; // 視窗類的游標
  HBRUSH    hbrBackground;// 視窗類的背景畫刷控制代碼,
  LPCTSTR   lpszMenuName; // 指定選單資源的名字
  LPCTSTR   lpszClassName; // 指定視窗類的名字
} WNDCLASS, *PWNDCLASS;

在程式中,我們建立一個視窗類物件,然後為該物件指定其屬性來完成視窗類的設計。

3.2.2 註冊視窗類

設計完視窗類之後,我們需要使用RegisterClass(CONST WNDCLASS *lpWndClass)函式來完成視窗類的註冊,註冊成功之後,我們才可以建立該型別的視窗。

3.2.3 建立視窗

註冊視窗類之後,即已經告知系統,我們已經存在這樣的一個視窗類,下面可以使用CreateWindow()函式來建立該型別的一個視窗,CreateWindow函式的定義如下:

HWND WINAPI CreateWindow(
  _In_opt_  LPCTSTR lpClassName,// 註冊視窗類的名字
  _In_opt_  LPCTSTR lpWindowName, // 視窗名,如果視窗樣式指定了標題欄,那麼視窗名將顯示在標題欄上
  _In_      DWORD dwStyle, // 指定建立視窗的樣式
  _In_      int x,// 視窗左上角x座標
  _In_      int y, // 視窗左上角y座標
  _In_      int nWidth,// 視窗寬度
  _In_      int nHeight,// 視窗高度
  _In_opt_  HWND hWndParent,// 視窗的父視窗控制代碼
  _In_opt_  HMENU hMenu,// 視窗選單控制代碼
  _In_opt_  HINSTANCE hInstance,// 包含視窗的應用程式例項
  _In_opt_  LPVOID lpParam // 作為WM_CREATE訊息的附加引數lParam傳入給視窗,WM_CREATE有兩個引數:wParam和lParam引數,更多內容參考MSDN:http://msdn.microsoft.com/en-us/library/ms632619(v=vs.85).aspx
);

3.2.4 顯示及更新視窗

建立視窗後,最好一步需要做的就是將視窗展示給使用者看,我們可以通過ShowWindow()和UpdateWindow()函式來完成,ShowWindow()函式來設定視窗的特殊狀態,呼叫完ShowWindow()之後,接下來呼叫UpdateWindow()函式來重新整理視窗。即把設定好的視窗繪製在桌面上。呼叫UpdateWindow()函式之後將傳送一個WM_PAINT訊息給視窗過程函式來進行處理,從而來重新整理視窗,注意,該WM_PAINT訊息是沒有放到前面介紹的訊息佇列中,屬於不入隊的訊息。

3.3 進行訊息迴圈,從訊息佇列中獲得一個訊息

接下面,我們需要實現一個訊息迴圈函式,來完成不斷從訊息佇列中取出訊息,並交給視窗過程函式進行處理。我們可以通過Windows API 中GetMessage()函式來完成這個過程,下面是該函式的原型:

BOOL WINAPI GetMessage(
  _Out_     LPMSG lpMsg, // 輸出引數,指向訊息的結構體指標
  _In_opt_  HWND hWnd, // 指定接收從哪個視窗的訊息
  _In_      UINT wMsgFilterMin,// 獲取訊息的最小值,通常設定為0
  _In_      UINT wMsgFilterMax// 獲取訊息的最大值。如果wMsgFilterMin和wMsgFilterMax都設定為0時,表示接收所有訊息
);

GetMessage()函式除了接收WM_QUIT訊息(接收WM_QUIT訊息返回0)外,接收其他函式都返回非零值,如果出現錯誤則返回-1。如引數hWnd為無效控制代碼時(即傳遞NULL),此時傳送錯誤返回為-1.

3.4 編寫視窗過程函式,用於處理訊息

LRESULT CALLBACK WindowProc(
  _In_  HWND hwnd,// 處理訊息的視窗控制代碼
  _In_  UINT uMsg,// 訊息程式碼
  _In_  WPARAM wParam, 
  _In_  LPARAM lParam // wParam和lParam是訊息的附加引數
);

3.5 完整的實現程式碼

有了上面的實現思路之後,那麼實現該程式將再簡單不過了,同時,大家可以根據下面程式碼來對比理解下上面介紹的理論,具體實現程式碼如下(這裡需要指明一點,如果不小心把回撥函式的實現的名字輸入錯誤時,將出現如下圖所示的錯誤):

// 手動實現一個Windows 程式
#include <Windows.h>
#include <stdio.h>


// 定義視窗過程函式,這裡可以設定為你想要的名字
LRESULT CALLBACK WinProc(
    HWND hwnd, // 視窗控制代碼
    UINT uMsg,// 訊息程式碼
    WPARAM wParam, // 第一個訊息引數
    LPARAM lParam // 第二個訊息引數
    );

int WINAPI WinMain(
    HINSTANCE hInstance, // 當前執行例項的控制代碼
    HINSTANCE hPrevInstance, // 當前例項的前一個例項控制代碼
    LPSTR lpCmdLine,// 指定傳遞給應用程式的命令列引數
    int nCmdShow // 指定程式的視窗如何顯示,例如最大化、最小化等
    )
{
    // 1. 設計一個視窗類
    WNDCLASS wndclass;
    wndclass.cbClsExtra=0;
    wndclass.cbWndExtra=0;
    wndclass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);// 指定背景畫刷
    wndclass.hCursor =LoadCursor(NULL,IDC_CROSS);// 指定視窗類的游標控制代碼
    wndclass.hIcon=LoadIcon(NULL,IDI_ERROR);
    wndclass.hInstance=hInstance;
    wndclass.lpfnWndProc=WinProc;
    wndclass.lpszClassName=L"learninghard2013"; // 設定視窗類的名稱
    wndclass.lpszMenuName=NULL;// 設計視窗類建立的視窗沒有預設的選單
    wndclass.style=CS_HREDRAW|CS_VREDRAW;// 設定視窗樣式為寬度和高度變化時,將重新繪製整個視窗

    // 2. 註冊視窗類
    RegisterClass(&wndclass);

    // 3. 建立視窗,定義一個變數來儲存成功建立視窗後返回的控制代碼
    HWND hwnd;
    hwnd =CreateWindow(L"learninghard2013",L"手動實現視窗應用程式",WS_OVERLAPPEDWINDOW,100,100,600,400,NULL,NULL,hInstance,NULL);

    // 4. 顯示和重新整理視窗
    ShowWindow(hwnd,SW_SHOWNORMAL);
    UpdateWindow(hwnd);

    // 定義訊息結構體,開始訊息迴圈
    MSG msg;
    BOOL breturn;
    // GetMessage接受到WM_QUIT訊息時返回為0,即為假
    while((breturn=GetMessage(&msg,hwnd,0,0))!=0)
    {
        if(breturn==-1)
        {
            // 出錯時退出
            return -1;
        }
        else
        {
            // 接受到訊息不為WM_QUIT訊息的情況
            // 將虛擬鍵訊息轉化為字元訊息,字元訊息被傳遞到呼叫執行緒的訊息佇列中,當下一次呼叫GetMessage函式被取出
            TranslateMessage(&msg);
            // 分發一個訊息到視窗過程,由視窗過程函式對訊息進行處理
            DispatchMessage(&msg);
        }
    }

    return msg.wParam;
}

// 實現視窗過程函式
LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam,LPARAM lParam)
{
    switch(uMsg)
    {
    case WM_CHAR:
        WCHAR szChar[20];
        swprintf(szChar,L"字元程式碼是 %d",wParam);
        MessageBox(hwnd,szChar,L"字元",0);
        break;
    case WM_LBUTTONDOWN:
        MessageBox(hwnd,L"滑鼠點選",L"訊息",0);
        swprintf(szChar,L"訊息附加資訊 %d",wParam);
        MessageBox(hwnd,szChar,L"訊息",0);
        HDC hdc;
        hdc =GetDC(hwnd);
        TextOut(hdc,0,50,L"LearningHard實現",wcslen(L"LearningHard實現"));
        ReleaseDC(hwnd,hdc);
        break;
    case WM_PAINT:
        HDC hDC;
        PAINTSTRUCT ps;
        hDC =BeginPaint(hwnd,&ps); // BeiginPaint只能在WM_PAINT訊息時呼叫
        TextOut(hDC,0,0,L"http://www.cnblogs.com/zhili/",wcslen(L"http://www.cnblogs.com/zhili/"));
        EndPaint(hwnd,&ps);
        break;
    case WM_CLOSE:
        if(IDYES==MessageBox(hwnd,L"是否真的結束",L"訊息視窗",MB_YESNO))
        {
            DestroyWindow(hwnd);
        }
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        // 呼叫預設的視窗過程
        return DefWindowProc(hwnd,uMsg,wParam,lParam);
    }
    return 0;
}

輸入上面的程式碼在VS中,按下Ctrl+F5按鈕執行程式,你將看到下面的視窗(你可以測試在視窗點選的效果和鍵盤按下效果):

四、小結

本專題介紹了Windows程式執行的內部機制,瞭解了本專題的內容,相信對.NET中WinForm應用程式背後實現的原理將不再陌生了。這裡再總結下純手動建立Windows 桌面程式的步驟:

  1. 檢視MSDN查詢WinMain的宣告並編寫應用程式中的WinMain函式
  2. 設計視窗類,檢視WNDCLASS
  3. 註冊視窗類,設計RegisterClass()函式
  4. 建立視窗,設計CreateWindow()函式
  5. 顯示並更新視窗,設計ShowWindow()和UpdateWindow()函式
  6. 實現訊息迴圈,從訊息佇列中取出訊息交給視窗過程函式處理,設計GetMessage()函式
  7. 實現視窗過程函式,檢視WindowProc函式。
本專題所有原始碼下載:Windows程式執行的內部機制原始碼