10. 功能表及其他資源

Post date: 2012/3/23 上午 05:38:47

10. 功能表及其他資源

大多數Windows程式都包含一個自訂的圖示,Windows將該圖示顯示在應用程式視窗標題列的左上角。當程式被列在「開始」功能表中,被顯示在螢幕底部的工作列中,被列在Windows Explorer中,或者作為快捷方式顯示在桌面上時,Windows也顯示該程式的圖示。有些程式-大部分是像小畫家一類的圖形繪製工具-也使用自訂滑鼠游標來表示程式的不同操作。還有許多Windows程式使用功能表和對話方塊。功能表、對話方塊加上捲動列,這是標準Windows使用者介面的賣點。

圖示、游標、功能表和對話方塊都是相互關聯的,它們是Windows的全部資源型態。資源即資料,它們被儲存在程式的.EXE檔案中,但是它們並非駐留在程式的資料區域中。也就是說,資源不能從程式原始碼中定義的變數直接存取,Windows提供函式直接或間接地把它們載入記憶體以備使用。我們已經遇到了兩個這樣的函式,即LoadIcon和LoadCursor,它們出現在範例程式,定義視窗類別結構的內容設定敘述中。它們從Windows中載入二進位圖示和游標映象,並傳回該圖示或游標的代號。在本章中,我們先建立自己的圖示,它會從程式自己的.EXE檔案中載入。

在本書中,我們將討論這些資源:

  • 圖示
  • 游標
  • 字串
  • 自訂資源
  • 功能表
  • 鍵盤加速鍵
  • 對話方塊
  • 點陣圖

前六個資源在本章討論,對話方塊在 第十一章 討論,而點陣圖在 第十四章 討論。

圖示、游標、字串和自訂資源

使用資源的好處之一,在於程式的許多元件能夠連結編譯進程式的.EXE檔案中。如果沒有資源這一個概念,如圖示圖像之類的二進位檔案可能會存放在單獨的檔案中,.EXE會把它讀入記憶體中使用。或者圖示不得不在程式中以位元組陣列的形式定義(這樣就無法看到實際的圖示圖像了)。作為資源,圖示儲存在開發者電腦上可單獨編輯的檔案中,但在編譯程序中被連結編譯進.EXE檔案中。

將圖示添加到程式

將資源添加到程式中需要Visual C++ Developer Studio的一些附加功能。對於圖示來說,可以使用「Image Editor」(也稱為「Graphics Editor」)來繪製圖示的圖像。該圖像被儲存在副檔名為.ICO的圖示檔案中。Developer Studio還產生一個資源描述檔(副檔名為.RC的檔案,有時也稱作資源定義檔案),它列出了程式的所有資源和一個讓程式引用資源的表頭檔案(RESOURCE.H)。

因此,您可以看到這些新檔案是如何組織在一起的,讓我們以建立名為ICONDEMO的新專案開始。像往常一樣,在Developer Studio中從 File 功能表中選擇 New ,然後依次選擇 專案 頁面標籤和 Win32 Application 。在 Project Name 欄中鍵入 ICONDEMO 並單擊 OK 。這時,Developer Studio建立了用於支援工作區和專案的五個檔案。這些檔案包括文字檔案ICONDEMO.DSW、ICONDEMO.DSP和ICONDEMO.MAK(假設當您從 Tools 功能表選擇 Open 後,在顯示的 Open 對話方塊中,從 Build 頁面標籤中選中 Export makefile when saving project file )。現在,讓我們像通常那樣所做的建立C原始碼檔案。從 File 功能表上選擇 New ,選擇 Files 頁面標籤,並單擊 C++Source File 。在 File Name 欄中鍵入ICONDEMO.C並單擊 OK 。此時,Developer Studio就建立了一個空的ICONDEMO.C檔案。鍵入程式10-1中的程式,或選擇 Insert 功能表,然後選擇 File As Text 選項,從本書附上的光碟中複製原始碼。

程式10-1 ICONDEMO ICONDEMO.C /*-------------------------------------------------------------------------- ICONDEMO.C -- Icon Demonstration Program (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { TCHAR szAppName[] = TEXT ("IconDemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ; 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 ; } hwnd = CreateWindow ( szAppName, TEXT ("Icon Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 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) { static HICON hIcon ; static int cxIcon, cyIcon, cxClient, cyClient ; HDC hdc ; HINSTANCE hInstance ; PAINTSTRUCT ps ; int x, y ; switch (message) { case WM_CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ; cxIcon = GetSystemMetrics (SM_CXICON) ; cyIcon = GetSystemMetrics (SM_CYICON) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (y = 0 ; y < cyClient ; y += cyIcon) for (x = 0 ; x < cxClient ; x += cxIcon) DrawIcon (hdc, x, y, hIcon) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

如果您試著編譯該程式,因為在程式開頭引用的RESOURCE.H檔案並不存在,所以會產生錯誤。然而,您不必直接建立RESOURCE.H檔案,而是由Developer Studio為您建立一個。

您可以通過將資源描述檔添加到專案中來做到這一點。從「File」功能表中選擇「New」,選擇「Files」頁面標籤,單擊「Resource Script」,在「File Name」欄中鍵入「ICONDEMO」,單擊OK。此時,Developer Studio會建立兩個文字檔案:ICONDEMO.RC(資源描述檔)和RESOURCE.H(允許C原始碼檔案和資源描述檔引用相同的已定義識別字)。不必直接編輯這兩個檔案,只要讓Developer Studio來維護它們就可以。如果您想查看資源描述檔和RESOURCE.H而不希望對Developer Studio產生干擾,可以用記事本打開它們。除非您對所做的動作很有把握,否則不要輕易地更改它們。請記住,只有在您下達明確的操作命令或重新編譯專案時,Developer Studio才會儲存這些檔案的新版本。

資源描述檔是文字檔案。它包括這些資源的可用文字形式表達的描述,例如功能表和對話方塊。資源描述檔也包括對非文字資源的二進位檔案的引用,例如圖示和自訂的滑鼠游標。

現在,已經存在RESOURCE.H檔案,您可以試著重新編譯一下ICONDEMO。現在會出現一條錯誤訊息,指出IDI_ICON還沒被定義。這個識別字第一次出現在下面的敘述中:

在本書前面的程式中,這個敘述是由下面的敘述代替的:

之所以改變敘述,是因為以前我們為應用程式使用的是標準的圖示,而這裏我們的目的是使用自訂圖示。

那麼讓我們建立一個圖示吧!在Developer Studio的「File View」視窗中,您會看到兩個檔案-ICONDEMO.C和ICONDEMO.RC。您開啟CONDEMO.C後,就可以編輯原始碼。開啟ICONDEMO.RC後,就可以把資源添加到檔案中或編輯已存在的資源。要添加圖示的話,請從「 Insert 」功能表上選擇「 Resource 」選擇您想添加的資源,也就是圖示,然後再按下「 New 」按鈕。

現在呈現的是一個空白的32×32圖素的圖示,您可以在其中填入顏色。您會看到帶有一組繪圖工具和可用顏色的浮動工具列。注意顏色工具列中包括兩個與顏色無關的選項,這兩種顏色選項有時被稱為「螢幕顏色」跟「反螢幕顏色」。當一個圖素在著色時選擇了「螢幕顏色」時,它實際上是透明的。不管圖示在什麼表面上顯示,圖示未著色的部分會顯示出底色。這樣我們就可以建立非矩形的圖示。

雙擊圍繞圖示的區域,會出現「Icon Properties」對話方塊,該對話方塊使您能夠更改圖示的ID和檔案名稱。Developer Studio可能已經將ID設定為IDI_ICON1,將它改為IDI_ICON,這樣ICONDEMO就可以引用圖示(字首IDI代表「圖示的ID」)。同樣地,將檔案名改為ICONDEMO.ICO。

現在選擇一種有特色的顏色(如紅色)並在圖示上畫一個大的B(代表BIG),請注意不必像圖10-1那麼整齊。

此時程式應該能夠編譯並執行得很好了。Developer Studio將在ICONDEMO.RC資源描述檔中劃一條橫線,表示下面是帶有識別字(IDI_ICON)的圖示檔案(ICONDEMO.ICO)。RESOURCE.H表頭檔案中會包含IDI_ICON識別字的定義。

Developer Studio通過資源編譯器RC.EXE編譯資源。文字資源描述檔被轉化為二進位形式,也就是具有副檔名.RES的檔案。然後,該已編譯的資源檔案隨同.OBJ和.LIB檔案一起在LINK步驟中被指定連結。這就是資源被添加到最後產生出來的.EXE檔案中的方式。

當您執行ICONDEMO時,程式圖示顯示在標題列的左上角和工作列中。如果您將程式添加到「開始」功能表中,或在桌面上放置捷徑,您也會在那兒看到該圖示。

ICONDEMO也在顯示區域水平和垂直地重複顯示該圖示。程式使用敘述

取得圖示的代號。使用敘述

取得圖示的大小。然後,程式通過多次呼叫

顯示圖示,其中x和y是被顯示圖示其左上角的座標。

在目前使用的大多數視訊顯示卡上,帶有SM_CXICON和SM_CYICON索引的GetSystemMetrics會回報圖示的大小為32×32圖素。這是我們在Developer Studio中建立的圖示大小,它也是圖示出現在桌面上和顯示在ICONDEMO程式顯示區域的大小。然而,這個大小並非顯示在程式的標題列或工作列中的圖示大小。小圖示的大小可以由帶有SM_CXSMSIZE和SM_CYSMSIZE索引的GetSystemMetrics獲得(第一個SM表示「system metrics(系統度量)」,被包含的SM表示「small(小)」)。對於目前使用的大多數顯示卡來說,小圖示的大小為16×16圖素。

這會產生問題。當Windows將32×32的圖示縮小為16×16的圖示時,必需減少圖素的行和列。這樣,對於某些比較複雜的圖示,就會失真。因此,我們應該為那些圖像縮小就會變形的圖示建立特殊的16×16圖素的圖示。在Developer Studio中圖示圖像的上面是標識為「Device」的下拉式清單方塊,在它的右邊有一個按鈕,按下該按鈕會彈出「New Icon Image」對話方塊,此時選擇「Small(16×16)」。現在您可以畫另一個圖示。如圖10-2所示,畫一個「S」(表示「小」)。

在該程式中您不必做任何事情。第二個圖示圖像被儲存在相同的ICONDEMO.ICO檔案中,並以相同的IDI_ICON識別字引用。在適當的時候,Windows會自動使用該較小的圖示,例如在標題列或工作列中。當在桌面上顯示快捷方式,以及程式呼叫DrawIcon裝飾顯示區域時,Windows會使用大圖示。

在掌握這些知識之後,讓我們看一看使用圖示的詳細情況。

取得圖示代號

如果您仔細閱讀ICONDEMO.RC和RESOURCE.H檔案,會看到由Developer Studio產生用於維護檔案的一些標記。然而,當編譯資源描述檔時,只有少數幾行是重要的。這些從ICONDEMO.RC和RESOURCE.H檔案中摘錄下來的關鍵部分被列在程式10-2中。

wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;

hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;

cxIcon = GetSystemMetrics (SM_CXICON) ; cyIcon = GetSystemMetrics (SM_CYICON) ;

DrawIcon (hdc, x, y, hIcon) ;

圖10-1 顯示在Developer Studio中的標準(32×32) ICONDEMO檔案

圖10-2 在Developer Studio中顯示的小(16×16) ICONDEMO檔案

ICONDEMO.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Icon IDI_ICON ICON DISCARDABLE "icondemo.ico"

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by IconDemo.rc #define IDI_ICON 101

程式10-2 ICONDEMO.RC和RESOURCE.H檔案的摘錄

程式10-2所顯示的ICONDEMO.RC和RESOURCE.H檔案與您在普通的文字編輯器中手動建立的很相似,80年代的Windows程式寫作者就是這樣做的。唯一不同的是AFXRES.H,它是個表頭檔案,包含了在建立由機器產生的MFC專案時由Developer Studio使用的常用識別字。在本書中,我們不會用到AFXRES.H。

ICONDEMO.RC中的這行

是資源描述檔的ICON敘述。該圖示有一個數值識別字IDI_ICON,等於101。由Developer Studio添加的DISCARDABLE關鍵字指出,必要時Windows可以從記憶體中丟棄圖示,以獲得額外的空間。之後不需要程式任何特定的操作,Windows就能夠重新載入圖示。DISCARDABLE屬性是內定的,不需要指定。只有在名稱和目錄路徑包含空格時,Developer Studio才將檔案名加上引號。

當資源編譯程序將編譯的資源儲存在ICONDEMO.RES中,並且由連結程式將資源添加到ICONDEMO.EXE中以後,該資源就可以經由一個資源型態(RT_ICON)和一個識別字(IDI_ICON或101)來標識。程式可以通過呼叫LoadIcon函式取得此圖示的代號:

請注意ICONDEMO在兩個地方呼叫這個函式,一次在定義視窗類別時,另一次在視窗訊息處理程式中取得圖示的代號用於繪製。LoadIcon傳回HICON型態的值,它是圖示的代號。

LoadIcon的第一個參數,是指出資源來自哪個檔案的執行實體代號。使用hInstance表示它來自程式自己的.EXE檔案。LoadIcon的第二個參數實際上被定義為指向字串的指標。待會將會看到,可以使用字串而不是用數值識別字標識資源。巨集MAKEINTRESOURCE(把整數轉換成資源字串)生成指向非數字的指標,如下所示:

LoadIcon知道,如果第二個參數的高字組為0,那麼低字組就為圖示的數值識別字。圖示的識別字必須為16位元值。

本書前面的範例程式使用了預先定義的圖示:

hInstance參數被設定為NULL,因此Windows知道這是預先定義的圖示。IDI_APPLICATION也在WINUSER.H中用MAKEINTRESOURCE定義:

LoadIcon的第二個參數帶來了一個有趣的問題:圖示的識別字能可以為字串嗎?答案是可以。方法如下:在 Developer Studio 中,在 ICONDEMO 專案的檔案列表上,選擇 IDONDEMO.RC 。您會看到頂端為「IconDemo Resource」的樹狀結構,然後是資源型態「Icon」,再下來是「IDI_ICON」。如果用滑鼠右鍵單擊圖示識別字,並從功能表上選擇「 Properties 」,您就能改變ID。實際上,您可以把名稱放在引號內將其更改為字串。我用這種方法指定資源名稱,並在本書的其他地方也使用該方法。

我喜歡為圖示(以及一些其他資源)使用文字名稱,因為名稱可以是程式的名稱。例如,假定檔案被命名為MYPROG。如果您使用「Icon Properties」對話方塊將圖示的ID指定為「MyProg」(包括引號),資源描述檔將包含下列敘述:

然而,在RESOURCE.H中並沒有#define敘述,來指出MYPROG是數值識別字。資源描述檔將假定MYPROG是字串識別字。

在C程式中,使用LoadIcon函式來取得圖示代號。您可能已經有了表示程式名的字串:

這意味著程式可以使用敘述:

來載入圖示,這比巨集MAKEINTRESOURCE更清晰一些。

但是如果您確實想用數字來命名,那麼您可以用數字代替識別字或字串。在「Icon Properties」對話方塊中,在ID欄中輸入數字。資源描述檔將有一個類似下面的ICON敘述:

可以使用兩種方法之一引用圖示。明顯易讀的方式是:

另一個不易閱讀的方式是:

Windows識別初始字元#作為ASCII形式中字元數值的開頭。

在程式中使用圖示

雖然Windows以幾種方式用圖示來代表程式,但是許多Windows程式僅在用WNDCLASS結構和RegisterClass定義視窗類別時指定一個圖示。如我們所看到的,這樣作用得很好,尤其當圖示檔案包含標準和較小的圖像大小時,更是如此。Windows在顯示圖示圖像時,它會在圖示檔案中選擇最合適的圖像大小。

RegisterClass有一個改進版本叫做RegisterClassEx,它使用名為WNDCLASSEX的結構。WNDCLASSEX有兩個附加的欄位:cbSize和hIconSm。cbSize欄位指出了WNDCLASSEX結構的大小,假設hIconSm被設定為小圖示的圖示代號。這樣,在WNDCLASSEX結構中,您可以設定與兩個圖示檔案相關的兩個圖示代號-一個用於標準圖示,一個用於小圖示。

有這種必要嗎?沒有。正如我們看到的,Windows已經從單個圖示檔案中提取了大小合適的圖示圖像。RegisterClassEx似乎沒有RegisterClass聰明。如果hIconSm欄位使用了包含多個圖像的圖示檔案,則只有第一個圖像能被利用。它可能是標準大小的圖示,使用時才被縮小。RegisterClassEx似乎是為了使用多個圖示圖像而設計的,每個圖像只包含一種圖示大小。因為現在可以將多個圖示大小包括在同一個圖示檔案中,所以我建議使用WNDCLASS和RegisterClass。

如果您想在程式執行的時候,動態地更改程式的圖示,可以使用SetClassLong來達到目的。例如,如果您有與識別字IDI_ALTICON相關的第二個圖示檔案,則您可以使用以下的敘述將其切換到那個圖示:

如果不想儲存程式圖示的代號,但要使用DrawIcon函式在別處顯示它,可以使用GetClassLong獲得代號。例如:

在Windows文件的某些部分,LoadIcon被稱為「過時的」,並推薦使用LoadImage(LoadIcon在/Platform SDK/User Interface Services/Resources/Icons中說明,LoadImage在/Platform SDK/User Interface Services/Resources/Resources中說明)。當然LoadImage更為靈活,但它沒有LoadIcon簡單。您會注意到,在ICONDEMO中對同一個圖示呼叫了LoadIcon兩次。這不會產生問題,也沒有使用額外的記憶體。LoadIcon是取得代號但不需要清除代號的少數幾個函式之一。實際上有一個DestroyIcon函式,但它與CreateIcon、CreateIconIndirect和CreateIconFromResource連在一起使用。這些函式使程式能夠動態地建立圖示圖像。

使用自訂游標

在程式中使用自訂的滑鼠游標與使用自訂的圖示相似,只是大多數程式寫作者總是使用Windows提供的游標。自訂游標一般為單色,大小為32×32圖素。在Developer Studio中建立游標與建立圖示的方法相同(從「 Insert 」功能表上選擇「 Resource 」,然後單擊「 Cursor 」),但不要忘記定義熱點。

可以在物件類別定義中設定自訂游標,敘述為:

如果游標用文字名稱定義,則為:

每當滑鼠位於根據這個類別建立的視窗上時,就會顯示與IDC_CURSOR或szCursor相對應的滑鼠游標。

如果使用了子視窗,那麼您可能希望游標隨著所在視窗的不同而有所區別。如果程式為這些子視窗定義了視窗類別,就可以在每個視窗類別中適當地設定hCursor欄位,讓每個視窗類別使用不同的游標。如果使用了預先定義的子視窗控制項,就可以使用以下方法改變視窗類別的hCursor欄位:

如果您將顯示區域劃分為較小的邏輯區域而不使用子視窗,就可以使用SetCursor來改變滑鼠游標:

在處理WM_MOUSEMOVE訊息處理期間,您應該呼叫SetCursor;否則,當游標移動時,Windows將使用視窗類別中定義的游標來重畫游標。文件指出,如果沒有改變游標,則SetCursor速度將會很快。

字串資源

把字串當成資源的觀念一開始可能令人覺得詭異。因為我們在使用原始碼中定義為變數的一般字串時,並沒有碰到任何問題。

字串資源主要是為了讓程式轉換成其他語言時更為方便。正如後面兩章中將看到的一樣,功能表和對話方塊也是資源描述檔的一部分。如果使用字串資源而不是將字串直接放入原始碼中,那麼程式所使用的所有文字將在同一檔案-資源描述檔中。如果轉換了資源描述檔中的文字,那麼建立程式的另一種語言版本所需做的一切就是重新連結程式。這種方法比重新組織原始碼安全得多(然而,除了下一個範例程式,我在本書的其他程式中不使用字串表,原因是字串表使程式碼看起來更為模糊和複雜)。

您可以在「 Insert 」功能表中選擇「 Resource 」,再選擇「 String Table 」,建立一個字串表。字串會顯示在螢幕右邊的列表中。通過雙擊字串就可以選中它。針對每個字串,您可以指定識別字和字串的內容。

在資源描述中,字串顯示在一個多行的敘述中,如下所示:

IDI_ICON ICON DISCARDABLE "icondemo.ico"

hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;

#define MAKEINTRESOURCE(i) (LPTSTR) ((DWORD) ((WORD) (i)))

LoadIcon (NULL, IDI_APPLICATION) ;

#define IDI_APPLICATION MAKEINTRESOURCE(32512)

MYPROG ICON DISCARDABLE myprog.ico

static TCHAR szAppName [] = TEXT ("MyProg") ;

hIcon = LoadIcon (hInstance, szAppName) ;

125 ICON DISCARDABLE myprog.ico

hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (125)) ;

hIcon = LoadIcon (hInstance, TEXT ("#125")) ;

SetClassLong (hwnd, GCL_HICON, LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ALTICON))) ;

DrawIcon (hdc, x, y, GetClassLong (hwnd, GCL_HICON)) ;

wndclass.hCursor = LoadCursor (hInstance, MAKEINTRESOURCE (IDC_CURSOR)) ;

wndclass.hCursor = LoadCursor (hInstance, szCursor) ;

SetClassLong (hwndChild, GCL_HCURSOR, LoadCursor (hInstance, TEXT ("childcursor")) ;

SetCursor (hCursor) ;

STRINGTABLE DISCARDABLE BEGIN IDS_STRING1, "character string 1" IDS_STRING2, "character string 2" 其他字串定義 END

如果您在替早期版本的Windows寫程式,並在文字編輯器中手動建立這個字串表(用Developer Studio來做這件事當然更容易得多了),您可以用左右大括弧代替BEGIN和END敘述。

資源描述可以包含多個字串表,但是每個ID必須唯一表示一個字串。每個字串占一行,最多4097個字元。\t可以作為跳位字元,\n則作為linefeed字元號。DrawText和MessageBox函式能夠識別這些控制符號。

您的程式可以使用LoadString呼叫把字串複製到程式資料段的緩衝區中:

參數id是ID,它加在資源描述檔中每個字串的前面;szBuffer是指向接收字串的字元陣列的指標;iMaxLength是送入szBuffer中的最大字元數。函式傳回字串中的字元數。

每個字串前面的ID一般是定義在表頭檔案中的巨集識別字。許多Windows程式寫作者使用字首IDS_ 來表示字串的ID。有時,檔案名稱或其他資訊需要在字串顯示時插入到字串中。在這種情況下,您可以將C的格式化字元放入字串,並把它用於wsprintf中作為一個格式化字串。

所有資源文字-包括字串表中的文字-以Unicode格式儲存在.RES編譯資源檔案以及最終的.EXE檔案中。LoadStringW函式直接載入Unicode文字。LoadStringA函式(僅在Windows 98下有效)完成由Unicode到本地內碼表的文字轉換。

讓我們來看一個程式,它使用三個字串,在訊息方塊中顯示三條錯誤資訊。RESOURCE.H表頭檔案為這些資訊定義了三個識別字:

LoadString (hInstance, id, szBuffer, iMaxLength) ;

#define IDS_FILENOTFOUND 1 #define IDS_FILETOOBIG 2 #define IDS_FILEREADONLY 3

資源描述檔具有此字串表:

STRINGTABLE BEGIN IDS_FILENOTFOUND, "File %s not found." IDS_FILETOOBIG, "File %s too large to edit." IDS_FILEREADONLY, "File %s is read-only." END

C原始碼檔案也包含這個表頭檔案,並定義了一個顯示訊息方塊的函式(我假定szAppName是一個包含程式名稱的整體變數)。

OkMessage (HWND hwnd, int iErrorNumber, TCHAR *szFileName) { TCHAR szFormat [40] ; TCHAR szBuffer [60] ; LoadString (hInst, iErrorNumber, szFormat, 40) ; wsprintf (szBuffer, szFormat, szFilename) ; return MessageBox ( hwnd, szBuffer, szAppName, MB_OK | MB_ICONEXCLAMATION) ; }

為了顯示包含「file not found」資訊的訊息方塊,程式呼叫:

自訂的資源

Windows也定義了「自訂資源」,這又稱為「使用者定義的資源」(使用者就是您-程式寫作者,而不是那個使用您程式的幸運者)。自訂資源讓連結.EXE檔案中的各種資料更為方便,對取得程式中的資料也是如此。資料可以是您需要的任何格式。程式用於存取自訂資源的Windows函式促使Windows將資料載入記憶體並傳回指向它的指標。然後您就可以對程式做任何操作。您會發現對於儲存和存取各種自己的資料,這要比把資料儲存在外部檔案中,再使用檔案輸入函式存取它要方便得多。

例如,您有一個檔案叫做BINDATA.BIN,它包含程式需要顯示的一些資料。您可以選擇這個檔案的格式。如果在MYPROG專案中有MYPROG.RC資源描述檔,您就可以在Developer Studio中從「 Insert 」功能表中選擇「 Resource 」並按「 Custom 」按鈕,來建立自訂的資源。鍵入表示資源的名稱:例如,BINTYPE。然後,Developer Studio會生成資源名稱(在這種情況下是IDR_BINTYPE1)並顯示讓您輸入二進位資料的視窗。但是您不必輸入什麼,用滑鼠右鍵單擊IDR_BINTYPE1名稱,並選擇 Properties ,然後就可以輸入一個檔案名稱:例如,BINDATA.BIN。

資源描述檔就會包含以下的一行敘述:

除了我們剛剛生成的BINTYPET資源型態外,這個敘述與ICONDEMO中的ICON敘述一樣。有了圖示後,您可以對資源名稱使用文字的名稱,而不是數字的識別字。

當您編譯並連結程式,整個BINDATA.BIN檔案會被併入MYPROG.EXE檔案中。

在程式的初始化(比如,在處理WM_CREATE訊息時)期間,您可以獲得資源的代號:

OkMessage (hwnd, IDS_FILENOTFOUND, szFileName) ;

IDR_BINTYPE1 BINTYPE BINDATA.BIN

hResource = LoadResource ( hInstance, FindResource ( hInstance, TEXT ("BINTYPE"), MAKEINTRESOURCE (IDR_BINTYPE1))) ;

變數hResource定義為HGLOBAL型態,它是指向記憶體區塊的代號。不管它的名稱是什麼,LoadResource不會立即將資源載入記憶體。把LoadResource和FindResource函式如上例般合在一起使用,在實質上就類似於LoadIcon和LoadCursor函式的做法。事實上,LoadIcon和LoadCursor函式就用到了LoadResource和FindResource函式。

當您需要存取文字時,呼叫LockResource:

LockResource將資源載入記憶體(如果還沒有載入的話),然後它會傳回一個指向資源的指標。當結束對資源的使用時,您可以從記憶體中釋放它:

當您的程式終止時,也會釋放資源,即使您沒有呼叫FreeResource.。

讓我們看一個使用三種資源-一個圖示、一個字串表和一個自訂的資源-的範例程式。程式10-3所示的POEPOEM程式在其顯示區域顯示Edgar Allan Poe的「Annabel Lee」文字。自訂的資源是檔案POEPOEM.TXT,它包含了一段詩文,此文字檔案以反斜線(\)結束。

pData = LockResource (hResource) ;

FreeResource (hResource) ;

程式10-3 POEPOEM POEPOEM.C /*--------------------------------------------------------------------------- POEPOEM.C -- Demonstrates Custom Resource (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { TCHAR szAppName [16], szCaption [64], szErrMsg [64] ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; LoadString ( hInstance, IDS_APPNAME, szAppName, sizeof (szAppName) / sizeof (TCHAR)) ; LoadString ( hInstance, IDS_CAPTION, szCaption, sizeof (szCaption) / sizeof (TCHAR)) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { LoadStringA (hInstance, IDS_APPNAME, (char *) szAppName, sizeof (szAppName)) ; LoadStringA (hInstance, IDS_ERRMSG, (char *) szErrMsg, sizeof (szErrMsg)) ; MessageBoxA (NULL, (char *) szErrMsg, (char *) szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, szCaption, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 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) { static char * pText ; static HGLOBAL hResource ; static HWND hScroll ; static int iPosition, cxChar, cyChar, cyClient, iNumLines, xScroll ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TEXTMETRIC tm ; switch (message) { case WM_CREATE : hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; xScroll = GetSystemMetrics (SM_CXVSCROLL) ; hScroll = CreateWindow (TEXT ("scrollbar"), NULL, WS_CHILD | WS_VISIBLE | SBS_VERT, 0, 0, 0, 0, hwnd, (HMENU) 1, hInst, NULL) ; hResource = LoadResource (hInst, FindResource (hInst, TEXT ("AnnabelLee"), TEXT ("TEXT"))) ; pText = (char *) LockResource (hResource) ; iNumLines = 0 ; while (*pText != '\\' && *pText != '\0') { if (*pText == '\n') iNumLines ++ ; pText = AnsiNext (pText) ; } *pText = '\0' ; SetScrollRange (hScroll, SB_CTL, 0, iNumLines, FALSE) ; SetScrollPos (hScroll, SB_CTL, 0, FALSE) ; return 0 ; case WM_SIZE : MoveWindow (hScroll, LOWORD (lParam) - xScroll, 0, xScroll, cyClient = HIWORD (lParam), TRUE) ; SetFocus (hwnd) ; return 0 ; case WM_SETFOCUS : SetFocus (hScroll) ; return 0 ; case WM_VSCROLL : switch (wParam) { case SB_TOP : iPosition = 0 ; break ; case SB_BOTTOM : iPosition = iNumLines ; break ; case SB_LINEUP : iPosition -= 1 ; break ; case SB_LINEDOWN : iPosition += 1 ; break ; case SB_PAGEUP : iPosition -= cyClient / cyChar ; break ; case SB_PAGEDOWN : iPosition += cyClient / cyChar ; break ; case SB_THUMBPOSITION : iPosition = LOWORD (lParam) ; break ; } iPosition = max (0, min (iPosition, iNumLines)) ; if (iPosition != GetScrollPos (hScroll, SB_CTL)) { SetScrollPos (hScroll, SB_CTL, iPosition, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; pText = (char *) LockResource (hResource) ; GetClientRect (hwnd, &rect) ; rect.left += cxChar ; rect.top += cyChar * (1 - iPosition) ; DrawTextA (hdc, pText, -1, &rect, DT_EXTERNALLEADING) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : FreeResource (hResource) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

POEPOEM.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // TEXT ANNABELLEE TEXT DISCARDABLE "poepoem.txt" ///////////////////////////////////////////////////////////////////////////// // Icon POEPOEM ICON DISCARDABLE "poepoem.ico" ///////////////////////////////////////////////////////////////////////////// // String Table STRINGTABLE DISCARDABLE BEGIN IDS_APPNAME "PoePoem" IDS_CAPTION """Annabel Lee"" by Edgar Allan Poe" IDS_ERRMSG "This program requires Windows NT!" END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by PoePoem.rc #define IDS_APPNAME 1 #define IDS_CAPTION 2 #define IDS_ERRMSG 3

POEPOEM.TXT It was many and many a year ago, In a kingdom by the sea, That a maiden there lived whom you may know By the name of Annabel Lee; And this maiden she lived with no other thought Than to love and be loved by me. I was a child and she was a child In this kingdom by the sea, But we loved with a love that was more than love -- I and my Annabel Lee -- With a love that the winged seraphs of Heaven Coveted her and me. And this was the reason that, long ago, In this kingdom by the sea, A wind blew out of a cloud, chilling My beautiful Annabel Lee; So that her highborn kinsmen came And bore her away from me, To shut her up in a sepulchre In this kingdom by the sea. The angels, not half so happy in Heaven, Went envying her and me -- Yes! that was the reason (as all men know, In this kingdom by the sea) That the wind came out of the cloud by night, Chilling and killing my Annabel Lee. But our love it was stronger by far than the love Of those who were older than we -- Of many far wiser than we -- And neither the angels in Heaven above Nor the demons down under the sea Can ever dissever my soul from the soul Of the beautiful Annabel Lee: For the moon never beams, without bringing me dreams Of the beautiful Annabel Lee; And the stars never rise, but I feel the bright eyes Of the beautiful Annabel Lee: And so, all the night-tide, I lie down by the side Of my darling -- my darling -- my life and my bride, In her sepulchre there by the sea -- In her tomb by the sounding sea. [May, 1849] \

POEPOEM.ICO

在POEPOEM.RC資源描述檔中,使用者定義的資源被定義為TEXT型態,取名為AnnabelLee:

在WndProc處理WM_CREATE時,使用FindResource和LoadResource取得資源代號。使用LockResource鎖定資源,並且使用一個小程式將檔案末尾的反斜線(\)換成0,這有利於後面WM_PAINT訊息處理期間使用的DrawText函式。

注意,這裏使用的是子視窗的捲動列,而不是視窗捲動列,這是因為子視窗捲動列有一個自動的鍵盤介面,因此在POEPOEM中沒有處理WM_KEYDOWN。

POEPOEM還使用三個字串,它們的ID在RESOURCE.H表頭檔案中定義。在程式的開始,IDS_APPNAME和IDS_CAPTIONPOEPOEM字串由LoadString載入記憶體:

ANNABELLEE TEXT POEPOEM.TXT

LoadString (hInstance, IDS_APPNAME, szAppName, sizeof (szAppName) / sizeof (TCHAR)) ; LoadString (hInstance, IDS_CAPTION, szCaption, sizeof (szCaption) / sizeof (TCHAR)) ;

注意RegisterClass前面的兩個呼叫。如果您在Windows 98下執行Unicode版本的POEPOEM,這兩個呼叫就都會失敗。因此,LoadStringA比LoadStringW要複雜得多(LoadStringA必須將資源字串由Unicode轉化為ANSI,而LoadStringW僅是直接載入它),LoadStringW在Windows 98下不被支援。這意味著在Windows 98下,當RegisterClassW函式失敗時,MessageBoxW函式(Windows 98支援)就不能使用LoadStringW載入程式的字串。由於這個原因,程式使用LoadStringA載入IDS_APPNAME和IDS_ERRMSG字串,並使用MessageBoxA顯示自訂的訊息方塊:

if (!RegisterClass (&wndclass)) { LoadStringA (hInstance, IDS_APPNAME, (char *) szAppName, sizeof (szAppName)) ; LoadStringA (hInstance, IDS_ERRMSG, (char *) szErrMsg, sizeof (szErrMsg)) ; MessageBoxA (NULL, (char *) szErrMsg, (char *) szAppName, MB_ICONERROR) ; return 0 ; }

注意,TCHAR字串變數是指向char的指標。

既然我們已經定義了用於POEPOEM的所有字串資源,那麼翻譯者將程式轉換成外語版本就很容易了。當然,它們將不得不翻譯「Annabel Lee」這個名字-我想,這會是一項困難得多的工作。

功能表

您還記得Monty Python有關乳酪店的幽默短劇嗎?那故事內容是這樣的:一個客人走進乳酪店想買某種乳酪。當然,店裏沒有這種乳酪。因此他又問有沒有另一種乳酪,然後再問另一種,再問另一種,不斷的問店家有沒有另一種乳酪(最後總共問了40種的乳酪),回答仍然是沒有,沒有,沒有,沒有,沒有。

這個不幸的事件可以通過功能表的使用來避免。一個功能表是一列可用的選項,它告訴饑餓的用餐者,廚房可以提供哪些服務,並且-對於Windows程式來說-還告訴使用者一個應用程式能夠執行哪些操作。

功能表可能是Windows程式提供的一致使用者介面中最重要的部分,而在您的程式中增加功能表,是Windows程式設計中相對簡單的部分。您在Developer Studio中定義功能表。每個可選的功能表項被賦予唯一的ID。您在視窗類別結構中指定功能表名稱。當使用者選擇一個功能表項時,Windows給您的程式發送包含該ID的WM_COMMAND訊息。

討論完功能表後,我還將討論鍵盤加速鍵,它們是一些鍵的組合,主要用於啟動功能表功能。

功能表概念

視窗的功能表列緊接在標題列的下方顯示,這個功能表列有時被稱為「主功能表」或「頂層功能表」。列在頂層功能表的項目通常是下拉式功能表,也叫做「突現式功能表」或「子功能表」。您也可以定義多重嵌套的突現式功能表,也就是說,在突現式功能表上的項目可以存取另一個突現式功能表。有時突現式功能表上的項目呼叫對話方塊以獲得更多的資訊(對話方塊在 下一章 介紹)。在標題列的最左端,很多父視窗都顯示程式的小圖示,這個圖示可以啟動系統功能表。它實際上是另一個突現式功能表。

突現式功能表的各項可以是「被選中的」,這意味著Windows在功能表文字的左端顯示一個小的選中標記,選中標記讓使用者知道從功能表中選中了哪些選項。這些選項之間可以是互斥的,也可以不互斥。頂層功能表項不能被選中。

頂層功能表或突現式功能表項可以被「啟用」、「禁用」或「無效化」。「啟動」和「不啟動」有時候被當作「啟用」和「禁用」的同義詞。被啟用或禁用的功能表項在使用者看來是一樣的,但是無效化的功能表項是使用灰色文字來顯示的。

從使用者的角度來看,啟用、禁用和無效化的功能表項都是可以「選擇的」(被選擇的功能表項目會被加高亮度顯示),也就是說,使用者可以使用滑鼠選擇被禁用的功能表項,將反相顯示游標列移動到禁用的功能表項上,或者使用功能表項的關鍵字母來選擇該功能表項。然而,從程式寫作者的角度來看,啟用、禁用和無效化功能表項的功能是不同的。Windows只為啟用的功能表項向程式發送WM_COMMAND訊息。要讓選項變得無效,可以把那些功能表項禁用和無效化。如果您想讓使用者知道選擇是無效的,那麼您可以讓一個功能表項無效化。

功能表結構

當您建立或改變程式中的功能表時,把頂層功能表和每一個突現式功能表想像成各自獨立的功能表是有用的。頂層功能表有一個功能表代號,在頂層功能表中的每一個突現式功能表也有它自己的功能表代號。系統功能表(也是一個突現式功能表)也有功能表代號。

功能表中的每一項都有三個特性。第一個特性是功能表中顯示什麼,它可以是字串或點陣圖。第二個特性是WM_COMMAND訊息中Windows發送給程式的功能表ID,或者是在使用者選擇功能表項時Windows顯示的突現式功能表的代號。第三個特性是功能表項的屬性,包括是否被禁用、無效化或被選中。

定義功能表

要使用Developer Studio來給程式資源描述檔添加功能表,可以從 Insert 功能表中選擇 Resource 並選擇 Menu (或者您可能已經知道了)。然後,您可以用交談式的方式定義功能表。功能表中每一項都有一個相關的 Menu Item Properties 對話方塊,指出該項目的字串。如果選中了 Pop-up 核取方塊,該項目就會呼叫一個突現式功能表,並且沒有ID與此項目相聯繫。如果沒有選中 Pop-up 核取方塊,該項目被選中時就會產生帶有特定ID的WM_COMMAND訊息。這兩類功能表項分別出現在資源描述檔的POPUP和MENUITEM敘述中。

當您為功能表中的項目鍵入文字時,可以鍵入一個「&」符號,指出後面一個字元在Windows顯示功能表時要加底線。這種底線字元是在您使用Alt鍵選擇功能表項時Windows要尋找的比對字元。如果在文字中不包括「&」符號,就不顯示任何底線,Windows會將功能表項文字的第一個字母用於Alt鍵查找。

如果在 Menu Items Properties 對話方塊中選中 Grayed 選項,則功能表項是不能啟動的,它的文字是灰色的,該項不產生WM_COMMAND訊息。如果選中 Inactive 選項,則功能表項也是不能啟動的,也不產生WM_COMMAND訊息,但是它的文字顯示正常。 Checked 選項在功能表項邊上放置一個選中標記。 Separator 選項在突現式功能表上產生一個分欄的橫線。

在突現式功能表的項目上,可以在字串中使用跳位字元\t。緊接著\t的文字被放置在距離突現式功能表的第一列右邊新的一列上。在本章後面,會看到在使用鍵盤加速鍵時它起的作用。字串中的\a使跟著它的文字向右對齊。

您指定的ID值是Windows發送給視窗訊息處理程式中功能表訊息中的數值。在功能表中ID值應該是唯一的。按照慣例,我使用以IDM(「ID for a Menu」)開頭的識別字。

在程式中引用功能表

大多數Windows應用程式在資源描述檔中只有一個功能表。您可以給功能表起一個與程式名稱相同的文字的名稱。程式寫作者經常將程式名用於功能表名稱,以便相同的字串可以用於視窗類別、程式的圖示名稱和功能表名稱。然後,程式在視窗的定義中為功能表引用該名稱:

雖然存取功能表資源的最常用方法是在視窗類別中指定功能表,您也可以使用其他方法。Windows應用程式可以使用LoadMenu函式將功能表資源載入記憶體中,如同LoadIcon和LoadCursor函式一樣。LoadMenu傳回一個功能表代號。如果您在資源描述檔中為功能表使用了名稱,敘述如下:

如果使用了數值,那麼LoadMenu呼叫採用如下的形式:

然後,您可以將這個功能表代號作為CreateWindow的第九個參數:

wndclass.lpszMenuName = szAppName ;

hMenu = LoadMenu (hInstance, TEXT ("MyMenu")) ;

hMenu = LoadMenu (hInstance, MAKEINTRESOURCE (ID_MENU)) ;

hwnd = CreateWindow ( TEXT ("MyClass"), TEXT ("Window Caption"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, hMenu, hInstance, NULL) ;

在這種情況下,CreateWindow呼叫中指定的功能表可以覆蓋視窗類別中指定的任何功能表。如果CreateWindow的第九個參數是NULL,那麼您可以把視窗類別中的功能表看作是這種視窗類別的視窗內定使用的功能表。這樣,您可以為依據同一視窗類別建立的幾個視窗使用不同的功能表。

您也可以在視窗類別中指定NULL功能表,並且在CreateWindow呼叫中也指定NULL功能表,然後在視窗被建立後再給視窗指定一個功能表:

這種形式使您可以動態地修改視窗的功能表。在本章後面的 NOPOPUPS程式 中我們將會看到這方面的例子。

當視窗被清除時,與視窗相關的所有功能表都將被清除。與視窗不相關的功能表在程式結束前通過呼叫DestroyMenu主動清除。

功能表和訊息

當使用者選擇一個功能表項時,Windows通常向視窗訊息處理程式發送幾個不同的訊息。在大多數情況下, 您的程式可以忽略大部分訊息,只需把它們傳遞給DefWindowProc即可。WM_INITMENU就是這一類的訊息,它具有下列參數:

wParam: 主功能表代號

lParam: 0

wParam值是您的主功能表代號,即使使用者選擇的是系統功能表中的項目。Windows程式通常忽略WM_INITMENU訊息。儘管在選中該項之前的訊息已經給程式提供了修改功能表的機會,但是我們覺得此刻改變頂層功能表是會擾亂使用者的。

程式也會接收到WM_MENUSELECT訊息。隨著使用者在功能表項中移動游標或者滑鼠,程式會收到許多WM_MENUSELECT訊息。這對實作那些包含對功能表項的文字描述的狀態列是很有幫助的。WM_MENUSELECT的參數如下所示:

LOWORD (wParam):被選中項目:功能表ID或者突現式功能表代號

HIWORD (wParam):選擇旗標

lParam: 包含被選中項目的功能表代號

WM_MENUSELECT是一個功能表追蹤訊息,wParam的值告訴您目前選擇的是功能表中的哪一項(加高亮度顯示的那個),wParam的高字組中的「選擇旗標」可以是下列這些旗標的組合:MF_GRAYED、MF_DISABLED、MF_CHECKED、MF_BITMAP、MF_POPUP、MF_HELP、MF_SYSMENU和MF_MOUSESELECT。如果您需要根據對功能表項的選擇來改變視窗顯示區域的內容,那麼您可以使用WM_MENUSELECT訊息。許多程式把該訊息發送給DefWindowProc。

當Windows準備顯示一個突現式功能表時,它給視窗訊息處理程式發送一個WM_INITMENUPOPUP訊息,參數如下:

wParam: 突現式功能表代號

LOWORD (lParam):突現式功能表索引

HIWORD (lParam): 系統功能表為1,其他為0

如果您需要在顯示突現式功能表之前啟用或者禁用功能表項,那麼這個訊息就很重要。例如,假定程式使用突現式功能表上的 Paste 命令從 剪貼簿 複製文字,當您收到突現式功能表中的WM_INITMENUPOPUP訊息時,應確定剪貼簿內是否有文字存在。如果沒有,那麼應該使 Paste 功能表項無效化。我們將在本章後面修改的POPPAD程式中看到這樣的例子。

最重要的功能表訊息是WM_COMMAND,它表示使用者已經從功能表中選中了一個被啟用的功能表項。第八章中的WM_COMMAND訊息也可以由子視窗控制項產生。如果您碰巧為功能表和子視窗控制項使用同一ID碼,那麼您可以通過lParam的值來區別它們,功能表項的lParam其值為0,請參見表10-1。

SetMenu (hwnd, hMenu) ;

表10-1

WM_SYSCOMMAND訊息類似於WM_COMMAND訊息,只是WM_SYSCOMMAND表示使用者從系統功能表中選擇一個啟用的功能表項:

wParam: 功能表ID

lParam: 0

然而,如果WM_SYSCOMMAND訊息是由按滑鼠按鍵產生的,LOWORD(lParam)和HIWORD(lParam)將包含滑鼠游標位置的x和y螢幕座標。

對於WM_SYSCOMMAND,功能表ID指示系統功能表中的哪一項被選中。對於預先定義的系統功能表項,較低的那四個位元應該和0xFFF0進行AND運算來遮罩掉,結果值應該為下列之一:SC_SIZE、SC_MOVE、SC_MINIMIZE、SC_MAXIMIZE、SC_NEXTWINDOW、SC_PREVWINDOW、SC_CLOSE、SC_VSCROLL、SC_HSCROLL、SC_ARRANGE、SC_RESTORE和SC_TASKLIST。此外,wParam可以是SC_MOUSEMENU或SC_KEYMENU。

如果您在系統功能表中添加功能表項,那麼wParam的低字組將是您定義的功能表ID。為了避免與預先定義的功能表ID相衝突,應用程式應該使用小於0xF000的值,這對於將一般的WM_SYSCOMMAND訊息發送給DefWindowProc是很重要的。如果您不這樣做,那麼您實際上就是禁用了正常的系統功能表命令。

我們將討論的最後一個訊息是WM_MENUCHAR。實際上,它根本不是功能表訊息。在下列兩種情況之一發生時,Windows會把這個訊息發送到視窗訊息處理程式:如果使用者按下Alt和一個與功能表項不匹配的字元時,或者在顯示突現式功能表而使用者按下一個與突現式功能表裏的項目不匹配的字元鍵時。隨WM_MENUCHAR訊息一起發送的參數如下所示:

LOWORD (wParam): 字元代碼(ASCII或Unicode)

HIWORD (wParam): 選擇碼

lParam: 功能表代號

選擇碼是:

  • 0 不顯示突現式功能表
  • MF_POPUP 顯示突現式功能表
  • MF_SYSMENU 顯示系統突現式功能表

Windows程式通常把該訊息傳遞給DefWindowProc,它一般給Windows傳回0,這會使Windows發出嗶聲。在 第十四章GRAFMENU程式 中會看到WM_MENUCHAR訊息的使用。

範例程式

讓我們來看一個簡單的例子。程式10-4所示的MENUDEMO程式,在主功能表中有五個選擇項-File、Edit、Background、Timer和Help,每一項都與一個突現式功能表相連。MENUDEMO只完成了最簡單、最通用的功能表處理操作,包括攔截WM_COMMAND訊息和檢查wParam的低字組。

程式10-4 MENUDEMO MENUDEMO.C /*--------------------------------------------------------------------- MENUDEMO.C -- Menu Demonstration (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("MenuDemo") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS 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.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 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) { static int idColor [5] = { WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, BLACK_BRUSH } ; static int iSelection = IDM_BKGND_WHITE ; HMENU hMenu ; switch (message) { case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: MessageBeep (0) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_BKGND_WHITE: // Note: Logic below case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE case IDM_BKGND_GRAY: // through IDM_BLACK are case IDM_BKGND_DKGRAY: // consecutive numbers in case IDM_BKGND_BLACK: // the order shown here. CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MF_CHECKED) ; SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (idColor [LOWORD (wParam) - IDM_BKGND_WHITE])) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_TIMER_START: if (SetTimer (hwnd, ID_TIMER, 1000, NULL)) { EnableMenuItem (hMenu, IDM_TIMER_START, MF_GRAYED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_ENABLED) ; } return 0 ; case IDM_TIMER_STOP: KillTimer (hwnd, ID_TIMER) ; EnableMenuItem (hMenu, IDM_TIMER_START, MF_ENABLED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_GRAYED) ; return 0 ; case IDM_APP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd,TEXT ("Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_ICONINFORMATION | MB_OK) ; return 0 ; } break ; case WM_TIMER: MessageBeep (0) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

MENUDEMO.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu MENUDEMO MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As...", IDM_FILE_SAVE_AS MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo", IDM_EDIT_UNDO MENUITEM SEPARATOR MENUITEM "C&ut", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT_PASTE MENUITEM "De&lete", IDM_EDIT_CLEAR END POPUP "&Background" BEGIN MENUITEM "&White", IDM_BKGND_WHITE, CHECKED MENUITEM "&Light Gray", IDM_BKGND_LTGRAY MENUITEM "&Gray", IDM_BKGND_GRAY MENUITEM "&Dark Gray", IDM_BKGND_DKGRAY MENUITEM "&Black", IDM_BKGND_BLACK END POPUP "&Timer" BEGIN MENUITEM "&Start", IDM_TIMER_START MENUITEM "S&top", IDM_TIMER_STOP, GRAYED END POPUP "&Help" BEGIN MENUITEM "&Help...", IDM_APP_HELP MENUITEM "&About MenuDemo...", IDM_APP_ABOUT END END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by MenuDemo.rc #define IDM_FILE_NEW 40001 #define IDM_FILE_OPEN 40002 #define IDM_FILE_SAVE 40003 #define IDM_FILE_SAVE_AS 40004 #define IDM_APP_EXIT 40005 #define IDM_EDIT_UNDO 40006 #define IDM_EDIT_CUT 40007 #define IDM_EDIT_COPY 40008 #define IDM_EDIT_PASTE 40009 #define IDM_EDIT_CLEAR 40010 #define IDM_BKGND_WHITE 40011 #define IDM_BKGND_LTGRAY 40012 #define IDM_BKGND_GRAY 40013 #define IDM_BKGND_DKGRAY 40014 #define IDM_BKGND_BLACK 40015 #define IDM_TIMER_START 40016 #define IDM_TIMER_STOP 40017 #define IDM_APP_HELP 40018 #define IDM_APP_ABOUT 40019

MENUDEMO.RC資源描述檔給了您定義功能表的提示。功能表的名稱為「MenuDemo」。大多數項目有底線字母,這就是說您必須在字母前鍵入『&』。MENUITEM SEPARATOR敘述是在「 Menu Item Properties 」對話方塊中選中「 Separator 」框產生的。注意功能表中有一個項目具有「 Checked 」選項,另一個具有「 Grayed 」選項。還有,「 Background 」突現式功能表中的五個項目應該按順序輸入,確保識別字是以數值的順序,本程式需要這樣。所有功能表項的識別字定義在RESOURCE.H中。

當收到突現式功能表「 File 」和「 Edit 」各項有關的WM_COMMAND訊息時,MENUDEMO程式只使系統發出嗶聲。「 Background 」突現式功能表列出MENUDEMO用來給背景著色的五種現有畫刷。在MENUDEMO.RC資源描述檔中,「 White 」功能表項(功能表ID為IDM_BKGND_WHITE)被標以「 CHECKED 」,它在功能表項旁邊設定選中標記。在MENUDEMO.C中,iSelection的值被初始化為IDM_BKGND_WHITE。

Background 」突現式功能表上的五種畫刷相互排斥。當MENUDEMO.C收到一個WM_COMMAND訊息,而該訊息中的wParam是「 Background 」突現式功能表上的五項之一時,它必須從先前選中的背景顏色中除掉選中標記,並把標記加到新的背景顏色上。為此,首先要得到功能表代號:

CheckMenuItem函式用來取消目前被選中的項目:

iSelection的值被設定為wParam的值,新的背景顏色被選中:

視窗類別中的背景顏色於是被替換為新的背景顏色,視窗顯示區域變為無效狀態,Windows使用新的背景顏色清除視窗。

Timer突現式功能表列出了兩個選項-「Start」和「Stop」。開始時,「Stop」選項變為灰色的(就像在資源描述檔中的功能表定義一樣)。當您選擇「Start」選項時,MENUDEMO試圖啟動一個計時器,如果成功,則無效化「Start」選項,並啟用「Stop」選項:

當收到一條WM_COMMAND訊息,並且wParam等於IDM_TIMER_STOP時,MENUDEMO程式會停止計數,啟用「 Start 」項,然後無效化「 Stop 」選項:

請注意,在計時器執行時,MENUDEMO程式不可能收到wParam等於IDM_TIMER_START的WM_COMMAND訊息。同樣地,在計時器關閉時,MENUDEMO程式也不可能收到wParam等於IDM_TIMER_STOP的WM_COMMAND訊息。

當MENUDEMO收到一個WM_COMMAND訊息,而該訊息的參數wParam等於IDM_APP_ABOUT或IDM_APP_HELP時,MENUDEMO程式顯示一個訊息方塊(在 下一章 中,我們將把訊息方塊變為對話方塊)。

當MENUDEMO程式收到一個WM_COMMAND訊息,其參數wParam等於IDM_APP_EXIT時,它給自己發送一個WM_CLOSE訊息。這個訊息與DefWindowProc收到WM_SYSCOMMAND訊息且wParam等於SC_CLOSE時發送給視窗訊息處理程式的訊息相同。我們將在本章後面介紹 POPPAD2 時再仔細研究這個問題。

功能表設計規範

在MENUDEMO中的「 File 」和「 Edit 」突現式功能表的格式與其他Windows程式中的格式非常類似。Windows的目的之一是為使用者提供一種易懂的介面,而不要求使用者為每個程式重新學習基本操作方式。如果「 File 」和「 Edit 」功能表在每個Windows程式中看起來都一樣,並且都使用同樣的字母和Alt鍵來進行選擇,那麼當然有助於減輕使用者的學習負擔。

除了「 File 」和「 Edit 」突現式功能表外,大多數Windows程式的功能表都是不同的。當設計一個功能表時,您應該看一看現有的Windows程式以儘量保持一致。當然,如果您認為別的程式是不對的,而您知道正確的方法,那麼沒有人能夠阻止您。同時記住,修改一個功能表,通常只需要修改資源描述檔而不必修改您的程式碼。即使以後要改變功能表項的位置,也不會有多大的問題。

雖然您的程式功能表在頂層可以有MENUITEM敘述,但這是不合規範的,因為這樣會很容易導致錯誤的選擇。如果您要這樣做,那麼請在字串後面加一個驚歎號,表示功能表項不會啟動突現式功能表。

較難的一種功能表定義方法

在程式的資源描述檔中定義功能表,通常是在您的視窗中添加功能表的最簡單方法,但不是唯一的方法。如果您沒有使用資源描述檔,那麼可以使用CreateMenu和AppendMenu兩個函式在程式中建立功能表。在您定義完功能表後,您可以將功能表代號發送給CreateWindow,或者使用SetMenu來設定視窗的功能表。

以下是具體的做法。CreateMenu簡單地把一個代號傳回給新功能表:

功能表一開始為空。AppendMenu將功能表項插入功能表中。您必須為頂層功能表項和每一個突現式功能表提供不同的功能表代號。突現式功能表是單獨構成的,然後將突現式功能表代號插入頂層功能表。程式10-5中所示的程式碼就是用這種方法建立功能表的,實際上,這個功能表與MENUDEMO程式中的功能表相同。為了簡化說明,代碼使用ASCII字串。

hMenu = GetMenu (hwnd) ;

CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ;

iSelection = wParam ; CheckMenuItem (hMenu, iSelection, MF_CHECKED) ;

EnableMenuItem (hMenu, IDM_TIMER_START, MF_GRAYED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_ENABLED) ;

EnableMenuItem (hMenu, IDM_TIMER_START, MF_ENABLED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_GRAYED) ;

hMenu = CreateMenu () ;

程式10-5 不使用資源描述檔建立與MENUDEMO程式相同功能表的C程式碼 hMenu = CreateMenu () ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_FILE_NEW, "&New"); AppendMenu (hMenuPopup, MF_STRING, IDM_FILE_OPEN, "&Open..."); AppendMenu (hMenuPopup, MF_STRING, IDM_FILE_SAVE, "&Save"); AppendMenu (hMenuPopup, MF_STRING, IDM_FILE_SAVE_AS, "Save &As..."); AppendMenu (hMenuPopup, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenuPopup, MF_STRING, IDM_APP_EXIT, "E&xit") ; AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&File") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_EDIT_UNDO,"&Undo") ; AppendMenu (hMenuPopup, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenuPopup, MF_STRING,IDM_EDIT_CUT, "Cu&t") ; AppendMenu (hMenuPopup, MF_STRING,IDM_EDIT_COPY,"&Copy") ; AppendMenu (hMenuPopup, MF_STRING,IDM_EDIT_PASTE,"&Paste") ; AppendMenu (hMenuPopup, MF_STRING,IDM_EDIT_CLEAR,"De&lete") ; AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Edit") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING| MF_CHECKED, IDM_BKGND_WHITE, "&White"); AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_LTGRAY, "&Light Gray"); AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_GRAY, "&Gray") ; AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_DKGRAY, "&Dark Gray"); AppendMenu (hMenuPopup, MF_STRING, IDM_BKGND_BLACK, "&Black") ; AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Background") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_TIMER_START, "&Start") ; AppendMenu (hMenuPopup, MF_STRING | MF_GRAYED, IDM_TIMER_STOP, "S&top") ; AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Timer") ; hMenuPopup = CreateMenu () ; AppendMenu (hMenuPopup, MF_STRING, IDM_HELP_HELP, "&Help") ; AppendMenu (hMenuPopup, MF_STRING, IDM_APP_ABOUT, "&About MenuDemo...") ; AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Help") ;

我認為您會同意底下這個觀點:使用資源描述檔功能表模板來製作功能表,會更容易而且更清楚。我並不鼓勵您使用這裏的方法定義功能表,而只是提供了一種實作功能表的方法。當然,您可以使用包含所有功能表項字串、ID和旗標等的結構陣列來壓縮程式碼大小。不過,如果您這麼做了,那麼您還可以利用Windows定義功能表的第三種方法。LoadMenuIndirect函式接受一個指向MENUITEMTEMPLATE型態的結構指標,並傳回功能表的代號,該函式在載入資源描述檔中的常規功能表模板後,在Windows中構造功能表,讀者不妨自己嘗試一下。

浮動突現式功能表

您還可以在沒有頂層功能表列的情況下使用功能表,也就是說,您可以使突現式功能表出現在螢幕頂層的任何位置。一種方法是使用滑鼠右鍵來啟動突現式功能表。程式10-6所示的POPMENU說明了這種方法。

程式10-6 POPMENU POPMENU.C /*---------------------------------------------------------------------- POPMENU.C -- Popup Menu Demonstration (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; TCHAR szAppName[] = TEXT ("PopMenu") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) 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 ; } hInst = hInstance ; hwnd = CreateWindow ( szAppName, TEXT ("Popup Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 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) { static HMENU hMenu ; static int idColor [5] = { WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, BLACK_BRUSH } ; static int iSelection = IDM_BKGND_WHITE ; POINT point ; switch (message) { case WM_CREATE: hMenu = LoadMenu (hInst, szAppName) ; hMenu = GetSubMenu (hMenu, 0) ; return 0 ; case WM_RBUTTONUP: point.x = LOWORD (lParam) ; point.y = HIWORD (lParam) ; ClientToScreen (hwnd, &point) ; TrackPopupMenu (hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_BKGND_WHITE: // Note: Logic below case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE case IDM_BKGND_GRAY: // through IDM_BLACK are case IDM_BKGND_DKGRAY: // consecutive numbers in case IDM_BKGND_BLACK: // the order shown here. CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MF_CHECKED) ; SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (idColor [LOWORD (wParam) - IDM_BKGND_WHITE])) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd, TEXT ("Popup Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_ICONINFORMATION | MB_OK) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_APP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return 0 ; } break ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

POPMENU.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu POPMENU MENU DISCARDABLE BEGIN POPUP "MyMenu" BEGIN POPUP "&File" BEGIN MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As", IDM_FILE_SAVE_AS MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo", IDM_EDIT_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT_PASTE MENUITEM "De&lete", IDM_EDIT_CLEAR END POPUP "&Background" BEGIN MENUITEM "&White", IDM_BKGND_WHITE, CHECKED MENUITEM "&Light Gray", IDM_BKGND_LTGRAY MENUITEM "&Gray", IDM_BKGND_GRAY MENUITEM "&Dark Gray", IDM_BKGND_DKGRAY MENUITEM "&Black", IDM_BKGND_BLACK END POPUP "&Help" BEGIN MENUITEM "&Help...", IDM_APP_HELP MENUITEM "&About PopMenu...", IDM_APP_ABOUT END END END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by PopMenu.rc #define IDM_FILE_NEW 40001 #define IDM_FILE_OPEN 40002 #define IDM_FILE_SAVE 40003 #define IDM_FILE_SAVE_AS 40004 #define IDM_APP_EXIT 40005 #define IDM_EDIT_UNDO 40006 #define IDM_EDIT_CUT 40007 #define IDM_EDIT_COPY 40008 #define IDM_EDIT_PASTE 40009 #define IDM_EDIT_CLEAR 40010 #define IDM_BKGND_WHITE 40011 #define IDM_BKGND_LTGRAY 40012 #define IDM_BKGND_GRAY 40013 #define IDM_BKGND_DKGRAY 40014 #define IDM_BKGND_BLACK 40015 #define IDM_APP_HELP 40016 #define IDM_APP_ABOUT 40017

資源描述檔POPMENU.RC定義的功能表與MENUDEMO.RC中的功能表非常相似。不同的是,在頂層功能表中只包含一項-一個突現式功能表「MyMenu」,它呼叫「File」、「Edit」、「Background」和「Help」選項。這四個選項垂直一行地出現在突現式功能表上,而不是水平一列地出現在主功能表上。

在WndProc中的WM_CREATE處理期間,POPMENU取得此突現式功能表的代號,就是帶有文字「MyMenu」的那個突現式功能表:

在WM_RBUTTONUP訊息處理期間,POPMENU提供了滑鼠指標的位置,將此位置轉換為螢幕座標,再將座標值傳遞給TrackPopupMenu:

hMenu = LoadMenu (hInst, szAppName) ; hMenu = GetSubMenu (hMenu, 0) ;

point.x = LOWORD (lParam) ; point.y = HIWORD (lParam) ; ClientToScreen (hwnd, &point) ; TrackPopupMenu (hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL) ;

然後,Windows顯示出具有「File」、「Edit」、「Background」和「Help」項的突現式功能表。選擇其中任何一項都可以使嵌套的突現式功能表顯示在右邊,功能表函式與一般的功能表一樣。

如果要使用與該程式的主功能表相同的功能表並帶有TrackPopupMenu,您會遇到一些問題,因為函式需要突現式功能表代號。在「Microsoft Knowledge Base」文章ID Q99806有提供一些資訊。

使用系統功能表

使用WS_SYSMENU樣式建立的父視窗,在其標題列的左側有一個系統功能表按鈕。如果您願意,可以修改這個功能表。在Windows程式設計的早期,程式寫作者一般把「About」功能表項放入系統功能表。雖然這種方法不常見,但是修改系統功能表往往是一種在短程式中添加功能表的快速偷懶方法。這裏唯一的限制是:在系統功能表中增加的命令其ID值必須小於0xF000;否則它們將會與Windows系統功能表命令所使用的ID值相衝突。還要記住,當您為這些新功能表項在視窗訊息處理程式中處理WM_SYSCOMMAND訊息時,您必須把其他的WM_SYSCOMMAND訊息發送給DefWindowProc。如果您不這樣做,那麼實際上是禁用了系統功能表上的所有正常選項。

程式10-7中所示的POORMENU(「設計不當的個人功能表」)在系統功能表中加入了一個分隔條和三個命令,最後一個命令將刪除這些附加的功能表項。

程式10-7 POORMENU POORMENU.C /*------------------------------------------------------------------------- POORMENU.C -- The Poor Person's Menu (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define IDM_SYS_ABOUT 1 #define IDM_SYS_HELP 2 #define IDM_SYS_REMOVE 3 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; static TCHAR szAppName[] = TEXT ("PoorMenu") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HMENU hMenu ; HWND hwnd ; MSG msg ; WNDCLASS 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.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("The Poor-Person's Menu"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; hMenu = GetSystemMenu (hwnd, FALSE) ; AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_ABOUT, TEXT ("About...")) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_HELP, TEXT ("Help...")) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_REMOVE, TEXT ("Remove Additions")) ; 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_SYSCOMMAND: switch (LOWORD (wParam)) { case IDM_SYS_ABOUT: MessageBox ( hwnd, TEXT ("A Poor-Person's Menu Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_OK | MB_ICONINFORMATION) ; return 0 ; case IDM_SYS_HELP: MessageBox ( hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; return 0 ; case IDM_SYS_REMOVE: GetSystemMenu (hwnd, TRUE) ; return 0 ; } break ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

三個功能表ID在POORMENU.C的開始部分定義:

#define IDM_ABOUT 1 #define IDM_HELP 2 #define IDM_REMOVE 3

在程式視窗建立之後,POORMENU得到一個系統功能表的代號:

第一次呼叫GetSystemMenu時,您應該為修改功能表作準備,將第二個參數設定為FALSE。

使用四個AppendMenu呼叫來實作對功能表的修改:

hMenu = GetSystemMenu (hwnd, FALSE) ;

AppendMenu (hMenu, MF_SEPARATOR, 0, NULL) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_ABOUT, TEXT ("About...")) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_HELP, TEXT ("Help...")) ; AppendMenu (hMenu, MF_STRING, IDM_SYS_REMOVE, TEXT ("Remove Additions"));

第一個AppendMenu呼叫是添加分隔條。選擇「Remove Additions」功能表項將使POORMENU刪除這些附加的功能表項,這只要把第二個參數設定為TRUE,再次呼叫GetSystemMenu即可:

標準系統功能表有下列選項:Restore、Move、Size、Minimize、Maximize和Close。它們產生wParam分別等於SC_RESTORE、SC_MOVE、SC_SIZE、SC_MINIMUM、SC_MAXIMUM和SC_CLOSE的WM_SYSCOMMAND訊息。儘管Windows程式一般不這樣做,但是您可以自己處理這些訊息,而不把它們留給DefWindowProc。您也可以使用下面所述的方法來禁止或者除掉系統功能表的標準選項。Windows文件中還介紹了一些系統功能表的標準附加項目,這些附加項目使用識別字SC_NEXTWINDOW、SC_PREVWINDOW、SC_VSCROLL、SC_HSCROLL和SC_ARRANGE。您也許會發現,在一些應用程式中將這些命令加入系統功能表是合適的。

改變功能表

我們已經看到了如何使用AppendMenu函式為程式定義功能表以及將功能表項加入到系統功能表中。在Windows 3.0之前,您不得不被迫使用ChangeMenu函式來完成這種工作。ChangeMenu函式有很多功能,至少在當時,整個Windows中它是最複雜的函式之一。現在,許多函式都比ChangeMenu函式還要複雜,並且ChangeMenu的功能被分解為五個新的函式:

GetSystemMenu (hwnd, TRUE) ;

  • AppendMenu 在功能表尾部添加一個新的功能表項目
  • DeleteMenu 刪除功能表中一個現有的功能表項並清除該項目
  • InsertMenu 在功能表中插入一個新項目
  • ModifyMenu 修改一個現有的功能表項目
  • RemoveMenu 從功能表中移走某一項目

如果功能表項是一個突現式功能表,那麼DeleteMenu和RemoveMenu之間的區別就很重要。DeleteMenu清除突現式功能表,但RemoveMenu不清除它。

其他功能表命令

下面是在使用功能表時一些有用的函式。

當您改變頂層功能表項時,直到Windows重畫功能表列時才顯示所做的改變。您可以通過下列呼叫來強迫執行功能表更新:

注意,DrawMenuBar的參數是視窗代號而不是功能表代號。

您可以使用下列命令來獲得突現式功能表的代號:

其中iPosition是hMenu指示的頂層功能表中突現式功能表項的索引(開始為0)。然後您可以在其他函式中使用突現式功能表代號(例如在AppendMenu函式中)。

您可以使用下列命令獲得頂層功能表或者突現式功能表中目前的項數:

您可以取得突現式功能表項的功能表ID:

其中iPosition是功能表項在突現式功能表中的位置(以0開始)。

在MENUDEMO中您已經看到如何選中、或者取消選中突現式功能表中的某一項:

在MENUDEMO中,hMenu是頂層功能表的代號,id是功能表ID,而iCheck的值是MF_CHECKED或MF_UNCHECKED。如果hMenu是突現式功能表代號,那麼參數id是位置索引而不是功能表ID。如果使用索引會更方便的話,那麼您可以在第三個參數中包含MF_BYPOSITION,例如:

除了第三個參數是MF_ENABLED、MF_DISABLED或MF_GRAYED外,EnableMenuItem函式與CheckMenuItem函式所完成的工作類似。如果您在具有突現式功能表的頂層功能表項上使用EnableMenuItem,那麼必須在第三個參數中使用MF_BYPOSITION識別字,因為功能表項沒有功能表ID。我們將在本章後面所示的 POPPAD2程式 中看到EnableMenuItem的一個例子。 HiliteMenuItem也類似於CheckMenuItem和EnableMenuItem,但是它使用的是MF_HILITE和MF_UNHILITE。當您在功能表項之間移動時,Windows使用反白顯示方式加亮顯示功能表項。您通常不需要使用HiliteMenuItem。

您還需要對您的功能表做些什麼呢?還記得我們在功能表中使用了哪些字串嗎?您可以透過下面的呼叫來回顧一下:

iFlag可以是MF_BYCOMMAND(其中id是功能表ID),也可以是MF_BYPOSITION(其中的id是位置索引)。函式將字串的iMaxCount個位元組複製到pString中,並傳回複製的位元組數。

或許您也想知道功能表項目前的屬性是什麼:

同樣地,iFlag可以是MF_BYCOMMAND或MF_BYPOSITION。傳回值iFlags是目前所有屬性的組合,您可以通過對MF_DISABLED、MF_GRAYED、MF_CHECKED、MF_MENUBREAK、MF_MENUBARBREAK和MF_SEPARATOR識別字的檢測來決定目前的屬性。

也許現在您對功能表有了一些瞭解。這時您可能想知道,如果您不再需要功能表時又應該如何處理。您可以使用下面的命令來清除功能表:

從而使功能表代號無效。

建立功能表的非正統方法

現在讓我們稍微偏離我們所討論的主題。如果在您的程式中沒有下拉式功能表,而是建立了多個沒有突現式功能表的頂層功能表,並呼叫SetMenu在頂層功能表之間切換,那會是什麼樣的情形呢?就像Lotus 1-2-3中老式的文字模式功能表那樣。程式10-8中的NOPOPUPS程式展示了處理這種情況。在這個程式中,「File」和「Edit」項與MENUDEMO程式中的類似,但是卻以另一種頂層功能表顯示出來。

DrawMenuBar (hwnd) ;

hMenuPopup = GetSubMenu (hMenu, iPosition) ;

iCount = GetMenuItemCount (hMenu) ;

id = GetMenuItemID (hMenuPopup, iPosition) ;

CheckMenuItem (hMenu, id, iCheck) ;

CheckMenuItem (hMenu, iPosition, MF_CHECKED | MF_BYPOSITION) ;

iCharCount = GetMenuString (hMenu, id, pString, iMaxCount, iFlag) ;

iFlags = GetMenuState (hMenu, id, iFlag) ;

DestroyMenu (hMenu) ;

程式10-8 NOPOPUPS NOPOPUPS.C /*------------------------------------------------------------------------- NOPOPUPS.C -- Demonstrates No-Popup Nested Menu (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("NoPopUps") ; HWND hwnd ; MSG msg ; WNDCLASS 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.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("No-Popup Nested Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; 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) { static HMENU hMenuMain, hMenuEdit, hMenuFile ; HINSTANCE hInstance ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ; hMenuFile = LoadMenu (hInstance, TEXT ("MenuFile")) ; hMenuEdit = LoadMenu (hInstance, TEXT ("MenuEdit")) ; SetMenu (hwnd, hMenuMain) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_MAIN: SetMenu (hwnd, hMenuMain) ; return 0 ; case IDM_FILE: SetMenu (hwnd, hMenuFile) ; return 0 ; case IDM_EDIT: SetMenu (hwnd, hMenuEdit) ; return 0 ; case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; } break ; case WM_DESTROY: SetMenu (hwnd, hMenuMain) ; DestroyMenu (hMenuFile) ; DestroyMenu (hMenuEdit) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

NOPOPUPS.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu MENUMAIN MENU DISCARDABLE BEGIN MENUITEM "MAIN:", 0, INACTIVE MENUITEM "&File...", IDM_FILE MENUITEM "&Edit...", IDM_EDIT END MENUFILE MENU DISCARDABLE BEGIN MENUITEM "FILE:", 0, INACTIVE MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open...", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As", IDM_FILE_SAVE_AS MENUITEM "(&Main)", IDM_MAIN END MENUEDIT MENU DISCARDABLE BEGIN MENUITEM "EDIT:", 0, INACTIVE MENUITEM "&Undo", IDM_EDIT_UNDO MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT_PASTE MENUITEM "De&lete", IDM_EDIT_CLEAR MENUITEM "(&Main)", IDM_MAIN END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by NoPopups.rc #define IDM_FILE 40001 #define IDM_EDIT 40002 #define IDM_FILE_NEW 40003 #define IDM_FILE_OPEN 40004 #define IDM_FILE_SAVE 40005 #define IDM_FILE_SAVE_AS 40006 #define IDM_MAIN 40007 #define IDM_EDIT_UNDO 40008 #define IDM_EDIT_CUT 40009 #define IDM_EDIT_COPY 40010 #define IDM_EDIT_PASTE 40011 #define IDM_EDIT_CLEAR 40012

在Microsoft Developer Studio中,您建立了三個功能表,而不是一個。從「Insert」中選擇「Resource」三次,每個功能表有一個不同的名稱。當視窗訊息處理程式處理WM_CREATE訊息時,Windows將每個功能表資源載入記憶體:

hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ; hMenuFile = LoadMenu (hInstance, TEXT ("MenuFile")) ; hMenuEdit = LoadMenu (hInstance, TEXT ("MenuEdit")) ;

開始時,程式只顯示主功能表:

主功能表使用字串「MAIN:」、「File...」和「Edit...」列出這三個選項。然而,「MAIN:」是禁用的,因此它不能使WM_COMMAND訊息被發送到視窗訊息處理程式。「File」和「Edit」功能表項以「FILE:」和「EDIT:」開始,表示它們是子功能表。每個功能表的最後一項都是字串「(Main)」,表示傳回到主功能表。在這三個功能表之間進行切換是很簡單的:

SetMenu (hwnd, hMenuMain) ;

case WM_COMMAND : switch (wParam) { case IDM_MAIN : SetMenu (hwnd, hMenuMain) ; return 0 ; case IDM_FILE : SetMenu (hwnd, hMenuFile) ; return 0 ; case IDM_EDIT : SetMenu (hwnd, hMenuEdit) ; return 0 ; 其他行程式 } break ;

在WM_DESTROY訊息處理期間,NOPOPUPS將程式的功能表設定為主功能表,並呼叫DestroyMenu來清除「File」和「Edit」功能表。當視窗被清除時,主功能表將被自動清除。

鍵盤加速鍵

加速鍵是產生WM_COMMAND訊息(有些情況下是WM_SYSCOMMAND)的鍵組合。許多時候,程式使用加速鍵來重複常用功能表項的動作(然而,加速鍵還可以用於執行非功能表功能)。例如,許多Windows程式都有一個包含「Delete」或「Clear」選項的「Edit」功能表,這些程式習慣上都將Del鍵指定為該選項的加速鍵。使用者可以通過「 Alt 鍵」從功能表中選擇「 Delete 」選項,或者只需按下加速鍵 Del 。當視窗訊息處理程式收到一個WM_COMMAND訊息時,它不必確定使用的是功能表還是加速鍵。

為什麼要使用加速鍵

您也許會問:為什麼我應該使用加速鍵?為什麼不能直接攔截WM_KEYDOWN或WM_CHAR訊息而自己實作同樣的功能表功能呢?好處又在哪裡呢?對於一個單視窗應用程式,您當然可以攔截鍵盤訊息,但是使用加速鍵可以得到一些好處:您不需要把功能表和加速鍵的處理方式重寫一遍。

對於有多個視窗和多個視窗訊息處理程式的應用程式來說,加速鍵是非常重要的。正如我們所看到的,Windows將鍵盤訊息發送給目前活動視窗的視窗訊息處理程式。然而對於加速鍵, Windows把WM_COMMAND訊息發送給視窗訊息處理程式,該視窗訊息處理程式的代號在Windows函式TranslateAccelerator中給出。通常這是主視窗,也是擁有功能表的視窗,這意味著無須每個視窗訊息處理程式都把加速鍵的操作處理程式重寫一遍。

如果您在主視窗的顯示區域中,使用了非系統模態對話方塊(在下一章中會討論)或者子視窗,那麼這種好處就變得非常重要。如果定義一個特定的加速鍵以便在不同的視窗之間移動,那麼,只需要一個視窗訊息處理程式有這個處理程式。子視窗就不會收到加速鍵引發的WM_COMMAND訊息。

安排加速鍵的幾條規則

理論上,您可以使用任何虛擬鍵或者字元鍵連同Shift鍵、Ctrl鍵或Alt鍵來定義加速鍵。然而,您應該盡力使應用程式之間協調一致,並且儘量避免干擾Windows的鍵盤使用。在加速鍵中,應該避免使用Tab、Enter、Esc和Spacebar鍵,因為這些鍵常常用於完成系統功能。

加速鍵最經常的用途是操作程式的「Edit」功能表中的各項。為這些功能表項推薦的加速鍵在Windows 3.0和Windows 3.1之間已有不同,因此通常都要支援如下所列的新舊兩套加速鍵:

表10-2

另一種常用的虛擬鍵是啟動輔助資訊的功能鍵F1。應該避免使用F4、F5和F6鍵,因為這些鍵常用在多重文件介面(MDI)程式中來完成特殊的功能(將在 第十九章 中討論)。

加速鍵表

您可以在Developer Studio中定義加速鍵表。為了讓程式中載入加速鍵表更為容易,給它和程式名相同的名稱(與功能表和圖示名也相同)。

每個加速鍵都有在 Accel Properties 對話方塊中定義的ID和按鍵組合。如果您已經定義了功能表,則功能表ID會出現在下拉式清單方塊中,因此不需要鍵入它們。

加速鍵可以是虛擬鍵或ASCII字元與Shift、Ctrl或Alt鍵的組合。可以通過在字母前鍵入『^』來指定帶有Ctrl鍵的ASCII字元。也可以從下拉式清單方塊中選取虛擬鍵。

當您為功能表項定義加速鍵時,應該將鍵的組合包含到功能表項的文字中。跳位字元(\t)將文字與加速鍵分割開,將加速鍵列在第二列。為了在功能表中為加速鍵做上標記,可以在文字「Ctrl」、「Shift」或「Alt」之後跟上一個「+」號和一個鍵名(例如,「Shift+F6」或「Ctrl+F6」)。

加速鍵表的載入

在您的程式中,您使用LoadAccelerators函式把加速鍵表載入記憶體,並獲得該表的代號。 LoadAccelerators敘述非常類似於LoadIcon、LoadCursor和LoadMenu敘述。

首先,把加速鍵表的代號定義為型態HANDLE:

然後載入加速鍵表:

正如圖示、游標和功能表一樣,您可以使用一個數值代替加速鍵表的名稱,然後在LoadAccelerators敘述中和MAKEINTRESOURCE巨集一起使用該數值,或者把它放在雙引號內,前面冠以字元「#」。

鍵盤代碼轉換

現在我們將討論底下這三行程式碼,在本書中,截至目前為止建立的所有Windows程式中都使用過它們。這些程式碼是標準的訊息迴圈:

HANDLE hAccel ;

hAccel = LoadAccelerators (hInstance, TEXT ("MyAccelerators")) ;

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

下面把上頭那段程式碼加以修改,以便使用加速鍵:

while (GetMessage (&msg, NULL, 0, 0)) { if (!TranslateAccelerator (hwnd, hAccel, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } }

TranslateAccelerator函式確認存放在msg訊息結構中的訊息是否為鍵盤訊息。如果是,該函式將找尋代號為hAccel的加速鍵表。如果找到了一個符合的,則呼叫代號為hwnd的視窗訊息處理程式。如果加速鍵ID與系統功能表的功能表項一致,則訊息就是WM_SYSCOMMAND;否則,訊息為WM_COMMAND。

當TranslateAccelerator傳回時,如果訊息已經被轉換(並且已經被發送給視窗訊息處理程式),那麼傳回值為非零;否則,傳回值為0。如果TranslateAccelerator傳回一個非零值,則不呼叫TranslateMessage和DispatchMessage,而是經過迴圈回到GetMessage呼叫中。

TranslateMessage中的參數hwnd看起來有點累贅,因為訊息迴圈中的其他三個函式都沒有要求這個參數。此外,訊息結構本身(結構變數msg)有一個叫做hwnd的成員,它是視窗代號。

該函式有些不同的原因在於:msg結構的欄位由GetMessage呼叫填入。當GetMessage的第二個參數為NULL時,函式會找尋應用程式所有視窗的訊息。當GetMessage傳回時,msg結構的hwnd是將要獲得訊息之視窗的視窗代號。然而,當TranslateAccelerator把鍵盤訊息轉換為WM_COMMAND或WM_SYSCOMMAND訊息時,它使用函式的第一個參數指定的視窗代號hwnd來代替視窗代號msg.hwnd。Windows就是這樣把所有加速鍵訊息發送給同一視窗訊息處理程式的,即使另一個應用視窗目前擁有輸入焦點。當系統模態對話方塊或者訊息方塊擁有輸入焦點時,TranslateAccelerator不會轉換鍵盤訊息,因為這些視窗的訊息是不經過程式的訊息迴圈的。

在某些情況下,當您程式的另一個視窗(比如一個非系統模態對話方塊)擁有輸入焦點時,您也許不想轉換加速鍵。您將在 下一章 中看到如何處理這種情況。

接收加速鍵訊息

當加速鍵與系統功能表中的功能表項相對應時,TranslateAccelerator給視窗訊息處理程式發送一個WM_SYSCOMMAND訊息,否則,TranslateAccelerator給視窗訊息處理程式發送一個WM_COMMAND訊息。下表所示為幾種可能接收到的WM_COMMAND訊息,這些訊息用於加速鍵、功能表命令以及子視窗控制項:

表10-3

如果加速鍵與一個功能表項對應,那麼視窗訊息處理程式還會收到WM_INITMENU、WM_INITMENUPOPUP和WM_MENUSELECT訊息,就好像選中了功能表選項一樣。在處理WM_INITMENUPOPUP時,程式往往啟用和禁用突現式功能表中的功能表項,因此,在使用加速鍵時,您仍然能夠實作這類功能。如果加速鍵與一個禁用或者無效化的功能表項相對應,那麼,TranslateAccelerator函式就不會向視窗訊息處理程式發送WM_COMMAND或WM_SYSCOMMAND訊息。

如果活動視窗已經被最小化,那麼TranslateAccelerator將為與啟用的系統功能表項相對應的加速鍵向視窗訊息處理程式發送WM_SYSCOMMAND訊息,而不是WM_COMMAND訊息。TranslateAccelerator也會為沒有任何功能表項與之對應的加速鍵,來向視窗訊息處理程式發送WM_COMMAND訊息。

功能表與加速鍵應用程式POPPAD

第九章 ,我們建立了一個叫做 POPPAD1 的程式,它使用了子視窗編輯控制項來實作基本的筆記本功能。在這一章中,我們將加入「File」和「Edit」功能表,並稱此程式為POPPAD2。「Edit」功能表的功能表項的功能全部可用;我們將在 第十一章 中完成「File」功能,在 第十三章 中完成「Print」功能。POPPAD2如程式10-9所示。

程式10-9 POPPAD2 POPPAD2.C /*--------------------------------------------------------------------------- POPPAD2.C -- Popup Editor Version 2 (includes menu) (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_EDIT 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT ("PopPad2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, szAppName, WS_OVERLAPPEDWINDOW, GetSystemMetrics (SM_CXSCREEN) / 4, GetSystemMetrics (SM_CYSCREEN) / 4, GetSystemMetrics (SM_CXSCREEN) / 2, GetSystemMetrics (SM_CYSCREEN) / 2, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) { if (!TranslateAccelerator (hwnd, hAccel, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return msg.wParam ; } AskConfirmation (HWND hwnd) { return MessageBox ( hwnd, TEXT ("Really want to close PopPad2?"), szAppName, MB_YESNO | MB_ICONQUESTION) ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndEdit ; int iSelect, iEnable ; switch (message) { case WM_CREATE: hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL, 0, 0, 0, 0, hwnd, (HMENU) ID_EDIT, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; return 0 ; case WM_SETFOCUS: SetFocus (hwndEdit) ; return 0 ; case WM_SIZE: MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM_INITMENUPOPUP: if (lParam == 1) { EnableMenuItem ((HMENU) wParam, IDM_EDIT_UNDO, SendMessage (hwndEdit, EM_CANUNDO, 0, 0) ? MF_ENABLED : MF_GRAYED) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_PASTE, IsClipboardFormatAvailable (CF_TEXT) ? MF_ENABLED : MF_GRAYED) ; iSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0) ; if (HIWORD (iSelect) == LOWORD (iSelect)) iEnable = MF_GRAYED ; else iEnable = MF_ENABLED ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_CUT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_COPY, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_CLEAR, iEnable) ; return 0 ; } break ; case WM_COMMAND: if (lParam) { if (LOWORD (lParam) == ID_EDIT && (HIWORD (wParam) == EN_ERRSPACE || HIWORD (wParam) == EN_MAXTEXT)) MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB_OK | MB_ICONSTOP) ; return 0 ; } else switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_FILE_PRINT: MessageBeep (0) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_UNDO: SendMessage (hwndEdit, WM_UNDO, 0, 0) ; return 0 ; case IDM_EDIT_CUT: SendMessage (hwndEdit, WM_CUT, 0, 0) ; return 0 ; case IDM_EDIT_COPY: SendMessage (hwndEdit, WM_COPY, 0, 0) ; return 0 ; case IDM_EDIT_PASTE: SendMessage (hwndEdit, WM_PASTE, 0, 0) ; return 0 ; case IDM_EDIT_CLEAR: SendMessage (hwndEdit, WM_CLEAR, 0, 0) ; return 0 ; case IDM_EDIT_SELECT_ALL: SendMessage (hwndEdit, EM_SETSEL, 0, -1) ; return 0 ; case IDM_HELP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"), szAppName, MB_OK | MB_ICONINFORMATION) ; return 0 ; } break ; case WM_CLOSE: if (IDYES == AskConfirmation (hwnd)) DestroyWindow (hwnd) ; return 0 ; case WM_QUERYENDSESSION: if (IDYES == AskConfirmation (hwnd)) return 1 ; else return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }

POPPAD2.RC (摘錄) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Icon POPPAD2 ICON DISCARDABLE "poppad2.ico" ///////////////////////////////////////////////////////////////////////////// // Menu POPPAD2 MENU DISCARDABLE BEGIN POPUP "&File" BEGIN MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open...", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As...", IDM_FILE_SAVE_AS MENUITEM SEPARATOR MENUITEM "&Print", IDM_FILE_PRINT MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tCtrl+Z", IDM_EDIT_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE MENUITEM "De&lete\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Select All", IDM_EDIT_SELECT_ALL END POPUP "&Help" BEGIN MENUITEM "&Help...", IDM_HELP_HELP MENUITEM "&About PopPad2...", IDM_APP_ABOUT END END ///////////////////////////////////////////////////////////////////////////// // Accelerator POPPAD2 ACCELERATORS DISCARDABLE BEGIN VK_BACK, IDM_EDIT_UNDO, VIRTKEY, ALT, NOINVERT VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT VK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT VK_F1, IDM_HELP_HELP, VIRTKEY, NOINVERT VK_INSERT, IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT "^C", IDM_EDIT_COPY, ASCII, NOINVERT "^V", IDM_EDIT_PASTE, ASCII, NOINVERT "^X", IDM_EDIT_CUT, ASCII, NOINVERT "^Z", IDM_EDIT_UNDO, ASCII, NOINVERT END

RESOURCE.H (摘錄) // Microsoft Developer Studio generated include file. // Used by POPPAD2.RC #define IDM_FILE_NEW 40001 #define IDM_FILE_OPEN 40002 #define IDM_FILE_SAVE 40003 #define IDM_FILE_SAVE_AS 40004 #define IDM_FILE_PRINT 40005 #define IDM_APP_EXIT 40006 #define IDM_EDIT_UNDO 40007 #define IDM_EDIT_CUT 40008 #define IDM_EDIT_COPY 40009 #define IDM_EDIT_PASTE 40010 #define IDM_EDIT_CLEAR 40011 #define IDM_EDIT_SELECT_ALL 40012 #define IDM_HELP_HELP 40013 #define IDM_APP_ABOUT 40014

POPPAD2.ICO

POPPAD2.RC資源描述檔包含功能表和加速鍵。您將注意到,所有加速鍵都表示在跳位字元(\t)後的「Edit」突現式功能表的字串中。

啟用功能表項

視窗訊息處理程式的工作包括啟用和無效化「Edit」功能表中的選項,這項工作在處理WM_INITMENUPOPUP時完成。首先,程式檢查是否要顯示「Edit」突現式功能表。因為功能表裏「Edit」的位置索引(「File」從0開始)是1,因此如果即將顯示「Edit」突現式功能表,那麼lParam應該等於1。

為了確定是否啟用「Undo」選項,POPPAD2給編輯控制項發送一條EM_CANUNDO訊息。如果編輯控制項能夠執行「Undo」動作,那麼SendMessage呼叫傳回非零值。在這種情況下,選項被啟用;否則,選項無效化:

EnableMenuItem (wParam, IDM_UNDO, SendMessage (hwndEdit, EM_CANUNDO, 0, 0) ? MF_ENABLED : MF_GRAYED) ;

只有當剪貼簿中包含文字時,「Paste」選項才能夠被啟用。我們可以使用CF_TEXT識別字通過IsClipboardFormatAvailable呼叫來確定這一點:

只有選擇了編輯控制項中的文字,「Cut」、「Copy」和「Delete」選項才能夠被啟用。給編輯控制項發送一條EM_GETSEL訊息,並傳回包含此資訊的整數:

iSelect的低位元字是第一個被選中字元的位置,iSelect的高字組是下一個被選中字元的位置。如果這兩個字相等,則表示沒有選中文字:

EnableMenuItem (wParam, IDM_PASTE, IsClipboardFormatAvailable (CF_TEXT) ? MF_ENABLED : MF_GRAYED) ;

iSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0) ;

if (HIWORD (iSelect) == LOWORD (iSelect)) iEnable = MF_GRAYED ; else iEnable = MF_ENABLED ;

然後可以將iEnable的值用於「Cut」、「Copy」和「Delete」選項:

EnableMenuItem (wParam, IDM_CUT, iEnable) ; EnableMenuItem (wParam, IDM_COPY, iEnable) ; EnableMenuItem (wParam, IDM_DEL, iEnable) ;

處理功能表項

當然,如果POPPAD2程式不使用子視窗編輯控制項,那麼我們將面臨一些問題,這涉及如何完成「Edit」功能表中的「Undo」、「Cut」、「Copy」、「Paste」、「Clear」和「Select All」選項。正是編輯控制項使得這種處理變得容易,因為對於每一個選項我們只需向編輯控制項發送一個訊息即可:

case IDM_UNDO : SendMessage (hwndEdit, WM_UNDO, 0, 0) ; return 0 ; case IDM_CUT : SendMessage (hwndEdit, WM_CUT, 0, 0) ; return 0 ; case IDM_COPY : SendMessage (hwndEdit, WM_COPY, 0, 0) ; return 0 ; case IDM_PASTE : SendMessage (hwndEdit, WM_PASTE, 0, 0) ; return 0 ; case IDM_DEL : SendMessage (hwndEdit, WM_DEL, 0, 0) ; return 0 ; case IDM_SELALL : SendMessage (hwndEdit, EM_SETSEL, 0, -1) ; return 0 ;

注意,我們可以更進一步簡化這些處理-只要使IDM_UNDO、IDM_CUT等等的值等於相對應的視窗訊息WM_UNDO、WM_CUT的值。

「File」突現式功能表上的「About」選項啟動一個簡單的訊息方塊:

case IDM_ABOUT : MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"), szAppName, MB_OK | MB_ICONINFORMATION) ; return 0 ;

下一章 中,我們將把它變成一個對話方塊。當您從功能表中選擇「Help」選項或者按下F1加速鍵時,同樣可以啟動一個訊息方塊。

「Exit」選項向視窗訊息處理程式發送一個WM_CLOSE訊息:

case IDM_EXIT : SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ;

這正是DefWindowProc收到一個wParam等於SC_CLOSE的WM_SYSCOMMAND訊息時所完成的工作。

在前面的那些程式中,我們沒有在視窗訊息處理程式中處理WM_CLOSE訊息,而只是簡單地把它送給DefWindowProc。DefWindowProc對WM_CLOSE的處理非常簡單:呼叫DestroyWindow函式。可以不把WM_CLOSE訊息送給DefWindowProc,而讓POPPAD2來處理它。這個事實到目前為止並不重要,但是在 第十一章 中當POPPAD可以真正編輯文字時,它就變得非常重要了。

case WM_CLOSE : if (IDYES == AskConfirmation (hwnd)) DestroyWindow (hwnd) ; return 0 ;

AskConfirmation是POPPAD2中的一個函式,它顯示一個請求確認關閉程式的訊息方塊:

AskConfirmation (HWND hwnd) { return MessageBox (hwnd, TEXT ("Really want to close Poppad2?"), szAppName, MB_YESNO | MB_ICONQUESTION) ; }

如果選擇了Yes按鈕的話,訊息方塊(以及AskConfirmation函式)將傳回IDYES。只有這樣,程式才會呼叫DestroyWindow,否則,程式不會結束。

如果要在程式結束之前確認使用者真的要結束程式,那麼您還必須處理WM_QUERYENDSESSION訊息。當使用者要關閉Windows時,Windows開始向每個視窗訊息處理程式發送一個WM_QUERYENDSESSION訊息。如果有任何一個視窗訊息處理程式處理這個訊息後傳回0,那麼Windows將不會結束。我們如下處理了WM_QUERYENDSESSION:

case WM_QUERYENDSESSION : if (IDYES == AskConfirmation (hwnd)) return 1 ; else return 0 ;

如果要在程式結束之前要求使用者的確認,必須處理WM_CLOSE和WM_QUERYENDSESSION這兩個訊息,這就是為什麼我們使POPPAD2中的「Exit」功能表選項只向視窗訊息處理程式發送一個WM_CLOSE訊息的原因。這樣做,我們避免了在別處進行請求確認的動作。

如果要處理WM_QUERYENDSESSION訊息,那麼您也許還會對WM_ENDSESSION訊息感興趣。Windows把這個訊息發送給先前收到WM_QUERYENDSESSION訊息的每個視窗訊息處理程式。如果由於另一個程式從WM_QUERYENDSESSION傳回了0而不能結束Windows的執行,那麼WM_ENDSESSION的wParam參數為0。WM_ENDSESSION訊息實際上回答了這個問題:我告訴過Windows可以把我結束掉,但是我真的被結束掉了嗎?

儘管在POPPAD2的「File」功能表中我加上了常見的「New」、「Open」、「Save」和「Save As」選項,但是它們現在並不起作用。要處理這些命令,我們需要使用對話方塊。現在是討論對話方塊的時機,也是您準備學習它們的時候了。