註:以下摘自侯捷老師《深入淺出MFC》部分內容,有刪節。原文基於VC5.0,部分之處陳舊但不影響整體。
Windows程式簡述
Windows 程式分為「程式碼」和「UI(User Interface)資源」兩大部份,兩部份最後以連接器整合為一個完整的EXE 檔案。所謂UI 資
源是指功能菜單、對話方塊外貌、程式表徵圖、游標形狀等等東西。這些UI 資源的實際內容(二進位代碼)系藉助各種工具產生,並以各種副檔名
存在,如.ico、.bmp、.cur 等等。程式員必須在一個所謂的資源描述檔(.rc)中描述它們。RC 編譯器(RC.EXE)讀取RC 檔的描述後將所
有UI資源檔集中製作出一個.RES 檔,再與程式碼結合在一起,這才是一個完整的Windows可執行檔。
(.LIB)
眾所周知Windows 支援動態連接。換句話說,應用程式所調用的Windows API 函數是在「執行時期」才連接上的。那麼,「連接時期」所
需的函數庫做什麼用?有哪些?並不是延伸檔名為.dll 者才是動態連接函數庫(DLL,Dynamic Link Library),事實
上.exe、.dll、.fon、.mod、.drv、.ocx 都是所謂的動態連接函數庫。Windows 程式調用的函數可分為C Runtimes 以及Windows API
兩大部分。
以下是它們的命名規則與使用時機:
■ LIBC.LIB - 這是C Runtime 函數庫的靜態連接版本。
■ MSVCRT.LIB - 這是C Runtime 函數庫動態連接版本(MSVCRT40.DLL)的
import 函數庫。如果連接此一函數庫,你的程式執行時必須有MSVCRT40.DLL在場。
另一組函數,Windows API,由作業系統本身(主要是Windows 三大模組GDI32.DLL 和USER32.DLL 和KERNEL32.DLL)提供。雖說動
態連接是在執行時期才發生「連接」事實,但在連接時期,連接器仍需先為調用者(應用程式本身)準備一些適當的資訊,才能夠在執行時期
順利「跳」到DLL 執行。如果該API 所屬之函數庫尚未載入,系統也才因此知道要先行載入該函數庫。這些適當的資訊放在所謂的「import
函數庫」中。32 位Windows 的三大模組所對應的import 函數庫分別為GDI32.LIB 和USER32.LIB和KERNEL32.LIB。
Windows 發展至今,逐漸加上的一些新的API 函數(例如Common Dialog、ToolHelp)並不放在GDI 和USER 和KERNEL 三大模組中,
而是放在諸如COMMDLG.DLL、TOOLHELP.DLL 之中。如果要使用這些APIs,連接時還得加上這些DLLs 所對應的import 函數庫,諸如
COMDLG32.LIB 和TH32.LIB。
可參考:
1.MSVC:關於編譯、連結、裝載、庫相關的一些概念
2.關於形如--error LNK2005: xxx 已經在 msvcrtd.lib ( MSVCR90D.dll ) 中定義--的問題分析解決
(.H)
所有Windows 程式都必須包含WINDOWS.H。除非你十分清楚什麼API 動作需要什麼標頭檔,否則為求便利,單單一個WINDOWS.H
也就是了。不過,WINDOWS.H 只照顧三大模組所提供的API 函數,如果你用到其它system DLLs,例如COMMDLG.DLL 或MAPI.DLL 或
TAPI.DLL 等等,就得包含對應的標頭檔,例如COMMDLG.H 或MAPI.H 或TAPI.H 等等。
事件為驅動,訊息為基礎
Windows 程式的進行系依靠外部發生的事件來驅動。換句話說,程式不斷等待(利用一個while 迴路),等待任何可能的輸入,然後做判
斷,然後再做適當的處理。上述的「輸入」是由作業系統捕捉到之後,以訊息形式(一種資料結構)進入程式之中。作業系統如何捕捉外圍設
備(如鍵盤和滑鼠)所發生的事件呢?噢,USER 模組掌管各個外圍的驅動程式,它們各有偵測迴路。如果把應用程式獲得的各種「輸入」分
類,可以分為由硬體裝置所產生的訊息(如滑鼠移動或鍵盤被按下),放在系統隊列(system queue)中,以及由Windows 系統或其它
Windows 程式傳送過來的訊息,放在程式隊列(application queue)中。以應用程式的眼光來看,訊息就是訊息,來自哪裡或放在哪裡其
實並沒有太大區別,反正程式調用GetMessage API 就取得一個訊息,程式的生命靠它來推動。所有的GUI 系統,包括UNIX的X Window
以及OS/2 的Presentation Manager,都像這樣,是以訊息為基礎的事件驅動系統。
圖 :Windows程式本體與作業系統的關係
可想而知,每一個Windows 程式都應該有一個迴路如下:
MSG msg;
while (GetMessage(&msg, NULL, NULL, NULL)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 以上出現的函數都是Windows API 函數
訊息,也就是上面出現的MSG 結構,其實是Windows 內定的一種資料格式:
/* Queued message structure */
typedef struct tagMSG
{
HWND hwnd;
UINT message; // WM_xxx,例如WM_MOUSEMOVE,WM_SIZE...
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
視窗過程函數
接受並處理訊息的主角就是視窗。每一個視窗都應該有一個函數負責處理訊息,程式員必須負責設計這個所謂的「視窗函數」(window
procedure,或稱為window function)。如果視窗獲得一個訊息,這個視窗函數必須判斷訊息的類別,決定處理的方式。
以上就是Windows 程式設計最重要的觀念。至於視窗的產生與顯示,十分簡單,有專門的API 函數負責。稍後我們就會看到Windows 程式
如何把這訊息的取得、指派、處理動作表現出來。
程式進入點WinMain
main 是一般C 程式的進入點:
int main(int argc, char *argv[ ], char *envp[ ]);
{
...
}
WinMain 則是Windows 程式的進入點:
int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
...
}
在Win32 中CALLBACK 被定義為__stdcall,是一種函數調用習慣,關係到參數擠壓到堆棧的次序,以及處理堆棧的責任歸屬。其它的函數調
用習慣還有 _pascal 和_cdecl。
可參考:
C/C++:函數的編譯方式與呼叫慣例
當Windows 的「外殼」(shell)偵測到使用者意欲執行一個Windows 程式,於是調用載入器把該程式載入,然後調用C startup code,
後者再調用WinMain,開始執進程式。WinMain 的四個參數由作業系統傳遞進來。
視窗類別別之註冊與視窗之誕生
一開始,Windows 程式必須做些初始化工作,為的是產生應用程式的工作舞台:視窗。這沒有什麼困難,因為API 函數CreateWindow 完
全包辦了整個巨大的工程。但是視窗產生之前,其屬性必須先設定好。所謂屬性包括視窗的「外貌」和「行為」,一個視窗的邊框、顏色、標
題、位置等等就是其外貌,而視窗接收訊息後的反應就是其行為(具體地說就是指視窗函數本身)。程式必須在產生視窗之前先利用API 函數
RegisterClass設定屬性(我們稱此動作為註冊視窗類別別)。RegisterClass 需要一個大型資料結構WNDCLASS 做為參數,
CreateWindow 則另需要11 個參數。
圖:RegisterClass與CreateWindow
初始化工作完成後,WinMain 進入所謂的訊息迴圈:
while (GetMessage(&msg,...)) {
TranslateMessage(&msg); // 轉換鍵盤訊息
DispatchMessage(&msg); // 指派訊息
}
其中的TranslateMessage 是為了將鍵盤訊息轉化,DispatchMessage 會將訊息傳給視窗函數去處理。沒有指定函數名稱,卻可以將訊息傳
送過去,豈不是很玄?這是因為訊息發生之時,作業系統已根據當時狀態,為它標明了所屬視窗,而視窗所屬之視窗類別別又已經明白標示了窗
口函數(也就是wc.lpfnWndProc 所指定的函數),所以DispatchMessage自有脈絡可尋。DispatchMessage 經過USER 模組的協助,才
把訊息交到視窗函數手中。
訊息迴圈中的DispatchMessage 把訊息分配到哪裡呢?它透過USER 模組的協助,送到該視窗的視窗函數去了。視窗函數通常利用
switch/case 方式判斷訊息種類,以決定處置方式。由於它是被Windows 系統所調用的(我們並沒有在應用程式任何地方調用此函數),所
以這是一種call back 函數,意思是指「在你的程式中,被Windows 系統調用」的函數。這些函數雖然由你設計,但是永遠不會也不該被你調
用,它們是為Windows 系統準備的。
程式進行過程中,訊息由輸入裝置,經由訊息迴圈的抓取,源源傳送給視窗並進而送到視窗函數去。視窗函數的體積可能很龐大,也可能很精
簡,依該視窗感興趣的訊息數量多寡而定。
至於視窗函數的形式,相當一致,必然是:
LRESULT CALLBACK WndProc(HWND hWnd,
UINT message,
WPARAM wParam,
LPARAM lParam)
注意,不論什麼訊息,都必須被處理,所以switch/case 指令中的default: 處必須調用DefWindowProc,這是Windows 內部預設的訊息處
理函數。
對話方塊
Windows 的對話方塊依其與父視窗的關係,分為兩類:
1. 「令其父視窗除能,直到對話方塊結束」,這種稱為modal 對話方塊。
2. 「父視窗與對話方塊共同運行」,這種稱為modeless 對話方塊。比較常用的是modal 對話方塊。
為了做出一個對話方塊,程式員必須準備兩樣東西:
1. 對話方塊模板(dialog template)。這是在RC 檔案中定義的一個對話方塊外貌,以各種方式決定對話方塊的大小、字形、內部有哪些控制組
件、各在什麼位置...等等。
2. 對話方塊函數(dialog procedure)。其類型非常類似視窗函數,但是它通常只處理WM_INITDIALOG 和WM_COMMAND 兩個訊息。對
話框中的各個控制組件也都是小小視窗,各有自己的視窗函數,它們以訊息與其管理者(父視窗,也就是對話方塊)溝通。而所有的控制組件傳
來的訊息都是WM_COMMAND,再由其參數分辨哪一種控制組件以及哪一種通告(notification)。Modal 對話方塊的啟用與結束,靠的是
DialogBox 和EndDialog 兩個API 函數。
圖:對話方塊的誕生、運行與結束
RC檔案
RC 檔案是一個以文字描述資源的地方。常用的資源有如下:ICON、CURSOR、BITMAP、FONT、DIALOG、MENU、TOOLBAR、
ACCELERATOR、STRING、VERSIONINFO。還可能有新的資源不斷加入。這些文字描述需經過RC 編譯器,才產生可使用的二進位代碼。
總結視窗的生命週期:
1. 程式初始化過程中調用CreateWindow,為程式建立了一個視窗,做為程式的螢幕舞台。CreateWindow 產生視窗之後會送出
WM_CREATE 直接給視窗函數,後者於是可以在此時機做些初始化動作(例如配置記憶體、開檔案、讀初始資料...)。
2. 程式活著的過程中,不斷以GetMessage 從訊息貯列中抓取訊息。如果這個訊息是WM_QUIT,GetMessage 會傳回0 而結束while 循
環,進而結束整個程式。
3. DispatchMessage 透過Windows USER 模組的協助與監督,把訊息指派至視窗函數。訊息將在該處被判別並處理。
4. 程式不斷進行2. 和3. 的動作。
5. 當使用者按下系統功能表中的Close 命令項,系統送出WM_CLOSE。通常程式的視窗函數不欄截此訊息,於是DefWindowProc 處理它。
6. DefWindowProc 收到WM_CLOSE 後, 調用DestroyWindow 把視窗清除。DestroyWindow 本身又會送出WM_DESTROY。
7. 程式對WM_DESTROY 的標準反應是調用PostQuitMessage。
8. PostQuitMessage 沒什麼其它動作,就只送出WM_QUIT 訊息,準備讓訊息迴圈中的GetMessage 取得,如步驟2,結束訊息迴圈。
圖:視窗的生命週期