創作內容

34 GP

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

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

事實上建視窗只用標準C語言是做不到的,必須跟作業系統打交道,使用作業系統提供的功能。除了圖形介面以外網路、多媒體、用來操作顯卡的Direct3D和OpenGL也一樣,要先進入作業系統API的大門才能寫出這些多采多姿的程式。

Windows上的作業系統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、型態和常數名稱是標準C沒有的,用全大寫字母表示,型態名稱有的是struct(如WNDCLASS),有的只是基本型態取另一個名字(如WPARAM和LPARAM在32位元環境是32 bit整數,64位元環境是64 bit整數),有興趣的話可以翻相關的header看看它們的本質是什麼。

  一開始的#define UNICODE很重要。Windows 2000、XP以後內部處理文字就都是Unicode了,不過為了跟舊軟體相容還保留了ANSI模式,而很多非英語的程式沒考慮到國際化還是用ANSI製作,在別種語言的環境字會變成亂碼甚至無法執行,應該很多人有開日文軟體時字變成亂碼的經驗,就是這個原因。
  第一行寫了#define UNICODE以後再#include<windows.h>,以後Windows API跟字串有關的函式和struct都會使用Unicode的版本,反之會用ANSI的版本,例如下面的RegisterClass,翻一下winuser.h可以找到RegisterClassA和RegisterClassW。現在是國際化的時代,請養成習慣在#include<windows.h>之前加上這行。
  L"window"的L表示這個字串是wide character,這是C語言標準不是Windows獨有的,在Windows此型態一個字元佔兩byte(其他OS不一定),沒加L就是char型態,一個字元佔1 byte。

之後的WndProc()先不要看,先從main()看起。

  第一步是註冊一個window class(視窗類別),做法是宣告一個WNDCLASS結構,填好裡面的內容再傳入RegisterClass()。
先用ZeroMemory()把每個byte全設為0,再填入需要的內容,至少需要的有5個。
wndclass.style:bit flags,一些開關型屬性,將要開啟的bit用位元or運算。這裡CS_HREDRAW、CS_VREDRAW是指視窗被蓋住或移動時要自動重畫。(CS=class style)
wndclass.lpfnWndProc:處理視窗事件的函式,填上面宣告的WndProc()。
wndclass.hCursor:滑鼠游標,這裡LoadCursor(NULL,????)是載入系統內建游標,如果不填這一項游標移到視窗內會消失(在XP確認,Win10似乎不會這樣)。
wndclass.hbrBackGround:背景顏色,這裡填一個系統定義的顏色,如果不填則視窗內部會變成空的,可看到後面的東西(同樣在XP確認,其他的Windows不一定)。
+1和轉型成HBRUSH是WNDCLASS的規定,可以去看MSDN的說明。
wndclass.lpszClassName:class名稱,等一下CreateWindow要用。
Windows API裡要一次傳入或傳回很多值,常常是宣告一個struct再把它的指標傳入。

  再來叫系統產生一個視窗,宣告一個HWND型態的變數再用CreateWindow macro。CreateWindow有11個參數看起來很討厭,不過這個函式使用率很高,我建議背下來。
1:class name,填剛剛在wndclass.lpszClassName指定的值。
2:window text,作用隨class而異,通常是顯示在螢幕上的文字,這裡我們要建的是頂層視窗,此值會變成視窗標題。
3:bit flags,WS_OVERLAPPED是指有標題和邊框,WS_SYSMENU是標題列有關閉按鈕,WS_VISIBLE當然是指看不看得到了,視窗預設都是隱藏的,除非指定WS_VISIBLE或之後呼叫ShowWindow()設定。(WS=window style)
4~7:X坐標、Y坐標、寬、高。
8:父視窗,這裡是頂層視窗,沒有父視窗所以填NULL。
9:目錄handle,這個視窗沒有目錄所以填NULL。
10:hInstance,這是為了相容95、98、ME的參數,之後版本的Windows可填NULL。
11:額外資料,這裡沒有額外資料故填NULL。

(註:實際存在的函式是CreateWindowEx,CreateWindow是個macro代換成CreateWindowEx。)

  視窗程式是一個迴圈不斷執行「等待→發生事件→處理事件→等待→……」。
GetMessage()讓程式進入等待狀態直到發生事件,它會把事件資訊存在MSG結構裡。
之後用DispatchMessage()呼叫適當callback處理事件,本例的WndProc會由它呼叫。
查MSDN可以看到GetMessage收到WM_QUIT訊息時傳回0,有error時傳回-1,這裡寫成當它傳回0或-1時跳出迴圈。

  再來看處理事件的函式WndProc,前面LRESULT CALLBACK是傳回值型態和calling convention,這個現在不講解,照MSDN的說明填就好。四個參數如下
hwnd:發生事件的視窗
message:一個ID代表事件種類
wparam和lparam:兩個整數額外資訊,意義隨事件種類而異
一般是先判斷message的值再做處理。本例只處理一個WM_DESTROY訊息,點視窗右上角的╳或按Alt+F4關閉視窗會產生這個訊息,裡面的PostQuitMessage()是發出一個WM_QUIT訊息讓程式可以跳出GetMessage迴圈,總合起來意義是「這個視窗被關掉的話就結束程式」。
  視窗還有畫圖、滑鼠點選等等的訊息,我們沒處理的事件就呼叫DefWindowProc()讓系統做預設處理。
其實執行到DefWindowProc()才能在畫面上看到視窗,前面CreateWindow()只是配置記憶體裡的資料並把「畫出視窗」的事件放進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是連結到Windows的視窗系統函式庫user32.dll,本篇提到函式都定義在這裡面。
使用IDE如果跳「無法解析的外部符號 __imp_GetMessageW ……」的錯誤,在連結函式庫的設定加上user32。

成功的話點兩下產生的window.exe就會開一個視窗了。




不過有沒有發現兩件事
1.在CreateWindow裡指定寬高各200,但這是包含邊框的大小,視窗工作區域變得小於200×200
2.執行時會同時出現一個命令列視窗

1用AdjustWindowRect函式
BOOL AdjustWindowRect(LPRECT lpRect,DWORD dwStyle,BOOL bMenu);
//RECT結構內容如下
struct RECT{
  LONG left;
  LONG top;
  LONG right;
  LONG bottom;
};

LPRECT型態前面的LP代表RECT的指標(即RECT*)。
先宣告一個RECT struct,填上需要的工作區大小,傳入這個函式就會傳回包含邊框的大小,之後再丟給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第二參數是window style(=CreateWindow的第三參數),此函式的說明中有寫不能包含WS_OVERLAPPED,所以改用WS_CAPTION。
第三個參數是視窗有沒有目錄,這個視窗沒有目錄所以填0。

2是因為Windows程式有分命令列和視窗模式兩種,沒指定時compiler會把它build成命令列模式,就如此例有個命令列視窗。
要怎麼改成視窗模式呢?GCC要加個參數-mwindows
gcc window.c -o window.exe -Os -s -luser32 -mwindows

Code::Blocks的話,把project設定成GUI application就會自動加上這個參數(自己找一下在哪裡設),可以注意一下IDE呼叫compiler的指令。

VC要把main函式改成如下
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

修改後的結果


加上AdjustWindowRect以後就是視窗程式的基本型,要寫什麼視窗程式都是先寫出這一段再加東西。
#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、該連結哪個library。
入口:http://msdn.microsoft.com/en-us/library
CreateWindow的說明
如果安裝過Visual Studio或Windows SDK可能會在電腦上裝一份,這樣就不用上網查了。

本篇用到很多函式和bit flag,MSDN的使用說明其實都是很長一篇,因為官方文件要把所有可能的情況都寫上去,詳細用法就請自己查。



名詞解釋

bit flags:

複習一下C語言語法,還記不記得or運算有位元or和邏輯or兩種?
or and not
位元 | & ~
邏輯 || && !

如果用一個bit代表一個開或關的屬性,一個32位元整數就可以記錄32個屬性。
舉個例子,CreateWindow第三參數如果要填以下值,用二進位表示是這樣。
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運算收集全部1的位元,就代表開啟這四個開關。
標準C裡很少用到bit flags,但世界上的函式庫很常見這種用法,不止Windows API。

視窗(window):

在Windows API裡視窗不只是有標題列、邊框的那種東西,所有圖形介面元件包括按鈕、下拉式選單、輸入框都是視窗,它們都在畫面上佔有一個矩形區域,產生的方法也都是CreateWindow()。
CreateWindow的第一參數:class name就是告訴系統要產生哪一種元件,除了自己用RegisterClass註冊的以外有很多系統內建的元件,例如按鈕、check box、radio button是L"BUTTON",下拉式選單是L"COMBOBOX"。

控制碼(handle):

代表一個Windows系統底層的物件,程式想下指示給物件就要在函式中把該物件的handle傳入。它跟指標同樣大小,在32位元環境下是32位元整數,64位元環境是64位元。
Windows API裡很多H開頭的型態名稱都是handle,如檔案和thread(HANDLE)、視窗(HWND),還有GDI裡的HFONT、HDC。
概念有點類似物件
//實際存在的函式
ShowWindow(hwnd, SW_MINIMIZE);
//有點像這樣
hwnd->Show(SW_MINIMIZE);
但Windows API要給很多程式語言用,純C語法比較能跟其他語言整合,後者就綁死在C++。



以上就是Windows最基本、最底層的圖形介面API,其他的圖形介面framework像是MFC、Windows Form、跨平台的wxWidgets,都是架在它上面。
遊戲一般是建一個視窗後畫面就全部自己畫,不用作業系統內建的視窗元件,用最直接的方法做事可減少多餘的東西提升效能。

但是用底層API建複雜的UI很煩雜,而且沒有一些方便的功能,例如視窗大小改變時自動改變元件的位置大小。如果想做office、繪圖軟體之類的軟體,使用以上那些整合framework比較不會混亂。

Windows API偉大的地方是這一套系統在Windows 3.1的時候就存在,一直到Windows 10仍然通用,寫Windows程式很少要考慮不同版本間的相容問題,得感謝微軟在向後相容做的努力,但也因此有很多歷史包袱。
相較起來Linux則是技術變更頻繁,寫程式要花很多時間處理相容性問題。

(總算知道怎麼在巴哈姆特打等寬字型了,這樣程式碼比較好看)
(也是現在才知道空白字元有字碼32和字碼160兩種,按空白鍵打出的是32的,但行開頭的空白要用160的,否則會被瀏覽器忽略)
引用網址:https://home.gamer.com.tw/TrackBack.php?sn=3724093
All rights reserved. 版權所有,保留一切權利

相關創作

同標籤作品搜尋:Windows程式設計|Windows API|C語言|程式

留言共 2 篇留言

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

09-16 20:35

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

12-30 22:57

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

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

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

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

追蹤私訊切換新版閱覽

作品資料夾

MoeTako來人R
來一起快樂畫圖喵 好缺人氣阿 想早日進階達人喵~ฅ(°ω°ฅ)看更多我要大聲說11小時前


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

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