程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Win32開發入門(2) 完整的開發流程

Win32開發入門(2) 完整的開發流程

編輯:關於C++

上一篇中我給各位說了一般人認為C++中較為難的東西——指針。其實對於C++,難點當然不局限在 指針這玩意兒上,還有一些有趣的概念,如模板類、虛基類、純虛函數等,這些都是概念性的東西,幾 乎每一本C++書上都會介紹,而平時我們除了會接觸到純虛函數外,其他的不多用。純虛函數,你可以 認為與C#中的抽象方法或接口中的方法類似,即只定義,不實現。好處就是多態,發何處理,由派生類 來決定。

在開始吹牛之前,我先推薦一套視頻教程,孫鑫老師的C++教程,共20課,我不是幫他 老人家打廣告,而是因為孫老師講的課是我聽過的最好的課,我都看過4次了,我完全可以用他的視頻 教程來復習C++的。

好了,F話說完了,下面我就扯一下編寫一個Win32應用程序的大致流程, 不管你的程序有多麼復雜,多麼變態,其基本思路和流程是不變的。這就好比你寫書法的時候,特別是 寫楷書,我不管你用的是歐體、顏體,還是柳體,你都得遵守“永字八法”基本規則。

那麼, 我們要編寫一個Win32應用程序,要經過哪幾個步驟呢?

你不妨想一想,你有一家工廠是生產女 性服裝的,如果你要生產一批新式服裝(例如某種冬裝),你會有哪些流程?

首先,如果我們 確定要做這麼一款服式,我們要請設計師來把服裝設計好,然後打版,打版就是生成基本樣本,以後工 人就按照這個樣本做就行了。

其次,注冊產品,向上級主管申報,登記後就轉入車間或下游加 工企業開工。

再次,為了展示你的新產品的特色,你要舉辦一場服裝表演。

接著、持續 更新,發現產品存在的問題,不斷改進修正。

最後,推向市場。

我們開發Win32應用程 序也是遵守這樣的規范。不過,我想現在很少人用Win32在實際開發中,畢竟它的開發效率是相當地低 下,所以,曾被某些人誤認為只適用於開發木馬程序。其實,也不一定的,不要太邪惡了。

MFC 對Win API函數的封裝,後來出現了托管C++,你可以用於寫WinForm程序,這樣可以提高開發效率。

如果你有足夠的時間,如果你還在學習編程,如果你是剛進入大學的年輕有為者,你不用急, 因為你會有更多的時間磨煉,你應當考慮多學一點C類語言,C++的學習你會發現你能學到很多其他語言 中學不到的知識,特別是接觸到不少原理性的東西,能加深你對編程哲學的認知。

一、WinMain 入口點

我們在學習標准C++的時候,都知道每個應用程序運行時都會先進入入口點函數main,而 當從main函數跳出時程序就結束了。在Windows編程裡面,也是一樣的,只是我們的入口點函數不叫 main,叫WinMain,這個函數不同於main,我們不能亂來,它的定義必須與聲明保持一致。

我建 議各位安裝VS的時候,都順便更新幫助文檔到本地硬盤,這樣我們可以方便查找。有一點要注意,目前 DestTop Develop的文檔基本上是英文的,做好心理准備。

WinMain函數怎麼寫呢,不用記的, 到MSDN文檔一搜,直接復制就行了。

int CALLBACK WinMain(     
    _In_  HINSTANCE hInstance,     
    _In_  HINSTANCE hPrevInstance,     
    _In_  LPSTR lpCmdLine,     
    _In_  int nCmdShow     
  );

這個函數帶了一個CALLBACK,說明它是一個回調函數,那麼這個CALLBACK是啥呢。我們 先不管,我們先動寫一個Windows,讓大家有一個更直觀的認識。

1、啟動你的開發工具,版本 任意。

2、從菜單欄中依次【文件】【新建】【項目】,在新建項目窗口中,選擇Win32-Win32 應用程序。

2、點擊確定後,會彈出一個向導,單擊【下一步】。項目類型選擇Windows應用程序,附加選項 選擇空項目,我們要自己編寫實現代碼。

3、單擊完成,項目創建成功。打開【解決方案資源管理器】,在“源文件”文件夾上右擊, 從菜單中找到【添加】【新建項】,注意,是源文件,不要搞到頭文件去了。

在新建項窗口中 選C++代碼文件,.cpp後綴的,不要選錯了,選成頭文件,不然無法編譯,因為頭文件是不參與編譯的 。文件名隨便。

包含Windows.h頭文件,這個是最基本的。

#include 

<Windows.h>

然後是入口點,這個我們直接把MSDN的聲明Ctrl + C,然後Ctrl + V上去 就行了。

int CALLBACK WinMain(     
    _In_  HINSTANCE hInstance,     
    _In_  HINSTANCE hPrevInstance,     
    _In_  LPSTR lpCmdLine,     
    _In_  int nCmdShow     
  )     
{     
         
    return 0;     
}

WinMain返回整型,返回0就行了,其實是進程的退出碼,一定要0,不要寫其他,因為0表示 正常退出,其他值表示非正常退出。

剛才我們提到這個函數帶了CALLBACK,那麼,它是什麼? 很簡單,你回到IDE,在CALLBACK上右擊,選【轉到定義】,看看吧。

我們看到它其實是一個宏 ,原型如下:

#define CALLBACK    __stdcall

這時候我們發現了,它其實就是 __stdcall,那麼這個__stdcall是什麼呢?它是和__cdecl關鍵字對應的,這些資料,你網上搜一下就 有了,如果你覺得不好理解,你不妨這樣認為,__stdcall是專門用來調用Win API 的,反正MSDN上也 是這樣說的,它其實是遵循Pascal的語法調用標准,相對應地,__cdecl是C語言的調用風格,這個也是 編譯器選項。
打開項目屬性,找到節點C/C++\高級,然後查看一下調用約定,我們看到默認是選擇C風格調用的,所 以,WIN API 函數才用上關鍵字__stdcall,如果你實在不懂,也沒關系,這個東西一般不影響我們寫 代碼,但屬性窗口中的編譯器選項不要亂改,改掉了可能會導致一些問題。

那麼CALLBACK有什麼特別呢?一句話:函數不是我們調用的,但函數只定義了模型沒有具體處理, 而代碼處理權在被調用者手裡。怎麼說呢,我們完全把它理解為.NET中的委托,我想這樣就好理解了, 委托只聲明了方法的參數和返回值,並沒有具體處理代碼。

WinMain是由系統調用的,而 WinMain中的代碼如何寫,那操作系統就不管了。就好像我告訴你明天有聚會,一起去爬山,反正我是 通知你了,至於去不去那是你決定了。

接下來看看入口點函數的參數。

注意,我們平時 看到很多如HANDLE,HINSTANCE,HBRUSH,WPARAM。LPARAM,HICON,HWND等一大串數據類型,也許我們 會說,怎麼Windows開發有那麼多數據類型。其實你錯了,人總是被眼睛所看到的東西欺騙,Win API 中根本沒有什麼新的數據類型,全都是標准C++中的類型,說白了,這些東西全是數字來的。如果你不 信,自己可以研究一下。

它定義這些名字,只是方便使用罷了,比如下面這樣:

int 

hWindow;     
int hIcon;     
int theAppInstance;

第一個變量指的是窗口的句柄,第二個指的是一個圖標的句柄,第三個 是當前應用程序的實例句柄,你看看,如果我們所有的句柄都是int,我們就無法判斷那些類型是專門 用來表示光標資源,不知道哪些類型是專用來表示位圖的句柄了,但是,如果我們這樣:

#defin HBRUSH  int64  

這樣就很直觀,我一看這名就知道是Brush Handlers,哦,我就明白它是專門用來管理內存中的畫刷資源的,看,這就很明了,所以,通常這些新 定義的類型或者宏,都是取有意義的名字。比如消息,它也是一個數字,如果我說115代表叫你去滾, 但光是一個115誰知道你什麼意思,但是,如果我們為它定義一個宏:

#define WM_GET_OUT    115  

這樣,只要我SendMessage(hwnd,  WM_GET_OUT, NULL, NULL) ,你就會收到一條消息,滾到一邊去。

WinMain的第一個參數是當前應用程序的實例句柄,第 二個參數是前一個實例,比如我把kill.exe運行了兩個實例,進程列表中會有兩個kill.exe,這時候第 一次運行的實例號假設為0001,就傳遞第一個參數hInstance,第二次運行的假設實例號為0002,就傳 給了hPrevInstance參數。

lpCmdLine參數從名字上就猜到了,就是命令行參數,那LPSTR是啥呢 ,它其實就是一個字符串,你可以跟入定義就知道了,它其實就是char*,指向char的指針,記得我上 一篇文章中說的指針有創建數組的功能嗎?對,其實這裡傳入的命令行參數應該是char[ ],這就是我 在第一篇文章中要說指針的原因。

這裡告訴大家一個技巧,我們怎麼知道哪些參數是指針類型 呢,因為不是所有參數都有 * 標識。技巧還是在命名上,以後,只要我們看到P開頭的,或者LP開頭的 ,都是指針類型。

比如LPWSTR,LPCTSTR,LPRECT等等。

最後一個參數nCmdShow是主窗 口的顯示方式。它定義了以下宏。

這個參數是操作系統傳入的,我們無法修改它。那麼,應用程序在運行時,是如何決定這個參數的呢? 看看這個,不用我介紹了吧,你一定很熟悉。

我們寫了WinMain,但我們還要在WinMain前面預先定義一個WindowProc函數。C++與C#,Java 這些語言不同,你只需記住,C++編譯器的解析是從左到右,從上到下的,如果某函數要放到代碼後面 來實現,但在此之前要使用,那麼你必須先聲明一下,不然編譯時會找不到。這裡因為我們通常會把 WindowProc實現放在WinMain之後,但是在WinMain中設計窗口類時要用到它的指針,這時候,我們必須 在WinMain之前聲明WindowProc。

同樣地,WindowProc的定義我們不用記,到MSDN直接抄就行了 。

#include <Windows.h>     
// 必須要進行前導聲明     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
);     
         
int CALLBACK WinMain(     
    _In_  HINSTANCE hInstance,     
    _In_  HINSTANCE hPrevInstance,     
    _In_  LPSTR lpCmdLine,     
    _In_  int nCmdShow     
  )     
{     
    return 0;     
}     
// 在WinMain後實現     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
)     
{     
    return DefWindowProc(hwnd, uMsg, wParam, lParam);     
}

前導聲明與後面實現的函數的簽名必須一致,編譯才會認為它們是同一個函數。在 WindowProc中返回DefWindowProc是把我們不感興趣或者沒有處理的消息交回給操作系統來處理。也許 你會問,函數的名字一定要叫WindowProc嗎?當然不是了,你可以改為其他名字,如MyProc,但前提是 返回值和參數的類型以及個數必須一致。

LRESULT CALLBACK MyProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
)

這個函數帶了CALLBACK,說明不是我們調用的,也是由操作系統調用的,我們在這個函數裡 面對需要處理的消息進行響應。至於,為什麼可以改函數的名字而系統為什麼能找到這個函數呢,後面 你就知道了。

二、設計與注冊窗口類

設計窗口類,其實就是設計我們程序的主窗口,如 有沒有標題欄,背景什麼顏色,有沒有邊框,可不可以調整大小等。要設計窗口類,我們用到一個結構 ——

typedef struct tagWNDCLASS {     
  UINT      style;     
  WNDPROC   lpfnWndProc;     
  int       cbClsExtra;     
  int       cbWndExtra;     
  HINSTANCE hInstance;     
  HICON     hIcon;     
  HCURSOR   hCursor;     
  HBRUSH    hbrBackground;     
  LPCTSTR   lpszMenuName;     
  LPCTSTR   lpszClassName;     
} WNDCLASS, *PWNDCLASS;

通常情況下,我們用WNDCLASS就可以了,當然還有一個WNDCLASSEX 的擴展結構,在API裡面,凡是看到EX結尾的都是擴展的意思,比如CreateWindowEx就是CreateWindow 的擴展函數。

第一個成員是窗口的類樣式,注意,不要和窗口樣式(WS_xxxxx)混淆了,這裡 指的是這個窗口類的特征,不是窗口的外觀特征,這兩個style是不一樣的。

它的值可以參考 MSDN,通常我們只需要兩個就可以了——CS_HREDRAW | CS_VREDRAW,從名字就看出來了,就是同時具 備水平重畫和垂直重畫。因為當我們的窗口顯示的時候,被其他窗口擋住後重新顯示,或者大小調整後 ,窗口都要發生繪制,就像我們在紙上塗鴉一樣,每次窗口的變化都會“粉刷”一遍,並發送WM_PAINT 消息。

lpfnWndProc參數就是用來設置你用哪個WindowProc來處理消息,前面我說過,我們只要 不更改回調函數的返回值和參數的類型和順序,就可以隨意設置函數的名字,那為什麼系統可以找到我 們用的回調函數呢,對的,就是通過lpfnWndProc傳進去的,它是一個函數指針,也就是它裡面保存的 是我們定義的WindowProc的入口地址,使用很簡單,我們只需要把函數的名字傳給它就可以了。

cbClsExtra和cbWndExtra通常不需要,設為0就OK。hInstance是當前應用程序的實例句柄,從 WinMain的hInstance參數中可以得到。hIcon和hCursor就不用我說了,看名字就知道了。

hbrBackground是窗口的背景色,你也可以不設置,但在處理WM_PAINT消息時必須繪制窗口背景 。也可以直接用系統定義的顏色,MSDN為我們列出這些值,大家不用記,直接到MSDN拿來用就行了,這 些都比較好理解,看名字就知道了。

   COLOR_ACTIVEBORDER
   COLOR_ACTIVECAPTION
   COLOR_APPWORKSPACE
   COLOR_BACKGROUND
   COLOR_BTNFACE
   COLOR_BTNSHADOW
   COLOR_BTNTEXT
   COLOR_CAPTIONTEXT
   COLOR_GRAYTEXT
   COLOR_HIGHLIGHT
   COLOR_HIGHLIGHTTEXT
   COLOR_INACTIVEBORDER
   COLOR_INACTIVECAPTION
   COLOR_MENU
   COLOR_MENUTEXT
   COLOR_SCROLLBAR
   COLOR_WINDOW                                         /*  這個就是窗口的默認背景色  */
   COLOR_WINDOWFRAME
   COLOR_WINDOWTEXT

lpszMenuName指的是菜單的ID,沒有菜單就NULL,lpszClassName就 是我們要向系統注冊的類名,字符,不能與系統已存在的類名沖突,如“BUTTON”類。

所以, 在WinMain中設計窗口類。

// 類名     
WCHAR* cls_Name = L"My Class";     
// 設計窗口類     
WNDCLASS wc;     
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;     
wc.lpfnWndProc = WindowProc;     
wc.lpszClassName = cls_Name;     
wc.hInstance = hInstance;

窗口類設計完成後,不要忘了向系統注冊,這樣系統才能知道有 這個窗口類的存在。向操作系統注冊窗口類,使用RegisterClass函數,它的參數就是一個指向 WNDCLASS結構體的指針,所以我們傳遞的時候,要加上&符號。

// 注冊窗口類     
RegisterClass(&wc);

三、創建和顯示窗口

窗口類注冊完成後,就應該創建窗口 ,然後顯示窗口,調用CreateWindow創建窗口,如果成功,會返回一個窗口的句柄,我們對這個窗口的 操作都要用到這個句柄。什麼是句柄呢?其實它就是一串數字,只是一個標識而已,內存中會存在各種 資源,如圖標、文本等,為了可以有效標識這些資源,每一個資源都有其唯一的標識符,這樣,通過查 找標識符,就可以知道某個資源存在於內存中哪一塊地址中,就好比你出身的時候,長輩都要為你取個 名字,你說名字用來干嗎?名字就是用來標識你的,不然,你見到A叫小明,遇到B又叫小明,那誰知道 哪個才是小明啊?就好像你上大學去報到號,會為你分配一個可以在本校學生中唯一標識你的學號,所 有學生的學號都是不同的,這樣,只要通過索引學號,就可以找到你的資料。

CreateWindow函 數返回一個HWND類型,它就是窗口類的句柄。

// 創建窗口     
HWND hwnd = CreateWindow(     
    cls_Name,           //類名,要和剛才注冊的一致     
    L"我的應用程序",          //窗口標題文字     
    WS_OVERLAPPEDWINDOW,        //窗口外觀樣式     
    38,             //窗口相對於父級的X坐標     
    20,             //窗口相對於父級的Y坐標     
    480,                //窗口的寬度     
    250,                //窗口的高度     
    NULL,               //沒有父窗口,為NULL     
    NULL,               //沒有菜單,為NULL     
    hInstance,          //當前應用程序的實例句柄     
    NULL);              //沒有附加數據,為NULL     
if(hwnd == NULL)                //檢查窗口是否創建成功     
    return 0;

窗外觀的樣式都是WS_打頭的,是Window Style的縮寫,這個我就不說了, MSDN上全有了。

窗口創建後,就要顯示它,就像我們的產品做了,要向客戶展示。顯示窗口調 用ShowWindow函數。

// 顯示窗口     
ShowWindow(hwnd, SW_SHOW);

既然要顯示窗口了,那麼ShowWindow的第一個參數就是剛才創 建的窗口的句柄,第二個參數控制窗口如何顯示,你可以從SW_XXXX中選一個,也可以用WinMain傳進來 的參數,還記得WinMain的最後一個參數嗎?

四、更新窗口(可選)

為什麼更新窗口這 一步可有可無呢?因為只要程序在運行著,只要不是最小化,只要窗口是可見的,那麼,我們的應用程 序會不斷接收到WM_PAINT通知。這裡先不說,後面你會明白的。好了,更新窗口,當然是調用 UpdateWindow函數。

// 更新窗口     
UpdateWindow(hwnd);

五、消息循環

Windows操作系統是基於消息控制機制的,用戶與 系統之間的交互,程序與系統之間的交互,都是通過發送和接收消息來完成的。就好像軍隊一樣,命令 一旦傳達,就要執行,當然,我們的應用程序和軍隊不一樣,我們收到指令不一要執行,我們是可以選 擇性地執行。

我們知道,代碼是不斷往前執行的,像我們剛才寫的WinMain函數一樣,如果你現 在運行程序,你會發現什麼都沒有,是不是程序不能運行呢,不是,其實程序是運行了,只是它馬上結 束了,只要程序執行跳出了WinMain的右大括號,程序就會結束了。那麼,要如何讓程序不結束了,可 能大家注意到我們在C程序中可以用一個getchar()函數來等到用戶輸入,這樣程序就人停在那裡,直到 用戶輸入內容。但我們的窗口應用不能這樣做,因為用戶有可能進行其他操作,如最小化窗口,移動窗 口,改變窗口大小,或者點擊窗口上的按鈕等。因此,我們不能簡地弄一個getchar在那裡,這樣就無 法響應用戶的其他操作了。

可以讓程序留在某處不結束的另一個方法就是使用循環,而且是死 循環,這樣程序才會永久地停在某個地方,但這個死循環必須具有跳出的條件,不然你的程序會永久執 行,直達停電或者把電腦砸了。

這樣消息循環就出現了,只要有與用戶交互,系統人不斷地向 應用程序發送消息通知,因為這些消息是不定時不斷發送的,必須有一個绶沖區來存放,就好像你去銀 行辦理手續要排隊一樣,我們從最前端取出一條一條消息處理,後面新發送的消息會一直在排隊,直到 把所有消息處理完,這就是消息隊列。

要取出一條消息,調用GetMessage函數。函數會傳 入一個MSG結構體的指針,當收到消息,會填充MSG結構體中的成員變量,這樣我們就知道我們的應用程 序收到什麼消息了,直到GetMessage函數取不到消息,條件不成立,循環跳出,這時應用程序就退出。 MSG的定義如下:

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

hwnd不用說了,就是窗口句柄,哪個窗口的句柄?還記得WindowProc 回調函數嗎?你把這個函數交給了誰來處理,hwnd就是誰的句柄,比如我們上面的代碼,我們是把 WindowProc賦給了新注冊的窗口類,並創建了主窗口,返回一個表示主窗口的句柄,所以,這裡MSG中 的hwnd指的就是我們的主窗口。

message就是我們接收到的消息,看到,它是一個數字,無符號 整型,所以我們操作的所有消息都是數字來的。wParam和lParam是消息的附加參數,其實也是數值來的 。通常,lParam指示消息的處理結果,不同消息的結果(返回值)不同,具體可參閱MSDN。

有 了一個整型的值來表示消息,我們為什麼還需要附加參數呢?你不妨想一下,如果接收一條 WM_LBUTTONDOWN消息,即鼠標左鍵按下時發送的通知消息,那麼,我們不僅知道左鍵按下這件事,我們 更感趣的是,鼠標在屏幕上的哪個坐標處按下左鍵,按了幾下,這時候,你公憑一條WM_LBUTTONDOWN消 息是無法傳遞這麼多消息的。可能我們需要把按下左鍵時的坐標放入wParam參數中;最典型的就是 WM_COMMAND消息,因為只要你使用菜單,點擊按鈕都會發送這樣一條消息,那麼我怎麼知道用戶點了哪 個按鈕呢?如果窗口中只有一個按鈕,那好辦,用戶肯定單擊了它,但是,如果窗口上有10個按鈕呢? 而每一個按鈕被單擊都會發送WM_COMMAND消息,你能知道用戶點擊了哪個按鈕嗎?所以,我們要把用戶 點擊了的那個按鈕的句柄存到lParam參數中,這樣一來,我們就可以判斷出用戶到底點擊了哪個按鈕了 。

GetMessage函數聲明如下:

BOOL WINAPI GetMessage(     
  _Out_     LPMSG lpMsg,     
  _In_opt_  HWND hWnd,     
  _In_      UINT wMsgFilterMin,     
  _In_      UINT wMsgFilterMax     
);

這個函數在定義時帶了一個WINAPI,現在,按照前面我說的方法,你應該猜到,它就是一 個宏,而真實的值是__stdcall,前文中說過了。

第一個參數是以LP開頭,還記得嗎,我說過的 ,你應該想到它就是 MSG* ,一個指向MSG結構的指針。第二個參數是句柄,通常我們用NULL,因為我 們會捕捉整個應用程序的消息。後面兩個參數是用來過濾消息的,指定哪個范圍內的消息我接收,在此 范圍之外的消息我拒收,如果不過濾就全設為0.。返回值就不說了,自己看。

// 消息循環   

  
MSG msg;     
while(GetMessage(&msg, NULL, 0, 0))     
{     
    TranslateMessage(&msg);     
    DispatchMessage(&msg);     
}

TranslateMessage是用於轉換按鍵信息的,因為鍵盤按下和彈起會發送WM_KEYDOWN和 WM_KEYUP消息,但如果我們只想知道用戶輸了哪些字符,這個函數可以把這些消息轉換為WM_CHAR消息 ,它表示的就是鍵盤按下的那個鍵的字符,如“A”,這樣我們處理起來就更方便了。

DispatchMessage函數是必須調用的,它的功能就相當於一根傳送帶,每收到一條消息, DispatchMessage函數負責把消息傳到WindowProc讓我們的代碼來處理,如果不調用這個函數,我們定 義的WindowProc就永遠接收不到消息,你就不能做消息響應了,你的程序就只能從運行就開始死掉了, 沒有響應。

六、消息響應

其實現在我們的應用程序是可以運行了,因為在WindowProc中 我們調用了DefWindowProc,函數,消息我們不作任何處理,又把控制權路由回到操作系統來默認處理 ,所以,整個過程中,我們現在的消息循環是成立的,只不過我們不做任何響應罷了。

好的, 現在我把完整的代碼貼一下,方便你把前面我們說的內容串聯起來。

#include 

<Windows.h>     
// 必須要進行前導聲明     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
);     
         
// 程序入口點     
int CALLBACK WinMain(     
    _In_  HINSTANCE hInstance,     
    _In_  HINSTANCE hPrevInstance,     
    _In_  LPSTR lpCmdLine,     
    _In_  int nCmdShow     
  )     
{     
    // 類名     
    WCHAR* cls_Name = L"My Class";     
    // 設計窗口類     
    WNDCLASS wc;     
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;     
    wc.lpfnWndProc = WindowProc;     
    wc.lpszClassName = cls_Name;     
    wc.hInstance = hInstance;     
    // 注冊窗口類     
    RegisterClass(&wc);     
         
    // 創建窗口     
    HWND hwnd = CreateWindow(     
        cls_Name,           //類名,要和剛才注冊的一致     
        L"我的應用程序",  //窗口標題文字     
        WS_OVERLAPPEDWINDOW, //窗口外觀樣式     
        38,             //窗口相對於父級的X坐標     
        20,             //窗口相對於父級的Y坐標     
        480,                //窗口的寬度     
        250,                //窗口的高度     
        NULL,               //沒有父窗口,為NULL     
        NULL,               //沒有菜單,為NULL     
        hInstance,          //當前應用程序的實例句柄     
        NULL);              //沒有附加數據,為NULL     
    if(hwnd == NULL) //檢查窗口是否創建成功     
        return 0;     
         
    // 顯示窗口     
    ShowWindow(hwnd, SW_SHOW);     
         
    // 更新窗口     
    UpdateWindow(hwnd);     
         
    // 消息循環     
    MSG msg;     
    while(GetMessage(&msg, NULL, 0, 0))     
    {     
        TranslateMessage(&msg);     
        DispatchMessage(&msg);     
    }     
    return 0;     
}     
// 在WinMain後實現     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
)     
{     
    return DefWindowProc(hwnd, uMsg, wParam, lParam);     
}

所有代碼看上去貌似很正常,也遵守了流程,設計窗口類,注冊窗口類,創建窗口,顯示窗 口,更新窗口,消息循環。是吧,這段代碼看上去毫無破綻,運行應該沒問題吧。好,如果你如此自信 ,那就試試吧。

按下F5試試運行。
哈哈,結果會讓很多人失望,很多初學者就是這樣, 一切看起來好像正常,於是有人開始罵VC是垃圾,是編譯器有bug,也有人開始想放棄了,媽的,這麼 難,不學了。人啊,總是這樣,老指責別人的問題,從不在自己身上找問題,是真的VC的bug嗎?

我前面說了,這段代碼貌似很正常,呵呵,你看到問題在哪嗎?給你兩分鐘來找錯。我提示一 下,這個程序沒有運行是因為主窗口根本就沒有創建,因為我在代碼裡面做了判斷,如果窗口順柄hwnd 為NULL,就退出,現在程序一運行就退出了,明顯是窗口創建失敗。

…………

好了,不 用找了,很多人找不出來,尤其是許多初學者,不少人找了一遍又一遍,都說沒有錯誤,至少代碼提示 沒說有錯,編譯運行也沒報錯,所以不少人自信地說,代碼沒錯。

其實你是對的,代碼確實沒 有錯,而問題就出在WNDCLASS結構上,認真看一下MSDN上有關RegisterClass函數說明中的一句話,這 句話很多人沒注意到,但它很關鍵。

You must fill the structure with the appropriate class attributes before passing it to the function.

現在你明白了吧,還不清楚?沒關系 ,看看我把代碼這樣改一下你就知道了。

// 設計窗口類     
WNDCLASS wc;     
wc.cbClsExtra = 0;     
wc.cbWndExtra = 0;     
wc.hCursor = LoadCursor(hInstance, IDC_ARROW);;     
wc.hIcon = LoadIcon(hInstance, IDI_APPLICATION);;     
wc.lpszMenuName = NULL;     
wc.style = CS_HREDRAW | CS_VREDRAW;     
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;     
wc.lpfnWndProc = WindowProc;     
wc.lpszClassName = cls_Name;     
wc.hInstance = hInstance;

現在,你運行一下,你一定能看到窗口。

但現在你對窗 口無法進行操作,因為後續的代碼還沒完成。

為什麼現在又可以了呢?MSDN那句話的意思就是 說我們在注冊窗口類之前必須填充WNDCLASS結構體,何為填充,就是要為結構的所有成員賦值,就算不 需要你也要為它賦一個NULL或0,因為結構在創建時沒有對成員進行初始化,這就導致變量無法正確的 分配內存,最後注冊失敗。

那麼,如果一個結構體成員很多,而我只需要用到其中三個,其他 的也要初始化,是不是很麻煩,是的,除了為每個成員賦值,還有一種較簡單的方法,就是在聲明變量 時給它賦一對大括號,裡面放置結構體的應該分配內存的大小,如:

// 設計窗口類     
WNDCLASS wc = { sizeof(WNDCLASS) };     
wc.hbrBackground = (HBRUSH)COLOR_WINDOW;     
wc.lpfnWndProc = WindowProc;     
wc.lpszClassName = cls_Name;     
wc.hInstance = hInstance;

這樣一來,我們也發現,窗口也可以成功創建。

我們還 可以更簡單,直接把sizeof也去掉,在聲明變量時,直接賦一對空的大括號就行了,就如這樣。

WNDCLASS wc = { };

這樣寫更簡單,窗口類同樣可以正常注冊。大括號代表的 是代碼塊,這樣,結構體有了一個初值,因此它會按照結構體的大小分配了相應的內存。

為什 麼會這樣呢?這裡涉及到一個關於結構體的一個很有趣的賦值方式。我們先放下我們這個例子,下面我 寫一個簡單的例子,你就明白了。

#include <stdio.h>     
 typedef struct rectStruct     
 {     
     int x;     
     int y;     
     int width;     
     int height;     
 } RECT, *PRECT;     
         
 void main()     
 {     
     RECT rect = { 0, 0, 20, 30 };     
     printf("矩形的坐標是:%d, %d\n矩形的大小:%d , %d", rect.x, rect.y, rect.width, 

rect.height);     
     getchar();     
 }

在本例中,我們定義了一個表示矩形的結構體 RECT ,它有四個成員,分別橫坐標,縱坐標,寬度 ,高度,但是,我們在聲明和賦值中,我們只用了一對大括號,把每個成員的值,按照定義的順序依次 寫到大括號中,即{ 0, 0, 20, 30 },x的值為0,y的值為0,width為20,height的值為30。

也 就是說,我們可以通過這種簡單的方法向結構變量賦值,注意值的順序要和成員變量定義的順序相同。

現在,回到我們的Windows程序來,我們明白了這種賦值方式,對於 WNDCLASS wc = {  } 就不難理解了,這樣雖然大括號裡面是空的,其實它已經把變量初始化了,都賦了默認值,這樣一來, 就可以正確分配內存了。

七、為什麼不能退出

通常情況下,當我們的主窗口關閉後,應 用程序應該退出(木馬程序除外),但是,我們剛才運行後發現,為什麼我的窗口關了,但程序不退出 呢?前面我說了,要退出程序,就要先跳出消息循環,和關閉哪個窗口無關。因此,我們要解決兩個問 題:

1、如果跳出消息循環;

2、什麼時候退出程序。

其實兩個問題是可以合並 到一起解決。

首先要知道,當窗口被關閉,為窗口所分配的內存會被銷毀,同時,我們會收到 一條WM_DESTROY消息,因而,我們只要在收到這條消息時調用PostQuitMessage函數,這個函數提交一 條WM_QUIT消息,而在消息循環中,WM_QUIT消息使GetMessage函數返回0,這樣一來,GetMessage返回 FALSE,就可以跳出消息循環了,這樣應用程序就可以退出了。

所以,我們要做的就是捕捉 WM_DESTROY消息,然後PostQuitMessage.

// 在WinMain後實現     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
)     
{     
    switch(uMsg)     
    {     
    case WM_DESTROY:     
        {     
            PostQuitMessage(0);     
            return 0;     
        }     
    }     
    return DefWindowProc(hwnd, uMsg, wParam, lParam);     
}

我們會收到很多消息,所以用switch判斷一下是不是WM_DESTROY消息,如果是,退出應用程 序。

好了,這樣,我們一個完整的Windows應用程序就做好了。

下面是完整的代碼清單。

#include <Windows.h>     
// 必須要進行前導聲明     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
);     
         
// 程序入口點     
int CALLBACK WinMain(     
    _In_  HINSTANCE hInstance,     
    _In_  HINSTANCE hPrevInstance,     
    _In_  LPSTR lpCmdLine,     
    _In_  int nCmdShow     
  )     
{     
    // 類名     
    WCHAR* cls_Name = L"My Class";     
    // 設計窗口類     
    WNDCLASS wc = { };     
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;     
    wc.lpfnWndProc = WindowProc;     
    wc.lpszClassName = cls_Name;     
    wc.hInstance = hInstance;     
    // 注冊窗口類     
    RegisterClass(&wc);     
         
    // 創建窗口     
    HWND hwnd = CreateWindow(     
        cls_Name,           //類名,要和剛才注冊的一致     
        L"我的應用程序",  //窗口標題文字     
        WS_OVERLAPPEDWINDOW, //窗口外觀樣式     
        38,                 //窗口相對於父級的X坐標     
        20,                 //窗口相對於父級的Y坐標     
        480,                //窗口的寬度     
        250,                //窗口的高度     
        NULL,               //沒有父窗口,為NULL     
        NULL,               //沒有菜單,為NULL     
        hInstance,          //當前應用程序的實例句柄     
        NULL);              //沒有附加數據,為NULL     
    if(hwnd == NULL) //檢查窗口是否創建成功     
        return 0;     
         
    // 顯示窗口     
    ShowWindow(hwnd, SW_SHOW);     
         
    // 更新窗口     
    UpdateWindow(hwnd);     
         
    // 消息循環     
    MSG msg;     
    while(GetMessage(&msg, NULL, 0, 0))     
    {     
        TranslateMessage(&msg);     
        DispatchMessage(&msg);     
    }     
    return 0;     
}     
// 在WinMain後實現     
LRESULT CALLBACK WindowProc(     
    _In_  HWND hwnd,     
    _In_  UINT uMsg,     
    _In_  WPARAM wParam,     
    _In_  LPARAM lParam     
)     
{     
    switch(uMsg)     
    {     
    case WM_DESTROY:     
        {     
            PostQuitMessage(0);     
            return 0;     
        }     
    }     
    return DefWindowProc(hwnd, uMsg, wParam, lParam);     
}
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved