1. 程式人生 > >windows基礎應用程式程式設計(一):基本框架

windows基礎應用程式程式設計(一):基本框架

從一開始程式設計時,大家面對著黑乎乎的控制檯視窗,就開始幻想什麼時候能進入windows下進行程式設計。能夠編寫出具有形形色色視窗的人往往都被我們認為是牛人。而從控制檯視窗進入windows下進行程式設計,對於初學者來講,絕對是一個很難跨越的坎。對此,自己也深有體會。因此,一直想寫一些真正能夠引導大家入門的windows基礎應用程式程式設計的文章。也順便把自己所知道的東西順便好好的整理一番。以此與大家共勉。 在開始學習windows應用程式程式設計時,往往大家都有很多誤區,大家可能不重視那些“從底做起”的所謂比較low的技術,而直接去追求高效率的框架技術。比如MFC等,然後,在一接觸MFC時,往往被這個龐大的東西弄得暈頭轉向,從而信心全無。從本人的經驗以及教訓來看,首先學習windows的SDK程式設計往往會起到事半功倍的效果。所以,在接下來的文章中,我們將首先接觸一下這個所謂的“windows底層程式設計”。

主函式

相信大家在學習C語言程式設計的過程中,在教材中都看到過這樣一段話“任何一個程式都是從main函式開始執行的,並且每一個程式有且僅有一個main函式”。那麼在windows程式中,存不存在這樣的一個函式呢?答案是肯定的,這個函式就叫做WinMain函式。它的定義如下:


其中_tWinMain即為WinMain,這是由於適應寬位元組所產生的結果,暫時可不用去理會。其中APIENTRY為__stdcall如下所示:
其指定了如何由程式產生機械碼以及如何在堆疊中放置函式的引數等。一般windows程式都指定為APIENTRY(WINAPI)。 我們還可以看到這個函式還存在著四個引數。並且引數的型別都不知所云,不用著急,我們一個個來分析它。 我們可以看到第一個和第二個引數的型別都叫做HINSTANCE。我們稱之為例項控制代碼。在windows中,執行一個程式,我們就叫做執行一個例項。而控制代碼在windows中就是用來標識某個東西的數字。有點類似於我們在學生在學校中的學號,我們通過這個學號就可以找到具體的這個人。當我們執行這個程式的時候,windows系統就會為我們的例項分配這樣一個“學號”。從而,我們可以根據這個控制代碼找到我們執行的這個例項。第二個引數在早期的windows版本中有意義,32位的版本中已經被拋棄,傳給它的值永遠為空。它實際上是指我們多次運行同一個程式的時候,我們也就建立了該程式的多個例項,這些例項共享一些資源。程式可以通過檢查第二個引數來確定是否有其他的例項在執行。 第三個引數的型別是LPTSTR,這是一個字元指標型別。用於執行程式的命令列。 最後一個引數指示了程式執行後最初的顯示方式,是最大化還是正常顯示還是最小化等等。

標頭檔案

我們知道在利用C編寫控制檯下的程式是,我們首先會毫不猶豫的寫上#include <stdio.h>。這個檔案中包含了我們經常使用的標準輸入輸出函式的定義。那麼在windows程式中我們也會首先包含一個檔案,即windows.h檔案。這個檔案中又包含了許多其他的表頭檔案。一般來講包含基本型別的定義,windows的一些核心函式,使用者介面函式等等。

視窗的誕生

windows程式之所以迷人就是有這友好的使用者介面,而這個介面就是視窗。那麼如何去產生一個視窗呢?看似是一個非常複雜的問題。不用怕,windows系統已經為我們做好了基本上所有的一切,我們只需要按照windows系統視窗產生的流程去提交我們的需求即可。 我們可以把windows系統想象成一個的生產車間。如果我們需要去生產一個產品,那麼第一步我們需要做的就是去設計這個產品。我們要規定這個產品是個什麼樣子,擁有哪些功能等等。那麼對於視窗來講,就是視窗的風格,有沒有選單,以及游標,圖示是什麼樣子等等。為此,windows為我們提供了一個WNDCLASS(WNDCLASSEX)結構體來進行視窗的設計。這就像為我們提供一張關於產品特點的表格一樣,我們根據這個表格所提供的項去填寫,填寫完成之後,我們就完成了對產品的設計。
WNDCLASSEX結構體是WNDCLASS的擴充套件版本。其中WNDCLASSEX結構體的定義如下:

UINT即為無符號整數型別。cbSize表示結構體的位元組大小,一般設定為sizeof(WNDCLASSEX)。 style表示視窗類風格,可以視窗類風格的任意組合,常見的視窗類風格有:
Style 說明
CS_DBLCLKS 允許使用者向視窗傳送雙擊滑鼠鍵的訊息
CS_HREDRAW 當視窗水平長度改變或移動視窗時,重畫整個視窗
CS_VREDRAW 當視窗垂直長度改變或移動視窗時,重畫整個視窗
CS_NOCLOSE 禁用視窗系統選單的關閉選項

lpfnWndProc指向視窗過程函式的指標,至於什麼是視窗過程將在下面講解。 cbClsExtra和cbWndExtra是在視窗類別結構和windows內部儲存的視窗結構中預留的一些額外空間。一般設定為0即可。 hInstance是例項控制代碼,即上面所講的。 hIcon是圖示控制代碼,還記得上面提到過windows用控制代碼來識別東西嗎?同樣,windows利用圖示控制代碼來識別圖示資源。我們可以利用LoadIcon函式來給它賦值。函式原型如下:
HICON LoadIcon( HINSTANCE hInstance, LPSTSTR lpIconName )
它包含兩個引數,第一個引數為例項控制代碼,第二個引數為一個字串指標,指向圖示的名稱。我們一般用MAKEINTRESOURCE巨集來把圖示的ID轉換成所需要的值。如VS2010為我們自動生成的windows程式框架中這樣來使用:
 wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WIN32));
如果我們想使用系統預先定義的圖示,那麼第一個引數應該設定為NULL。 hCursor表示游標的控制代碼。和圖示類似,我們使用LoadCursor函式來設定。 hbrBackground是背景畫刷類的控制代碼,它必須是用於繪製背景的物理畫刷的控制代碼,或者是一個顏色值,如果給出一個顏色的值,它必須是下列標準系統顏色之一(系統將對所選顏色加1)。如果給出了顏色值,它必須轉換成下列HBRUSH型別之一的顏色。 COLOR_ACTIVEBORDER COLOR_ACTIVECAPTION COLOR_APPWORKSPACE COLOR_BACKGROUND COLOR_BTNFACE COLOR_BTHSHADOW 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 例如我們可以這樣來使用:
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
lpszMenuName描述選單的資源名,如果使用整數標識選單,則需要使用MAKEINTRESOURCE巨集。如果為空,則表明視窗沒有預設選單。 lpszClassName指向視窗類名,好比我們為自己設計的產品所取得名稱。 hIconSm是分配給視窗類的小圖示控制代碼,如果設定為NULL,系統自動搜尋一個由hIcon所指示的接近小圖示尺寸的圖示資源作為小圖示。
我們在“填寫設計表格”之後,就需要把表格提交給“生產車間”進行備案,從而才能夠使生產車間根據我們的要求進行生產,這個過程我們成為註冊。windows系統中註冊視窗我們使用RegisterClass(RegisterClassEx)函式來完成。我們只需要把WNDCLASS(WNDCLASSEX)的地址傳入即可。如 RegisterClassEx(&wcex); 註冊失敗將返回0。 註冊之後我們就需要根據要求進行生產了,這一步我們通過CreateWindow函式來完成。CreateWindow函式的原型如下:
HWND CreateWindow(
     LPCTSTR lpClassName,     // 註冊類的名稱
     LPCTSTR lpWindowName,     // 視窗名稱,顯示在標題欄
     DWORD dwStyle     // 視窗風格
     int x,     // 視窗的水平位置
     int y,     // 視窗的垂直位置
     int nWidth,     // 視窗的寬度
     int nHeight,     // 視窗的高度
     HWND hWndParent,     // 視窗父視窗的控制代碼,(關於父視窗的概念以後講解)
     HMENU hMenu,     // 選單控制代碼
     HINSTANCE hInstance,     // 例項控制代碼
     LPVOID lpParam     // 視窗建立資料
);
引數意義比較簡單,不再相信講解,最後一個引數涉及WM_CREATE訊息。在此也先不講述。關於視窗的風格常見的有以下幾種:
Style 說明
WS_BORDER 視窗具有瘦線條邊界
WS_CAPTION 視窗具有標題欄(包含了WS_BORDER風格)
WS_CHILD 子視窗,這種視窗不能有選單欄,不能夠使用WS_POPUP風格
WS_CHILDWINDOW 同WS_CHILD風格
WS_DLGFRAME 擁有對話方塊似的邊界,沒有標題欄
WS_HSCROLL 有水平的滾動條
WS_MAXIMIZE 初始最大化
WS_MAXIMIZEBOX 有最大化按鈕
WS_MINIMIZE 初始最小化
WS_MINIMIZEBOX 有最小化按鈕
WS_OVERLAPPED 有標題欄和邊界
WS_OVERLAPPEDWINDOW WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, WS_THICKFRAME, WS_MINIMIZEBOX, WS_MAXIMIZEBOX風格的組合
WS_POPUP 彈出視窗,這種風格不能使用WS_CHILD風格
WS_SYSMENU 有系統選單,必須指定WS_CAPTION風格
WS_THICKFRAME 有尺寸邊界,和WS_SIZEBOX風格一樣
WS_VISIBLE 初始化可見,可以通過ShowWindow函式或SetWindowLong函式來開啟或關閉
WS_VSCROLL 有垂直滾動條
關於視窗的顯示位置和寬度及高度可以設定成自己想要的大小,也可以設定為CW_USEDEFAULT,這樣系統將使用預設大小,如果x(nWidth)被設定為CW_USEDEFAULT,那麼系統將忽視引數y(nHeight)的作用。 我們可以這樣來使用CreateWindow函式
CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
函式返回建立的視窗控制代碼。 建立完視窗之後,我們還需要把視窗顯示出來,我們需要使用ShowWindow函式和UpdateWindow函式,如下所示:
 BOOL ShowWindow(HWND hWnd, int nCmdShow);

hWnd為視窗控制代碼,nCmdShow為顯示方式,一般可以取值為SW_MAXIMIZE(最大化顯示),SW_MINIMIZE(最小化顯示),SW_SHOW(按當前的尺寸和位置進行顯示)
UpdateWindow(HWND hWnd);

至此一個視窗就誕生了,那麼這樣就結束了嗎?

“訊息”

我們知道,如果我們開啟一個windows程式之後,不再進行任何操作,那麼這個程式將會永無止境的等待下去,除非你在上面進行什麼操作,它才會給你反應。那麼windows系統是如何知道我們什麼時候操作了程式,什麼時候沒有操作呢?windows系統為我們每一個程式都建立一個訊息佇列的東西。當對程式進行某種操作,這時就會產生一系列對應的訊息,windows系統把這些訊息投放到對應的訊息佇列中,而程式內部必然有一個迴圈不斷的從訊息佇列中取訊息進行逐個處理。所以在沒有事件發生時(即沒有對應用程式進行任何操作)就不會產生任何訊息,訊息佇列為空,程式不處理任何操作。 說了那麼多關於訊息的東西,那麼在程式中訊息到底是什麼呢?訊息其實是一個名為MSG的結構體變數。其定義如下:
typedef struct tagMSG
{
     HWND hwnd;
     UINT message;
     WPARAM wParam;
     LPARAM lParam;
     DWORD time;
     POINT pt;
}MSG;

hwnd:接受訊息的視窗控制代碼。 message:訊息識別符號,一個數值,每個訊息都對應一個識別符號。用來區分是哪個訊息。比如是鍵盤按下訊息還是單擊滑鼠左鍵訊息。 wParam和lParam:訊息的附加資訊,具體指根據訊息的不同而不同,比如在鍵盤按下時,根據他們的值可以判斷是哪個鍵按下。 time:訊息放入訊息佇列中的時間 pt:這是一個POINT型別的值,POINT型別是一個包含x和y值的結構體,pt表示事件發生時滑鼠的座標值。(在螢幕座標中,原點位於左上角,水平向右為x軸的正方向,垂直向下為y軸的正方向)。 那麼我們在程式中可以使用下面這個迴圈來不斷的從訊息佇列中取訊息。
  MSG msg;
  while (GetMessage(&msg, NULL, 0, 0))
  {     
     TranslateMessage(&msg);
     DispatchMessage(&msg);
  }

我們使用GetMessage函式來從訊息佇列中取得一個訊息。第一個引數指向取得的訊息。第二個引數表示對應的視窗控制代碼,第三個和第四個引數分別表示第一個訊息和最後一個訊息,用來進行訊息過濾。如果第二個引數設定為NULL,第三個和第四個引數設定為0表示,程式接受它自己建立的所有視窗的所有訊息。 TranlateMessage(&msg); 用來進行一些鍵盤轉換。暫時不詳細講解。 DispatchMessage(&msg); 把取到的訊息重新傳給windows,然後由windows尋找響應的視窗訊息處理函式進行處理。(希望大家不要對這句話視而不見,這是我們下面所要講述的內容)。 那麼訊息迴圈什麼時候結束呢?即GetMessage函式返回一個零值時,GetMessage函式每取到一個訊息,都會檢視這個訊息的message欄位,如果該欄位的值不為WM_QUIT(由巨集對應一個UINT值),那麼該函式就返回非零值,否則返回零值,退出迴圈。

訊息處理函式

我們上面所產生的視窗可以說是空有其表,其所有的功能也僅僅是顯示一個視窗,什麼都做不了,甚至連最起碼的關閉都關閉不了。那麼如何去新增這些功能呢?在開始講解之前,我們必須要解開windows系統中的一層面紗。那就是大名鼎鼎的“Don't call me,I'll call you”! 我們現在已經很清楚的知道,如果要使程式有反應,那麼我們就必須要對取到的訊息進行處理。我們上面在DispatchMessage函式中講到,該函式又將取到的msg訊息送回給windows,然後由windows系統根據訊息的視窗控制代碼來呼叫該視窗的訊息處理函式,這個訊息處理函式是讓我們自己來編寫的。一個windows應用程式的主要編寫工作就在於對訊息的處理,即編寫這個訊息處理函式。這和傳統上的C語言程式設計有很大的區別,在之前的程式設計中,我們都是去呼叫系統提供給我們的函式,而在這裡我們則需要編寫函式來供windows系統來呼叫。這就是“Don't call me, I'll call you”的含義。換句話說,我們只需要編寫如何對各個訊息進行處理,而不用去管,這個訊息的處理程式什麼時候被呼叫。 那麼如何編寫一個視窗的訊息處理函式呢?其定義如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

LRESULT是一個64位整數值型別。 其中我們看到有4個引數,其中的含義之前已經說過,這裡不再贅述。 我們可以在訊息處理函式體中,使用switch/case結構通過判斷message的值來判定是哪個訊息,最後,不要忘記呼叫預設的訊息處理函式,因為我們不可能對所有的訊息都處理完。例如:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
     switch(message)
{
     case WM_DESTROY:
          訊息的處理...
          break;
     case WM_....
          ...
     default:
          return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}

一個完整的例子

開啟VS2010,檔案->新建->專案,選擇Win32專案,在設定中選擇空檔案,輸入以下程式碼
// win32.cpp : 定義應用程式的入口點。
//
#include <windows.h>
#include <tchar.h>

// 全域性變數:
HINSTANCE hInst;								// 當前例項
// 此程式碼模組中包含的函式的前向宣告:
ATOM				MyRegisterClass(HINSTANCE hInstance);
BOOL				InitInstance(HINSTANCE, int);
LRESULT CALLBACK	WndProc(HWND, UINT, WPARAM, LPARAM);

int APIENTRY _tWinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPTSTR    lpCmdLine,
                     int       nCmdShow)
{
	MSG msg;
	MyRegisterClass(hInstance);

	// 執行應用程式初始化:
	if (!InitInstance (hInstance, nCmdShow))
	{
		return FALSE;
	}

	// 主訊息迴圈:
	while (GetMessage(&msg, NULL, 0, 0))
	{
			TranslateMessage(&msg);
			DispatchMessage(&msg);
	}

	return (int) msg.wParam;
}



//
//  函式: MyRegisterClass()
//
//  目的: 註冊視窗類。
ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;

	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style			= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon			= LoadIcon(NULL, MAKEINTRESOURCE(IDI_APPLICATION));
	wcex.hCursor		= LoadCursor(NULL, MAKEINTRESOURCE(IDC_ARROW));
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= NULL;
	wcex.lpszClassName	= TEXT("MyFirstApp");
	wcex.hIconSm		= NULL;

	return RegisterClassEx(&wcex);
}

//
//   函式: InitInstance(HINSTANCE, int)
//
//   目的: 儲存例項控制代碼並建立主視窗
//
//   註釋:
//
//        在此函式中,我們在全域性變數中儲存例項控制代碼並
//        建立和顯示主程式視窗。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;

   hInst = hInstance; // 將例項控制代碼儲存在全域性變數中

   hWnd = CreateWindow(TEXT("MyFirstApp"), TEXT("MyFirstApp"), WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

//
//  函式: WndProc(HWND, UINT, WPARAM, LPARAM)
//
//  目的: 處理主視窗的訊息。
//
//  WM_COMMAND	- 處理應用程式選單
//  WM_PAINT	- 繪製主視窗
//  WM_DESTROY	- 傳送退出訊息並返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
	switch (message)
	{
	case WM_DESTROY:
		PostQuitMessage(0);
		break;
	default:
		return DefWindowProc(hWnd, message, wParam, lParam);
	}
	return 0;
}

執行結果


相信經過上面的分析,你能夠很輕鬆的看懂這個具體的框架了。