ETH官方钱包

創(chuàng)作內(nèi)容

34 GP

【程式】如何建一個視窗—Windows API篇

作者:Shark│2017-09-16 17:39:08│巴幣:3,160│人氣:30781
基本程式語言課程只會教語法,也只寫命令列程式,至於圖形介面程式是怎麼寫出來的,想必很多人有不得其門而入的神秘感。

事實(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/library
CreateWindow的說明
如果安裝過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的,否則會被瀏覽器忽略)
引用網(wǎng)址:http://www.jamesdambrosio.com/TrackBack.php?sn=3724093
All rights reserved. 版權(quán)所有,保留一切權(quán)利

相關(guān)創(chuàng)作

同標(biāo)籤作品搜尋:Windows程式設(shè)計(jì)|Windows API|C語言|程式

留言共 2 篇留言

龍恩
讓我想起以前寫win32的回憶了(dos環(huán)境下不能用mfc....)

09-16 20:35

薩利昂
請教一下學(xué)GUI要先安裝什嗎軟體?

12-30 22:57

Shark
做GUI有很多種程式語言和framework可以用,不只一種方法。

想照這篇用C/C++和Windows API來寫的話,需要C編譯器和Windows SDK。
Windows SDK通常安裝Visual Studio或GCC就會內(nèi)附,可以先照這篇寫個程式試試,如果沒有跳「找不到windows.h」的錯誤就表示有裝了。
C的語法,以及編譯器和IDE要怎麼用就請找其他教學(xué)。12-31 22:32
我要留言提醒:您尚未登入,請先登入再留言

34喜歡★shark0r 可決定是否刪除您的留言,請勿發(fā)表違反站規(guī)文字。

前一篇:我過去的作品:「卡比金剛... 後一篇:這幾天在研究無接縫til...


face基於日前微軟官方表示 Internet Explorer 不再支援新的網(wǎng)路標(biāo)準(zhǔn),可能無法使用新的應(yīng)用程式來呈現(xiàn)網(wǎng)站內(nèi)容,在瀏覽器支援度及網(wǎng)站安全性的雙重考量下,為了讓巴友們有更好的使用體驗(yàn),巴哈姆特即將於 2019年9月2日 停止支援 Internet Explorer 瀏覽器的頁面呈現(xiàn)和功能。
屆時建議您使用下述瀏覽器來瀏覽巴哈姆特:
。Google Chrome(推薦)
。Mozilla Firefox
。Microsoft Edge(Windows10以上的作業(yè)系統(tǒng)版本才可使用)

face我們了解您不想看到廣告的心情? 若您願意支持巴哈姆特永續(xù)經(jīng)營,請將 gamer.com.tw 加入廣告阻擋工具的白名單中,謝謝 !【教學(xué)】