基本程式語言課程只會教語法,也只寫命令列程式,至於圖形介面程式是怎麼寫出來的,想必很多人有不得其門而入的神秘感。
事實(shí)上建視窗只用標(biāo)準(zhǔn)C語言是做不到的,必須跟作業(yè)系統(tǒng)打交道,使用作業(yè)系統(tǒng)提供的功能。除了圖形介面以外網(wǎng)路、多媒體、用來操作顯卡的Direct3D和OpenGL也一樣,要先進(jìn)入作業(yè)系統(tǒng)API的大門才能寫出這些多采多姿的程式。
Windows上的作業(yè)系統(tǒng)API就稱為Windows API,本篇介紹用Windows API寫一個最基本的GUI程式。
這也是自製遊戲引擎的第一步,必須先建一個視窗才能做顯示畫面、處理輸入等其他事。
(本篇以前貼在另一個地方,移到這裡並做些修改)
先把程式整個貼出來,再一行行講解。
#define UNICODE #include<windows.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ switch(message){ case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd,message,wparam,lparam); }
int main(){ WNDCLASS wndclass; ZeroMemory(&wndclass, sizeof(WNDCLASS)); wndclass.style=CS_HREDRAW|CS_VREDRAW; wndclass.lpfnWndProc=WndProc; wndclass.hCursor=LoadCursor(NULL,IDC_ARROW); wndclass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1); wndclass.lpszClassName=L"window"; RegisterClass(&wndclass);
HWND window=CreateWindow(L"window", L"title", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE,CW_USEDEFAULT,CW_USEDEFAULT, 200,200,NULL,NULL,NULL,NULL);
MSG msg; int ret; for(;;){ ret=GetMessage(&msg,NULL,0,0); if(ret<=0) break; DispatchMessage(&msg); }
return 0; } |
首先可以看到Windows API裡很多macro、型態(tài)和常數(shù)名稱是標(biāo)準(zhǔn)C沒有的,用全大寫字母表示,型態(tài)名稱有的是struct(如WNDCLASS),有的只是基本型態(tài)取另一個名字(如WPARAM和LPARAM在32位元環(huán)境是32 bit整數(shù),64位元環(huán)境是64 bit整數(shù)),有興趣的話可以翻相關(guān)的header看看它們的本質(zhì)是什麼。
一開始的#define UNICODE很重要。Windows 2000、XP以後內(nèi)部處理文字就都是Unicode了,不過為了跟舊軟體相容還保留了ANSI模式,而很多非英語的程式?jīng)]考慮到國際化還是用ANSI製作,在別種語言的環(huán)境字會變成亂碼甚至無法執(zhí)行,應(yīng)該很多人有開日文軟體時字變成亂碼的經(jīng)驗(yàn),就是這個原因。
第一行寫了#define UNICODE以後再#include<windows.h>,以後Windows API跟字串有關(guān)的函式和struct都會使用Unicode的版本,反之會用ANSI的版本,例如下面的RegisterClass,翻一下winuser.h可以找到RegisterClassA和RegisterClassW。現(xiàn)在是國際化的時代,請養(yǎng)成習(xí)慣在#include<windows.h>之前加上這行。
L"window"的L表示這個字串是wide character,這是C語言標(biāo)準(zhǔn)不是Windows獨(dú)有的,在Windows此型態(tài)一個字元佔(zhàn)兩byte(其他OS不一定),沒加L就是char型態(tài),一個字元佔(zhàn)1 byte。
之後的WndProc()先不要看,先從main()看起。
第一步是註冊一個window class(視窗類別),做法是宣告一個WNDCLASS結(jié)構(gòu),填好裡面的內(nèi)容再傳入RegisterClass()。
先用ZeroMemory()把每個byte全設(shè)為0,再填入需要的內(nèi)容,至少需要的有5個。
wndclass.style:bit flags,一些開關(guān)型屬性,將要開啟的bit用位元or運(yùn)算。這裡CS_HREDRAW、CS_VREDRAW是指視窗被蓋住或移動時要自動重畫。(CS=class style)
wndclass.lpfnWndProc:處理視窗事件的函式,填上面宣告的WndProc()。
wndclass.hCursor:滑鼠游標(biāo),這裡LoadCursor(NULL,????)是載入系統(tǒng)內(nèi)建游標(biāo),如果不填這一項(xiàng)游標(biāo)移到視窗內(nèi)會消失(在XP確認(rèn),Win10似乎不會這樣)。
wndclass.hbrBackGround:背景顏色,這裡填一個系統(tǒng)定義的顏色,如果不填則視窗內(nèi)部會變成空的,可看到後面的東西(同樣在XP確認(rèn),其他的Windows不一定)。
+1和轉(zhuǎn)型成HBRUSH是WNDCLASS的規(guī)定,可以去看MSDN的說明。
wndclass.lpszClassName:class名稱,等一下CreateWindow要用。
Windows API裡要一次傳入或傳回很多值,常常是宣告一個struct再把它的指標(biāo)傳入。
再來叫系統(tǒng)產(chǎn)生一個視窗,宣告一個HWND型態(tài)的變數(shù)再用CreateWindow macro。CreateWindow有11個參數(shù)看起來很討厭,不過這個函式使用率很高,我建議背下來。
1:class name,填剛剛在wndclass.lpszClassName指定的值。
2:window text,作用隨class而異,通常是顯示在螢?zāi)簧系奈淖郑@裡我們要建的是頂層視窗,此值會變成視窗標(biāo)題。
3:bit flags,WS_OVERLAPPED是指有標(biāo)題和邊框,WS_SYSMENU是標(biāo)題列有關(guān)閉按鈕,WS_VISIBLE當(dāng)然是指看不看得到了,視窗預(yù)設(shè)都是隱藏的,除非指定WS_VISIBLE或之後呼叫ShowWindow()設(shè)定。(WS=window style)
4~7:X坐標(biāo)、Y坐標(biāo)、寬、高。
8:父視窗,這裡是頂層視窗,沒有父視窗所以填NULL。
9:目錄handle,這個視窗沒有目錄所以填NULL。
10:hInstance,這是為了相容95、98、ME的參數(shù),之後版本的Windows可填NULL。
11:額外資料,這裡沒有額外資料故填NULL。
(註:實(shí)際存在的函式是CreateWindowEx,CreateWindow是個macro代換成CreateWindowEx。)
視窗程式是一個迴圈不斷執(zhí)行「等待→發(fā)生事件→處理事件→等待→……」。
GetMessage()讓程式進(jìn)入等待狀態(tài)直到發(fā)生事件,它會把事件資訊存在MSG結(jié)構(gòu)裡。
之後用DispatchMessage()呼叫適當(dāng)callback處理事件,本例的WndProc會由它呼叫。
查MSDN可以看到GetMessage收到WM_QUIT訊息時傳回0,有error時傳回-1,這裡寫成當(dāng)它傳回0或-1時跳出迴圈。
再來看處理事件的函式WndProc,前面LRESULT CALLBACK是傳回值型態(tài)和calling convention,這個現(xiàn)在不講解,照MSDN的說明填就好。四個參數(shù)如下
hwnd:發(fā)生事件的視窗
message:一個ID代表事件種類
wparam和lparam:兩個整數(shù)額外資訊,意義隨事件種類而異
一般是先判斷message的值再做處理。本例只處理一個WM_DESTROY訊息,點(diǎn)視窗右上角的╳或按Alt+F4關(guān)閉視窗會產(chǎn)生這個訊息,裡面的PostQuitMessage()是發(fā)出一個WM_QUIT訊息讓程式可以跳出GetMessage迴圈,總合起來意義是「這個視窗被關(guān)掉的話就結(jié)束程式」。
視窗還有畫圖、滑鼠點(diǎn)選等等的訊息,我們沒處理的事件就呼叫DefWindowProc()讓系統(tǒng)做預(yù)設(shè)處理。
其實(shí)執(zhí)行到DefWindowProc()才能在畫面上看到視窗,前面CreateWindow()只是配置記憶體裡的資料並把「畫出視窗」的事件放進(jìn)message queue,直到DefWindowProc()處理事件才真正畫出東西。
命令列的話用這個指令build
GCC: gcc window.c -o window.exe -Os -s -luser32 VC: cl window.c /Fewindow.exe /O2 /MD /link user32.lib
|
user32.lib是連結(jié)到Windows的視窗系統(tǒng)函式庫user32.dll,本篇提到函式都定義在這裡面。
使用IDE如果跳「無法解析的外部符號 __imp_GetMessageW ……」的錯誤,在連結(jié)函式庫的設(shè)定加上user32。
成功的話點(diǎn)兩下產(chǎn)生的window.exe就會開一個視窗了。
不過有沒有發(fā)現(xiàn)兩件事
1.在CreateWindow裡指定寬高各200,但這是包含邊框的大小,視窗工作區(qū)域變得小於200×200
2.執(zhí)行時會同時出現(xiàn)一個命令列視窗
1用AdjustWindowRect函式
BOOL AdjustWindowRect(LPRECT lpRect,DWORD dwStyle,BOOL bMenu); //RECT結(jié)構(gòu)內(nèi)容如下 struct RECT{ LONG left; LONG top; LONG right; LONG bottom; };
|
LPRECT型態(tài)前面的LP代表RECT的指標(biāo)(即RECT*)。
先宣告一個RECT struct,填上需要的工作區(qū)大小,傳入這個函式就會傳回包含邊框的大小,之後再丟給CreateWindow。
RECT rect={0,0,200,200}; AdjustWindowRect(&rect,WS_CAPTION|WS_SYSMENU|WS_VISIBLE,0);
HWND window=CreateWindow(L"window", L"title", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE,CW_USEDEFAULT,CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL);
|
AdjustWindowRect第二參數(shù)是window style(=CreateWindow的第三參數(shù)),此函式的說明中有寫不能包含WS_OVERLAPPED,所以改用WS_CAPTION。
第三個參數(shù)是視窗有沒有目錄,這個視窗沒有目錄所以填0。
2是因?yàn)閃indows程式有分命令列和視窗模式兩種,沒指定時compiler會把它build成命令列模式,就如此例有個命令列視窗。
要怎麼改成視窗模式呢?GCC要加個參數(shù)-mwindows
gcc window.c -o window.exe -Os -s -luser32 -mwindows
|
Code::Blocks的話,把project設(shè)定成GUI application就會自動加上這個參數(shù)(自己找一下在哪裡設(shè)),可以注意一下IDE呼叫compiler的指令。
VC要把main函式改成如下
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
|
修改後的結(jié)果
加上AdjustWindowRect以後就是視窗程式的基本型,要寫什麼視窗程式都是先寫出這一段再加?xùn)|西。
#define UNICODE #include<windows.h>
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ switch(message){ case WM_DESTROY: PostQuitMessage(0); return 0; } return DefWindowProc(hwnd,message,wparam,lparam); }
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow){ WNDCLASS wndclass; ZeroMemory(&wndclass, sizeof(WNDCLASS)); wndclass.style=CS_HREDRAW|CS_VREDRAW; wndclass.lpfnWndProc=WndProc; wndclass.hCursor=LoadCursor(NULL,IDC_ARROW); wndclass.hbrBackground=(HBRUSH)(COLOR_BTNFACE+1); wndclass.lpszClassName=L"window"; RegisterClass(&wndclass);
RECT rect={0,0,200,200}; AdjustWindowRect(&rect,WS_CAPTION|WS_SYSMENU|WS_VISIBLE,0);
HWND window=CreateWindow(L"window", L"title", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE,CW_USEDEFAULT,CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL);
MSG msg; int ret; for(;;){ ret=GetMessage(&msg,NULL,0,0); if(ret<=0) break; DispatchMessage(&msg); }
return 0; } |
最後介紹前面提到很多次的MSDN,這是微軟官方的說明文件,不知道函式或struct該填什麼時可以好好利用,也會寫函式放在哪個header、該連結(jié)哪個library。
入口:
http://msdn.microsoft.com/en-us/libraryCreateWindow的說明如果安裝過Visual Studio或Windows SDK可能會在電腦上裝一份,這樣就不用上網(wǎng)查了。
本篇用到很多函式和bit flag,MSDN的使用說明其實(shí)都是很長一篇,因?yàn)楣俜轿募阉锌赡艿那闆r都寫上去,詳細(xì)用法就請自己查。
名詞解釋
bit flags:
複習(xí)一下C語言語法,還記不記得or運(yùn)算有位元or和邏輯or兩種?
|
or |
and |
not |
位元 |
| |
& |
~ |
邏輯 |
|| |
&& |
! |
如果用一個bit代表一個開或關(guān)的屬性,一個32位元整數(shù)就可以記錄32個屬性。
舉個例子,CreateWindow第三參數(shù)如果要填以下值,用二進(jìn)位表示是這樣。
WS_CAPTION |
00000000 11000000 00000000 00000000 |
WS_SYSMENU |
00000000 00001000 00000000 00000000 |
WS_MINIMIZEBOX |
00000000 00000010 00000000 00000000 |
WS_VISIBLE |
00010000 00000000 00000000 00000000 |
四個值位元or |
00010000 11001010 00000000 00000000 |
用位元or運(yùn)算收集全部1的位元,就代表開啟這四個開關(guān)。
標(biāo)準(zhǔn)C裡很少用到bit flags,但世界上的函式庫很常見這種用法,不止Windows API。
視窗(window):
在Windows API裡視窗不只是有標(biāo)題列、邊框的那種東西,所有圖形介面元件包括按鈕、下拉式選單、輸入框都是視窗,它們都在畫面上佔(zhàn)有一個矩形區(qū)域,產(chǎn)生的方法也都是CreateWindow()。
CreateWindow的第一參數(shù):class name就是告訴系統(tǒng)要產(chǎn)生哪一種元件,除了自己用RegisterClass註冊的以外有很多系統(tǒng)內(nèi)建的元件,例如按鈕、check box、radio button是L"BUTTON",下拉式選單是L"COMBOBOX"。
控制碼(handle):
代表一個Windows系統(tǒng)底層的物件,程式想下指示給物件就要在函式中把該物件的handle傳入。它跟指標(biāo)同樣大小,在32位元環(huán)境下是32位元整數(shù),64位元環(huán)境是64位元。
Windows API裡很多H開頭的型態(tài)名稱都是handle,如檔案和thread(HANDLE)、視窗(HWND),還有GDI裡的HFONT、HDC。
概念有點(diǎn)類似物件
//實(shí)際存在的函式 ShowWindow(hwnd, SW_MINIMIZE); //有點(diǎn)像這樣 hwnd->Show(SW_MINIMIZE);
|
但Windows API要給很多程式語言用,純C語法比較能跟其他語言整合,後者就綁死在C++。
以上就是Windows最基本、最底層的圖形介面API,其他的圖形介面framework像是MFC、Windows Form、跨平臺的wxWidgets,都是架在它上面。
遊戲一般是建一個視窗後畫面就全部自己畫,不用作業(yè)系統(tǒng)內(nèi)建的視窗元件,用最直接的方法做事可減少多餘的東西提升效能。
但是用底層API建複雜的UI很煩雜,而且沒有一些方便的功能,例如視窗大小改變時自動改變元件的位置大小。如果想做office、繪圖軟體之類的軟體,使用以上那些整合framework比較不會混亂。
Windows API偉大的地方是這一套系統(tǒng)在Windows 3.1的時候就存在,一直到Windows 10仍然通用,寫Windows程式很少要考慮不同版本間的相容問題,得感謝微軟在向後相容做的努力,但也因此有很多歷史包袱。
相較起來Linux則是技術(shù)變更頻繁,寫程式要花很多時間處理相容性問題。
(總算知道怎麼在巴哈姆特打等寬字型了,這樣程式碼比較好看)
(也是現(xiàn)在才知道空白字元有字碼32和字碼160兩種,按空白鍵打出的是32的,但行開頭的空白要用160的,否則會被瀏覽器忽略)