3. 視窗和訊息

Post date: 2012/3/23 上午 05:35:56

3. 視窗和訊息

在前兩章,程式使用了同一個函式MessageBox來向使用者輸出文字。MessageBox函式會建立一個「視窗」。在Windows中,「視窗」一詞有確切的含義。一個視窗就是螢幕上的一個矩形區域,它接收使用者的輸入並以文字或圖形的格式顯示輸出內容。

MessageBox函式建立一個視窗,但這只是一個功能有限的特殊視窗。訊息視窗有一個帶關閉按鈕的標題列、一個選項圖示、一行或多行文字,以及最多四個按鈕。當然,必須選擇Windows提供給您的圖示與按鈕。

MessageBox函式非常有用,但下面不會過多地使用它。我們不能在訊息方塊中顯示圖形,而且也不能在訊息方塊中添加功能表。要添加這些物件,就需要建立自己的視窗,現在就開始。

自己的視窗

建立視窗很簡單,只需呼叫CreateWindow函式即可。

好啦,雖然建立視窗的函式的確名為CreateWindow,而且您也能在/Platform SDK/User Interface Services/Windowing/Windows/Window Reference/Window Functions找到此文件,但您將發現CreateWindow的第一個參數就是所謂的「視窗類別名稱」,並且該視窗類別連接所謂的「視窗訊息處理程式」。在我們呼叫CreateWindow之前,有一點背景知識會對您大有幫助。

總體結構

進行Windows程式設計,實際上是在進行一種物件導向的程式設計(OOP)。這一點在Windows中使用得最多的物件上表現最為明顯。這種物件正是Windows之所以命名為「Windows」的原因,它具有人格化的特徵,甚至可能會在您的夢中出現,這就是那個叫做「視窗」的東西。

桌面上最明顯的視窗就是應用程式視窗。這些視窗含有顯示程式名稱的標題列、功能表甚至可能還有工具列和捲動列。另一類視窗是對話方塊,它可以有標題列也可以沒有標題列。

裝飾對話方塊表面的還有各式各樣的按鍵、單選按鈕、核取方塊、清單方塊、捲動列和文字輸入區域。其中每一個小的視覺物件都是一個視窗。更確切地說,這些都稱為「子視窗」或「控制項視窗」或「子視窗控制項」。

作為物件,使用者會在螢幕上看到這些視窗,並通過鍵盤和滑鼠直接與它們進行交互操作。更有趣的是,程式寫作者的觀點與使用者的觀點極其類似。視窗以「訊息」的形式接收視窗的輸入,視窗也用訊息與其他視窗通訊。對訊息的理解將是學習如何寫作Windows程式所必須越過的障礙之一。

這有一個Windows的訊息範例:我們知道,大多數的Windows程式都有大小合適的應用程式視窗。也就是說,您能夠通過滑鼠拖動視窗的邊框來改變視窗的大小。通常,程式將通過改變視窗中的內容來回應這種大小的變化。您可能會猜測(並且您也是正確的),是Windows本身而不是應用程式在處理與使用者重新調整視窗大小相關的全部雜亂程式。由於應用程式能改變其顯示的樣子,所以它也「知道」視窗大小改變了。

應用程式是如何知道使用者改變了視窗的大小的呢?由於程式寫作者習慣了往常的文字模式程式,作業系統沒有設置將此類訊息通知給使用者的機制。問題的關鍵在於理解Windows所使用的架構。當使用者改變視窗的大小時,Window給程式發送一個訊息指出新視窗的大小。然後程式就可以調整視窗中的內容,以回應大小的變化。

「Windows給程式發送訊息。」我們希望讀者不要對這句話視而不見。它到底表達了什麼意思呢?我們在這裏討論的是程式碼,而不是一個電子郵件系統。作業系統怎麼給程式發送訊息呢?

其實,所謂「Windows給程式發送訊息」,是指Windows呼叫程式中的一個函式,該函式的參數描述了這個特定訊息。這種位於Windows程式中的函式稱為「視窗訊息處理程式」。

無疑,讀者對程式呼叫作業系統的做法是很熟悉的。例如,程式在打開磁片檔案時就要使用有關的系統呼叫。讀者所不習慣的,可能是作業系統呼叫程式,而這正是Windows物件導向架構的基礎。

程式建立的每一個視窗都有相關的視窗訊息處理程式。這個視窗訊息處理程式是一個函式,既可以在程式中,也可以在動態連結程式庫中。Windows通過呼叫視窗訊息處理程式來給視窗發送訊息。視窗訊息處理程式根據此訊息進行處理,然後將控制傳回給Windows。

更確切地說,視窗通常是在「視窗類別」的基礎上建立的。視窗類別標識了處理視窗訊息的視窗訊息處理程式。使用視窗類別使多個視窗能夠屬於同一個視窗類別,並使用同一個視窗訊息處理程式。例如,所有Windows程式中的所有按鈕均依據同一個視窗類別。這個視窗類別與一個處理所有按鈕訊息的視窗訊息處理程式(位於Windows的動態連結程式庫中)聯結。

在物件導向的程式設計中,物件是程式與資料的組合。視窗是一種物件,其程式是視窗訊息處理程式。資料是視窗訊息處理程式保存的資訊和Windows為每個視窗以及系統中那個視窗類別保存的資訊。

視窗訊息處理程式處理給視窗發送訊息。這些訊息經常是告知視窗,使用者正使用鍵盤或者滑鼠進行輸入。這正是按鍵視窗知道它被「按下」的奧妙所在。在視窗大小改變,或者視窗表面需要重畫時,由其他訊息通知視窗。

Windows程式開始執行後,Windows為該程式建立一個「訊息佇列」。這個訊息佇列用來存放該程式可能建立的各種不同視窗的訊息。程式中有一小段程式碼,叫做「訊息迴圈」,用來從佇列中取出訊息,並且將它們發送給相應的視窗訊息處理程式。有些訊息直接發送給視窗訊息處理程式,不用放入訊息佇列中。

如果您對這段Windows架構過於簡略的描述將信將疑,就讓我們去看看在實際的程式中,視窗、視窗類別、視窗訊息處理程式、訊息佇列、訊息迴圈和視窗訊息是如何相互配合的。這或許會對您有些幫助。

HELLOWIN程式

建立一個視窗首先需要註冊一個視窗類別,那需要一個視窗訊息處理程式來處理視窗訊息。處理視窗訊息對每個Windows程式都帶來了些負擔。程式3-1所示的HELLOWIN程式中整個做的事情差不多就是料理這些事情。

程式3-1 HELLOWIN HELLOWIN.C /*------------------------------------------------------------------------ HELLOWIN.C -- Displays "Hello, Windows 98!" in client area (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("HelloWin") ; HWND hwnd ; MSG msg ; WNDCLAS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuNam = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow( szAppName, // window class name TEXT ("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 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) { HDC hdc ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_CREATE: PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ; return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; GetClientRect (hwnd, &rect) ; DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

程式建立一個普通的應用程式視窗,如圖3-1所示。在視窗顯示區域的中央顯示「Hello, Windows 98!」。如果安裝了音效卡,那麼您還可以聽到相應的朗讀聲音。

提醒您注意:如果您使用Microsoft Visual C++ 為此程式建立新專案,那麼您得加上連結程式所需的程式庫檔案。從 Project 功能表選擇 Setting 選項,然後選取 Link 頁面標籤。從 Category 清單方塊中選擇 General ,然後在 Object/Library Modules 文字方塊添加 WINMM.LIB Windows multimedia Windows多媒體 )。您這樣做是因為HELLOWIN將使用多媒體功能呼叫,而內定的專案中又不包括多媒體程式庫檔案。不然連結程式報告了錯誤資訊,表明PlaySound函式不可用。

HELLOWIN將存取檔案HELLOWIN.WAV,該檔案在本書所附光碟的HELLOWIN目錄中。執行HELLOWIN.EXE時,內定的目錄必須是HELLOWIN。在Visual C++中執行此程式時,雖然執行檔會產生在HELLOWIN的RELEASE或DEBUG子目錄中,但執行程式的目錄還是必須在HELLOWIN中。

通盤考量

實際上,每一個Windows程式碼中都包括HELLOWIN.C程式的大部分。沒人能真正記住此程式的全部寫法;通常,Windows程式寫作者在開始寫一個新程式時總是會複製一個現有的程式,然後再做相應的修改。您可以按此習慣自由使用本書附帶光碟中的程式。

上面提到,HELLOWIN將在其視窗的中央顯示字串。這種說法不是完全正確的。文字實際顯示在程式顯示區域的中央,它在圖3-1中是標題列和邊界範圍內的大片白色區域。這區別對我們來說很重要;顯示區域就是程式自由繪圖並且向使用者顯示輸出結果的視窗區域。

如果您認真思考一下,將會發現雖然只有80行程式碼,這個視窗卻令人驚訝地具有許多功能。您可以用滑鼠按住標題列,在螢幕上移動視窗;可以按住大小邊框,改變視窗的大小。在視窗大小改變時,程式自動地將「Hello, Windows 98!」字串重新定位在顯示區域的中央。您可以按最大化按鈕,放大HELLOWIN以充滿整個螢幕;也可以按最小化按鈕,將程式縮小成一個圖示。您可以在系統功能表中執行所有選項(就是按下在標題列最左端的小圖示);也可以從系統功能表中選擇 Close 選項,或者單擊標題列最右端的關閉按鈕,或者雙擊標題列最左端的圖示,來關閉視窗以終止程式的執行。

我們將在本章的餘下部分對此程式作一詳細的檢查。當然,我們首先要從整體上看一下。

與前兩章中的範例程式一樣,HELLOWIN.C也有一個WinMain函式,但它還有另外一個函式,名為WndProc。這就是視窗訊息處理程式。注意,在HELLOWIN.C中沒有呼叫WndProc的程式碼。當然,在WinMain中有對WndProc的參考,而這就是該函式要在程式開頭附近宣告的原因。

Windows函式呼叫

HELLOWIN至少呼叫了18個Windows函式。下面以它們在HELLOWIN中出現的次序列出這些函式以及各自的簡明描述:

圖3-1 HELLOWIN視窗

  • LoadIcon 載入圖示供程式使用。
  • LoadCursor 載入滑鼠游標供程式使用。
  • GetStockObject 取得一個圖形物件(在這個例子中,是取得繪製視窗背景的畫刷物件)。
  • RegisterClass 為程式視窗註冊視窗類別。
  • MessageBox 顯示訊息方塊。
  • CreateWindow 根據視窗類別建立一個視窗。
  • ShowWindow 在螢幕上顯示視窗。
  • UpdateWindow 指示視窗自我更新。
  • GetMessage 從訊息佇列中取得訊息。
  • TranslateMessage 轉譯某些鍵盤訊息。
  • DispatchMessage 將訊息發送給視窗訊息處理程式。
  • PlaySound 播放一個音效檔案。
  • BeginPaint 開始繪製視窗。
  • GetClientRect 取得視窗顯示區域的大小。
  • DrawText 顯示字串。
  • EndPaint 結束繪製視窗。
  • PostQuitMessage 在訊息佇列中插入一個「退出程式」訊息。
  • DefWindowProc 執行內定的訊息處理。

這些函式均在Platform SDK文件中說明,並在不同的表頭檔案中宣告,其中絕大多數宣告在WINUSER.H中。

大寫字母識別字

讀者可能注意到,HELLOWIN.C中有幾個大寫的識別字,這些識別字是在Windows表頭檔案中定義的。有些識別字含有兩個字母或者三個字母的字首,這些字首後頭接著一個底線:

這些是簡單的數值常數。字首指示該常數所屬的類別,如表3-1所示。

表3-1

奉勸程式寫作者不要費力氣去記憶Windows程式設計中的數值常數。實際上,Windows中使用的每個數值常數在表頭檔案中均有相應的識別字定義。

新的資料型態

HELLOWIN.C中的其他識別字是新的資料型態,也在Windows表頭檔案中使用typedef敘述或者#define敘述加以定義了。最初是為了便於將Windows程式從原來的16位元系統上移植到未來的使用32位元(或者其他)技術的作業系統上。這種作法並不如當時每個人想像的那樣順利,但是這種概念基本上是正確的。

有時這些新的資料型態只是為了方便縮寫。例如,用於WndProc的第二個參數的UINT資料型態只是一個unsigned int (無正負號整數),在Windows 98中,這是一個32位元的值。用於WinMain的第三個參數的PSTR資料型態是指向一個字串的指標,即是一個char *。

其他資料型態的含義不太明顯。例如,WndProc的第三和第四個參數分別被定義為WPARAM和LPARAM,這些名字的來源有點歷史背景:當Windows還是16位元系統時,WndProc的第三個參數被定義為一個WORD,這是一個16位元的 無正負號短 (unsigned short)整數,而第四個參數被定義為一個LONG,這是一個32位元有正負號長整數,從而導致了文字「PARAM」前面加上了前置字首「W」和「L」。當然,在32位元的Windows中,WPARAM被定義為一個UINT,而LPARAM被定義為一個LONG(這就是C中的long整數型態),因此視窗訊息處理程式的這兩個參數都是32位元的值。這也許有點奇怪,因為WORD資料型態在Windows98中仍然被定義為一種16位元的 無正負號 整數,因此「PARAM」前的「W」就有點誤用了。

WndProc函式傳回一個型態為LRESULT的值,該值簡單地被定義為一個LONG。WinMain函式被指定了一個WINAPI型態(在表頭檔案中定義的所有Windows函式都被指定這種型態),而WndProc函式被指定一個CALLBACK型態。這兩個識別字都被定義為_stdcall,表示在Windows本身和使用者的應用程式之間發生的函式呼叫的呼叫參數傳遞方式。

HELLOWIN還使用了Windows表頭檔案中定義的四種資料結構(我們將在本章稍後加以討論)。這些資料結構如表3-2所示。

表3-2

前面兩個資料結構在WinMain中使用,分別定義了兩個名為msg和wndclass的結構,後面兩個資料結構在WndProc中使用,分別定義了ps和rect結構。

代號簡介

最後,還有三個大寫識別字(見表3-3),用於不同型態的「代號」:

表3-3

代號在Windows中使用非常頻繁。在本章結束之前,我們將遇到HICON(圖示代號)、HCURSOR(滑鼠游標代號)和HBRUSH(畫刷代號)。

代號是一個(通常為32位元的)整數,它代表一個物件。Windows中的代號類似傳統C或者MS-DOS程式設計中使用的檔案代號。程式幾乎總是通過呼叫Windows函式取得代號。程式在其他Windows函式中使用這個代號,以使用它代表的物件。代號的實際值對程式來說是無關緊要的。但是,向您的程式提供代號的Windows模組知道如何利用它來使用相對應的物件。

匈牙利表示法

讀者可能注意到,HELLOWIN.C中有一些變數的名字顯得很古怪。如szCmdLine,它是傳遞給WinMain的參數。

許多Windows程式寫作者使用一種叫做「匈牙利表示法」的變數命名通則。這是為了紀念傳奇性的Microsoft程式寫作者Charles Simonyi。非常簡單,變數名以一個或者多個小寫字母開始,這些字母表示變數的資料型態。例如,szCmdLine中的sz代表「以0結尾的字串」。在hInstance和hPrevInstance中的h字首表示「代號」;在iCmdShow中的i字首表示「整數」。 WndProc的後兩個參數也使用匈牙利表示法。正如我在前面已經解釋過的,儘管wParam應該更適當地被命名為uiParam(代表「無正負號整數」),但是因為這兩個參數是使用資料型態WPARAM和LPARAM定義的,因此保留它們傳統的名字。

在命名結構變數時,可以用結構名(或者結構名的一種縮寫)的小寫作為變數名的字首,或者用作整個變數名。例如,在HELLOWIN. C的WinMain函式中,msg變數是MSG型態的結構;wndclass是WNDCLASSEX型態的一個結構。在WndPmc函式中,ps是一個PAINTSTRUCT結構,rect是一個RECT結構。

匈牙利表示法能夠幫助程式寫作者及早發現並避免程式中的錯誤。由於變數名既描述了變數的作用,又描述了其資料型態,就比較容易避免產生資料型態不合的錯誤。

表3-4列出了在本書中經常用到的變數字首。

表3-4

註冊視窗類別

視窗依照某一視窗類別建立,視窗類別用以標識處理視窗訊息的視窗訊息處理程式。

不同視窗可以依照同一種視窗類別建立。例如,Windows中的所有按鈕視窗-包括按鍵、核取方塊,以及單選按鈕-都是依據同一種視窗類別建立的。視窗類別定義了視窗訊息處理程式和依據此類別建立的視窗的其他特徵。在建立視窗時,要定義一些該視窗所獨有的特徵。

在為程式建立視窗之前,必須首先呼叫RegisterClass註冊一個視窗類別。該函式只需要一個參數,即一個指向型態為WNDCLASS的結構指標。此結構包括兩個指向字串的欄位,因此結構在WINUSER.H表頭檔案中定義了兩種不同的方式,第一個是ASCII版的WNDCLASSA:

typedef struct tagWNDCLASSA { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCSTR lpszMenuName ; LPCSTR lpszClassName ; } WNDCLASSA, * PWNDCLASSA, NEAR * NPWNDCLASSA, FAR * LPWNDCLASSA ;

在這裏提示一下資料型態和匈牙利表示法:其中的lpfn字首代表「指向函式的長指標」。(在Win32 API中,長指標和短指標(或者近程指標)沒有區別。這只是16位元Windows的遺物。)cb字首代表「位元組數」而且通常作為一個常數來表示一個位元組的大小。h字首是一個代號,而hbr字首代表「一個畫刷的代號」。lpsz字首代表「指向以0結尾字串的指標」。

Unicode版的結構定義如下:

typedef struct tagWNDCLASSW { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCWSTR lpszMenuName ; LPCWSTR lpszClassName ; } WNDCLASSW, * PWNDCLASSW, NEAR * NPWNDCLASSW, FAR * LPWNDCLASSW ;

與前者唯一的區別在於最後兩個欄位定義為指向寬字串常數,而不是指向ASCII字串常數。

WINUSER.H定義了WNDCLASSA和WNDCLASSW結構(以及指向結構的指標)以後,表頭檔案依據對UNICODE識別字的解釋,定義了WNDCLASS和指向WNDCLASS的指標(包括一些向後相容的程式碼):

#ifdef UNICODE typedef WNDCLASSW WNDCLASS ; typedef PWNDCLASSW PWNDCLASS ; typedef NPWNDCLASSW NPWNDCLASS ; typedef LPWNDCLASSW LPWNDCLASS ; #else typedef WNDCLASSA WNDCLASS ; typedef PWNDCLASSA PWNDCLASS ; typedef NPWNDCLASSA NPWNDCLASS ; typedef LPWNDCLASSA LPWNDCLASS ; #endif

本書後面列出結構時,將只列出功用相同的結構定義,對WNDCLASS就像這樣:

typedef struct { UINT style ; WNDPROC lpfnWndProc ; int cbClsExtra ; int cbWndExtra ; HINSTANCE hInstance ; HICON hIcon ; HCURSOR hCursor ; HBRUSH hbrBackground ; LPCTSTR lpszMenuName ; LPCTSTR lpszClassName ; } WNDCLASS, * PWNDCLASS ;

我也不再著重說明指標的定義。一個程式寫作者的程式不應該因為使用以LP或NP為字首的不同指標型態而被攪亂。

在WinMain中為WNDCLASS定義一個結構,通常像這樣:

然後,你就可以初始化該結構的10個欄位,並呼叫RegisterClass。

在WNDCLASS結構中最重要的兩個欄位是第二個和最後一個,第二個欄位(lpfnWndProc) 是依據這個類別來建立的所有視窗所使用的視窗訊息處理程式的位址。在HELLOWIN.C中,這個是WndProc函式。最後一個欄位是視窗類別的文字名稱。程式寫作者可以隨意定義其名稱。在只建立一個視窗的程式中,視窗類別名稱通常設定為程式名稱。

其他欄位依照下面的方法描述了視窗類別的一些特徵。讓我們依次看看WNDCLASS結構中的每個欄位。

敘述

使用C的位元「或」運算子結合了兩個「視窗類別樣式」識別字。在表頭檔案WINUSER.H中,已定義了一整組以CS為字首的識別字:

WNDCLASS wndclass ;

wndclass.style = CS_HREDRAW | CS_VREDRAW ;

#define CS_VREDRAW 0x0001 #define CS_HREDRAW 0x0002 #define CS_KEYCVTWINDOW 0x0004 #define CS_DBLCLKS 0x0008 #define CS_OWNDC 0x0020 #define CS_CLASSDC 0x0040 #define CS_PARENTDC 0x0080 #define CS_NOKEYCVT 0x0100 #define CS_NOCLOSE 0x0200 #define CS_SAVEBITS 0x0800 #define CS_BYTEALIGNCLIENT 0x1000 #define CS_BYTEALIGNWINDOW 0x2000 #define CS_GLOBALCLASS 0x4000 #define CS_IME 0x00010000

由於每個識別字都可以在一個複合值中設置一個位元的值,所以按這種方式定義的識別字通常稱為「位元旗標」。通常我們只使用少數的視窗類別樣式。HELLOWIN中用到的這兩個識別字表示,所有依據此類別建立的視窗,每當視窗的水平方向大小(CS_HREDRAW)或者垂直方向大小(CS_VREDRAW)改變之後,視窗要完全重畫。改變HELLOWIN的視窗大小,可以看到字串仍然顯示在視窗的中央,這兩個識別字確保了這一點。不久我們就將看到視窗訊息處理程式是如何得知這種視窗大小的變化的。

WNDCLASS結構的第二個欄位由以下敘述進行初始化:

這條敘述將這個視窗類別的視窗訊息處理程式設定為WndProc,即HELLOWIN.C中的第二個函式。這個過程將處理依據這個視窗類別建立的所有視窗的全部訊息。在C語言中,像這樣在結構中使用函式名時,真正提供的是指向函式的指標。

下面兩個欄位用於在視窗類別結構和Windows內部保存的視窗結構中預留一些額外空間:

程式可以根據需要來使用預留的空間。HELLOWIN沒有使用它們,所以設定值為0。否則,和匈牙利表示法所指示的一樣,這個欄位將被當成「預留的位元組數」。(在 第七章的程式CHECKER3 將使用cbWndExtra欄位。)

下一個欄位就是程式的執行實體代號(它也是WinMain的參數之一):

敘述

為所有依據這個視窗類別建立的視窗設置一個圖示。圖示是一個小的點陣圖圖像,它對使用者代表程式,將出現在Windows工作列中和視窗的標題列的左端。在本書的後面,您將學習如何為您的Windows程式自訂圖示。現在,為了方便起見,我們將使用預先定義的圖示。

要取得預先定義圖示的代號,可以將第一個參數設定為NULL來呼叫LoadIcon。在載入程式寫作者自訂的圖示時(圖示應該存放在磁片上的.EXE程式檔案中),這個參數應該被設定為程式的執行實體代號hInstance。第二個參數代表圖示。對於預先定義圖示,此參數是以IDI開始的識別字(「ID代表圖示」),識別字在WINUSER.H中定義。IDI_APPLICATION圖示是一個簡單的視窗小圖形。LoadIcon函式傳回該圖示的代號。我們並不關心這個代號的實際值,它只用於設置hIcon欄位元的值。該欄位在WNDCLASS結構中定義為HICON型態,此型態名的含義為「handle to an icon(圖示代號)」。

敘述

與前一條敘述非常相似。LoadCursor函式載入一個預先定義的滑鼠游標(命名為IDC_ARROW),並傳回該游標的代號。該代號被設定給WNDCLASS結構的hCursor欄位。當滑鼠游標在依據這個類別建立的視窗的顯示區域上出現時,它變成一個小箭頭。

下一個欄位指定依據這個類別建立的視窗背景顏色。hbrBackground欄位名稱中的hbr字首代表「handle to a brush(畫刷代號)」。畫刷是個繪圖詞彙,指用來填充一個區域的著色樣式。Windows有幾個標準畫刷,也稱為「備用(stock)」畫刷。這裏所示的GetStockObject呼叫將傳回一個白色畫刷的代號:

這意味著視窗顯示區域的背景完全為白色,這是一種極其普遍的做法。

下一個欄位指定視窗類別功能表。HElLOWIN沒有應用程式功能表,所以該欄位被設定為NULL:

最後,必須給出一個類別名稱。對於小程式,類別名稱可以與程式名相同,即存放在szAppName變數中的「HelloWin」字串。

至於該字串由ASCII字元組成或由Unicode字元組成,取決於是否定義了UNICODE識別字。

在初始化該結構的10個欄位後,HELLOWIN呼叫RegisterClass來註冊這個視窗類別。該函式只有一個參數,即指向WNDCLASS結構的指標。實際上,RegisterClassA函式將獲得一個指向WNDCLASSA結構的指標,而RegisterClassW函式將獲得一個指向WNDCLASSW結構的指標。程式要使用哪個函式來註冊視窗類別,取決於發送給視窗的訊息包含ASCII文字還是Unicode文字。

現在有一個問題:如果用定義的UNICODE識別字編譯了程式,程式將呼叫RegisterClassW。該程式可以在Microsoft Windows NT中執行良好。但如果此程式在Windows 98上執行,RegisterClassW函式並未真地被執行到。函式有一個進入點,但函式呼叫後只傳回0,表明錯誤。對於在Windows 98下執行的Unicode程式來說,這是一個通知使用者有問題並終止執行的好機會。這是本書中多數程式處理RegisterClass函式呼叫的方法:

wndclass.lpfnWndProc = WndProc ;

wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ;

wndclass.hInstance = hInstance ;

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;

wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ;

wndclass.lpszMenuName = NULL ;

wndclass.lpszClassName = szAppName ;

if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; }

由於MessageBoxW是可在Windows 98環境下執行的幾個Unicode函式之一,所以其執行正常。

當然,這段程式假定RegisterClass不會因為其他原因而呼叫失敗,諸如WNDCLASS結構中lpfnWndProc欄位被設定成NULL之類的錯誤。GetLastError函式會幫助您確定在這樣的情況下產生錯誤的原因。GetLastError是Windows中常用的函式,它可以在函式呼叫失敗時獲得更多錯誤資訊。不同函式的文件將指出您是否能夠用GetLastError來獲得這些資訊。在Windows 98中呼叫RegisterClassW時,GetLastError將傳回120。在WINERROR.H中您可以看到,值120與識別字ERROR_CALL_NOT_IMPLEMENTED相等。您也可以在/Platform SDK/Windows Base Services/Debugging and Error Handling/Error Codes/System Errors - Numerical Order查看錯誤。

一些Windows程式寫作者喜歡檢查所有可能發生錯誤的函式呼叫的傳回值。這麼做確實有點道理,相信您也非常習慣在配置記憶體後檢查錯誤。而許多Windows函式需要配置記憶體。例如,RegisterClass需要配置記憶體,以保存視窗類別的資訊。如此一來,您就應該要檢查這個函式的執行結果。另一方面說來,如果由於RegisterClass不能得到所需要的記憶體,它會宣告呼叫失敗,而Windows大概也快當掉了。

在本書的範例程式中,我做了最少的錯誤檢查。這不是因為我認為錯誤檢查不是一個好方法,而是因為這會讓我們在程式舉例中分心。

最後,一個老經驗是:在一些Windows範例程式中,您可能在WinMain中看到以下程式碼:

if (!hPrevInstance) { wndclass.cbStyle = CS_HREDRAW | CS_VREDRAW ; 初始化其他 wndclass RegisterClass (&wndclass) ; }

這是出於「舊習難改」的原因。在16位元的Windows中,如果您啟動正在執行的程式的一個新執行實體,WinMain的hPrevInstance參數將是前一個執行實體的執行實體代號。為節省記憶體,兩個或多個執行實體就可能會共用相同的視窗類別。這樣,視窗類別就只在hPrevInstance是NULL的時候才註冊,這表明程式沒有其他執行實體。

在32位元的Windows中,hPrevInstance總是NULL。此程式碼會正常執行,而實際上也沒必要檢查hPrevInstance。

建立視窗

視窗類別定義了視窗的一般特徵,因此可以使用同一視窗類別建立許多不同的視窗。實際呼叫CreateWindow建立視窗時,可能指定有關視窗的更詳細的資訊。

Windows程式設計新手有時會混淆視窗類別和視窗之間的區別,以及為什麼一個視窗的所有特徵不能被一次設定好。實際上,以這種方式分開這些樣式資訊是非常方便的。例如,所有的按鈕視窗都可以依據同樣的視窗類別來建立,與這個視窗類別相關的視窗訊息處理程式位於Windows內部。由視窗類別來負責處理按鈕的鍵盤和滑鼠輸入,並定義按鈕在螢幕上的外觀形象。從這一點看來,所有的按鈕都是以同樣的方式工作的。但是並非所有的按鈕都是一樣的。它們可以有不同的大小,不同的螢幕位置,以及不同的字串。後面的這樣一些特徵是視窗定義的一部分,而不是視窗類別定義的。

傳遞給RegisterClass函式的資訊會在一個資料結構中設定好,而傳遞給CreateWindow函式的資訊會在函式單獨的參數中設定好。下面是HELLOWIN.C中的CreateWindows呼叫,每一個欄位都做了完整的說明:

hwnd = CreateWindow (szAppName, // window class name TEXT ( "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

在這裡,我不想提實際上有CreateWindowA函式和CreateWindowW函式,兩個函式分別將前兩個參數當成ASCII或者Unicode字串來處理。

標記為「window class name」的參數是szAppName,它含有字串「HelloWin」-這是程式註冊的視窗類別名稱。這就是我們建立的視窗聯結視窗類別的方式。

此程式建立的視窗是一個普通的重疊式視窗。它含有一個標題列,標題列左邊有一個系統功能表按鈕,標題列右邊有縮小、放大和關閉圖示,四周還有一個表示視窗大小的邊框。這是標準樣式的視窗,名為WS_OVERLAPPEDWINDOW,出現在CreateWindow的「視窗樣式」參數中。如果看一下WINUSER.H,您將會發現此樣式是幾種位元旗標的組合:

#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \ WS_CAPTION | \ WS_SYSMENU | \ WS_THICKFRAME | \ WS_MINIMIZEBOX | \ WS_MAXIMIZEBOX)

「視窗標題」是顯示在標題列中的文字。

注釋著「initial x position」和「initial y position」的參數指定了視窗左上角相對於螢幕左上角的初始位置。由於這些參數使用CW_USEDEFAULT識別字,指示Windows使用重疊視窗的內定位置。(CW_USEDEFAULT定義為0x80000000。)內定情況下,Windows依次對新建立的視窗定位,使各視窗左上角的垂直和水平距離在螢幕上按一定的大小遞增。與此類似,注釋著「initial x size」和「initial y size」的參數分別指定視窗的寬度和高度。同樣使用了CW_USEDEFAULT識別字,表明希望Windows使用內定尺寸。

在建立一個「最上層」視窗,如應用程式視窗時,注釋為「父視窗代號」的參數設定為NULL。通常,如果視窗之間存在有父子關係,則子視窗總是出現在父視窗的上面。應用程式視窗出現在桌面視窗的上面,但不必為呼叫CreateWindow而找出桌面視窗的代號。

因為視窗沒有功能表,所以「視窗功能表代號」也設定為NULL。「程式執行實體代號」設定為執行實體代號,它是作為WinMain的參數傳遞給這個程式的。最後,「建立參數」指標設定為NULL,可以用這個參數存取稍後程式中可能引用到的資料。

CreateWindow傳回被建立的視窗的代號,該代號存放在變數hwnd中,後者被定義為HWND型態(「視窗代號型態」)。Windows中的每個視窗都有一個代號,程式用代號來使用視窗。許多Windows函式需要使用hwnd作為參數,這樣,Windows才能知道函式是針對哪個視窗的。如果一個程式建立了許多視窗,則每個視窗均有一個代號。視窗代號是Windows程式所處理最重要的代號之一。

顯示視窗

在CreateWindow呼叫傳回之後,Windows內部已經建立了這個視窗。這就是說,Windows已經配置了一塊記憶體,用來保存在CreateWindow呼叫中指定視窗的全部資訊跟一些其他資訊,而Windows稍後就是依據視窗代號找到這些資訊的。

然而,光是這樣子,視窗並不會出現在視訊顯示器上。您還需要兩個函式呼叫,一個是:

第一個參數是剛剛用CreateWindow建立的視窗代號。第二個參數是作為參數傳給WinMain的iCmdShow。它確定最初如何在螢幕上顯示視窗,是一般大小、最小化還是最大化。在開始功能表中安裝程式時,使用者可能做出最佳選擇。如果視窗按一般大小顯示,那麼WinMain接收到後傳遞給ShowWindow的就是SW_SHOWNORMAL﹔如果視窗是最大化顯示的,則為SW_SHOWMAXIMIZED。而如果視窗只顯示在工作列上,則是SW_SHOWMINNOACTIVE。

ShowWindow函式在顯示器上顯示視窗。如果ShowWindow的第二個參數是SW_SHOWNORMAL,則視窗的顯示區域就會被視窗類別中定義的背景畫刷所覆蓋。函式呼叫

會重畫顯示區域。它經由發送給視窗訊息處理程式(即HELLOWIN.C中的WndProc函式)一個WM_PAINT訊息做到這一點。後面,我們將說明WndProc如何處理這個訊息。

訊息迴圈

呼叫UpdateWindow之後,視窗就出現在視訊顯示器上。程式現在必須準備讀入使用者用鍵盤和滑鼠輸入的資料。Windows為當前執行的每個Windows程式維護一個「訊息佇列」。在發生輸入事件之後,Windows將事件轉換為一個「訊息」並將訊息放入程式的訊息佇列中。

程式通過執行一塊稱之為「訊息迴圈」的程式碼從訊息佇列中取出訊息:

ShowWindow (hwnd, iCmdShow) ;

UpdateWindow (hwnd) ;

while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; }

msg變數是型態為MSG的結構,型態MSG在WINUSER.H中定義如下:

typedef struct tagMSG { HWND hwnd ; UINT message ; WPARAM wParam ; LPARAM lParam ; DWORD time ; POINT pt ; } MSG, * PMSG ;

POINT資料型態也是一個結構,它在WINDEF.H中定義如下:

typedef struct tagPOINT { LONG x ; LONG y ; } POINT, * PPOINT;

訊息迴圈以GetMessage呼叫開始,它從訊息佇列中取出一個訊息:

這一呼叫傳給Windows一個指標,指向名為msg的MSG結構。第二、第三和第四個參數設定為NULL或者0,表示程式接收它自己建立的所有視窗的所有訊息。Windows用從訊息佇列中取出的下一個訊息來填充訊息結構的各個欄位,結構的各個欄位包括:

GetMessage (&msg, NULL, 0, 0)

  • hwnd 接收訊息的視窗代號。在HELLOWIN程式中,這一參數與CreateWindow傳回的hwnd值相同,因為這是該程式擁有的唯一視窗。
  • message 訊息識別字。這是一個數值,用以標識訊息。對於每個訊息,均有一個對應的識別字,這些識別字定義於Windows表頭檔案(其中大多數在WINUSER.H中),以字首WM(「window message」,視窗訊息)開頭。例如,使用者將滑鼠游標放在HELLOWIN顯示區域之內,並按下滑鼠左按鈕,Windows就在訊息佇列中放入一個訊息,該訊息的message欄位等於WM_LBUTTONDOWN。這是一個常數,其值為0x0201。
  • wParam 一個32位元的「message parameter(訊息參數)」,其含義和數值根據訊息的不同而不同。
  • lParam 一個32位元的訊息參數,其值與訊息有關。
  • time 訊息放入訊息佇列中的時間。
  • pt 訊息放入訊息佇列時的滑鼠座標。

只要從訊息佇列中取出訊息的message欄位不為WM_QUIT(其值為0x0012),GetMessage就傳回一個非零值。WM_QUIT訊息將導致GetMessage傳回0。

敘述

將msg結構傳給Windows,進行一些鍵盤轉換。(關於這一點,我們將在 第六章 中深入討論。)

敘述

又將msg結構回傳給Windows。然後,Windows將該訊息發送給適當的視窗訊息處理程式,讓它進行處理。這也就是說,Windows將呼叫視窗訊息處理程式。在HELLOWIN中,這個視窗訊息處理程式就是WndProe函式。處理完訊息之後,WndProc傳回到Windows。此時,Windows還停留在DispatchMessage呼叫中。在結束DispatchMessage呼叫的處理之後,Windows回到HELLOWIN,並且接著從下一個GetMessage呼叫開始訊息迴圈。

視窗訊息處理程式

以上我們所討論的都是必要的負擔:註冊視窗類別,建立視窗,然後在螢幕上顯示視窗,程式進入訊息迴圈,然後不斷從訊息佇列中取出訊息來處理。

實際的動作發生在視窗訊息處理程式中。視窗訊息處理程式確定了在視窗的顯示區域中顯示些什麼以及視窗怎樣回應使用者輸入。

在HELLOWIN中,視窗訊息處理程式是命名為WndProc的函式。視窗訊息處理程式可任意命名(只要求不和其他名字發生衝突)。一個Windows程式可以包含多個視窗訊息處理程式。一個視窗訊息處理程式總是與呼叫RegisterClass註冊的特定視窗類別相關聯。CreateWindow函式根據特定視窗類別建立一個視窗。但依據一個視窗類別,可以建立多個視窗。

視窗訊息處理程式總是定義為如下形式:

注意,視窗訊息處理程式的四個參數與MSG結構的前四個欄位是相同的。第一個參數hwnd是接收訊息的視窗的代號,它與CreateWindow函式的傳回值相同。對於與HELLOWIN相似的程式(只建立一個視窗),這個參數是程式所知道的唯一視窗代號。如果程式是依據同一視窗類別(同時也是同一視窗訊息處理程式)建立多個視窗,則hwnd標識接收訊息的特定視窗。

第二個參數與MSG結構中的message欄位相同,它是標識訊息的數值。最後兩個參數都是32位元的訊息參數,提供關於訊息的更多資訊。這些參數包含每個訊息型態的詳細資訊。有時訊息參數是兩個存放在一起的16位元值,而有時訊息參數又是一個指向字串或資料結構的指標。

程式通常不直接呼叫視窗訊息處理程式,視窗訊息處理程式通常由Windows本身呼叫。通過呼叫SendMessage函式,程式能夠直接呼叫它自己的視窗訊息處理程式。我們將在後面的章節討論SendMessage函式。

處理訊息

視窗訊息處理程式所接受的每個訊息均是用一個數值來標識的,也就是傳給視窗訊息處理程式的message參數。Windows表頭檔案WINUSER.H為每個訊息參數定義以「WM」(視窗訊息)為字首開頭的識別字。

一般來說,Windows程式寫作者使用switch和case結構來確定視窗訊息處理程式接收的是什麼訊息,以及如何適當地處理它。視窗訊息處理程式在處理訊息時,必須傳回0。視窗訊息處理程式不予處理的所有訊息應該被傳給名為DefWindowProc的Windows函式。從DefWindowProc傳回的值必須由視窗訊息處理程式傳回。

在HELLOWIN中,WndProc只選擇處理三種訊息:WM_CREATE、WM_PAINT和WM_DESTROY。視窗訊息處理程式的結構如下:

TranslateMessage (&msg) ;

DispatchMessage (&msg) ;

LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

switch (iMsg) { case WM_CREATE : 處理WM_CREATE訊息 return 0 ; case WM_PAINT : 處理WM_PAINT訊息 return 0 ; case WM_DESTROY : 處理WM_DESTROY訊息 return 0 ; } return DefWindowProc (hwnd, iMsg, wParam, lParam) ;

呼叫DefWindowProc來為視窗訊息處理程式不予處理的所有訊息提供內定處理,這是很重要的。不然一般動作,如終止程式,將不會正常執行。

播放音效檔案

視窗訊息處理程式接收的第一個訊息-也是WndProc選擇處理的第一個訊息-是WM_CREATE。當Windows在WinMain中處理CreateWindow函式時,WndProc接收這個訊息。就是說,在HELLOWIN呼叫CreateWindow時,Windows將做一些它必須做的工作。在這些工作中,Windows呼叫WndProc,將第一個參數設定為視窗代號,第二個參數設定為WM_CREATE。WndProc處理WM_CREATE訊息並將控制傳回給Windows。 Windows然後可以從CreateWindow呼叫中傳回到HELLOWIN中,繼續在WinMain中進行下一步的處理。

通常,視窗訊息處理程式在WM_CREATE處理期間進行一次視窗初始化。HELLOWIN對這個訊息的處理中播放一個名為HELLOWIN.WAV的音效檔案。它使用簡單的PlaySound函式來做到這一點。該函式說明在/Platform SDK/Graphics and Multimedia Services/Multimedia Audio/Waveform Audio中,而文件在/Platform SDK/Graphics and Multimedia Services/Multimedia Reference/Multimedia Functions中。

PlaySound的第一個參數是音效檔案的名稱(它也可能是在Control Panel的Sounds中定義的一種聲音的別名,或者是一個程式資源)。第二個參數只有當音效檔案是一種資源時才被使用。第三個參數指定一些選項。在這個例子中,我指定第一個參數是一個檔案名,並且非同步地播放聲音,即PlaySound函式呼叫在音效檔案開始播放時立即傳回,而不會等待它的完成。在這種方法下,程式能夠繼續初始化。

WndProc通過從視窗訊息處理程式中傳回0,結束了整個WM_CREATE的處理。

WM_PAINT訊息

WndProc處理的第二個訊息為WM_PAINT。這個訊息在Windows程式設計中是很重要的。當視窗顯示區域的一部分顯示內容或者全部變為「無效」,以致於必須「更新畫面」時,將由這個訊息通知程式。

顯示區域的顯示內容怎麼會變得無效呢?在最初建立視窗的時候,整個顯示區域都是無效的,因為程式還沒有在視窗上畫什麼東西。第一條WM_PAINT訊息(通常發生在WinMain中呼叫UpdateWindow時)指示視窗訊息處理程式在顯示區域上畫一些東西。

在使用者改變HELLOWIN視窗的大小後,顯示區域的顯示內容重新變得無效。讀者應該還記得,HELLOWIN中wndclass結構的style欄位設定為標誌CS_HREDRAW和CS_VREDRAW,這樣的格式設定指示Windows,在視窗大小改變後,就把整個視窗顯示內容當成無效。然後,視窗訊息處理程式將收到一條WM_PAINT訊息。

當使用者將HELLOWIN最小化,然後再次將視窗恢復為以前的大小時,Windows將不會保存顯示區域的內容。在圖形環境下,視窗顯示區域涉及的資料量很大。因此,Windows令視窗無效,視窗訊息處理程式接收一條WM_PAINT訊息,並自動恢復其視窗的內容。

在移動視窗以致其相互重疊時,Windows不保存一個視窗中被另一個視窗所遮蓋的內容。在這一部分不再被遮蓋之後,它就被標誌為無效。視窗訊息處理程式接收到一條WM_PAINT訊息,以更新視窗的內容。

對WM_PAINT的處理幾乎總是從一個BeginPaint呼叫開始:

而以一個EndPaint呼叫結束:

在這兩個呼叫中,第一個參數都是程式的視窗代號,第二個參數是指向型態為PAINTSTRUCT的結構指標。PAINTSTRUCT結構中包含一些視窗訊息處理程式,可以用來更新顯示區域的內容。我們將在下一章中討論該結構的各個欄位。現在我們只在BeginPaint和EndPaint函式中用到它。

在BeginPaint呼叫中,如果顯示區域的背景還未被刪除,則由Windows來刪除。它使用註冊視窗類別的WNDCLASS結構的hbrBackground欄位中指定的畫刷來刪除背景。在HELLOWIN中, 這是一個白色備用畫刷。這意味著,Windows將通過把視窗背景設定為白色來刪除視窗背景。BeginPaint呼叫令整個顯示區域有效,並傳回一個「裝置內容代號」。裝置內容是指實體輸出設備(如視訊顯示器)及其裝置驅動程式。在視窗的顯示區域顯示文字和圖形需要裝置內容代號。但是從BeginPaint傳回的裝置內容代號不能在顯示區域之外繪圖,讀者可以試一試。EndPaint釋放裝置內容代號,使之不再有效。

如果視窗訊息處理程式不處理WM_PAINT訊息(這是很罕見的),它們必須被傳送給DefWindowProc。DefWindowProc只是依次呼叫BeginPaint和EndPaint,以使顯示區域有效。

呼叫完BeginPaint之後,WndProc接著呼叫GetClientRect:

第一個參數是程式視窗的代號。第二個參數是一個指標,指向一個RECT型態的rectangle結構。該結構有四個LONG欄位,分別為left、top、right和bottom。GetClientRect將這四個欄位設定為視窗顯示區域的尺寸。left和top欄位通常設定為0,right和bottom欄位設定為顯示區域的寬度和高度(圖元點數)。

WndProc除了將該RECT結構指標作為DrawText的第四個參數傳遞外,不再對它做其他處理:

DrawText可以輸出文字(正如其名字所表明的一樣)。由於該函式要輸出文字,第一個參數是從BeginPaint傳回的裝置內容代號,第二個參數是要輸出的文字,第三個參數是 -1,指示字串是以位元組0終結的。

DrawText最後一個參數是一系列位元旗標,它們均在WINUSER.H中定義(雖然由於其顯示輸出的效果,使得DrawText像一個GDI函式呼叫,但它確實因為相當高級的畫圖功能而成為User模組的一部分。此函式在/Platform SDK/Graphics and Multimedia Services/GDI/Fonts and Text中說明)。旗標指示了文字必須顯示在一行上,水平方向和垂直方向都位於第四個參數指定的矩形中央。因此,這個函式呼叫將讓字串「Hello, Windows 98!」顯示在顯示區域的中央。

一旦顯示區域變得無效(正如在改變大小時所發生的情況一樣),WndProc就接收到一個新的WM_PAINT訊息。WndProc通過呼叫GetClientRect取得變化後的視窗大小,並在新視窗的中央顯示文字。

WM_DESTROY訊息

WM_DESTROY訊息是另一個重要訊息。這一個訊息指示,Windows正在根據使用者的指示關閉視窗。該訊息是使用者單擊Close按鈕或者在程式的系統功能表上選擇 Close時發生的(在本章的後面,我們將詳細討論WM_DESTROY訊息是如何生效的)。

HELLOWIN通過呼叫PostQuitMessage以標準方式回應WM_DESTROY訊息:

該函式在程式的訊息佇列中插入一個WM_QUIT訊息。前面提到過,GetMessage對於除了WM_QUIT之外的從訊息佇列中取出的所有訊息都傳回非0值。而當GetMessage得到一個WM_QUIT訊息時,它傳回0。這將導致WinMain退出訊息迴圈,並終止程式。然後程式執行下面的敘述:

結構的wParam欄位是傳遞給PostQuitMessage函式的值(通常是0)。然後return敘述將退出WinMain並終止程式。

WINDOWS程式設計的難點

即使有了對HELLOWIN的說明,讀者對程式的結構和原理可能仍然覺得神秘。在為傳統環境編寫簡單的C程式時,整個程式可能包含在main函式中。而在HELLOWIN中,WinMain只包含了註冊視窗類別,建立視窗,從訊息佇列中取出訊息和發送訊息所必須的程式碼。

程式的所有實際動作均在視窗訊息處理程式中發生。在HELLOWIN中,這些動作不多,WndProc只是簡單地播放了一個音效檔案並在視窗中顯示一個字串。但是在後面的章節中,讀者將發現,Windows程式所作的一切,都是回應發送給視窗訊息處理程式的訊息。這是概念上的主要難點之一,在開始寫作Windows程式之前,必須先搞清楚。

別呼叫我,我會呼叫您

前面我們提到過,程式寫作者已經熟悉了使用作業系統呼叫的做法。例如,C程式寫作者使用fopen函式打開檔案。fopen函式最終通過呼叫作業系統來打開檔案,這一點問題也沒有。

但是Windows不同,儘管Windows有1000個以上的函式可供程式呼叫,但Windows也呼叫使用者程式,比如前面定義的視窗訊息處理程式WndProc。視窗訊息處理程式與視窗類別相關,視窗類別是程式呼叫RegisterClass註冊的。依據該類別建立的視窗使用這個視窗訊息處理程式來處理視窗的所有訊息。Windows通過呼叫視窗訊息處理程式對視窗發送訊息。

在第一次建立視窗時,Windows呼叫WndProc。在視窗關閉時,Windows也呼叫WndProc。視窗改變大小、移動或者變成圖示時,從功能表中選擇某一項目、挪動捲動列、按下滑鼠按鈕或者從鍵盤輸入字元時,以及視窗顯示區域必須被更新時,Windows都要呼叫WndProc。

所有這些WndProc呼叫都以訊息的形式進行。在大多數Windows程式中,程式的主要部分都用來處理訊息。Windows可以發送給視窗訊息處理程式的訊息通常都以WM開頭的名字標識,並且都在WINUSER.H表頭檔案中定義。

實際上,從程式外呼叫程式內的常式這一種做法,在傳統的程式設計中並非前所未聞。C中的signal函式可以攔截Ctrl-C中斷或作業系統的其他中斷。為MS-DOS編寫的老程式中經常有攔截硬體中斷的程式碼。

但在Windows中,這種概念擴展為包括一切事件。視窗中發生的一切都以訊息的形式傳給視窗訊息處理程式。然後,視窗訊息處理程式以某種方式回應這個訊息,或者將訊息傳給DefWindowProc,進行內定處理。

在HELLOWIN中,視窗訊息處理程式的wParam和lParam參數除了作為傳遞給DefWindowProc的參數外,不再有其他用處。這些參數給出了關於訊息的其他資訊,參數的含義與具體訊息相關。

讓我們來看一個例子。一旦視窗的顯示區域大小發生了改變,Windows就呼叫視窗的視窗訊息處理程式。視窗訊息處理程式的hwnd參數是改變大小的視窗的代號(請記住,一個視窗訊息處理程式能處理依據同一個視窗類別建立的多個視窗的訊息。參數hwnd讓視窗訊息處理程式知道是哪個視窗在接收訊息)。參數message是WM_SIZE。訊息WM_SIZE的參數wParam的值是SIZE_RESTORED、SIZE_MINIMIZED、SIZE_MAXIMIZED、SIZE_MAXSHOW或SIZE_MAXHIDE (在WINUSER.H表頭檔案中分別定義為數字0到4)。也就是說,參數wParam表明視窗是非最小化還是非最大化,是最小化、最大化,還是隱藏。

lParam參數包含了新視窗的大小,新寬度和新高度均為16位元值,合在一起成為32位元的lParam。WINDEF.H中提供了幫助程式寫作者從lParam中取出這兩個值的巨集,我們將在 下一章 說明這個巨集。

有時候,DefWindowProc處理完訊息後會產生其他的訊息。例如,假設使用者執行HELLOWIN,並且使用者最終單擊了 Close 按鈕,或者假設用鍵盤或滑鼠從系統功能表中選擇了 Close , DefWindowProc處理這一鍵盤或者滑鼠輸入,在檢測到使用者選擇了 Close 選項之後,它給視窗訊息處理程式發送一條WM_SYSCOMMAND訊息。WndProc將這個訊息傳給DefWindowProc。DefWindowProc給視窗訊息處理程式發送一條WM_CLOSE訊息來回應之。WndProc再次將它傳給DefWindowProc。DestroyWindow呼叫DestroyWindow來回應這條WM_CLOSE訊息。DestroyWindow導致Windows給視窗訊息處理程式發送一條WM_DESTROY訊息。WndProc再呼叫PostQuitMessage,將一條WM_QUIT訊息放入訊息佇列中,以此來回應此訊息。這個訊息導致WinMain中的訊息迴圈終止,然後程式結束。

佇列化訊息與非佇列化訊息

我們已經談到過,Windows給視窗發送訊息,這意味著Windows呼叫視窗訊息處理程式。但是,Windows程式也有一個訊息迴圈,它呼叫GetMessage從訊息佇列中取出訊息,並且呼叫DispatchMessage將訊息發送給視窗訊息處理程式。

那麼,Windows程式是依次等待訊息(類似于普通程式中相同的鍵盤輸入),然後將訊息送到某地方去的嗎?或者,它是直接從程式外面接收訊息的嗎?實際上,兩種情況都存在。

訊息能夠被分為「佇列化的」和「非佇列化的」。佇列化的訊息是由Windows放入程式訊息佇列中的。在程式的訊息迴圈中,重新傳回並分配給視窗訊息處理程式。非佇列化的訊息在Windows呼叫視窗時直接送給視窗訊息處理程式。也就是說,佇列化的訊息被「發送」給訊息佇列,而非佇列化的訊息則「發送」給視窗訊息處理程式。任何情況下,視窗訊息處理程式都將獲得視窗所有的訊息--包括佇列化的和非佇列化的。視窗訊息處理程式是視窗的「訊息中心」。

佇列化訊息基本上是使用者輸入的結果,以擊鍵(如WM_KEYDOWN和WM_KEYUP訊息)、擊鍵產生的字元(WM_CHAR)、滑鼠移動(WM_MOUSEMOVE)和滑鼠按鈕(WM_LBUTTONDOWN)的形式給出。佇列化訊息還包含時鐘訊息(WM_TIMER)、更新訊息(WM_PAINT)和退出訊息(WM_QUIT)。

非佇列化訊息則是其他訊息。在許多情況下,非佇列化訊息來自呼叫特定的Windows函式。例如,當WinMain呼叫CreateWindow時,Windows將建立視窗並在處理中給視窗訊息處理程式發送一個WM_CREATE訊息。當WinMain呼叫ShowWindow時,Windows將給視窗訊息處理程式發送WM_SIZE和WM_SHOWWINDOW訊息。當WinMain呼叫UpdateWindow時,Windows將給視窗訊息處理程式發送WM_PAINT訊息。鍵盤或滑鼠輸入時發出的佇列化訊息信號,也能在非佇列化訊息中出現。例如,用鍵盤或滑鼠選擇了一個功能表項時,鍵盤或滑鼠訊息就是佇列化的,而說明功能表項已選中的WM_COMMAND訊息則可能就是非佇列化的。

這一過程顯然很複雜,但幸運的是,其中的大部分是由Windows解決的,不關我們的程式的事。從視窗訊息處理程式的角度來看,這些訊息是以一種有序的、同步的方式進出的。視窗訊息處理程式可以處理它們,也可以不處理。

當我說訊息是以一種有序的同步的方式進出時,我是說首先訊息與硬體的中斷不同。在一個視窗訊息處理程式中處理訊息時,程式不會被其他訊息突然中斷。

雖然Windows程式可以多執行緒執行,但每個執行緒的訊息佇列只為視窗訊息處理程式在該執行緒中執行的視窗處理訊息。換句話說,訊息迴圈和視窗訊息處理程式不是並發執行的。當一個訊息迴圈從其訊息佇列中接收一個訊息,然後呼叫DispatchMessage將訊息發送給視窗訊息處理程式時,直到視窗訊息處理程式將控制傳回給Windows,DispatchMessage才能結束執行。

當然,視窗訊息處理程式能呼叫給視窗訊息處理程式發送另一個訊息的函式。這時,視窗訊息處理程式必須在函式呼叫傳回之前完成對第二個訊息的處理。那時視窗訊息處理程式將處理最初的訊息。例如,當視窗程序呼叫UpdateWindow時,Windows將呼叫視窗訊息處理程式來處理WM_PAINT訊息。視窗訊息處理程式處理WM_PAINT訊息結束以後,UpdateWindow呼叫將把控制傳回給視窗訊息處理程式。

這也就是說視窗訊息處理程式必須是可重入。在大多數情況下,這不會帶來問題,但是程式寫作者應該意識到這一點。例如,假設您在視窗訊息處理程式中處理一個訊息時設置了一個靜態變數,然後呼叫了一個Windows函式。在這個函式傳回時,您還能保證那個變數的值還是原來那個嗎?難說--很可能您呼叫的Windows函式產生了另外一個訊息,並且視窗訊息處理程式在處理這個訊息時改變了該變數的值。這也是在編譯Windows程式時,有些編譯最佳化選項必須關閉的原因之一。

在許多情況下,視窗訊息處理程式必須保存它從訊息中取得的資訊,並在處理另一個訊息時使用這些資訊。這些資訊可以儲存在視窗的靜態(static)變數或整體變數中。

當然,讀者將在下面幾章對此有一個更清楚的了解,因為視窗訊息處理程式將處理更多的訊息。

行動迅速

Windows 98和Windows NT都是優先權式的多工環境。這意味著當一個程式在進行一項長時間工作時,Windows可以允許使用者將控制切換到另一個程式中。這是一件好事,也是現在的Windows優越於以前16位元Windows的地方。

然而,由於Windows設計的方式,這種優先權式多工並不總是以您希望的樣子工作。例如,假設您的程式花費一分鐘左右來處理某一個訊息。是的,使用者可以將控制切換到另一個程式,但是卻無法對您的程式進行任何動作。使用者無法移動您的程式視窗、縮放它、最小化、關閉它、什麼都不能做。這是因為您的視窗訊息處理程式正忙於進行一項長時間的作業。表面上並不是視窗訊息處理程式在執行它自己的移動和縮放操作,但實際上確實是它在做。這就是DefWindowProc部分的工作,它必須被考慮為您的視窗訊息處理程式的一部分。

如果您的程式在處理某些訊息時需要長時間的作業的話,可以選擇我在 第二十章 裏描述的那些方法來做得更有優雅一些。即使是在優先權式多工環境中,也不應該讓您的程式呆在螢幕上一動不動。這會讓使用者討厭的,他們會認為您的程式中有bug、不標準的動作,說明檔案沒寫好。最好讓使用者覺得程式只停了一下子就把全部訊息中快速料理完了。

hdc = BeginPaint (hwnd, &ps) ;

EndPaint (hwnd, &ps) ;

GetClientRect (hwnd, &rect) ;

DrawText ( hdc, TEXT ("Hello, Windows 98!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;

PostQuitMessage (0) ;

return msg.wParam ;