從WinMain開始

Just4life發表於2013-05-02

本文應一個初學Windows程式設計的朋友而作。


目錄

  1. 抽象滲漏法則(摘自Joel)
  2. 針對Windows GUI程式設計的封裝
  3. 只用API函式建立GUI程式
  4. 完整的示例程式碼

一、抽象滲漏法則

根據Joel的抽象滲漏法則所有重大的抽象機制在某種程度上都是有漏洞的。Joel舉過一個例子:

C++字串型別應該能讓你假裝字串是個基本型別,它們嘗試“字串很難處理”這個事實抽象掉,讓它使用上象整型一樣容易,幾乎所有C++字串型別都會過載加號運算子,才能把字串連線寫成s + "bar"。不過你知道嗎?不管怎麼努力,世上還是沒有C++字串型別能讓你寫成 "foo"+"bar",因為C++裡的字串常數一定是char *,絕對不會變成字串。這個抽象機制呈現一個程式語言本身不給補的漏洞。

當我想訓練某人成為C++程式設計師時,最好能完全不教char *和指標運算,直接去學STL字串。問題是總有一天他們會寫出 "foo" + "bar" 這樣的程式碼,然後看到怪事出現,於是我就得停下來教他們有關char *的事情。他們也可能會試著呼叫某個需要OUT LPTSTR引數的Windows API,於是又得把char *、指標、Unicode、wchar_t以及tchar.h搞懂,才會知道如何呼叫。而這些全都是漏洞。

二、針對Windows GUI程式設計的封裝
而針對Windows的GUI程式設計,有很多封裝,如VCL、MFC、WTL等,凡此種種,都把WinMain、CreateWindow和 RegisterClassEx這些API與程式設計師隔離開來,對一個一開始就只接觸這些類庫的初學者來說,根本不知道原來一個Windows程式的入口點 其實是WinMain(事實上,一個Win32 EXE的入口點也並不是WinMain,而是程式語言的Runtime庫,不過,這裡把它抽象掉似乎更有益於理解)。

用API來搭建一個GUI程式是比較枯燥的,這種對於Windows GUI程式的枯燥搭建進行的抽象封裝,它的所謂“某種程度上的漏洞”,也許就是使程式設計師根本不知道每個視窗都有一個視窗類(不是指OO語言裡的Class),而每一個視窗類都有一個回撥函式(Callback)來對不同的視窗訊息進行不同的響應。

做為現在才接觸Windows GUI程式設計的初學者,幾乎都不瞭解一個Windows GUI程式是從WinMain開始的(前面說過,從WinMain開始也只是一個抽象而已,真實的情況並不是這樣),那麼如何僅僅使用Windows的API函式來建立一個GUI程式呢?

三、只用API搭建Windows GUI程式

1、WinMain()函式

首先,必須要宣告一個WinMain()函式(為了簡明起見,這裡先不討論_tWinMain這個巨集,也不考慮Unicode的問題),它的原型在Windows.h中定義:

  1. int WINAPI WinMain( 
  2.     HINSTANCE hInstance,       //程式當前例項的控制程式碼,以後隨時可以用GetModuleHandle(0)來獲得 
  3.     HINSTANCE hPrevInstance,   //這個引數在Win32環境下總是0,已經廢棄不用了 
  4.     char * lpCmdLine,          //指向以/0結尾的命令列,不包括EXE本身的檔名, 
  5.                                //以後隨時可以用GetCommandLine()來獲取完整的命令列 
  6.     int nCmdShow               //指明應該以什麼方式顯示主視窗 
  7. ); 
int WINAPI WinMain(
    HINSTANCE hInstance,       //程式當前例項的控制程式碼,以後隨時可以用GetModuleHandle(0)來獲得
    HINSTANCE hPrevInstance,   //這個引數在Win32環境下總是0,已經廢棄不用了
    char * lpCmdLine,          //指向以/0結尾的命令列,不包括EXE本身的檔名,
                               //以後隨時可以用GetCommandLine()來獲取完整的命令列
    int nCmdShow               //指明應該以什麼方式顯示主視窗
);


宣告,並且實現這個函式,讓Linker程式可以找到它,讓程式語言的執行時刻庫在完成一些必要的初始化工作後,能夠正確地呼叫它。所以,認為它就是程式的入口點,也是一種簡單的“抽象法則”。

在這個入口點函式中,需要按順序做下面幾件事(如果是基於事先設計並存放在資源裡的對話方塊的程式,稍有不同,以後再說):

  • 用RegisterClassEx函式登記一個獨一無二的Class
  • 用CreateWindowEx函式建立一個主視窗
  • 進入一個”訊息迴圈“,直到收到WM_QUIT訊息
  • 從WinMain函式返回

基本上所有的流程都如出一轍,所以完全可以設計出一個“Template模式”出來重用,讓以後的程式直接從某個抽象基類繼承,實現基類所需的虛方法就可以了,不過為了不偏離重心,還是用C語言的方式寫出來:

  1. int WINAPI WinMain(HINSTANCE, HINSTANCE, char *, int cmdShow) { 
  2.     if (registerMyClass() && createMyWindow(cmdShow)) { 
  3.         return messageLoop(); 
  4.     } else
  5.         std::ostringstream msg; 
  6.         msg << "建立主視窗失敗,錯誤程式碼:" << GetLastError(); 
  7.         MessageBoxA(0, msg.str().c_str(), 0, MB_OK | MB_ICONSTOP); 
  8.         return 0; 
  9.     } 
int WINAPI WinMain(HINSTANCE, HINSTANCE, char *, int cmdShow) {
    if (registerMyClass() && createMyWindow(cmdShow)) {
        return messageLoop();
    } else {
        std::ostringstream msg;
        msg << "建立主視窗失敗,錯誤程式碼:" << GetLastError();
        MessageBoxA(0, msg.str().c_str(), 0, MB_OK | MB_ICONSTOP);
        return 0;
    }
}

如此簡單,WinMain這個函式只有這麼短,分別呼叫三個自定義函式就OK了。

       

2、視窗訊息回撥函式

簡單地說,回撥(Callback)函式就是一個按規定原型實現的一個函式,當別人來呼叫。比如說,每個視窗都有一個視窗類(用RegisterClassEx登記的Class,或者系統預設已實現的Class),每個視窗類有一個回撥函式,當視窗收到WIndows訊息的時候,就會去呼叫這個回撥函式,而這個回撥函式的程式碼是程式設計師自己寫的,用來根據實際情況處理不同的視窗訊息。

  1. LRESULT CALLBACK onMainWndMessage(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) { 
  2.     switch (msg) { 
  3.     case WM_DESTROY: 
  4.         PostQuitMessage(0); //如果是“視窗銷燬”事件,則應該在訊息佇列中投遞 
  5.         break;              //一個WM_QUIT訊息,使GetMessage()返回FALSE 
  6.     default
  7.         return DefWindowProc(wnd, msg, wParam, lParam); 
  8.     } 
  9.     return 0; 
LRESULT CALLBACK onMainWndMessage(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) {
    switch (msg) {
    case WM_DESTROY:
        PostQuitMessage(0); //如果是“視窗銷燬”事件,則應該在訊息佇列中投遞
        break;              //一個WM_QUIT訊息,使GetMessage()返回FALSE
    default:
        return DefWindowProc(wnd, msg, wParam, lParam);
    }
    return 0;
}


       

3、登記視窗類

在建立主視窗之前,一定要先用RegisterClassEx這個API函式登記一個類,類名必須是獨一無二的,所以一般都用GUID字串來做類名。

  1. bool registerMyClass() { 
  2.     WNDCLASSEX  wce = {0}; 
  3.     wce.cbSize          = sizeof(wce); 
  4.     wce.style           = CS_VREDRAW | CS_HREDRAW; 
  5.     wce.lpfnWndProc     = &onMainWndMessage;  //指明回撥函式 
  6.     wce.hInstance       = GetModuleHandle(0); 
  7.     wce.hIcon           = LoadIcon(0, MAKEINTRESOURCE(IDI_WINLOGO)); 
  8.     wce.hCursor         = LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW)); 
  9.     wce.hbrBackground   = reinterpret_cast<HBRUSH>(COLOR_BTNFACE+1); 
  10.     wce.lpszClassName   = CLASS_NAME; //獨一無二的類名 
  11.     wce.hIconSm         = wce.hIcon; 
  12.     return 0!=RegisterClassEx(&wce); 
bool registerMyClass() {
    WNDCLASSEX  wce = {0};
    wce.cbSize          = sizeof(wce);
    wce.style           = CS_VREDRAW | CS_HREDRAW;
    wce.lpfnWndProc     = &onMainWndMessage;  //指明回撥函式
    wce.hInstance       = GetModuleHandle(0);
    wce.hIcon           = LoadIcon(0, MAKEINTRESOURCE(IDI_WINLOGO));
    wce.hCursor         = LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW));
    wce.hbrBackground   = reinterpret_cast<HBRUSH>(COLOR_BTNFACE+1);
    wce.lpszClassName   = CLASS_NAME; //獨一無二的類名
    wce.hIconSm         = wce.hIcon;
    return 0!=RegisterClassEx(&wce);
}


    

4、建立主視窗

(略,直接看完整程式碼

5、訊息迴圈

訊息迴圈很簡單,僅當GetMessage這個API函式返回FALSE時,才退出迴圈。而GetMessage()僅當處理到訊息佇列中的WM_QUIT訊息時才會返回FALSE。

  1. int messageLoop() { 
  2.     MSG msg; 
  3.     while (GetMessage(&msg, 0, 0, 0)) { 
  4.         TranslateMessage(&msg); 
  5.         DispatchMessage(&msg); 
  6.     } 
  7.     return static_cast<int>(msg.wParam); 
int messageLoop() {
    MSG msg;
    while (GetMessage(&msg, 0, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    return static_cast<int>(msg.wParam);
}


    

四、完整的示例程式碼

  1. #include <sstream> 
  2. #include <Windows.h> 
  3.  
  4. //獨一無二的類名,一般用GUID字串,以免與其他程式的類名重複 
  5. static const char * CLASS_NAME = "{198CEAB2-AD78-4ed3-B099-247639080CB0}"
  6.  
  7. /************************************************************************
  8.     回撥函式,當主視窗收到任何Windows訊息時被呼叫
  9. ************************************************************************/ 
  10. LRESULT CALLBACK onMainWndMessage(HWND wnd, UINT msg, WPARAM wParam, LPARAM lParam) { 
  11.     switch (msg) { 
  12.     case WM_DESTROY: 
  13.         PostQuitMessage(0); //如果是“視窗銷燬”事件,則應該在訊息佇列中投遞 
  14.         break;              //一個WM_QUIT訊息,使GetMessage()返回FALSE 
  15.     default
  16.         return DefWindowProc(wnd, msg, wParam, lParam); 
  17.     } 
  18.     return 0; 
  19.  
  20. /************************************************************************
  21.     登記自己的視窗類
  22. ************************************************************************/ 
  23. bool registerMyClass() { 
  24.     WNDCLASSEX  wce = {0}; 
  25.     wce.cbSize          = sizeof(wce); 
  26.     wce.style           = CS_VREDRAW | CS_HREDRAW; 
  27.     wce.lpfnWndProc     = &onMainWndMessage;  //指明回撥函式 
  28.     wce.hInstance       = GetModuleHandle(0); 
  29.     wce.hIcon           = LoadIcon(0, MAKEINTRESOURCE(IDI_WINLOGO)); 
  30.     wce.hCursor         = LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW)); 
  31.     wce.hbrBackground   = reinterpret_cast<HBRUSH>(COLOR_BTNFACE+1); 
  32.     wce.lpszClassName   = CLASS_NAME; //獨一無二的類名 
  33.     wce.hIconSm         = wce.hIcon; 
  34.     return 0!=RegisterClassEx(&wce); 
  35.  
  36. /************************************************************************
  37.     建立並顯示主視窗
  38. ************************************************************************/ 
  39. bool createMyWindow(int cmdShow) { 
  40.     HWND mainWnd = CreateWindowEx(0, CLASS_NAME, "Demo", WS_OVERLAPPEDWINDOW, 
  41.         CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 
  42.         0, 0, GetModuleHandle(0), 0); 
  43.     if (0!=mainWnd) { 
  44.         ShowWindow(mainWnd, cmdShow); 
  45.         UpdateWindow(mainWnd); 
  46.         return true
  47.     } else
  48.         return false
  49.     } 
  50.  
  51. /************************************************************************
  52.     訊息迴圈
  53. ************************************************************************/ 
  54. int messageLoop() { 
  55.     MSG msg; 
  56.     while (GetMessage(&msg, 0, 0, 0)) { 
  57.         TranslateMessage(&msg); 
  58.         DispatchMessage(&msg); 
  59.     } 
  60.     return static_cast<int>(msg.wParam); 
  61.  
  62. /************************************************************************
  63.     WinMain,程式入口
  64. ************************************************************************/ 
  65. int WINAPI WinMain(HINSTANCE, HINSTANCE, char *, int cmdShow) { 
  66.     if (registerMyClass() && createMyWindow(cmdShow)) { 
  67.         return messageLoop(); 
  68.     } else
  69.         std::ostringstream msg; 
  70.         msg << "建立主視窗失敗,錯誤程式碼:" << GetLastError(); 
  71.         MessageBoxA(0, msg.str().c_str(), 0, MB_OK | MB_ICONSTOP); 
  72.         return 0; 
  73.     } 
  74. }  

相關文章