Ch 16 再談選單

在第六章堙A小木偶已經談過選單的基本用法,這一章堭N介紹如何更動選單、如何在選單中顯示 BMP 檔、如何改變選單中的字型,最後實作一個 MENU2 程式,藉由此程式展示選單性質。


選單

選單的種類

選單可分為系統選單、彈出式選單與主選單三種。系統選單是指使用者在標題欄最左邊的圖示按下滑鼠左鍵時,所出現的選單,如下圖左上角紅色框框所標示:

彈出式選單是指使用者在視窗的工作區中,按下滑鼠右鍵,所彈出的選單。而主選單是在標題欄下面的選單,如上圖右上角以紫色框框所框起來的部份就是主選單。

主選單、子選單與選項

下圖是一個常見的視窗,在標題欄下面的選單,一般稱為『主選單』,所有選單或選項都由它開始,在下面的例子中,主選單包含檔案、編輯、檢視、影像、色彩、說明六個彈出選項 ( pop menu )。『彈出選項』是指被使用者點選之後,會出現另一個選單,所出現的選單是由某個選單的彈出選項所引發,因此一般稱為『子選單』( submenu ),例如,點選『檢視』之後,會出現一個『子選單』,所以檢視被稱為彈出選項,而出現的這個選單稱為『子選單』,包含工具箱、色塊、狀態列、文字工具列、分隔線、縮放、檢視點陣圖等項目,其中縮放也是一個彈出選項,它會引出含有標準大小、放大、自訂、分隔線、顯示格線、顯示縮圖的子選單 ( 可以把它想成『孫』選單,但是在文獻上還是稱它為子選單 )。再回到檢視所彈出的子選單,包含了工具箱、色塊、狀態列、文字工具列、檢視點陣圖,它們都是選項 ( menuitem ),使用者點選選項時,會引發某些功能,可能是某個對話盒或其他功能,也可能是某個子選單。

在主選單或子選單上的每個項目都有三種性質:

  1. 名稱:名稱是顯示在選單上的文字字串。有時也可以用位元圖顯示。
  2. 識別碼 ( ID ) 或選單代碼 ( menu handle ):當使用者選擇某個選項時,系統會把該選項的識別碼會包含在 WM_COMMAND 訊息媔レ^給視窗函式;但是使用者按下彈出選項時,表示還未選定,因此彈出選項是沒有識別碼。當使用者按下彈出選項,引發一個子選單時,這個子選單有一選單代碼,但是沒有識別碼。因為有主選單或子選單有選單代碼,我們可以對選單做一些特別的繪製,例如更改選項、新增選項、用點陣圖代替字串等等。分隔線僅用於表示視覺上分類效果之用,所以沒有識別碼也沒有代碼。
  3. 屬性:選項可以有下面三組屬性:(1)啟用、禁用、無效的,(2)被勾選的、不被勾選的,(3)被選擇的、不被選擇的。
    禁用與無效的選項都以灰色字串表示。使用者仍可以用滑鼠或 游標鍵移到禁用的選項,而該選項會以反白顯示,如下圖顯示縮圖選項;但是使用者無法用滑鼠或游標移到無效的選項使之反白。不管是無效或禁用的選項,都不能發出 WM_COMMAND 訊息,只有啟用的選項可以發出 WM_COMMAND。
    被勾選的選項前會有一個記號,像下圖的工具箱、色塊等選項,這些被勾選的可以一個或多個。
    另外選項也有被選擇或不被選擇的屬性,當滑鼠游標移到某個選項時,該選項背景會以高亮度光棒顯示,文字則以反白顯示,則此選項稱為被選擇的選項;反之則是不被選擇的選項。

建立、修改選單

建立選單的方法,最常用的是在資源描述檔中建立 MENU 區塊,然後用 LoadMenu 載入到記憶體中,詳細情形請參閱第六章。除了這個方法外,也可以在程式中使用 CreateMenu 先建立一個空的選單,然後用 InsertMenuItem 或 AppendMenu 加入選項、用 SetMenuItemInfo 改變選項性質,用 DeleteMenu 刪除選項。

CreateMenu 與 AppendMenu

要建立一個選單,需用 CreateMenu,CreateMenu 沒有任何參數,如果失敗,EAX 傳回 NULL;如果成功,EAX 會傳回新建立的選單代碼,而這個選單內容是空的,意思是堶惆S有任何選項,必須再由 AppendMenu、InsertMenuItem 加入選項。

AppendMenu 的功用是在主選單、子選單或彈出式選單中再加上一個選項,其原型是:

BOOL AppendMenu(
  HMENU   hMenu,        // handle to menu to be changed
  UINT    uFlags,       // menu-item flags
  UINT    uIDNewItem,   // menu-item identifier or handle to drop-down menu or submenu
  LPCTSTR lpNewItem     // menu-item content
);

hMenu 是指選項要加在那一個選單中,uFlags 指加入選項的特性,可以是下面前四組數值之一或其互相組合,但同一組之間的數值不能同時使用,例如選項是正常啟用的 ( MF_ENABLE ) 就不能同時又禁用的 ( MF_DISABLED )。底下是 uFlags 參數的說明:

SetMenu API

要設定某一視窗的主選單,則使用 SetMenu,其原型為:

BOOL SetMenu(
  HWND  hWnd,  // handle to window
  HMENU hMenu  // handle to menu
);

底下的程式,MENU1.ASM,說明如何在程式中而不是在資源描述檔中建立選單,以及 MF_SEPARATOR、MF_MENUBARBREAK、MF_MENUBARBREAK 屬性所產生的效果。

        .586
        .model  flat,stdcall
        option  casemap:none

include         windows.inc
include         kernel32.inc
include         user32.inc
includelib      kernel32.lib
includelib      user32.lib
;***********************************************************
                .data
hInstance       HINSTANCE       ?
hwnd            HWND            ?
hMenu           HMENU           ?
hSubMenuFile    HMENU           ?
hSubMenuHelp    HMENU           ?
msg             MSG             <?>
wc              WNDCLASSEX      <30h,?,?,0,0,?,?,?,?,0,offset szClassName,?>
szClassName     BYTE            'MENU_TEST',0
szAppName       BYTE            '測試選單',0
szFile          BYTE            '檔案',0
szNew           BYTE            '開新檔案',0
szOpen          BYTE            '開啟舊檔',0
szSave          BYTE            '儲存檔案',0
szSaveAs        BYTE            '另存新檔',0
szClose         BYTE            '關閉檔案',0
szPrint         BYTE            '列印',0
szExit          BYTE            '退出',0
szHelp          BYTE            '說明',0
szAbout         BYTE            '關於',0
szVersion       BYTE            '版本',0
szQuery         BYTE            'HELP',0
;***********************************************************
        .code
;-----------------------------------------------------------
WndProc proc    hWnd:HWND,uMsg:UINT,wParam:WPARAM,lParam:LPARAM
.if uMsg==WM_COMMAND
        mov     ecx,wParam
    .if cx==106h
        jmp     exit
    .endif

.elseif uMsg==WM_CREATE
        ;建立一個空的選單,設此選單為主選單,並保存其選單代碼
        invoke  CreateMenu              
        mov     hMenu,eax

        ;再建立兩個空的選單,分別是檔案子選單與說明子選單,並保存其選單代碼
        invoke  CreateMenu
        mov     hSubMenuFile,eax
        invoke  CreateMenu
        mov     hSubMenuHelp,eax

        ;在檔案子選單中加入各選項
        invoke  AppendMenu,hSubMenuFile,MF_STRING,100h,offset szNew
        invoke  AppendMenu,hSubMenuFile,MF_STRING,101h,offset szOpen
        invoke  AppendMenu,hSubMenuFile,MF_STRING,102h,offset szSave
        invoke  AppendMenu,hSubMenuFile,MF_STRING,103h,offset szSaveAs
        invoke  AppendMenu,hSubMenuFile,MF_STRING,104h,offset szClose   ;47 關閉檔案選項
        invoke  AppendMenu,hSubMenuFile,MF_STRING,105h,offset szPrint
        invoke  AppendMenu,hSubMenuFile,MF_SEPARATOR,NULL,NULL          ;48 分隔線    
        invoke  AppendMenu,hSubMenuFile,MF_STRING,106h,offset szExit

        ;在說明子選單中加入各選項
        invoke  LoadBitmap,hInstance,offset szQuery
        invoke  AppendMenu,hSubMenuHelp,MF_BITMAP,200h,eax
        invoke  AppendMenu,hSubMenuHelp,MF_STRING,201h,offset szVersion

        ;把子選單加入到主選單中,uFlags 用 MF_POPUP,而第一個參數用主選單代碼
        invoke  AppendMenu,hMenu,MF_POPUP,hSubMenuFile,offset szFile
        invoke  AppendMenu,hMenu,MF_POPUP,hSubMenuHelp,offset szHelp

        ;設定此視窗的主選單為 hMenu
        invoke  SetMenu,hWnd,hMenu

.elseif uMsg==WM_DESTROY
exit:   invoke  PostQuitMessage,NULL

.else
        invoke  DefWindowProc,hWnd,uMsg,wParam,lParam
        ret
.endif
        xor     eax,eax
        ret
WndProc endp
;-----------------------------------------------------------
start:  invoke  GetModuleHandle,NULL
        mov     hInstance,eax
        invoke  GetCommandLine
        mov     wc.style,CS_HREDRAW or CS_VREDRAW
        mov     wc.lpfnWndProc,offset WndProc
        mov     eax,hInstance
        mov     wc.hInstance,eax
        mov     wc.hbrBackground,COLOR_WINDOW+1
        invoke  LoadIcon,NULL,IDI_APPLICATION
        mov     wc.hIcon,eax
        mov     wc.hIconSm,eax
        invoke  LoadCursor,NULL,IDC_ARROW
        mov     wc.hCursor,eax
        invoke  RegisterClassEx,offset wc
        invoke  CreateWindowEx,NULL,offset szClassName,offset szAppName,\ 
                WS_OVERLAPPEDWINDOW,0,10,200,200,0,NULL,hInstance,NULL 
        mov     hwnd,eax
        invoke  ShowWindow,eax,SW_SHOWDEFAULT
        invoke  UpdateWindow,hwnd
.while  TRUE
        invoke  GetMessage,offset msg,NULL,0,0
.break  .if     !eax
        invoke  TranslateMessage,offset msg
        invoke  DispatchMessage,offset msg
.endw
        mov     eax,msg.wParam
        invoke  ExitProcess,eax
;***********************************************************
        end     start

底下是 MENU1.RC 資源描述檔的內容:

HELP    BITMAP  "HELP.BMP"

右圖是 HELP.BMP 檔:

在『命令提示字元』中組譯此程式,輸入下面兩道指令:

E:\HomePage\SOURCE>rc menu1.rc [Enter]

E:\HomePage\SOURCE>ml menu1.asm /link menu1.res [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: menu1.asm
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"menu1.obj"
"/OUT:menu1.exe"
"menu1.res"

E:\HomePage\SOURCE>

上面程式執行後得下圖最左邊的選單,在子選單中,有一分隔線。若把第 47 行的『MF_STRING』改成『MF_STRING or MF_MENUBREAK』,並刪除第 49 行,再組譯,則得到的選單如下圖左二。如果再把『MF_STRING or MF_MENUBREAK』改成『MF_STRING or MF_MENUBARBREAK』,則選單變成下圖右二。下圖最右邊則是『MF_BITMAP』屬性。


MENUITEMINFO 結構體

用 InsertMenuItem 添加選單

AppendMenu 是在 Win 3.1 時代就已有的 API,但是在 Win32 時代,已被微軟列為將被淘汰的 API,微軟建議以 InsertMenuItem 取代 AppendMenu。顧名思義,InsertMenuItem 是用來把某一選項加入到一個子選單或主選單堙A此處的某一選項也可以是彈出選項。InsertMenuItem 原型為:

BOOL WINAPI InsertMenuItem(
  HMENU          hMenu,
  UINT           uItem,
  BOOL           fByPosition,
  LPMENUITEMINFO lpmii
);

hMenu 是指選項要加入到那一個子選單或主選單中。fByPosition 可以是 FALSE 或 TRUE,當 fByPosition 為 FALSE 時表示 uItem 是選項識別碼;為 TRUE 時表示 uItem 為加入選項在選單的位置,此位置由 0 開始。例如在 MENU1 的例子堙A檔案子選單媔}新檔案的位置是 0,開啟舊檔位置是 1,儲存檔案的位置是 2……,依此類推。

lpmii 是一個指標,指向 MENUITEMINFO 結構體的位址,MENUITEMINFO 堨]含了大部分的選項性質,是個複雜的結構體。MENUITEMINFO 結構體各欄位如下:

MENUITEMINFO    STRUCT
cbSize          UINT     ?
fMask           UINT     ?
fType           UINT     ?
fState          UINT     ?
wID             UINT     ?
hSubMenu        HMENU    ?
hbmpChecked     HBITMAP  ?
hbmpUnchecked   HBITMAP  ?
dwItemData      DWORD    ?
dwTypeData      LPTSTR   ?
cch             UINT     ?
hbmpItem        HBITMAP  ?
MENUITEMINFO    ENDS

第一個欄位,cbSize,表示 MENUITEMINFO 結構體佔用多少位元組,這是與 API 的版本有關。fMask 可以是 MIIM_BITMAP、MIIM_CHECKMARKS、MIIM_DATA、MIIM_ID、MIIM_STATE、MIIM_SUBMENU、MIIM_TYPE 等數值的任意組合,當 fMask 設某個數值時,MENUITEMINFO 的某個欄位必須設好。例如 fMask 設為 MIIM_ID 時,表示要設定選項的識別碼,因此 wID 欄位必須好識別碼。小木偶整理成下面簡表,如欲知更詳細情形還得參閱 MSDN:

fMask 須設定欄位 說   明
MIIM_BITMAPhbmpItem fMask 指定 MIIM_BITMAP 時,必須同時在 hbmpItem 指定一個位元圖代碼,表示選項以一個位元圖表示。hbmpItem 也可以是內建的位元圖,例如 HBMMENU_MBAR_CLOSE、HBMMENU_MBAR_CLOSE_D 等等;也可以是 HBMMENU_CALLBACK ( 此時程式必須處理 WM_MEASUREITEM 或 WM_DRAWITEM 訊息 )。
MIIM_BITMAP 是 Win 2k/98 以後用來取代 MFT_BITMAP 的,在 MASM32 Ver 7 所包含的 WINDOWS.INC 1.25a 版中的 MENUITEMINFO 結構體,並沒有 hbmpItem 欄位,得自行修改。
MIIM_CHECKMARKShbmpChecked
hbmpUnchecked
MIIM_CHECKMARKS 是設定在選項文字之前,以位元圖來表示選項是被勾選的或沒被勾選的。以 hbmpChecked 指定被勾選時所顯示的位元圖;而以 hbmpUnchecked 指定未被勾選時所顯示的位元圖。hbmpChecked、hbmpUnchecked 都應該是位元圖代碼,但是如果 hbmpChecked 是 NULL,而且 fType 設為 MFT_RADIOCHECK 時,則以一圓點表示被勾選的選項;若在 fType 也沒指定,則以一個小勾勾表示被勾選的選項。
MIIM_DATAdwItemData MIIM_DATA 可以由程式自行定義數值,但小木偶才疏學淺不知可以怎麼用。設定 MIIM_DATA 時,dwItemData 可以設定一個 32 位元整數。
MIIM_FTYPEfType 選項外觀分成許多種類,有分隔線、位元圖、文字等,所以要取得或設定不同外觀時,先把 fMask 設為 MIIM_TYPE,再於 fType 做進一步設定。
MIIM_IDwID 當 fMask 設定 MIIM_ID 時,表示要設定選項的識別碼,該識別碼存於 wID 欄位。
MIIM_STATEfState 當 fMask 設定 MIIM_STATE 時,表示設定選項的狀態,選項的狀態可以有下面幾種:
  1. 被勾選 ( MFS_CHECKED ) 或未被勾選 ( MFS_UNCHECKED ):即選項文字前有小勾勾或其他符號。
  2. 內定的:MFS_DEFAULT,內定的選項文字會以粗體字表示。
  3. 禁用的 ( MFS_DISABLED )、啟用的 ( MFS_ENABLED )、無效的 ( MFS_GRAYED )
  4. 高亮度 ( MFS_HILITE ) 或正常亮度 ( MFS_UNHILITE )的。
以上四組,每一組內只能一項被設定,例如不可能有一個選項是被勾選的而同時又是未被勾選的。不過在一子選單堨i以同時有數個選項是被勾選的,另外勾選或不勾選的必須還要用程式配合處理,系統只負責顯示這些狀態而已。
MIIM_STRINGdwTypeData
cch
當 fMask 設為 MIIM_STRING 時,表示可設定或取得選項的文字,而選項的文字存放在 dwTypeData 所指的位址堙A長度為 cch ( 單位為字元 )。
MIIM_SUBMENUhSubMenu 當 fMask 設為 MIIM_SUBMENU 時,表示這個選項其實是一個彈出選項,而這個彈出選項要被加進一個子選單堙A這個子選單在 InsertMenuItem 的第一個參數 hMenu 中設定。當設定 MIIM_SUBMENU 時,必須把此彈出選項所彈出的子選單代碼存入 hSubMenu 欄位堙C
MIIM_TYPEfType

dwTypeData
當 fMask 設為 MIIM_TYPE 時,表示要設定選項內容。選項內容與選項的類型有關,選項類型記錄在 fType 堙A不同類型的選項其內容不同,而選項內容記錄在 dwTypeData 或其他欄位。fType 可以有下面幾種:
  1. MFT_BITMAP:此選項為一位元圖,該位元圖代碼須存入 dwTypeData 堙C要注意的是此選項就是以一張位元圖來代替文字字串,此位元圖別和被勾選的位元圖搞混,後者只是在文字字串前有一位元圖表是此選項被勾選。在 Win 2k/98 以後已被 MIIM_BITMAP 取代
  2. MFT_MENUBARBREAK:請參考上面
  3. MFT_MENUBREAK:請參考上面
  4. MFT_SEPARATOR:此選項其實是一水平分隔線,此時 dwTypeData 並無作用。
  5. MFT_STRING:此選項內容以文字字串表示,這是最常見的類型。dwTypeData 指向以零為結尾的文字字串位址,cch 為該字串所佔的位元組數,不過小木偶實驗結果 cch 似乎無作用。
  6. MFT_OWNERDRAW:此選項為擁有者選項,或稱自繪選項,亦即必須由程式或程式設計師自行繪製此選項外觀,此時 dwTypeData 可以為程式自行定義的一數值,在某些情況中可以使用。具有 MFT_OWNERDRAW 的選項會對視窗函式發出 WM_MEASUREITEM 與 WM_DRAWITEM 訊息,小木偶稍後敘述詳情。
  7. MFT_RADIOCHECK:用圓點顯示被勾選的選項,但先決條件該選項的是 hbmpChecked 為 NULL。
  8. MFT_RIGHTJUSTIFY:
  9. MFT_RIGHTORDER:由右向左書寫,如阿拉伯文。
在 Windows 98/Me/2K/XP 之後的系統,MIIM_BITMAP、MIIM_FTYPE、MIIM_STRING 可取代 MIIM_TYPE。意思是,在 Windows 95 時,如要設定選項文字,必須把 fMask 設為 MIIM_TYPE,再把 fType 設為 MFT_STRING,然後把 dwTypeData 設為字串位址;到了 Windows 98 以後,只需把 fMask 設為 MIIM_STRING,再把 dwTypeData 設為字串位址。

用 GetMenuItemInfo 取得選項資料、用 SetMenuItemInfo 改變選項資料

GetMenuItemInfo 可以取得選項資料,其原型為:

BOOL GetMenuItemInfo(
  HMENU hMenu,
  UINT  uItem,
  BOOL  fByPosition,
  LP    lpmii
);

呼叫 GetMenuItemInfo 之前先把 lpmii 指向一個 MENUITEMINFO 結構體,要取得選項的那一個資料,必須先在 MENUITEMINFO 結構體的 fMask 設定好,而 uItem 與 fByPosition 和 InsertMenuItem 意義相同,呼叫後由系統傳來的選項資料就會被存進 lpmii 所指定的結構體中。

要改變選項的某一資料,則是呼叫 SetMenuItemInfo,其原型為:

BOOL SetMenuItemInfo(
  HMENU          hMenu,
  UINT           uItem,
  BOOL           fByPosition,
  LPMENUITEMINFO lpmii
);

具有 MFT_OWNERDRAW 的選項

在許多商業軟體中,有許多漂亮的選單,小木偶猜測這些選單可能是用自繪選項來製作的。所謂自繪選項指的是具有 MFT_OWNERDRAW 風格的選項,抑或稱擁有者選項。稍後小木偶將示範一個程式,MENU2.ASM,來說明如何製作這種特別的選單,此刻先來看看 MENU2 選單畫面:

上圖中顏色子選單是一般常見的選單,其勾選的圖形是一位元圖,CHECK.BMP,而不用內定的勾選符號。檔案與字形子選單都具有 MFT_OWNERDRAW 屬性,檔案子選單中的文字是用 DrawText 畫上去的,其前面的圖形也是位元圖,是以 BitBlt 畫上去的;字形子選單中的文字也是以 DrawText 畫上去的,注意到他們的字形就是如顯示的文字般;其背景則是一個位元圖,以 BitBlt 畫上去的。底下說明如何產生這些效果。

WM_MEASUREITEM 與 WM_DRAWITEM 訊息

具有 MFT_OWNERDRAW 屬性的選項或擁有自繪風格的控制項 ( 見第 12 章 ) 會在要繪製選項或控制項之初,對視窗函式發出 WM_MEASUREITEM 訊息,此訊息是用來讓程式設計師規劃該選項或控制項的大小,當程式處理完 WM_MEASUREITEM 訊息後應該傳回 TRUE 返回系統,讓系統得知我們處理了此訊息。接下來系統將會處理必要的事項,例如為此選項或控制項建立設備內容 ( device context )、建立必要的資料結構等等,然後系統會再對視窗函式發出 WM_DRAWITEM 訊息,程式必須在此訊息中自行繪製選項或控制項的圖形。在 WM_DRAWITEM 訊息中,會把該選項或控制項的設備內容及其寬與高傳來,我們的程式所要做的事就如同對視窗工作區繪製圖形一般。當完成 WM_DRAWITEM 後,同樣也要把 TRUE 傳回給系統。

WM_MEASUREITEM 訊息中的 wParam 是控制項的識別碼,若發出此訊息的是選單,則 wParam 為零。WM_MEASUREITEM 訊息另一參數 lParam 則是指向一個 MEASUREITEMSTRUCT 結構體,程式設計師可以在此結構體內填入適當的或是您想要的數值來規劃選項,MEASUREITEMSTRUCT 成員如下:

MEASUREITEMSTRUCT STRUCT 
CtlType           UINT    ?
CtlID             UINT    ?
itemID            UINT    ?
itemWidth         UINT    ?
itemHeight        UINT    ?
itemData          DWORD   ?
MEASUREITEMSTRUCT ENDS

CtlType 是發出 WM_MEASUREITEM 的控件或選項,此欄位在系統呼叫視窗函式時已被系統設定好了,藉以指示是誰發出此訊息的,可以是下面數值:

類型數值意  義
ODT_MENU1 具有 MFT_OWNERDRAW 的選項
ODT_LISTBOX2 具有 LBS_OWNERDRAWFIXED 或 LBS_OWNERDRAWVARIABLE 風格的清單
ODT_COMBOBOX3 具有 CBS_OWNERDRAWFIXED 或 CBS_OWNERDRAWVARIABLE 風格的複合框
ODT_BUTTON4 具有 BS_OWNERDRAW 風格的按鈕控件
ODT_STATIC5 具有 SS_OWNERDRAW 風格的靜態控件

CtlID 是發出 WM_MEASUREITEM 訊息的控件識別碼,此欄位對選項是無效的。

itemID 是發出 WM_MEASUREITEM 訊息的選項識別碼。當具有 CBS_OWNERDRAWVARIABLE 風格的複合框中的清單或具有 LBS_OWNERDRAWVARIABLE 風格的清單堶悸瑪嚝僆紫o出 WM_MEASUREITEM 時,itemID 欄位是在清單中的位置,此位置由零開始算起。CtlID、itemID 都是在系統呼叫視窗函式時,就已依據程式所設定之值設好傳給視窗函式。

itemWidth、 itemHeight 是自繪選項或其他具有自繪控件的寬與高。程式必須在結束視窗函式返回系統之前,設好這兩個欄位,否則系統就不知道選項或控制項的大小,便會無法顯示出來。對選項而言,itemData 是由程式所定義的 32 位元數值。

在 WM_DRAWITEM 訊息中,程式設計師得依據 DRAWITEMSTRUCT 結構體的資料繪製選項。DRAWITEMSTRUCT 結構體位於 WM_DRAWITEM 的 lParam 所指位址之處,此結構體的各欄位已在第 12 章說明過了,請參閱該章,此處也不重複了。WM_DRAWITEM 的另一參數 wParam 意義與 WM_MEASUREITEM 相同,因此不再贅述。在 DRAWITEMSTRUCT 結構體中,較重要的欄位是 hdc 和 rcItem。hdc 是指要繪製的控制項或選項的設備內容代碼,取得這個代碼後,我們便可在控制項或選項畫上圖形、字串、位元圖等,一如我們對工作區 ( client area ) 繪製圖形一般。例如想使用特殊字形時,得先用 CreateFont 建立一邏輯字形,然後用 SelectObject 選擇此字形,然後再用 DrawText 繪製出文字字串。rcItem 是指控制項或選項的高與寬,我們所繪製的圖形、字串、位元圖等都將會顯示於 rcItem 所指的 RECT 結構體內的矩形之中。不過對選單而言,還有一處得注意。一個子選單堶悼i能有多個選項每個選項高度可能不同,可以在 WM_MEASUREITEM 中設定,但是最後在 WM_DRAWITEM 中畫出選項時,整個子選單都視為一個設備內容( device context ),其高度是各選項的總和。

自繪選項的高亮度光棒

當使用者的滑鼠游標在選單移動時,會有一個高亮度光棒表示滑鼠移到那一個位置,例如上圖中的 MENU2 是以一個藍色光棒表示。在一般選項堙A這項工作是由系統來做,但是在自繪選項堳o必須由程式自行顯示。

當滑鼠游標移到某個選項時,該選項會被系統設為被選擇到的,此狀態會被傳到自繪選項中 DRAWITEMSTRUCT 結構體的 itemState 欄位堙A並使 itemState 設為 ODS_SELECTED ( 其實 ODS_SELECTED 是 itemState 欄位的第零個位元設為一 )。因此視窗函式在處理 WM_DRAWITEM 訊息時,可以檢查這個欄位是否被設定,如果被設定可以用高亮度背景色 ( 即高亮度光棒 ) 表示;反之則用選單內定的背景顏色表示。實際的做法可以參考 MENU2.ASM 的例子。


彈出式選單 ( popup menu )

彈出式選單是指使用者在工作區中按下滑鼠右鍵 ( 當然您也可以用左鍵引發它 ) 所蹦出的選單,這種選單無法用資源描述檔事先定義,必須用 CreatePopupMenu API 製作,因此比較麻煩。不過幸運的是,彈出式選單的子選單卻可以在資源描述檔中定義,因此可以簡化一些問題。用 CreatePopupMenu 建立好的彈出式選單也是一個空的選單,必須用 AppendMenu、InsertMenuItem 等 API 把彈出選單、選項等加進彈出式選單堙A也可以用 ModifyMenu、SetMenuItemInfo 改變選項的特性……,這些都與主選單相同。

CreatePopupMenu API

CreatePopupMenu 用來建立一個空無一物的彈出式選單,其原型如下:

HMENU CreatePopupMenu(VOID);

它沒有參數,如果成功的建立彈出式選單,CreatePopupMenu 傳回所建立的彈出式選單代碼;若失敗,則傳回 NULL。

TrackPopupMenu API

彈出式選單的引發方式,一般是由使用者在工作區按下滑鼠右鍵,然後系統便顯示出彈出式選單。因此一般程式是在 WM_RBUTTONDOWN 訊息中顯示彈出式選單,顯示的方式是呼叫 TrackPopupMenu 或 TrackPopupMenuEx API。TrackPopupMenu 原型是:

BOOL TrackPopupMenu(
  HMENU hMenu,         // handle to shortcut menu
  UINT  uFlags,        // screen-position and mouse-button flags
  int   x,             // horizontal position, in screen coordinates
  int   y,             // vertical position, in screen coordinates
  int   nReserved,     // reserved, must be zero
  HWND  hWnd,          // handle to owner window
  CONST RECT *prcRect  // ignored
);

其中 hMenu 是要顯示的彈出式選單代碼。uFlags 是指彈出式選單顯示的位置與使用者按下滑鼠右鍵時,滑鼠游標位置的關係,uFlags 可以是下面數值:

  1. TPM_CENTERALIGN、TPM_LEFTALIGN、TPM_RIGHTALIGN:指滑鼠游標在彈出式選單的中間、左邊邊線、右邊邊線上。此三個數值表示滑鼠游標在選單 X 軸的那一個地方。
  2. TPM_BOTTOMALIGN、TPM_TOPALIGN、TPM_VCENTERALIGN :指滑鼠游標在彈出式選單的下邊邊線、中間、上邊邊線上。此三個數值表示滑鼠游標在選單 X 軸的那一個地方。
  3. TPM_NONOTIFY、TPM_RETURNCMD:
  4. TPM_RIGHTBUTTON、TPM_LEFTBUTTON:如果設定 TPM_RIGHTBUTTON,表示滑鼠左間或右鍵均可選按選項;如果設定 TPM_LEFTBUTTON,就只有滑鼠左鍵可選按選項。

x、y 是指按下滑鼠左鍵或右鍵時,滑鼠游標所在位置的 X 座標及 Y 座標,此處的座標原點是螢幕左上角。nReserved 是保留欄位,必須是零。最後一個是指向結構體 RECT 的指標,prcRect,也被保留,可設為零。


系統選單 ( window menu )

系統選單一般是在標題欄最左邊的圖示堙A當使用者以滑鼠左鍵或右鍵按下此圖示時所彈出的選單就是系統選單。修改系統選單必須先用 GetSystemMenu 取得系統選單代碼,GetSystemMenu 原型如下:

HMENU GetSystemMenu(
  HWND  hWnd,    // handle to window to own window menu
  BOOL  bRevert  // reset flag
); 

其中 hWnd 是要取得系統選單代碼的視窗代碼。bRevert 可以是 FALSE 或 TRUE,FALSE 指示系統取得系統選單代碼;TRUE 表示系統重設系統選單變為內定狀態。所謂內定狀態指的是系統選單原本包含的『還原』、『移動』、『大小』、『最小化』、『最大化』、分隔線、『關閉』七個選項。如果系統選項曾被修改過,執行

invoke  GetSystemMenu,hWnd,TRUE

後,會恢復原來內定狀態。程式可以用 InsertMenuItem、SetMenuItem、AppendMenu、InsertMenu、ModifyMenu 等 API 來修改系統選單。

一般選單中的選項,被使用者點選後,會發出 WM_COMMAND 訊息,視窗函式處理這個訊息,以完成使用者需求;但是系統選單內的選項並不發出 WM_COMMAND,而是發出 WM_SYSCOMMAND 訊息。WM_SYSCOMMAND 訊息中的 wParam 是使用者所點選的選項識別碼,可能是下面數值:

識 別 碼十六進位數 說 明
SC_SIZE 0f000h
SC_MOVE 0f010h 移動視窗
SC_MINIMIZE 0f020h 最小化視窗
SC_MAXIMIZE 0f030h 最大化視窗
SC_NEXTWINDOW 0f040h
SC_PREVWINDOW 0f050h
SC_CLOSE 0f060h 關閉視窗

仔細觀察,這些識別碼由 0F000h 開始,因此當程式在系統選單內添加選項時,其選項的識別碼最好是小於 0F000h。此外當系統選單發出 WM_SYSCOMMAND 訊息時,如果這些系統內定的選項不由程式處理的話,都應交由 DefWindowProc 處理,如果不這麼做的話,那麼系統選單將無法使用。

lParam 內存有使用者在系統選單按下滑鼠時,滑鼠游標的座標,較低的 16 位元是 X 座標,較高的 16 位元是 Y 座標。若 lParam 為零,表示使用者用數字鍵選擇系統選單,若為-1,表示使用者用加速鍵選擇系統選單。


MENU2

底下的程式,MENU2,演示了上述所提到的一些主題,包含建立主選單及其子選單、建立彈出式選單、修改系統選單,同時也示範了自繪選項。其執行畫面如下圖:

MENU2 的原始碼

要成功組譯 MENU2,需要很多檔案,大部分是位元圖,這些位元圖在選項文字之前,它們都在資源描述檔,MENU2.RC,堶惟w義:

MENU_TEST   ICON    "MENU.ICO"
NewFile     BITMAP  "NEWFILE.BMP"
OpenFile    BITMAP  "OPENFILE.BMP"
SaveFile    BITMAP  "SAVEFILE.BMP"
Exit        BITMAP  "EXIT.BMP"
Check       BITMAP  "CHECK.BMP"
BK          BITMAP  "BK.BMP"

MENU2.RC 內所含的位元圖檔、圖示檔內容如下圖:


小木偶將這七個檔案連同 MENU2.ASM、MENU2.RC、MENU2.MAK 打包成一個壓縮檔,MENU2.RAR。組譯時,把 MENU2.RAR 內所有檔案解壓縮到同一子目錄,並於『命令提示字元』中執行下面指令:

E:\HomePage\SOURCE>nmake -f menu2.mak [Enter]

Microsoft (R) Program Maintenance Utility   Version 1.50
Copyright (c) Microsoft Corp 1988-94. All rights reserved.

        rc MENU2.rc
        ml MENU2.asm /link MENU2.res
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: MENU2.asm
Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"MENU2.obj"
"/OUT:MENU2.exe"
"MENU2.res"

E:\HomePage\SOURCE>

若無法正確組譯,應該是您的環境並未設定好,請參考第零章的設定組譯環境。底下小木偶簡單的解說 MENU2.ASM,您可下載 MENU2.RAR 檔,對照著以下說明驗證看看:


在 WM_CREATE 做了那些事?

MENU2 在 WM_CREATE 訊息中,大概都是取得一些資料,包括取得字形資料、取得選單文字及背景顏色、取得檔案子選單的位元圖大小,接下來便是建立各子選單以及其內選項。

GetTextMetrics API

為了畫出特定字形,以及決定字形子選單的大小,小木偶在 WM_CREATE 中呼叫了 GetTextMetrics 以取得現在顯示的字形性質。GetTextMetrics 的原型是:

BOOL GetTextMetrics(
  HDC          hdc,   // handle to device context
  LPTEXTMETRIC lptm   // pointer to text metrics structure
);

字形是 GDI 物件之一,因此必須先取得設備內容 ( device context,縮寫為 DC ) 之後才能獲得字形資料。在 Windows 作業系統中每個視窗的字形可以不同,而在同一個視窗堣]可能有兩個以上的設備內容,例如工作區或記憶體設備內容,因此要取的某個視窗的字形資料得先取得設備內容。在 WM_PAINT 訊息中,以 BeginPaint 取得設備內容代碼,以 EndPaint 釋放;在其他訊息中用 GetDC 取得設備內容代碼,然後以 ReleaseDC 釋放設備內容 ( 稍後再提 )。GetTextMetrics 的第一個參數就是由 GetDC 或 BeginPaint 所得到的設備內容代碼。第二個參數是一個指標,指向一個稱為 TEXTMETRIC 的結構體位址,當 GetTextMetrics 成功的獲得字形的資料時,會在此結構體填上字形的資料,並於 EAX 暫存器中傳回一個非零值。TEXTMETRIC 結構體也是一個很複雜的結構體,其各欄位說明如下,並參考右下圖:

TEXTMETRIC              STRUCT
tmHeight                DWORD   ?
tmAscent                DWORD   ?
tmDescent               DWORD   ?
tmInternalLeading       DWORD   ?
tmExternalLeading       DWORD   ?
tmAveCharWidth          DWORD   ?
tmMaxCharWidth          DWORD   ?
tmWeight                DWORD   ?
tmOverhang              DWORD   ?
tmDigitizedAspectX      DWORD   ?
tmDigitizedAspectY      DWORD   ?
tmFirstChar             BYTE    ?
tmLastChar              BYTE    ?
tmDefaultChar           BYTE    ?
tmBreakChar             BYTE    ?
tmItalic                BYTE    ?
tmUnderlined            BYTE    ?
tmStruckOut             BYTE    ?
tmPitchAndFamily        BYTE    ?
tmCharSet               BYTE    ?
TEXTMETRIC              ENDS

由上圖右很清楚看出 tmHeight、tmAscent、tmDescent、tmInternalLeading、tmExternalLeading 所代表的意義。tmAveCharWidth、tmMaxCharWidth 分別代表字元平均寬度與最大寬度,對定寬字體而言,例如『Courier New』或中文字,來講,這兩者是相同的;但不定寬字體,例如『Times New Roman』,而言,每個字母的寬度未必一樣,例如『i』與『m』就不同寬。tmWeight 是指字形粗細,可以由 0 到 900,每 100 為一單位,數值越大字體越粗,見下表:

粗 細數值 粗 細數值
FW_DONTCARE0 FW_THIN100
FW_EXTRALIGHT、FW_ULTRALIGHT200 FW_LIGHT300
FW_NORMAL、FW_REGULAR400 FW_MEDIUM500
FW_SEMIBOLD、FW_DEMIBOLD600 FW_BOLD700
FW_EXTRABOLD、FW_ULTRABOLD800 FW_HEAVY、FW_BLACK900

tmOverhang 是指定字元的額外寬度,它是用於粗體字與斜體字時所多佔用的寬度。tmItalic、tmUnderlined、tmStruckOut 分別表示字形為斜體、底線、刪除線;當為零時,不具上述特性;為零時,表示具有上述特性。至於其他欄位本章並沒有用到,小木偶也不多說了。一般而言,要設定或取得文字大小,只需用到 tmHeight 欄位。

TEXTMETRIC 事實上有兩種版本,ANSI 與 UNICODE,兩種成員不太相同,下面左邊的是 ANSI 版,右邊則是萬國碼版本:

TEXTMETRICA             STRUCT                  TEXTMETRICW            STRUCT
  tmHeight              DWORD     ?             tmHeight               DWORD     ?
  tmAscent              DWORD     ?             tmAscent               DWORD     ?
  tmDescent             DWORD     ?             tmDescent              DWORD     ?
  tmInternalLeading     DWORD     ?             tmInternalLeading      DWORD     ?
  tmExternalLeading     DWORD     ?             tmExternalLeading      DWORD     ?
  tmAveCharWidth        DWORD     ?             tmAveCharWidth         DWORD     ?
  tmMaxCharWidth        DWORD     ?             tmMaxCharWidth         DWORD     ?
  tmWeight              DWORD     ?             tmWeight               DWORD     ?
  tmOverhang            DWORD     ?             tmOverhang             DWORD     ?
  tmDigitizedAspectX    DWORD     ?             tmDigitizedAspectX     DWORD     ?
  tmDigitizedAspectY    DWORD     ?             tmDigitizedAspectY     DWORD     ?
  tmFirstChar           BYTE      ?             tmFirstChar            WORD      ?
  tmLastChar            BYTE      ?             tmLastChar             WORD      ?
  tmDefaultChar         BYTE      ?             tmDefaultChar          WORD      ?
  tmBreakChar           BYTE      ?             tmBreakChar            WORD      ?
  tmItalic              BYTE      ?             tmItalic               BYTE      ?
  tmUnderlined          BYTE      ?             tmUnderlined           BYTE      ?
  tmStruckOut           BYTE      ?             tmStruckOut            BYTE      ?
  tmPitchAndFamily      BYTE      ?             tmPitchAndFamily       BYTE      ?
  tmCharSet             BYTE      ?             tmCharSet              BYTE      ?
TEXTMETRICA             ENDS                    TEXTMETRICW            ENDS

由上面的比較可知,ANSI 版共佔 53 個位元組,而萬國碼版本佔了 57 個位元組,兩者都是奇數個位元組。因此在把 TEXTMETRIC 結構體作為區域變數時,要小心使用,有可能會覆蓋前一個區域變數之值。

GetDC 與 ReleaseDC API

GetDC 可以取得視窗工作區的設備內容代碼,其原型為:

HDC GetDC(
  HWND hWnd   // handle to a window
);

它只有一個參數,就是視窗代碼,如果成功,則 EAX 暫存器傳回此視窗工作區的設備內容代碼;如果失敗,EAX 傳回 NULL。如果 hWnd 設為 0,那麼 GetDC 將傳回整個螢幕的設備內容代碼,亦即桌面的設備內容代碼,在 Win 98/2K 之後的系統中,如果 hWnd 為零,則傳回第一個螢幕的設備內容代碼。當不須繪圖時,必須呼叫 ReleaseDC 釋放設備內容,以節省系統資源,ReleaseDC 的原型是

int ReleaseDC(
  HWND hWnd,  // handle to window
  HDC  hDC    // handle to device context
);

若成功釋放設備內容,則 EAX 傳回 TRUE,否則傳回 NULL。

在 MENU2 的 WM_CREATE 訊息中,還要取得系統的某些顏色,如選單背景色、選單文字顏色等等,這些顏色將在畫出自繪選項時塗上光棒顏色及文字顏色之用,至於要取得那些顏色,請參考下圖:

我們把每個選項看成一塊矩形區域,文字是由許多線畫上去的,在線與線之間的空隙是背景,可用 SetBkColor 設定顏色;而文字的顏色可用 SetTextColor 設定。要注意的是,SetBkColor 只能設定包圍在文字的背景顏色,也就是上圖中紅色矩形範圍內的顏色,矩形之外的顏色無法由 SetBkColor 設定 ( 矩形之外即為光棒 ),因此得另外用 FillRect API 來做這件事。不過在 WM_CREATE 訊息階段堙A我們僅僅只要取得顏色並存於幾個變數堙A以便在呼叫 SetBkColor、SetTextColor 時使用,同時也要取得畫刷以呼叫 FillRect 時使用。而必須等到處理 WM_DRAWITEM 訊息時,才真正地以呼叫 SetBkColor、SetTextColor 設好顏色,呼叫 DrawText、FillRect 真正地把文字、光棒的畫出來,見稍後的『在 WM_MEASUREITEM 與 WM_DRAWITEM 兩訊息中做了那些事?』。( 此處又是一例,DOS 程式設計與 Windows 很不一樣。)

但此處先說明要取得的四種顏色:未被選到的文字顏色、未被選到的背景顏色、被選到的文字顏色及被選到的背景顏色,不過觀察上圖,您會發現未被選到的背景顏色與被選到的文字同色,所以實際上只要取得三種顏色,皆以 GetSysColor 取得,存於 dwTextColor、dwSelBkColor、dwSelTextColor。除此之外,為了在 WM_DRAWITEM 訊息中畫出光棒,還得建立兩個畫刷,以便在 WM_DRAWITEM 訊息中呼叫 FillRect 之用。這兩個畫刷分別是 hbrMenuBkHiligh 及 mi.hbrBack,前者是被選到選項的背景畫刷,小木偶以被選到的選項背景顏色,即 dwSelBkColor,建立 hbrMenuBkHiligh 畫刷,這個工作可以呼叫 CreateSolidBrush API 完成。

GetSysColor API

欲取得文字顏色、背景顏色這些資料可藉由呼叫 GetSysColor API 取得,GetSysColor 原型為:

DWORD GetSysColor(
  int nIndex   // display element
);

GetSysColor 只有一個參數,nIndex,它表示要取得那一個物件的顏色,MENU2 中使用的物件列在下表:

nIndex數值 意  義
COLOR_MENUTEXT07H 選單中文字顏色
COLOR_MENU04H 選單的背景顏色
COLOR_HIGHLIGHT0DH 被選到選項的背景顏色

如果 GetSysColor 成功取得顏色,該顏色存於 EAX,如果失敗,則傳回 NULL。

CreateSolidBrush API

CreateSolidBrush 是用來建立某一顏色的畫刷,其原型為:

HBRUSH CreateSolidBrush(
  COLORREF crColor   // brush color value
);

此 API 只有一個參數,crColor,表示所建立畫刷的顏色。如果成功的建立畫刷,則傳回畫刷代碼;反之失敗時,EAX 為 NULL。

GetMenuInfo API

另一個未被選到的背景畫刷是如何取得的呢?小木偶是用呼叫 GetMenuInfo API 得到。事實上 GetMenuInfo 可以得到許多選單的資料,GetMenuInfo 的原型為:

BOOL GetMenuInfo(
  HMENU       hmenu,      // handle for a menu
  LPCMENUINFO lpcmi,      // pointer to a MENUINFO structure
);

要取得資料的選單放在第一個參數,呼叫後返回的資料存於第二個參數所指的位址。這個位址是一個稱為 MENUINFO 的結構體,其欄位說明如下:

MENUINFO        STRUCT
cbSize          DWORD   ?
fMask           DWORD   ?
dwStyle         DWORD   ?
cyMax           UINT    ?
hbrBack         HBRUSH  ?
dwContextHelpID DWORD   ?
dwMenuData      DWORD   ?
MENUINFO        ENDS

MENUINFO 的結構體之於 GetMenuInfo 的關係,和 MENUITEMINFO 之於 GetMenuItemInfo 的關係是一樣的,這兩個結構體也類似。MENUINFO 中的 cbSize 是此結構體佔用的位元組數,用於系統分辨版本之用。fMask 是用指示 GetMenuInfo 來取得那一個欄位之用,詳細情形如下表,當您把 fMask 設為 MIM_STYLE 時,表示 GetMenuInfo 將取得 dwStyle 的資料;當 fMask 設為 MIM_MENUDATA 時,表示 GetMenuInfo 取得 dwMenuData 的資料。

fMask取得欄位說  明
MIM_STYLEdwStyle dwStyle 可以是下面幾種:
  1. MNS_AUTODISMISS:滑鼠游標在選單外約十秒後,選單會自動消失。
  2. MNS_CHECKORBMP:
  3. MNS_MODELESS:非模式 ( modeless ) 選項,
  4. MNS_NOCHECK:在選項左側不留勾選符號的空間。
MIM_MENUDATAdwMenuData應用程式自行定義之數值。
MIM_MAXHEIGHTcyMax選項最大高度,當超過時會出現捲軸。
MIM_HELPIDdwContextHelpID 
MIM_BACKGROUNDhbrBack選單的背景畫刷

這個 MENUINFO 結構體還有部份內容小木偶尚未搞懂,不過對於我們要取得未被選擇到的背景畫刷已經足夠了,就像下面程式片段:

MENUINFO        STRUCT
cbSize          DWORD   ?
fMask           DWORD   ?
dwStyle         DWORD   ?
cyMax           UINT    ?
hbrBack         HBRUSH  ?
dwContextHelpID DWORD   ?
dwMenuData      DWORD   ?
MENUINFO        ENDS
.data
mi      MENUINFO    <?>
……………………
        mov     mi.cbSize,SIZEOF MENUINFO
        mov     mi.fMask,MIM_BACKGROUND
        invoke  GetMenuInfo,hMenu,OFFSET mi

此處小木偶僅為了獲得背景畫刷,以便在選項未被選擇時畫出光棒顏色。因此在 fMask 設為 MIM_BACKGROUND,則會在 hbrBack 傳回未被選到的背景畫刷,這個背景畫刷和未被選到的光棒畫刷是相同的。此外 MENUINFO 結構體並沒有收錄在 WINDOWS.INC 檔案中,因此得自行在原始檔中定義。

接下來的程式是取得字形子選單堛漲U選項背景位元圖大小,這時可用 GetObject 取得,請參考第 13 章的說明。取得字形背景位元圖,BK.BMP,寬與高後,分別存在 dwFontWidth、dwFontHeight,不過字形子選單中有五個選項,所以每次只需顯示五分之一的高度就夠了,小木偶把 dwFontHeight 除以五之後的商存於 dwFontHighSlit 變數堙A等 WM_DRAWITEM 時畫在選項的設備內容堙C

接下來 WM_CREATE 訊息中處理的就是建立主選單、彈出式選單及修改系統選單。在建立主選單時,分別呼叫兩個副程式,CreateSubMenuFile、CreateSubMenuView,建立檔案子選單及檢視子選單。在 CreateSubMenuFile 堙A呼叫四次 LoadBitmap 與 GetObject 以便由資源中獲得位元圖代碼 ( 此位元圖是用來在檔案子選單中,顯示於文字前的位元圖 ) 及其寬與高,並使位元圖代碼存入 hbmpFile 陣列堙A使位元圖的寬與高分別存入 dwFileWidth、dwFileHeight 兩陣列中。往後在繪出位元圖時,只要以選項位置或選項識別碼當做依據,就可以由陣列中查出位元圖代碼和其寬與高。CreateSubMenuFile 副程式同時也計算選項的累積高度,每個選項的高度是位元圖高度再加 4,加四的目的是使位元圖上下各空兩點,然後存於 ybmPosition 陣列堙A這個陣列用來存放每個選項在選單中起始高度位置,以便在 WM_DRAWITEM 時畫出位元圖及文字。此處有個問題,小木偶一直無法有效解決,那就是如何取得分隔線高度,如有先進知道,麻煩告知,小木偶不勝感激。

接著 CreateSubMenuFile 副程式中的工作是在檔案子選單中添加自繪選項,此時只需要設定好 MENUITEMINFO 的 fType 欄位為 MFT_OWNERDRAW 即可再呼叫 InsertMenuItem 即可,不需要在此處畫出選項內容,而是等處理 WM_DRAWITEM 時再做這件事。建立檢視子選單也類似,小木偶就不再多說了。


在 WM_MEASUREITEM 與 WM_DRAWITEM 兩訊息中做了那些事?

如前所說,在 WM_MEASUREITEM 訊息中,主要工作是設定選項高度與寬度,小木偶利用 EDX 作為指向 MEASUREITEMSTRUCT 結構體的位址,然後利用

        mov     MEASUREITEMSTRUCT.itemWidth[edx],ecx    ;位元圖的寬
        mov     MEASUREITEMSTRUCT.itemHeight[edx],eax   ;位元圖的高

把位元圖的寬與高存入 MEASUREITEMSTRUCT 結構體的 itemWidth 與 itemHeight 欄位。此處以結構體資料型態為偏移位址,基準位址為 EDX,換句話說此兩行實際上是:

        mov     [edx+0ch],ecx
        mov     [edx+10h],eax

上面的 0ch、10h 即為相對於 MEASUREITEMSTRUCT 結構體起始位址的 itemWidth、itemHeight 偏移位址。原理請參考組合語言第 19 章的說明。

至於在 WM_DRAWITEM 訊息中,就是繪製出各個自繪選項。首先前八行是把 lParam 所指 DRAWITEMSTRUCT 結構體的資料複製到 dis 中,接下來做的事情是依照選項識別碼 ( 選項識別碼存於 DRAWITEMSTRUCT 結構體的 itemID 欄位 ) 分成兩種:一種是在檔案子選單的選項,此種較單純,所要做的事是畫出文字前位元圖以及文字。第二種是在字形子選單的選項,此種選項必須畫出背景圖及文字,畫出文字時還得事先設好所用字形。不管是那一種選項,都要畫出位元圖及光棒,因此先用 CreateCompatibleDC 建立一相同的裝置內容,然後用畫出矩形的方式模擬光棒,而此光棒的顏色與背景色相同,這個過程可用 FillRect。

FillRect API

顧名思義,FillRect 是在一塊矩形區域填入顏色:

int FillRect(
  HDC    hDC,           // handle to device context
  CONST  RECT *lprc,    // pointer to structure with rectangle
  HBRUSH hbr            // handle to brush
);

hDC 是指設備內容代碼,您或許覺得奇怪,此處既沒有呼叫 BeginPaint,也沒有 GetDC,那麼那來的 hDC 呢?還記得之前提到 WM_DRAWITEM 的 lParam 參數中便包含了設備內容代碼。或許您可以想像:當 Windows 要畫出一般的選項時,其內部必定也有一設備內容供系統繪製之用,但是遇到具有 MFT_OWNERDRAW 類型的選項時,卻是交由程式的視窗函式自行繪製,因此藉由 WM_DRAWITEM 的 DRAWITEMSTRUCT 結構體將繪製所需資料傳給視窗函式,其內便包含了設備內容代碼及設備內容的範圍。

lprc 是指向一個描述矩形的結構體,其欄位參考第三章。hbr 是畫刷代碼,此畫刷將被用來填入前述矩形範圍內,這個畫刷也就是我們在 WM_CREATE 所建立的 hbrMenuBkHiligh 與 mi.hbrBack,至於要使用那一種,由滑鼠是否移到該選項上決定,這個可以由 DRAWITEMSTRUCT 中的 itemState 欄位決定,若該欄位的 ODS_SELECTED 被設定 ( 為 1 時 ) 表示滑鼠移到該選項上面,此時需用 hbrMenuBkHiligh,反之則用 mi.hbrBack。

在描繪檔案子選單堛瑪龠筑氶A先取得選項識別碼,選項識別碼減去 100h 即為選項位置,再乘以四即得選項在 lpszFile、hbmpFile、dwFileWidth、dwFileHeight、ybmPosition 各陣列位址,接下來就是把選項文字位址推入堆疊,接著呼叫 SelectObject 選擇位元圖代碼 ( 此代碼由 hbmpFile 獲得 ),接著就是呼叫 BitBlt 把記憶體設備內容拷貝到選項設備內容堙A其位元圖大小由 dwFileWidth、dwFileHeight 兩陣列取得,而要拷貝到選項設備內容的何處則由 ybmPosition 陣列取得。接著設定文字要畫在何處,小木偶設為在位元圖之後六點的地方,由程式

        add     dis.rcItem.left,6
        add     dis.rcItem.left,eax

達成。緊接著是設定文字顏色與文字背景色,分別呼叫 SetTextColor 與 SetBkColor 兩個 Win32 API。程式如下:

        test    dis.itemState,ODS_SELECTED
        jz      n_sel0
        mov     ecx,dwSelTextColor
        push    dwSelBkColor
        jmp     @f
n_sel0: mov     ecx,dwTextColor
        push    dwSelTextColor          ;沒被選擇的背景色即為被選到的文字顏色
@@:     invoke  SetTextColor,dis.hdc,ecx
        push    dis.hdc
        call    SetBkColor

最後一行小木偶並非用

        invoke  SetBkColor,dis.hdc,背景顏色

這是因為在呼叫 SetTextColor 之前背景色就已經被推入堆疊了 ( 因為滑鼠游標是否在選項上,所顯示背景色不同,因此推入不同之顏色,見紅色的那兩行 )。還記得,invoke 是一個假指令,可以分成許多 push 指令及一個 call 指令嗎?若不記得的話,請參考第一章。小木偶這樣寫是為了節省幾個位元組,當然這樣會造成維護不易,這是組合語言的特點,不僅在 DOS 等 16 位元的 Assembly 如此,在 Win32 Assembly 也會有這樣的特色。如果不這樣寫的話,也可以像下面這樣:

        test    dis.itemState,ODS_SELECTED
        jz      n_sel0
        mov     ecx,dwSelTextColor
        mov     edx,dwSelBkColor
        jmp     @f
n_sel0: mov     ecx,dwTextColor
        mov     edx,dwSelTextColor
@@:     push    edx
        invoke  SetTextColor,dis.hdc,ecx
        pop     edx
        invoke  SetBkColor,dis.hdc,edx

不過佔用位元組較多,速度稍慢 ( 可能慢個幾奈秒吧?)。

在描繪字形子選單時,先取得選項文字位址,同樣也推入堆疊,再依此位址之字形建立邏輯字形,用 SelectObject 選擇此新建立的邏輯字形。接著依據滑鼠游標是否在選項上分成兩種情形。第一種是滑鼠游標在選項上,此時只需設定文字及文字背景色即可。第二種情形是滑鼠游標不在選項上時,此時除了必須設定文字外,並不需要設定文字背景顏色,這是因為我們想把文字的背景設為透明的,這樣才能較完整的表現出背景位元圖上的美女,所以此處牽涉了兩個工作:用 BitBlt 畫出位元圖,用 SetBkMode 設定透明的背景顏色。接著不管滑鼠游標是否在選項上,都必需設定文字顏色。最後再畫出文字即可。底下是這段程式所用到的 Win32 API。

CreateFont API

CreateFont 是用來建立一個邏輯字形。如果讀者曾用過 Lotus SmartSuite Word Pro、MS Word 等文書軟體,想必曾用過各種不同的字形。一種邏輯字形可有三種性質:字形名稱、字形大小、字形風格。一般使用者可能會先選那一種字形名稱,例如『Courier New』、『新細明體』等,然後是字形點數大小,接著是字形風格,例如否斜體、筆畫粗細、是否有底線等等。要畫出多變的字形都得先建立邏輯字形才可以,即使您要顯示的字形都是名為 Times New Roman,但一是斜體、一是含有底線,那麼您也需建立兩個邏輯字形,然後在畫出字串前,用 SelectObject 選擇不同的邏輯字形,再以 DrawText 畫出。邏輯字形在不使用時,應該用 DeleteObject 將他刪除,以免耗費過多資源。一般來說,當選用一個新的邏輯字形時,舊的邏輯字形大概就用不著了,恰好 SelectObject 就是傳回舊的邏輯字形,因此小木偶就立即將舊的邏輯字形刪除了。說了這麼多,底下來看看 CreateFont 的原型:

HFONT CreateFont(
  int     nHeight,             // logical height of font
  int     nWidth,              // logical average character width
  int     nEscapement,         // angle of escapement
  int     nOrientation,        // base-line orientation angle
  int     fnWeight,            // font weight
  DWORD   fdwItalic,           // italic attribute flag
  DWORD   fdwUnderline,        // underline attribute flag
  DWORD   fdwStrikeOut,        // strikeout attribute flag
  DWORD   fdwCharSet,          // character set identifier
  DWORD   fdwOutputPrecision,  // output precision
  DWORD   fdwClipPrecision,    // clipping precision
  DWORD   fdwQuality,          // output quality
  DWORD   fdwPitchAndFamily,   // pitch and family
  LPCTSTR lpszFace             // pointer to typeface name string
);

nEscapement、nOrientation 是指逆時針旋轉的角度,以 0.1 度為單位。舉例說明:若 nEscapement 為 900 時,表示整行字串逆時針旋轉 90 度;若 nOrientation 為 450,則是每個字逆時針旋轉 45 度。( 但是 nEscapement 似乎不像 MSDN 所說的這樣,小木偶試不出來,還請諸位前輩告知原因。 )

fnWeight 是指字形的粗細,與 TEXTMETRIC 結構體中的 tmWeight 欄位意義相同。接下來的 fdwItalic、fdwUnderline、fdwStrikeOut 分別表示是否斜體、有底線、有刪除線,如果斜體、有底線、有刪除線的話則用 TRUE,否則為 FALSE。fdwCharSet 表示字元集,可以是 ANSI_CHARSET、BALTIC_CHARSET、CHINESEBIG5_CHARSET ( 正體中文 BIG-5 碼 )、DEFAULT_CHARSET、EASTEUROPE_CHARSET、GB2312_CHARSET、GREEK_CHARSET、HANGUL_CHARSET、MAC_CHARSET、OEM_CHARSET、RUSSIAN_CHARSET、SHIFTJIS_CHARSET、SYMBOL_CHARSET、TURKISH_CHARSET,如果是 NT/2K 以上的版本,還可以用 HEBREW_CHARSET、ARABIC_CHARSET、THAI_CHARSET。lpszFace 是指向一個以 NULL 為結尾的字串,此字串指定字形名稱。

如果成功的建立邏輯字形,EAX 會傳回新的邏輯字形代碼;否則傳回 NULL。此邏輯字形代碼就是即將被 SelectObject 所選入到設備內容堛滿C

SetBkMode API

這個 API 是用來設定文字的背景、非 PS_SOLID 畫筆、某些畫刷中背景的模式。某些畫筆 ( 例如虛線畫筆 ) 或畫刷 ( 斜線圖案 ) 的背景以及文字背景可以顯出顏色或是不受影響的。SetBkMode 原型為:

int SetBkMode(
  HDC hdc,      // handle of device context
  int iBkMode   // flag specifying background mode
);

iBkMode 可以有兩種選擇:OPAQUE 與 TRANSPARENT,前者為不透明的,亦即顯示背景顏色;後者為透明的,亦即不受背景色之影響。


後記

MENU2 的講解大致就是如此,有關如何依據某些資料設定選單,可參考附錄一 CPUID 介紹。


回到首頁到第十五章到第十七章