Windows 視窗程式設計 (1) 靜宜大學資訊管理學系蔡奇偉副教授 大綱 Windows 視窗系統的特性 Windows API MSDN 線上說明文件 匈牙利 (Hungarian) 命名法 一個最少行的 Windows 視窗程式 Windows 程式的事件處理模型 視窗程式的骨架 1
Windows 視窗系統的特性 圖形化的人機介面 圖形顯示器 視窗 滑鼠 + 鍵盤 Multiprocessing & Multithreading 可同時處理多個程式 一個程式可有多個執行緒 (thread) 事件驅動模式 程式的執行是依據事件而定 Windows API Windows API (Application Program Interface) 是一套微軟公司所提供的函式庫, 專門用來撰寫 Windows 的應用程式 裏面有上千個函式 它們的功能包括 : 繪圖 列印 記憶體管理 網路連接 輸出 / 入裝置的讀寫 檔案的讀寫 建立選單 程式資源管理 等等, 應有盡有 要完全熟悉它們, 可得花上一段長久的歲月 幸好, 寫 Windows 的遊戲程式時, 我們只要知道少部分 的 Windows API 即可 2
MSDN 線上說明文件 MSDN (Microsoft Developer Network) 是微軟公司提供給程式發展者的一套鉅大資料庫, 裏面涵括所有微軟産品的技術文件 說明手冊 API 定義 一些微軟的書籍 以及發展中的程式庫 你可以到 MSDN 網站 (http://msdn.microsoft.com) 直接閱覽或下載這些資料, 也可以安裝 MSDN 光碟片, 在你的電腦上更快速地瀏覽 任何一位 Windows 視窗程式的設計者, 都應該善用 MSDN 裏豐富的資訊, 一方面增強自已的實力, 一方面也可避免無謂的錯誤 3
匈牙利命名法 匈牙利命名法 (Hungarian Notations) 是微軟公司的 Charles Simonyi 先生所提出的一套變數命名的法則 : 把型態的簡稱加在變數名稱之前 比方說 : 假定 value 是一個整數變數, 則取名成 ivalue name 是一個字串變數, 則取名成 strname 等等 微軟公司認為這套準則有益於大型軟體計畫的維護, 所以在其 Windows API 中都採用這套命名法 學習 Windows 程式設計的你, 應該早一點熟悉它 下一頁我們列出常用的型態字首 字首 代表的型態 範例 c char ccode by BYTE (unsigned char) byheader n int ncount i int inumber x, y short ( 存座標值 ) xcoord cx, cy short( 存座標計數值 ) cxoffset b BOOL(int 代表布林值 ) bflag w UINT(unsigned int) wpara l LONG(long) lamount dw DWORD(unsigned long) dwflags 4
字首 代表的型態 範例 fn function fnsort s string sname sz, str C 字串 ( 最後字元為 \0 ) szname lp 32-bit long pointer lpproc h handle hinstance msg message msginfo 註 : 上述非 C 的標準資料型態者 ( 如 BOOL ), 是微軟公司所自定的 舉例來說,BOOL 在 windows.h 中被定義成 : typedef int BOOL; 一個最少行的 Windows 視窗程式 底下是一個號稱最少行的 Windows 視窗程式 : #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <windowsx.h> // main entry point for all windows program int WINAPI WinMain (HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) // call message box API MessageBox(NULL, "What's up, world!", "My first Windows Program", MB_OK); // exit program return 0; 5
執行前一頁的程式, 我們可以看到下面的對話視窗 : 你可以在螢幕上任意移動它的位置 按下 確定 按鈕後, 即可關閉視窗並結束程式的執行 底下我們逐行地剖析前述的程式 不過, 我們先從第 2 行說起 #include <windows.h> 所有的 Windows 視窗程式都必須加入 windows.h 這個系統標頭檔, 因為其中宣告了使用 Winsows API 所需的常數 資料型態 函式原型等等 回到第一行 : #define WIN32_LEAN_AND_MEAN 加入這巨集定義的目的是為了把 windows.h 瘦身, 排除掉一些不常用的宣告, 如此一來, 可以加速編譯的過程, 節省一些程式發展的時間 6
第三行 : #include <windowsx.h> windowsx.h 這個系統標頭檔定義了一些好用的巨集, 可以簡化程式的撰寫, 所以我們把它加進來 第四個指令 : int WINAPI WinMain (HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) 所有的 Windows 視窗程式的執行進入點是 WinMain 這個函式 ( 非 Windows 視窗的程式則是以傳統的 main 函式為執行進入點 ) 由於早期的 Windows API 是用 Pascal 程式語言來撰寫, 而 Pascal 的參數傳遞順序 (calling sequence) 與 C 恰恰相反 因此, 在 C 程式中使用這些 API 函式時, 需要加上 WINAPI 這個宣告, 告訢編譯程式採用 Pascal 的參數傳遞順序 若是自已寫的 C 函式, 就不用加了 Windows 作業系統透過 WinMain 的四個參數, 傳遞一些資訊給你的程式, 這些參數的意義如下 : HINSTANCE hinstance 資料型態 HINSTANCE 是一個 32-bit 的 unsigned int 參數 hinstance 從作業系統接收一個代碼 (handle), 此代碼在作業系統代表這個目前正執行的程式 7
HINSTANCE hprevinstance hprevinstance 是此程式前一個開啟且正執行的代碼 Win32 系統已經不用這一個參數, 而且把它永遠設成 NULL (0) LPSTR lpcmdline LPSTR 是一個指標型態 參數 lpcmdline 是一個指到 C 字串的指標, 用來接收從作業系統傳來的指令行參數 比方說, 若下達的指令行為 : prog a n data.txt 則 lpcmdline 指到的字串值是 a n data.txt, 注意 : 程式名稱 prog 並不在這字串中 int ncmdshow ncmdshow 是此程式開啟時的視窗顯示方式 常見值如下 : SW_HIDE SW_MAXIMIZE SW_MINIMIZE SW_SHOW SW_SHOWNORMAL 隱藏視窗視窗放大至整個螢幕視窗縮至最小顯示並啟動視窗顯示正常大小的視窗並啟動 其他的可能值請參閱 MSDN 線上手冊 8
第五個指令 : MessageBox(NULL, "What's up, world!", "My first Windows Program", MB_OK); MessageBox 這個 Windows API 在螢幕上開啟一個訊息對話視窗 其宣告如下 : int MessageBox ( HWND hwnd, LPCTSTR lptext, LPCTSTR lpcaption, UINT utype ); // handle to owner window // text in message box // message box title // message box style 由於我們沒有建立視窗, 這個 message box 並無父視窗, 所以參數 hwnd 設定成 NULL message box 的顯示樣式和操作模式可以用參數 utype 來設定 比方說, 如果想再加上 Cancel 按鈕和顯示一個警告的圖像 (icon), 你可以改成以下呼叫方式 : MessageBox(NULL, "What's up, world!", "My first Windows Program", MB_OKCANCEL MB_ICONWARNING); utype 詳細的說明, 請參閱 MSDN 線上的手冊 9
用 MessageBox 來檢視變數 偵錯 Windows 程式時, 我們可以用 MessageBox 函式來檢視變數的值 比方說, 我們先寫下面的函式 : int showvalue (hwnd hwin, char *name, int ivalue) char szvalue[20]; sprintf(szvalue, %s = %d, name, ivalue); MessageBox(hwin, szvalue, Debug, MB_OK); 寫好之後, 我們就可以在程式中加入 showvalue 函式來顯示整數變數的值, 譬如 : int seeme = 1234; // 假定 main_window 是目前開啟的視窗 showvalue(main_window, seeme, seeme); 則會在螢幕上顯示出下面的視窗 : 10
視窗程式的事件驅動模型 視窗程式是採用事件驅動的模型, 換句話說, 程式的執行流程是根據事件發生的時間與種類而定 事件可因為使用者的操作而產生, 如滑鼠移動 按下滑鼠鍵 按下鍵盤 選擇功能表的項目 隱藏的視窗被提到幕前 等, 也可由硬體産生, 如主機板上的時脈產生器 (clock), 或由軟體 ( 作業系統或程式本身 ) 產生 作業系統中, 有一個稱之為訊息佇列 (message queue) 的資料結構, 用來儲存事件的訊息 作業系統會負責把這些訊息送到適當的視窗應用程式來處理, 如下一頁的圖片所示 WinMain () 視窗應用程式 Message Queue msg 1 msg 2 msg 3 WinMain () 視窗應用程式 使用者輸入 msg n WinMain () 視窗應用程式 11
每個視窗應用程式也內建一個 local message queue 來儲存所接收到的事件訊息 不過做為一個視窗應用程式設計師的我們, 並不需要了解這些內部結構, 我們只要知道如何擷取與處理這些訊息 這些工作可透過 Windows API 的函式呼叫即可 所有視窗程式的基本架構都大同小異, 長像都近似下一頁所描繪的樣子 WinMain () // code to setup windows msg 1 msg 1 // enter the event loop while (GetMessage) TranslateMessage DispatchMessage msg n WndProc (msg) switch (msg) case MOUSE_DOWN: case KEY_PRESS: case WM_PAINT: 12
在 WinMain 主函式中, 我們必須先設定好視窗物件, 讓視窗開始接收事件訊息, 然後進入所謂的事件迴圈 (event loop) 在事件迴圈中, 我們首先呼叫 GetMessage 來取得下一筆事件的訊息 然後呼叫 TranslateMessage 把訊息轉化成可以進一步處理的格式 最後呼叫 DispatchMessage 把訊息傳到我們寫好的 WndProc 函式來判讀與處理 WndProc 通常只是一個很大的 switch 敘述, 以訊息的種類來分派至適當的程式碼去執行 WndProc 稱為視窗的訊息處理函式 視窗程式的骨架 底下我們來探討視窗程式的基本架構 首先, 視窗的設定包括下面的步驟 : 1. 選擇適當的視窗類別 2. 註冊視窗類別 3. 建立視窗物件 4. 顯示視窗接著我們說明事件迴圈中的 GetMessage TranslateMesssage DispatchMessage 三個 API 以及常見的事件 13
選擇適當的視窗類別 在 WinMain 中, 你要宣告一個如下的變數 : WNDCLASSEX wndclass 然後把適當的值填入 wndclass 這個結構變數, 來選取視窗的類別 附註說明 : 另有一個叫 WNDCLASS 的資料型態, 它已被上述的 WNDCLASSEX 所取代 凡是新的 Windows API, 都用 EX 當作字尾 微軟公司建議新的視窗應用程式應該都採用新版的 Windows API typedef struct _WNDCLASSEX WNDCLASSEX 結構 UINT cbsize; 的定義 UINT style; WNDPROC lpfnwndproc; int cbclsextra; int cbwndextra; HINSTANCE hinstance; HICON hicon; HCURSOR hcursor; HBRUSH hbrbackground; LPCTSTR lpszmenuname; LPCTSTR lpszclassname; HICON hiconsm; WNDCLASSEX, *PWNDCLASSEX; 14
WNDCLASSEX 的欄位說明 UINT cbsize 這欄必須設成 WNDCLASSEX 結構的大小, 即等於 sizeof(wndclassex) UINT style 這欄設定視窗類別的樣式 多重的樣式可用運算子 結合起來 常用的樣式為 :CS_HREDRAW CS_VREDRAW 使得視窗改變水平和垂直大小時, 會重畫視窗的內容 若你希望視窗也處理 double click 事件的話, 可加上 CS_DBLCLKS 這項樣式 (CS: Class Style) WNDPROC lpfnwndproc 這欄是個函式指標 (function pointer), 必須設成前述之訊息處理的函式 int cbclsextra 指定在視窗類別結構後附加的記憶體大小, 這塊記憶體可用來儲存一些視窗類別額外的資訊 通常此欄是設成 0 (cb: count byte) int cbwndextra 指定在視窗物件結構後附加的記憶體大小, 這塊記憶體可用來儲存視窗物件額外的資訊 通常此欄是設成 0 15
HINSTANCE hinstance 這欄設成視窗物件的 handle, 且此視窗必須包含之前 lpfnwndproc 所指定訊息處理函式 HICON hicon 指定視窗類別的 icon handle 此欄必須設成 icon 的 resource 若設成 NULL 的話, 系統會自動選用一個預設的 icon HCURSOR hcursor 指定視窗類別的 cursor handle 此欄必須設成 cursor 的 resource 若設成 NULL 的話, 當游標移入視窗時, 你的程式必須自行設定游標的形狀 HBRUSH hbrbackground; 指定背景筆刷 (background brush) 的 handle 此筆刷用來塗抹視窗的背景圖案或顏色 LPCTSTR lpszmenuname 設為一個 C 型態的字串, 此字串是視窗類別的 menu resource 名稱 若視窗類別沒用到 menu 的話, 可以把此欄設成 NULL LPCTSTR lpszclassname 設為一個 C 型態的字串, 此字串指定視窗類別的名稱 16
HICON hiconsm 指定 small icon 的 handle 若設成 NULL 的話, 系統會用之前 hicon 指定的 icon 來產生 small icon 以下是 wndclass 變數的一個設定範例 : int WINAPI WinMain (HINSTANCE hinstance, HINSTANCE hprevinstance, PSTR szcmdline, int icmdshow) char szappname[] = "HelloWin"; WNDCLASSEX wndclass; wndclass.cbsize = sizeof(wndclassex); wndclass.style = CS_HREDRAW CS_VREDRAW; wndclass.lpfnwndproc = WndProc; wndclass.cbclsextra = 0; wndclass.cbwndextra = 0; wndclass.hinstance = hinstance; wndclass.hicon = LoadImage (0, IDI_APPLICATION, IMAGE_ICON, 0, 0, 0); wndclass.hcursor = LoadImage (0, IDC_ARROW, IMAGE_CURSOR, 0, 0, 0); wndclass.hbrbackground = (HBRUSH) GetStockObject (WHITE_BRUSH); wndclass.lpszmenuname = NULL ; wndclass.lpszclassname = szappname; wndclass.hiconsm = 0; 17
LoadImage The LoadImage function loads an icon, cursor, animated cursor, or bitmap. HANDLE LoadImage ( ); HINSTANCE hinst, LPCTSTR lpszname, UINT utype, int cxdesired, int cydesired, UINT fuload // handle to instance // name or identifier of the image // image type // desired width // desired height // load options 註冊視窗類別 完成視窗類別結構的設定後, 接下來要註冊這個視窗類別 : RegisterClassEx (&wndclass); 18
建立視窗物件 hwnd = CreateWindowEx ( 0, // extended window style szappname, // window class name "The Hello Program", // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hinstance, // program instance handle NULL) ; // creation parameters CreateWindowEx The CreateWindowEx function creates an overlapped, pop-up, or child window with an extended window style; otherwise, this function is identical to the CreateWindow function. HWND CreateWindowEx ( DWORD dwexstyle, LPCTSTR lpclassname, LPCTSTR lpwindowname, DWORD dwstyle, int x, int y, int nwidth, int nheight, HWND hwndparent, window HMENU hmenu, HINSTANCE hinstance, LPVOID lpparam ); // extended window style // registered class name // window name // window style // horizontal position of window // vertical position of window // window width // window height // handle to parent or owner // menu handle or child identifier // handle to application instance // window-creation data 19
顯示視窗 建立視窗物件之後, 我們用下面兩個函式把視窗顯示在螢幕上 : ShowWindow (hwnd, icmdshow) ; UpdateWindow (hwnd) ; ShowWindow 把視窗 hwnd 放到螢幕上, icmdshow 是 WinMain 接收到的視窗顯示方式的參數 UpdateWindow 送 WM_PAINT 訊息到視窗訊息處理函式, 藉此來繪製視窗的內部區域 訊息迴圈 顯示視窗之後, 程式就進入下列的訊息迴圈 : while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; GetMessage 取出下一個訊息的資料並擺入參數 mgs 中 若事件是 WM_QUIT 的話, 函式傳回 0, 而終止迴圈 TranslateMessage 把鍵盤的按鍵碼轉換成字元碼 DispatchMessage 呼叫視窗的訊息處理函式 20
變數 msg 的資料型態為底下的 MSG 結構 : typedef struct tagmsg HWND hwnd; // window handle UINT message; // 訊息型態 WPARAM wparam; // 訊息資料 ( 依訊息型態而定 ) LPARAM lparam; // 訊息資料 ( 依訊息型態而定 ) DWORD time; // 訊息發生的時間 POINT pt; // 發生訊息時游標在螢幕上的位置 MSG; typedef struct tagpoint LONG x; LONG y; POINT, *PPOINT; // x 座標 // y 座標 訊息處理函式 LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) switch (message) case WM_CREATE: // do some initialization when window is created return 0 ; case WM_DESTROY: PostQuitMessage (0); return 0; // handle other messages return DefWindowProc (hwnd, message, wparam, lparam) ; 21
視窗的訊息處理函式必須宣告成以下的格式 : LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) ( 你可改用其他函式名稱, 不一定要用 WndProc 這名稱 ) 你不需要在程式中呼叫 WndProc 函式, 因為 DispatchMessage 函式會自動呼叫它, 而且傳入適當的參數值 在 WndProc 中, 我們用 switch 敘述, 依據訊息的型態 (message), 分別做適當的處理 在 case 敘述中, 我們用 Windows 的訊息常數 這些常數都以 WM_ 開頭 (WM 代表 Windows Message) 訊息常數是定義在 winuser.h 中 (windows.h 會自動加入這個檔 ) WM_CREATE 呼叫 CreateWindowEx 函式會產生這個訊息 你可以在這裏加入視窗初始化的程式碼, 然後 return 0 若初始化失敗, 則可以 return ( 1) 來終止視窗 通常這是程式所收到的第一個訊息 WinMain ( ) CreateWindowEx( ); WndProc ( ) switch (message) case WM_CREATE: // winodw initialization return 0; 22
WM_DESTROY 這個訊息發生在視窗終止時 ( 例如使用者關閉視窗 ) 通常我們在這裏呼叫 PostQuitMessage (0) 送出 WM_QUIT 訊息來終止事件迴圈 while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; WndProc ( ) switch (message) case WM_DESTROY: PostQuitMessage(0); return 0; Windows 有上百個不同的訊息型態 你可以把不需要特別處理的訊息交給 DefWindowProc 函式來處理 所以 WndProc 最後一行的敘述如下 : return DefWindowProc (hwnd, message, wparam, lparam) ; 寫一般簡單的 Windows 程式時, 你的主要工作就是在 WndProc 中加入適當的程式碼來處理種種的訊息 23
視窗程式的骨架 : #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hinstance, HINSTANCE hprevinstance, PSTR szcmdline, int icmdshow) char szappname[] = "HelloWin"; HWND hwnd ; MSG msg ; WNDCLASSEX wndclass ; wndclass.cbsize = sizeof(wndclassex) ; wndclass.style = CS_HREDRAW CS_VREDRAW ; wndclass.lpfnwndproc = WndProc ; wndclass.cbclsextra = 0 ; wndclass.cbwndextra = 0 ; wndclass.hinstance = hinstance ; wndclass.hicon = LoadImage (0, IDI_APPLICATION, IMAGE_ICON, 0, 0, 0); wndclass.hcursor = LoadImage (0, IDC_ARROW, IMAGE_CURSOR, 0, 0, 0); wndclass.hbrbackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszmenuname = NULL ; wndclass.lpszclassname = szappname ; wndclass.hiconsm = 0 ; RegisterClassEx (&wndclass); hwnd = CreateWindowEx ( 0, szappname, // window class name The Hello Program", // window caption WS_OVERLAPPEDWINDOW, // window style CW_USEDEFAULT, // initial x position CW_USEDEFAULT, // initial y position CW_USEDEFAULT, // initial x size CW_USEDEFAULT, // initial y size NULL, // parent window handle NULL, // window menu handle hinstance, // program instance handle NULL) ; // creation parameters 24
ShowWindow (hwnd, icmdshow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) TranslateMessage (&msg) ; DispatchMessage (&msg) ; return msg.wparam ; LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) switch (message) case WM_CREATE: return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; return DefWindowProc (hwnd, message, wparam, lparam) ; 25