windows窗口基礎—菜鳥篇

 

多數的Windows程序都需要Windows.hWindowsx.h這兩個頭文件,要確保使用它們。當然,你還需要其它的標準的C的頭文件,象stdio.hconio.h等。除了這些,你還會經常看到在程序的開始有這樣一行代碼:

#define WIN32_LEANAND_MEAN


  它表示Windows的頭文件中將拒絕接受MFC的東西,這將加速你的build時間。如果你從沒有打算應用MFC在你的遊戲編程中,那就使用它吧。如果你以前從沒有看過這種聲明類型——#define後,直接加上一個單詞,那麼它的作用就是有條件編譯。看看下面的例子:

#ifdef DEBUG_MODE
printf("Debug mode is active!");
#endif


  意思是:如果程序的開始包含#define DEBUG_MODE,那麼就printf(),否則退出。這個對於你跟蹤程序的邏輯錯誤是很有幫助的。

  WinMain()函數

  DOS下的C語言從main()開始,Windows下的C語言從WinMain()開始,一個空的WinMain()函數是這樣的:

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
return(0);
}


  一個函數即使什麼也沒做,也應該返回一個值。是的,有好多東西我們不熟悉。首先的首先,WINAPI是個什麼聲明?WINAPI是在windows.h頭文件中定義的一個宏,它把函數調用翻譯成正確的調用約定。當我們在程序中需要用到彙編語言的時候,我們在來深究它好了,記住,如果要用WinMain(),就必須要有WINAPI

  下一步讓我們來看看括號裏的四個參數:

  ◎ HINSTANCE hinstanceHINSTANCE是一個句柄類型的標識符。變量hinstance是一個整數,用於標識程序實例。Windows設置這個參數的值,並把它傳遞給你的程序代碼。很多Windows函數都要用到它。

  ◎ HINSTANCE hPreInstance:你不用擔心這個參數,它已經被廢掉了。它只是爲古老的Windows版本服務的。你將還會看到類似的情況。

  ◎ LPSTR lpCmdLine:是一個指向字符串的指針,它僅在程序名是從DOS命令行輸入或是從Run對話框中輸入時才起作用。因此,很少被程序代碼所用。

  ◎ int nCmdShow:決定了窗口在初始顯示時的狀態。Windows通常給這個參數分配一個值。通常是SW_打頭的一個常量。例如SW_SHOWNORMAL表示默認狀態,SW_MAXINIZESW_MINIMIZE分別表示最大和最小模式等等。


  消息

  當你在DOS下編程的時候,你不必擔心其它程序的運行,因爲DOS是獨佔模式。但你在Windows平臺上編程時,你不得不考慮其它正在運行的程序。鑑於此,Windows通過消息來連接操作申請和具體操作。簡單的說,就是我們指示程序或程序本身向Windows發出諸如移動窗口、放大窗口、關閉窗口等地申請,Windows再根據申請,考察實地情況,拒絕或發出指令,讓程序(計算機)作出相應的動作。再例如,鼠標隨時向Windows發出消息,彙報光標位置,左鍵或右鍵是否按下等,Windows再根據消息作出相應的反應。總之,無論何時,Windows都要隨時掌控所有的消息,而且,Windows是一直不斷地接收到各種消息。

  這種功能是通過一種被命名爲CALLBACK函數類型實現的。不用害怕,消息的傳遞來,傳遞去都是由Windows自己完成的,你只要聲明一個CALLBACK函數就可以了,就像WINAPI用在WinMain()前一樣。如果還沒有明白,不要緊,往下看你就明白了。現在,我要離開這個話題一會,因爲你只有先建立窗口(Windows),傳遞消息纔有可能實現。

  窗口類

  現在談論一點C++的知識,因爲要想建立一個窗口,你就得先建立一個窗口類。窗口類包含所有的有關窗口的信息,如用什麼樣的鼠標符號,菜單樣式等等。開發任何一個窗口程序,都離不開窗口類的建立。爲了達到此目的,你必須填寫WNDCLASSEX結構。EX的意思是擴充的意思,因爲有一個老的結構叫作WNDCLASS,這裏,我們將使用WNDCLASSEX結構,它的樣子如下:

typedef struct _WNDCLASSEX {

UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;


  這個結構有不少成員,討厭的是,你必須爲窗口類設置每一個成員。莫發愁,紙老虎一個。讓我們來個速成。

  ※ UINT cbSize:指定了以字節爲單位的結構的大小。這個成員是通過sizeof(WNDCLASSEX)實現的。你將會經常看到它,尤其是你使用了DirectX

  ※ UINT style:指定了窗口的風格。它經常被以CS_打頭的符號常量定義。兩種或兩種以上的風格可以通過C語言中的|)運算符加以組合。大多數情況我們只應用四種風格,出於對文章長度的考慮,我們只列出這四種。若你還需要其它的,到MSDN裏找一下好了。別告訴我你用的不是Visual C++啊!

  ◎ CS_HREDRAW:一旦移動或尺寸調整使客戶區的寬度發生變化,就重新繪製窗口。

  ◎ CS_VREDRAW:一旦移動或尺寸調整使客戶區的高度發生變化,就重新繪製窗口。

  ◎ CS_OWNDC:爲該類中的每一個窗口分配一個唯一的設備上下文。


  ◎ CS_DBLCLKS:當用戶雙擊鼠標時向窗口過程發送雙擊消息。

  ※ WNDPROC lpfnWndProc:是指向窗口過程的指針。一般都指向CALLBACK函數。如果你沒有用過函數指針,簡單理解爲函數的地址就是函數的名字,名字後面別帶括號。

  ※ int cbClsExtra:它是爲類保留的額外信息 。大多數程序員不用它,你在在寫遊戲程序時也不太可能用它,所以,設爲0好了。

  ※ int cbWndExtra:同上一個差不多,設爲0好了。

  ※ HANDLE hInstance:是指向窗口過程實例的句柄。同時也是WinMain()函數的參數之一。應該設置爲hinstance

  ※ HICON hIcon:指向窗口圖標的句柄,它通常被LoadIcon()函數設置。在你學會如何在你的程序中使用資源前,你先設置成如下樣子:LoadIconNULLIDI_WINLOGO)。當然,還有一些其它的IDI_打頭的符號常量,你自己去幫助文件裏找吧。

  ※ HCURSOR hCursor:指向窗口光標的句柄,它通常被LoadCursor()函數設置,在你學會如何在你的程序中使用資源前,你先用Windows默認的吧,LoadCursor(NULL,IDC_ARROW)

  ※ HBRUSH hbrBackground:當你的窗口過程得到消息,要求刷新(或重畫)窗口時,至少要用一種純色或“brush”(畫刷)重畫窗口區域,畫刷是由參數確定的。你可以使用GetStockObject()函數調用幾種常備的畫刷,如BLACK_BRUSH, WHITE_BRUSH, GRAY_BRUSH等。現在,你就用GetStockObject(BLACK_BRUSH)吧。對不起,你可能覺得我說的太簡單了,但我不想把開始弄得太複雜。我在以後的篇幅裏會詳細講的,我保證。

  ※ LPCTSTR lpszMenuName:如果你想建立一個有下拉菜單的窗口,你得給這個參數賦一個菜單名稱(這涉及到資源),由於你還不知道怎麼創建菜單,你就先用NULL設置成一個沒有菜單的窗口吧。

  ※ LPCSTR lpszClassName:很顯然,你需要給類起個名字,隨你便,如“**”。要用雙引號引上啊!

  ※ HICON hIconSm:指向小圖標的句柄。小圖標用來顯示在窗口的標題欄裏。要用到LoadIcon()函數,現在,你就用Windows默認的吧,LoadIconNULLIDI_WINLOGO)。

  好了,你關於WNDCLASSEX結構知道的差不多了,你可以自己設置它了。下面是一個例子:

WNDCLASSEX sampleClass; // declare structure variable
sampleClass.cbSize = sizeof(WNDCLASSEX); // always use this!
sampleClass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; // standard settings
sampleClass.lpfnWndProc = MsgHandler; // we need to write this!
sampleClass.cbClsExtra = 0; // extra class info, not used
sampleClass.cbWndExtra = 0; // extra window info, not used
sampleClass.hInstance = hinstance; // parameter passed to WinMain()
sampleClass.hIcon = LoadIcon(NULL, IDI_WINLOGO); // Windows logo
sampleClass.hCursor = LoadCursor(NULL, IDC_ARROW); // standard cursor
sampleClass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // a simple black brush
sampleClass.lpszMenuName = NULL; // no menu
sampleClass.lpszClassName = "Sample Class" // class name
sampleClass.hIconSm = LoadIcon(NULL, IDI_WINLOGO); // Windows logo again
......


  我想,你已經有點兒不太崇拜Windows程序員了。言歸正傳,有一點我得提醒你,注意函數GetStockObject()前的(HBRUSH)類型配置,這是因爲GetStockObject()可以調用其它的對象,不僅僅是“brush”,所以你需要一個HBRUSH類型配置。在Visual C++舊版本里不用配置,但新的6.0版本需要它,否則會編譯出錯。

  下一件事是註冊這個窗口類,只有這樣,你才能創建新的窗口。十分簡單,你只需要調用一個RegisterClassEX()函數,它只有一個參數,就是你的窗口類的地址(名字),根據我上面給的例子,這裏應該這樣:

RegisterClassEx(&sampleClass);


  嗨,我們的窗口類創建完了,我們可以用它創建一個窗口了。只是時間問題嘍!

  創建窗口

  好消息,創建窗口你所要做的只是調用一個CreateWindowEx()函數。壞消息是,這個函數有好多的參數。嘿!把刀放下,有話好說嗎!真的不難,做事情總得走走形式嘛!以下是函數原形:

HWND CreateWindowEx(
DWORD dwExStyle, // extended window style
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu, or child-window identifier
HINSTANCE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);


  首先的首先:函數的返回值。也就是函數的類型。是不是所有創建窗口用的函數的類型的討厭樣子都感覺親切了一點兒?還沒有?不要緊,你會習慣的,肯定比你想象的速度要快。這裏返回的類型是HWND,是一個窗口的句柄(句柄就是窗口的標識符)。你將把CreateWindowEx()的返回值傳遞給一個窗口的句柄,就像一個參數一樣。現在,我們來琢磨一下這些參數,很多根據名字就知道它是幹什麼的了。

  ※ DWORD dwExStyle:擴充的窗口風格。你將很少使用擴充的窗口風格,所以多數時間你會把它設置爲NULL。如果有興趣,查一下幫助文件,可以一試由WS_EX_打頭的擴充風格。

  ※ LPCTSTR lpClassName:還記得你的窗口類的名稱嗎?再用一次。

  ※ LPCTSTR lpWindowName:將顯示在窗口的標題欄裏的簡短文字。

  ※ DWORD dwStyle:窗口的風格。它將允許你詳細的描繪你所要創建的窗口的風格。有很多風格你可以利用哦,都是以WS_打頭的,你可以利用(|)符號組合利用它們。我將在這兒介紹幾個常用的。

  ◎ WS_POPUP 指定一個彈出的窗口。

  ◎ WS_OVERLAPPED 指定一個具有標題欄和邊界的重疊窗口。

  ◎ WS_OVERLAPPEDWINDOW 指定一個具有所有標準控件的窗口。

  ◎ WS_VISIBLE 指定一個初始時可見的窗口。

  看得出,WS_OVERLAPPEDWINDOW是一個組合體。簡單的說,你可以按照如下規律:如果你要創建一個可以最大化、最小化、隨意改變大小等等地窗口,就選擇WS_OVERLAPPEDWINDOW;如果你只想要一個具有標題欄、可改變大小的窗口,就選擇WS_OVERLAPPED;如果你只想要一個光禿禿的窗口,就選擇WS_POPUP;如果你只想顯示一個黑色的大方框,可能你要用它寫一個全屏的遊戲,選擇WS_VISIBLE是沒錯的。

  ※ int x,y:你所要創建的窗口的左上角的座標。

  ※ int nWidth,nHeight:猜也猜到了,窗口的長和高,單位是『象素』。

  ※ HWND hWndParent:指向父窗口的句柄。你若想在窗口下再建立一個窗口,那麼第一個窗口就叫父窗口。咱先建立一個主窗口,所以設置爲NULL,也就意味着Windows桌面是父窗口。

  ※ HMENU hMenu:這是用在窗口上的菜單句柄。若你學會建立和使用資源,即建立自己的菜單,你可以用LoadMenu()函數調用自己的菜單資源。目前,咱先設爲NULL

  ※ HINSTANCE hInstance:是一個名柄,它指向由Windows傳遞給WinMain()的實例。

  ※ LPVOID lpParam:對於遊戲編程來說,沒有什麼用的東西,只有簡單的窗口程序用到它。設置爲NULL好了。

  同志們,我們現在萬事具備,東風也有了。我先給個示例:

HWND hwnd;
if (!(hwnd = CreateWindowEx(NULL, // extended style, not needed
"Sample Class", // class identifier
"Sample Window", // window title
WS_POPUP | WS_VISIBLE,// parameters
0, 0, 320, 240, // initial position, size
NULL, // handle to parent (the desktop)
NULL, // handle to menu (none)
hinstance, // application instance handle
NULL))) // who needs it?
return(0);


  你可能會在遊戲編程中用上這這段代碼,因爲它是一個彈出式窗口。注意,我用了if形式,目的是一旦CreateWindowsEX()函數失靈,返回一個NULL,也就意味着如果窗口由於某種原因不能被建立,那麼WinMain()就被簡單的返回,程序結束。

  現在我們學會了足夠的知識建立一個小有功能的窗口了。還記得我們建立窗口類“sample class”時,一個指向“CALLBACK”類型函數的指針嗎?對,是“lpfnWndProc”。要想讓你的窗口真正做點事兒,我們還得來處理一下它指向的窗口過程函數。

  顯示窗口

  CreateWindowEx()從內部創建窗口,但並不顯示它。要顯示這個窗口,必須調用另外兩個函數:ShowWindow()UpdateWindow()。頭一個設置窗口的顯示狀態,後一個則更新窗口的客戶區。對於程序的主窗口,ShowWindow()必須被調用一次,調用代碼如下:

ShowWindow(hwnd,nCmdShow);


  第一個參數是由CreateWindowEx()函數返回的窗口句柄;第二個參數就是窗口的顯示模式參數,在☆WinMain()函數中提到過,就不重複了。UpdateWindow()函數的調用代碼如下:

UpdateWindow(hwnd);


  參數hwndShowWindow()函數的hwnd一樣。

  消息的處理

  我已經說過消息在窗口裏的作用了,下面讓我們來仔細學習一下它。處理消息的函數結構如下:

LRESULT CALLBACK MsgHandler( 【有時被命名爲WndProc,隨便你】
HWND hwnd, // window handle
UINT msg, // the message identifier
WPARAM wparam, // message parameters
LPARAM lparam // more message parameters
;


  這個LRESULT類型要求返回一個32位的整數。實際取值依賴於消息,但是這個值很少在應用程序代碼中得到應用。以前我們談到過一點CALLBACK協定,它的參數很簡單:

  ※ HWND hwnd:是接收消息的窗口的句柄,也是由CreateWindowEx()函數返回的句柄。

  ※ UINT msg:這是一個消息標識符,都是以WM_打頭的符號常量,意思是“Windows Message”。很多的,這裏只介紹一些常用的:

  ◎ WM_ACTIVATE:一個新窗口被激活。
  ◎ WM_CLOSE:一個窗口被關閉。
  ◎ WM_COMMAND:一個菜單功能被選擇。
  ◎ WM_CREATE:一個窗口被建立。
  ◎ WM_LBUTTONDBLCLK:鼠標左鍵被雙擊。
  ◎ WM_LBUTTONDOWN:鼠標左鍵被按下。
  ◎ WM_MOUSEMOVE:鼠標被移動。
  ◎ WM_MOVE:一個窗口被移動。
  ◎ WM_PAINT:窗口的一部分需要重畫。
  ◎ WM_RBUTTONDBLCLK:鼠標的右鍵被雙擊。
  ◎ WM_RBUTTONDOWN:鼠標的右鍵被按下。
  ◎ WM_SIZE:窗口的大小被改變。
  ◎ WM_USER:幹你想幹的。

  ※ WPARAM wparamLPARAM lparam:消息參數。它們提供有關消息的附加信息,這兩個值對於每條消息來說都是特定的。

  你要把所有要發生的消息都寫進程序代碼的話,我想你可能已經累瘋了。我想我會的。感謝上帝,Windows提供了默認消息處理,如果你沒有任何特殊的消息需要處理了,你總是要用DefWindowPorc()函數的,下面給一個最簡單的例子,沒有任何特定的消息要處理的例子:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
return(DefWindowProc(hwnd, msg, wparam, lparam));
}


  簡單吧!但通常你都需要處理一些自己的消息,你要寫自己的程序代碼,然後返回0,來告訴程序你幹完了。下面是一個例子,當窗口建立時,你調用了一個初始化的函數Initialize_Game(),然後返回0,最後告訴程序自己處理那些默認的消息吧:

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
if (msg == WM_CREATE)
{
Initialize_Game();
return(0);
}

return(DefWindowProc(hwnd, msg, wparam, lparam));
}


  你很可能需要一個“switch”結構來手動完成你想要控制的消息,然後把剩下的交給DefWindowProc()去做。大功告成前,我不得不提醒您一件事,就是怎樣使你的消息控制得到響應呢?

  讀取消息隊列

  我先給你一個switch結構的例子吧:(感性的)

LRESULT CALLBACK MsgHandler(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
switch(msg)
{
case WM_CREAT:
[
初始化遊戲]
return 0

case WM_PAINT:
[
畫一架飛機]
return 0

case ……………………
……………………
}

return(DefWindowProc(hwnd, msg, wparam, lparam));
}


  在進入程序的主循環前,你需要看看你的消息控制(就是你在switch結構裏編的那些),尤其是還沒有用到的消息控制是否被機器存了起來,以備一旦用到,馬上響應。做到正確的響應,你需要做幾件事。首先你需要PeekMessage()函數。下面是它的原形:

BOOL PeekMessage(
LPMSG lpMsg, // pointer to structure for message
HWND hWnd, // handle to window
UINT wMsgFilterMin, // first message
UINT wMsgFilterMax, // last message
UINT wRemoveMsg // removal flags
);


  這是一個布爾類型,也就是一個int型,不過只有兩個值,TRUEFALSE,如果有一條消息在隊列中等待,函數返回TRUE,否則,返回FALSE。它的參數很簡單:

  ※ LPMSG lpMsg:這是一個MSG類型的指針變量。如果有消息在等待,消息信息將被填入該變量。

  ※ HWND hWnd:你所要檢查的消息隊列的窗口的句柄。

  ※ UINT wMsgFilterMinwMsgFilterMax:索引第一個和最後一個消息,一般你都從第一個消息開始檢索,所以把它們都設置爲0好了。

  ※ UINT wRemoveMsg:一般來說,它有兩個指,PM_REMOVE或者PM_NOREMOVE。使用前者會在消息被讀取後從隊列中移除,後者是繼續保留。通常,我們選擇前者PM_REMOVE

  真正處理消息時,你需要做兩件事,很簡單,第一件是TranslateMessage(),第二件是DispatchMessage()。它們的原形很相似:

BOOL TranslateMessage(CONST MSG *lpmsg);
LONG DispatchMessage(CONST MSG *lpmsg);


  頭一個是把消息翻譯過來,第二個是從MSG結構中調用相應的信息。你只需要知道這麼多。伴隨着程序主循環的反覆執行,如果有消息出現,你就調用這兩個函數,函數MsgHandler()會安排好一切的。下面是個例子:

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))【現在有時用GetMessage(&msg,NULL,0,0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}


  沒問題,你現在完全可以寫一個窗口程序了。不壞吧?在結束本章前,我還有幾點要提醒你。還記得我們在談論消息時,說要在後面進一步討論它嗎?你忘了?我可沒有忘記。怎樣主動向Windows發送消息呢?

  發送消息

  有兩種辦法可以做到。PostMessage()函數或SendMessage()函數。它們的原形很相似:

BOOL PostMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);

LRESULT SendMessage(
HWND hWnd, // handle of destination window
UINT Msg, // message to post
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);


  它們的參數相同,並且和前面講過的函數MsgHandler()的參數功能相同,就不重複了。我只談談它們之間的區別。

  PostMessage()被經常用來向隊列中加入消息,成功,返回TRUE,否則,返回FALSE。它只是簡單的把消息加入到隊列中,然後返回。多數情況下,調用它將返回TRUE

  SendMessage()則有些不同,它並不是把消息加入到隊列裏,而是直接翻譯消息和調用消息處理,直到消息處理完成後才返回。所以,SendMessage()PostMessage()有更高的應急性。你想立刻乾的事情,就應該調用它。

  消息是DOSWindows編程之間重要的區別標誌。

  程序的流程

  在DOS中,我們不必擔心消息這種東西,不必擔心多個程序同時運行,但在Windows裏,你必須考慮這些。在Windows平臺上編程,有一些不同於DOS下編程的地方。讓我們看看下面這段虛擬的代碼:

// main game loop
do
{
// handle messages here

// ...

// update screen if necessary
if (new_screen)
{
FadeOut();
LoadNewMap();
FadeIn();
}

// perform game logic
WaitForInput();
UpdateCharacters();
RenderMap();

} while (game_active);


  假設FadeOut()函數這樣工作:當函數被調用,在一秒內屏幕圖象暗淡下來,當屏幕完全黑了,函數返回。LoadNewMap()調用一個新的圖象;FadeIn()使屏幕逐漸亮起來,好顯示新圖象。當有鍵子按下,調用WaitForInput()函數,再繼續調用下去。這在DOS遊戲編程裏是合情合理的,但在Windows下不行。

  爲什麼呢?讓我們看看新畫面誕生的過程。畫面逐漸變黑,調用圖片,逐漸恢復。這大概要2秒鐘,用戶可以等待,也可能要移動一下窗口,但程序只專心的幹調用圖片的工作,不會對窗口的移動作出反應。這是很糟糕的,你幹了機器不知道的事情,這可能導致系統崩潰,我們必須要讓機器對用戶的任何操作作出正確的反應。不多說了,總之你要換一換腦筋,如果你從來就沒在DOS下編過程序,那正好,你趕上潮流了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章