原文地址:http://blog.csdn.net/tcjiaan/article/details/8497535
一、WinMain入口點
我們在學習標準C++的時候,都知道每個應用程序運行時都會先進入入口點函數main,而當從main函數跳出時程序就結束了。在Windows編程裏面,也是一樣的,只是我們的入口點函數不叫main,叫WinMain,這個函數不同於main,我們不能亂來,它的定義必須與聲明保持一致。
我建議各位安裝VS的時候,都順便更新幫助文檔到本地硬盤,這樣我們可以方便查找。有一點要注意,目前DestTop Develop的文檔基本上是英文的,做好心理準備。
WinMain函數怎麼寫呢,不用記的,到MSDN文檔一搜,直接複製就行了。
這個函數帶了一個CALLBACK,說明它是一個回調函數,那麼這個CALLBACK是啥呢。我們先不管,我們先動寫一個Windows,讓大家有一個更直觀的認識。
1、啓動你的開發工具,版本任意。
2、從菜單欄中依次【文件】【新建】【項目】,在新建項目窗口中,選擇Win32-Win32應用程序。
2、點擊確定後,會彈出一個嚮導,單擊【下一步】。項目類型選擇Windows應用程序,附加選項選擇空項目,我們要自己編寫實現代碼。
3、單擊完成,項目創建成功。打開【解決方案資源管理器】,在“源文件”文件夾上右擊,從菜單中找到【添加】【新建項】,注意,是源文件,不要搞到頭文件去了。
在新建項窗口中選C++代碼文件,.cpp後綴的,不要選錯了,選成頭文件,不然無法編譯,因爲頭文件是不參與編譯的。文件名隨便。
包含Windows.h頭文件,這個是最基本的。
然後是入口點,這個我們直接把MSDN的聲明Ctrl + C,然後Ctrl + V上去就行了。
WinMain返回整型,返回0就行了,其實是進程的退出碼,一定要0,不要寫其他,因爲0表示正常退出,其他值表示非正常退出。
剛纔我們提到這個函數帶了CALLBACK,那麼,它是什麼?很簡單,你回到IDE,在CALLBACK上右擊,選【轉到定義】,看看吧。
我們看到它其實是一個宏,原型如下:
這時候我們發現了,它其實就是__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,我們就無法判斷那些類型是專門用來表示光標資源,不知道哪些類型是專用來表示位圖的句柄了,但是,如果我們這樣:
這樣就很直觀,我一看這名就知道是Brush Handlers,哦,我就明白它是專門用來管理內存中的畫刷資源的,看,這就很明瞭,所以,通常這些新定義的類型或者宏,都是取有意義的名字。比如消息,它也是一個數字,如果我說115代表叫你去滾,但光是一個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直接抄就行了。
前導聲明與後面實現的函數的簽名必須一致,編譯纔會認爲它們是同一個函數。在WindowProc中返回DefWindowProc是把我們不感興趣或者沒有處理的消息交回給操作系統來處理。也許你會問,函數的名字一定要叫WindowProc嗎?當然不是了,你可以改爲其他名字,如MyProc,但前提是返回值和參數的類型以及個數必須一致。
這個函數帶了CALLBACK,說明不是我們調用的,也是由操作系統調用的,我們在這個函數裏面對需要處理的消息進行響應。至於,爲什麼可以改函數的名字而系統爲什麼能找到這個函數呢,後面你就知道了。
二、設計與註冊窗口類
設計窗口類,其實就是設計我們程序的主窗口,如有沒有標題欄,背景什麼顏色,有沒有邊框,可不可以調整大小等。要設計窗口類,我們用到一個結構——
通常情況下,我們用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中設計窗口類。
窗口類設計完成後,不要忘了向系統註冊,這樣系統才能知道有這個窗口類的存在。向操作系統註冊窗口類,使用RegisterClass函數,它的參數就是一個指向WNDCLASS結構體的指針,所以我們傳遞的時候,要加上&符號。
三、創建和顯示窗口
窗口類註冊完成後,就應該創建窗口,然後顯示窗口,調用CreateWindow創建窗口,如果成功,會返回一個窗口的句柄,我們對這個窗口的操作都要用到這個句柄。什麼是句柄呢?其實它就是一串數字,只是一個標識而已,內存中會存在各種資源,如圖標、文本等,爲了可以有效標識這些資源,每一個資源都有其唯一的標識符,這樣,通過查找標識符,就可以知道某個資源存在於內存中哪一塊地址中,就好比你出身的時候,長輩都要爲你取個名字,你說名字用來幹嗎?名字就是用來標識你的,不然,你見到A叫小明,遇到B又叫小明,那誰知道哪個纔是小明啊?就好像你上大學去報到號,會爲你分配一個可以在本校學生中唯一標識你的學號,所有學生的學號都是不同的,這樣,只要通過索引學號,就可以找到你的資料。
CreateWindow函數返回一個HWND類型,它就是窗口類的句柄。
窗外觀的樣式都是WS_打頭的,是Window Style的縮寫,這個我就不說了,MSDN上全有了。
窗口創建後,就要顯示它,就像我們的產品做了,要向客戶展示。顯示窗口調用ShowWindow函數。
既然要顯示窗口了,那麼ShowWindow的第一個參數就是剛纔創建的窗口的句柄,第二個參數控制窗口如何顯示,你可以從SW_XXXX中選一個,也可以用WinMain傳進來的參數,還記得WinMain的最後一個參數嗎?
四、更新窗口(可選)
爲什麼更新窗口這一步可有可無呢?因爲只要程序在運行着,只要不是最小化,只要窗口是可見的,那麼,我們的應用程序會不斷接收到WM_PAINT通知。這裏先不說,後面你會明白的。好了,更新窗口,當然是調用UpdateWindow函數。
五、消息循環
Windows操作系統是基於消息控制機制的,用戶與系統之間的交互,程序與系統之間的交互,都是通過發送和接收消息來完成的。就好像軍隊一樣,命令一旦傳達,就要執行,當然,我們的應用程序和軍隊不一樣,我們收到指令不一要執行,我們是可以選擇性地執行。
我們知道,代碼是不斷往前執行的,像我們剛纔寫的WinMain函數一樣,如果你現在運行程序,你會發現什麼都沒有,是不是程序不能運行呢,不是,其實程序是運行了,只是它馬上結束了,只要程序執行跳出了WinMain的右大括號,程序就會結束了。那麼,要如何讓程序不結束了,可能大家注意到我們在C程序中可以用一個getchar()函數來等到用戶輸入,這樣程序就人停在那裏,直到用戶輸入內容。但我們的窗口應用不能這樣做,因爲用戶有可能進行其他操作,如最小化窗口,移動窗口,改變窗口大小,或者點擊窗口上的按鈕等。因此,我們不能簡地弄一個getchar在那裏,這樣就無法響應用戶的其他操作了。
可以讓程序留在某處不結束的另一個方法就是使用循環,而且是死循環,這樣程序纔會永久地停在某個地方,但這個死循環必須具有跳出的條件,不然你的程序會永久執行,直達停電或者把電腦砸了。
這樣消息循環就出現了,只要有與用戶交互,系統人不斷地嚮應用程序發送消息通知,因爲這些消息是不定時不斷髮送的,必須有一個綬衝區來存放,就好像你去銀行辦理手續要排隊一樣,我們從最前端取出一條一條消息處理,後面新發送的消息會一直在排隊,直到把所有消息處理完,這就是消息隊列。
要取出一條消息,調用GetMessage函數。函數會傳入一個MSG結構體的指針,當收到消息,會填充MSG結構體中的成員變量,這樣我們就知道我們的應用程序收到什麼消息了,直到GetMessage函數取不到消息,條件不成立,循環跳出,這時應用程序就退出。MSG的定義如下:
hwnd不用說了,就是窗口句柄,哪個窗口的句柄?還記得WindowProc回調函數嗎?你把這個函數交給了誰來處理,hwnd就是誰的句柄,比如我們上面的代碼,我們是把WindowProc賦給了新註冊的窗口類,並創建了主窗口,返回一個表示主窗口的句柄,所以,這裏MSG中的hwnd指的就是我們的主窗口。
message就是我們接收到的消息,看到,它是一個數字,無符號整型,所以我們操作的所有消息都是數字來的。wParam和lParam是消息的附加參數,其實也是數值來的。通常,lParam指示消息的處理結果,不同消息的結果(返回值)不同,具體可參閱MSDN。
有了一個整型的值來表示消息,我們爲什麼還需要附加參數呢?你不妨想一下,如果接收一條WM_LBUTTONDOWN消息,即鼠標左鍵按下時發送的通知消息,那麼,我們不僅知道左鍵按下這件事,我們更感趣的是,鼠標在屏幕上的哪個座標處按下左鍵,按了幾下,這時候,你公憑一條WM_LBUTTONDOWN消息是無法傳遞這麼多消息的。可能我們需要把按下左鍵時的座標放入wParam參數中;最典型的就是WM_COMMAND消息,因爲只要你使用菜單,點擊按鈕都會發送這樣一條消息,那麼我怎麼知道用戶點了哪個按鈕呢?如果窗口中只有一個按鈕,那好辦,用戶肯定單擊了它,但是,如果窗口上有10個按鈕呢?而每一個按鈕被單擊都會發送WM_COMMAND消息,你能知道用戶點擊了哪個按鈕嗎?所以,我們要把用戶點擊了的那個按鈕的句柄存到lParam參數中,這樣一來,我們就可以判斷出用戶到底點擊了哪個按鈕了。
GetMessage函數聲明如下:
這個函數在定義時帶了一個WINAPI,現在,按照前面我說的方法,你應該猜到,它就是一個宏,而真實的值是__stdcall,前文中說過了。
第一個參數是以LP開頭,還記得嗎,我說過的,你應該想到它就是 MSG* ,一個指向MSG結構的指針。第二個參數是句柄,通常我們用NULL,因爲我們會捕捉整個應用程序的消息。後面兩個參數是用來過濾消息的,指定哪個範圍內的消息我接收,在此範圍之外的消息我拒收,如果不過濾就全設爲0.。返回值就不說了,自己看。
TranslateMessage是用於轉換按鍵信息的,因爲鍵盤按下和彈起會發送WM_KEYDOWN和WM_KEYUP消息,但如果我們只想知道用戶輸了哪些字符,這個函數可以把這些消息轉換爲WM_CHAR消息,它表示的就是鍵盤按下的那個鍵的字符,如“A”,這樣我們處理起來就更方便了。
DispatchMessage函數是必須調用的,它的功能就相當於一根傳送帶,每收到一條消息,DispatchMessage函數負責把消息傳到WindowProc讓我們的代碼來處理,如果不調用這個函數,我們定義的WindowProc就永遠接收不到消息,你就不能做消息響應了,你的程序就只能從運行就開始死掉了,沒有響應。
六、消息響應
其實現在我們的應用程序是可以運行了,因爲在WindowProc中我們調用了DefWindowProc,函數,消息我們不作任何處理,又把控制權路由回到操作系統來默認處理,所以,整個過程中,我們現在的消息循環是成立的,只不過我們不做任何響應罷了。
好的,現在我把完整的代碼貼一下,方便你把前面我們說的內容串聯起來。
所有代碼看上去貌似很正常,也遵守了流程,設計窗口類,註冊窗口類,創建窗口,顯示窗口,更新窗口,消息循環。是吧,這段代碼看上去毫無破綻,運行應該沒問題吧。好,如果你如此自信,那就試試吧。
按下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.
現在你明白了吧,還不清楚?沒關係,看看我把代碼這樣改一下你就知道了。
現在,你運行一下,你一定能看到窗口。
但現在你對窗口無法進行操作,因爲後續的代碼還沒完成。
爲什麼現在又可以了呢?MSDN那句話的意思就是說我們在註冊窗口類之前必須填充WNDCLASS結構體,何爲填充,就是要爲結構的所有成員賦值,就算不需要你也要爲它賦一個NULL或0,因爲結構在創建時沒有對成員進行初始化,這就導致變量無法正確的分配內存,最後註冊失敗。
那麼,如果一個結構體成員很多,而我只需要用到其中三個,其他的也要初始化,是不是很麻煩,是的,除了爲每個成員賦值,還有一種較簡單的方法,就是在聲明變量時給它賦一對大括號,裏面放置結構體的應該分配內存的大小,如:
這樣一來,我們也發現,窗口也可以成功創建。
我們還可以更簡單,直接把sizeof也去掉,在聲明變量時,直接賦一對空的大括號就行了,就如這樣。
這樣寫更簡單,窗口類同樣可以正常註冊。大括號代表的是代碼塊,這樣,結構體有了一個初值,因此它會按照結構體的大小分配了相應的內存。
爲什麼會這樣呢?這裏涉及到一個關於結構體的一個很有趣的賦值方式。我們先放下我們這個例子,下面我寫一個簡單的例子,你就明白了。
在本例中,我們定義了一個表示矩形的結構體 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.
我們會收到很多消息,所以用switch判斷一下是不是WM_DESTROY消息,如果是,退出應用程序。
好了,這樣,我們一個完整的Windows應用程序就做好了。
下面是完整的代碼清單。
原文地址:http://blog.csdn.net/tcjiaan/article/details/8497535