4.vc從零開始 -- Win32專案hello world(二)
上文講了其他的檔案,這次就把主使用者程式碼檔案testSDK.cpp講一下。
全域性變數、宣告、定義
第1-18行:
// testSDK.cpp : 定義應用程式的入口點。
//
#include "stdafx.h"
#include "testSDK.h"
#define MAX_LOADSTRING 100
// 全域性變數:
HINSTANCE hInst; // 當前例項
TCHAR szTitle[MAX_LOADSTRING]; // 標題欄文字
TCHAR
szWindowClass[MAX_LOADSTRING
// 此程式碼模組中包含的函式的前向宣告:
ATOM MyRegisterClass(HINSTANCE hInstance);
BOOL InitInstance(HINSTANCE, int);
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM);
標頭檔案
先是兩個#include包含標頭檔案,這兩個檔案之前都講過的;
巨集定義
接著是一個#define,這個定義了一個巨集MAX_LOADSTRING,關於這個define巨集,我們暫時只需要記住一點,就是下面程式碼中出現的整詞匹配的MAX_LOADSTRING,就會直接替換為100;define巨集還有一些高階用法,有興趣可以自行搜尋。
巨集定義並不是必須的,如果程式碼中多次使用到同一個常量,為了便於修改,就可以使用巨集定義。
全域性變數
下面定義了3個全域性變數hInst,szTitle,szWindowClass,全域性變數的意思就是接下來整個程式碼檔案中任何地方都可以直接使用這些變數。對於全域性變數,個人建議加上g_字首,以便與區域性變數進行區分,比如g_hInst
HINSTANCE hInst;
這個需要解釋的就是變數型別HINSTANCE,這個型別其實是VS自己定義的變數型別,真實型別是void*,是一個指標型別。
TCHAR szTitle[MAX_LOADSTRING];和TCHAR szWindowClass[MAX_LOADSTRING];
TCHAR自適應Unicode和非Unicode編譯,當Unicode編譯時,TCHAR被識別為wchar_t,非unicode編譯則識別為char;
而MAX_LOADSTRING會被替換為100,變成TCHAR szTitle[100];
對比第一個變數"HINSTANCE hInst;"的宣告,發現多了一對中括號[],這個[]表示宣告的變數是陣列變數,[100]就表示有100個TCHAR在szTitle變數中;
陣列的使用方式是szTitle[n],n的索引從0開始,到陣列數量減一結尾。
比如szTitle 有100個元素,那麼第一個元素就是szTitle[0],結尾的元素就是szTitle[99];當單獨使用szTitle不帶中括號的時候,表示使用過的是這一塊記憶體的起始指標;
在c/c++/vc中,陣列的宣告必須確定陣列大小,比如TCHAR szTitle[100];,但是不能這樣寫:
int nSize = 100;
char szTitle[nSize];
編譯無法通過,會報錯。
字串屬於特殊的陣列,必須以0作為最後一個元素。比如定義一個字串:char szTitle[] = "testSDK";
那麼每個元素對應如下:
索引 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
內容 | 't' | 'e' | 's' | 't' | 'S' | 'D' | 'K' | 0 |
szWindowClass同上。
函式宣告
4個函式的宣告:MyRegisterClass,InitInstance,WndProc,About。
在開頭把函式宣告一下的好處是,在下面的程式碼中可以隨時使用。如果函式沒有宣告的話,那麼只能在函式定義之後才能使用。
main函式
程式入口點,最重要的函式。
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置程式碼。
MSG msg;
HACCEL hAccelTable;
// 初始化全域性字串
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_TESTSDK, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 執行應用程式初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_TESTSDK));
// 主訊息迴圈:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
函式定義
使用了一些vs自家的封裝,我還原一下:
int __stdcall WinMain(void* hInstance,
void* hPrevInstance,
char* lpCmdLine,
int nCmdShow)
{
...
}
int表示函式結束的時候需要return返回一個int型別的值;
__stdcall是一種函式協議,這些函式協議表示函式的引數傳遞順序,以及堆疊的平衡是由函式呼叫方還是函式自身來做。這個對於使用者來說暫時不需要關心,有興趣的同樣可以搜尋(關鍵字:stdcall cdecl fastcall)。
WinMain就是函式名稱,後面括號表示函式需要的引數,一個需要四個引數:
hInstance表示當前程序模組的INSTANCE,可以認為是一個識別符號,大概類似於控制代碼;
hPrevInstance字面意思是上一個程序模組的控制代碼,然而可能只是為了相容舊版本系統而留的,實際是0;
lpCmdLine指向一個字串,表示啟動程序的引數。如果沒有傳遞引數,該字串就是空串。
nCmdShow是一個整數,表示該程式執行之後是什麼狀態,顯示/隱藏/最小化/最大化/常規等,具體數值定義是:#define
SW_HIDE 0
#define SW_SHOWNORMAL 1
#define SW_NORMAL 1
#define SW_SHOWMINIMIZED 2
#define SW_SHOWMAXIMIZED 3
#define SW_MAXIMIZE 3
#define SW_SHOWNOACTIVATE 4
#define SW_SHOW 5
#define SW_MINIMIZE 6
#define SW_SHOWMINNOACTIVE 7
#define SW_SHOWNA 8
#define SW_RESTORE 9
#define SW_SHOWDEFAULT 10
#define SW_FORCEMINIMIZE 11
#define SW_MAX 11
函式體
UNREFERENCED_PARAMETER
UNREFERENCED_PARAMETER是一個vs自己定義的巨集,從字面意思可以看到,這個巨集包起來的引數下面不會用到。使用這個巨集把未使用的引數包起來,可以避免編譯的時候出現"未使用的引數"的警告warning。
MSG結構體
函式接著定義了一個MSG型別的變數msg,MSG是一個結構體,主要用於記錄訊息相關內容,結構體含有以下成員:struct MSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} 結構體就相當於是一個衣櫃,裡面的各個成員就相當於衣櫃的各個隔間和抽屜。
MSG結構體的使用方法是msg.hwnd,msg.message,這樣子的。
HACCEL加速鍵
繼續看下面,定義了一個HACCEL hAccelTable;變數,這個HACCEL型別姑且先當作一個識別符號控制代碼,用於指向一個加速鍵的表。
LoadString
LoadString是Windows提供的一個API函式,用於載入資源中的字串表的某一項內容。
int LoadString(
HINSTANCE hInstance,
UINT uID,
LPTSTR lpBuffer,
int nBufferMax
);
這是這個函式的定義,根據MSDN的解釋,第一個引數是上文提到的模組控制代碼;第二個引數是資源ID(在資源的字串表中定義);第三個引數是字串空間的指標,用於接收讀取到的字串;第四個引數表示第三個引數指向的空間有多少個字元char或寬字元wchar_t,讀取到的字元數將不會超過這個數值,超出部分被截斷。
函式返回讀取到的字元數,不包括字串結尾的0;所以假如要讀取一個字串"testSDK",長度是7,那麼需要的空間至少是8。
LoadString(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadString(hInstance, IDC_TESTSDK, szWindowClass, MAX_LOADSTRING);
意思就是把ID為IDS_APP_TITLE的資源讀取到szTitle中,把ID為IDC_TESTSDK的資源讀取到szWindowClass中。
註冊視窗類
呼叫了MyRegisterClass函式註冊一個視窗類,後面會詳細解說該函式。
建立視窗
呼叫了InitInstance函式建立一個視窗,後面詳細解說該函式。
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
這裡用了一個判斷,如果InitInstance函式返回FALSE(表示:錯誤/假/非法)的話,那麼就變成了:
if (!FALSE)
{
return FALSE;
}
感嘆號"!"表示"非",那麼"非FALSE",就等於"TRUE(表示:正確/真/合法)",那麼就變成
if (TRUE)
{
return FALSE;
}
條件成立,就會執行大括號內的"return FALSE;",導致WinMain返回錯誤,程式中止。
所以這個判斷最終的意思是,如果InitInstance函式成功建立了視窗,那麼就繼續執行下面的程式碼;如果InitInstance建立窗口出現錯誤,那麼下面的程式碼也就沒有繼續執行的必要了。
LoadAccelerators
該函式是Windows提供的一個API函式,用於載入一個加速鍵表,以便在後面使用,將資源中定義的快捷鍵生效。
需要注意的是第二個引數,使用了MAKEINTRESOURCE巨集。這個巨集其實並沒有進行什麼操作,只是進行了一個強制型別轉換,因為LoadAccelerators函式的第二個引數需要字串指標型別,而我們定義的ID是整數型的。比如:#define
IDC_TESTSDK 109
我的IDC_TESTSDK定義是整數的109,換算成十六進位制就是0x
6D,進行強制轉換成字串指標之後變成0x0000006D,其實還是0x6D,對於LoadAccelerators函式來說,接收到的是同樣的內容,只是編譯器要求必須使用字串指標。通過解析這個MAKEINTRESOURCE巨集,可以把這個函式呼叫換成這樣: hAccelTable = LoadAccelerators(hInstance, (LPCSTR)IDC_TESTSDK);
我直接使用了"(LPCSTR)"來進行一個強制型別轉換,是一樣的效果。
訊息迴圈
while迴圈
訊息迴圈使用了一個while迴圈,迴圈條件是"GetMessage(&msg, NULL, 0, 0)",即是根據GetMessage的返回結果來確定繼續迴圈還是結束迴圈。假如GetMessage一直返回TRUE值,那麼該迴圈會一直繼續下去。那麼什麼時候GetMessage返回FALSE呢?當GetMessage接收到一個WM_QUIT訊息就會返回FALSE,於是while迴圈就結束。
GetMessage
該函式是Windows提供的一個API函式,用於接收本執行緒收到的訊息,可以接收系統下發的訊息,也可以接收其他執行緒與程序傳送的訊息。
接收到的訊息,會把訊息的相關內容存放在msg結構體中,所以該函式的第一個引數我們傳遞了一個msg的指標(就是"&msg")進去。'&'符號在這裡表示取後面變數msg的地址指標。這個涉及到兩個方面的知識,一個是函式的"多返回值",還有一個是"指標和記憶體"。是相對深入的內容,後面再專門講解。
當沒有訊息可以接收的時候,該函式會一直等待。當收到WM_QUIT訊息就會返回FALSE。
TranslateAccelerator
該函式是Windows提供的一個API函式,用於接收熱鍵訊息。
這個函式是作為一個if語句的判斷條件出現的,該判斷語句以TranslateAccelerator的返回值來判斷是否執行if判斷體內容,具體可以參考上文"建立視窗"部分關於if的講解。
TranslateAccelerator有三個引數,第一個是視窗控制代碼,就是前面建立的視窗控制代碼,GetMessage函式會填充到msg.hwnd中;第二個引數是加速鍵表控制代碼hAccelTable,就是前面LoadAccelerators獲取到的;第三個引數是訊息結構體的指標地址&msg。其實既然第三個引數把msg指標傳遞進去了,那麼第一個引數msg.hwnd實際上可以省略掉,不知道為何保留著。
TranslateAccelerator函式內部會根據msg的各項內容,確定是否按鍵訊息,該按鍵訊息又是否是hAccelTable加速表中註冊過的,符合條件的話,函式會呼叫熱鍵處理函式並等待熱鍵處理函式結束後才返回,返回值為非0值(一般是1),表示該訊息已經由TranslateAccelerator函式處理過了;而如果不是按鍵訊息,或者不是加速鍵表中註冊過的按鍵訊息,又或者出現了其他錯誤導致函式失敗,函式將返回0,表示函式沒有處理該訊息,需要下發下去繼續處理。
TranslateMessage
該函式是Windows提供的一個API函式,用於轉換按鍵訊息,將虛擬按鍵訊息WM_KEYDOWN/WM_SYSKEYDOWN與WM_KEYUP/WM_SYSKEYUP轉換為字元訊息WM_CHAR,並把新生成的WM_CHAR字元訊息加入到訊息佇列中。
DispatchMessage
該函式是Windows提供的一個API函式,用於將沒人認領的訊息下發給視窗處理函式並等待視窗處理函式的處理結果。
WinMain函式返回
當GetMessage收到WM_QUIT訊息之後,while迴圈結束,這裡可以處理一些資源釋放或儲存操作。然後就返回,退出程式。
return (int) msg.wParam;
一般我都是直接返回0。