Windows菜单位图与菜单绘制

文章介绍了如何在Windows环境下自定义绘制菜单,包括菜单项的位图、文本和分隔条,以及如何处理菜单的单选和复选状态。还提供了通用的函数来处理菜单项的位图和状态,并展示了如何在WM_DRAWITEM消息中绘制菜单项。此外,文章讨论了菜单栏背景色的设置和菜单项的初始化尺寸计算。

Windows菜单位图使用与菜单绘制

Windows菜单位图使用与菜单绘制的基本内容在Windows文档使用菜单中有详细说明,但涉及菜单的绘制方面的介绍只是简单的一些内容。本文比较全面地介绍菜单位图的使用技巧与菜单绘制的方法。

菜单绘制是窗口要素绘制中最具有挑战性的活,所涉用的内容比较多,所以本文提供全部源码。

在介绍菜单绘制前,必须先编写一些基本的实用函数,使得条理更清楚。

1. 菜单状态位图的操作技巧

状态位图就是菜单的单(复)选位图,默认的单选位图是一个圆点,复选位图是一个勾子,用户可以使用SetMenuItemInfo函数设置自己的位图。

但使用单(复)选状态位图有两个不方便的地方,第一是复选菜单项并没有在MENUITEMINFO结构的fType成员中有对应的志标位,无法识别某一菜单项是否为复选菜单项;第二是单选菜单项没有组别的信息,使用CheckMenuRadioItem函数去更新一组单选菜单项状态时需要提供该组的最小菜单ID与最大菜单ID。因为这两个问题的存在,我们没有办法编写一个共用的函数来自动处理单(复)选状态,虽然可以使用MENUITEMINFO结构的dwItemData成员设置附加志标来解决这个问题,但dwItemData成员在某些地方是很有用的,如果要编写一个共用的处理函数不建议去占用它。

为了编写一个菜单的单(复)选状态自动处理函数,本人通过在菜单ID中设置志标位来解决上述问题。将所有复选菜单ID的最高位置为"1",例将8001h、8002h等值作为复选菜单ID。单选菜单需要3个标识值,一个菜单ID为一个16位值,共有4个十六进制值,最低2个值用作单选组组内序号,并约定组内首个菜单ID的内部序号为01;第3个值用作单选组组内项目数;第4个值用作单选组组号。

例:

 ;复选菜单ID
 IDM_WRAP    EQU 8301h

 ;第1组单选菜单ID
 IDM_SIMPLE  EQU 1301h
 IDM_TRAD    EQU 1302h
 IDM_ENGLISH EQU 1303h

 ;第2组单选菜单ID
 IDM_LEFT    EQU 2401h
 IDM_TOP     EQU 2402h
 IDM_DEFAULT EQU 2403h
 IDM_SNAP    EQU 2404h

下面编写一个通用函数来处理菜单。

根据上述的约定,定义几个常量:

;--------------------------------------------------
;菜单ID志标: 单选组组内首个菜单的组内序号必须为01h。
;--------------------------------------------------
 MENU_CHECKITEM    EQU 8000h   ;复选菜单项志标位
 MENU_RADIO_GIDMK  EQU 0f000h  ;单选组组号掩码(0-f)
 MENU_RADIO_GNMK   EQU 0f00h   ;单选组组内项目数掩码(0-f)
 MENU_RADIO_IIDMK  EQU 0ffh    ;单选组组内序号掩码(1-ff)
;--------------------------------------------------

;===========================================
;改变菜单项的单(复)选按钮状态
;入: uhMenu=菜单句柄
;    Id=要改变的菜单项ID
;出: EAX=该菜单的当前状态:
;        MFS_CHECKED: 被选中
;        0: 未选中,或不是单(复)选菜单项.
;===========================================
Menu_ChangeCheck proc uhMenu:QWORD,Id:DWORD
 LOCAL ss_mi:MENUITEMINFO
;---取菜单的状态与类型---
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_STATE or MIIM_FTYPE
 invoke GetMenuItemInfo,uhMenu,Id,NULL,ADDR ss_mi
 test ss_mi.fType,MFT_RADIOCHECK
 jnz ss_radio
 test Id,MENU_CHECKITEM
 jz ss_no
;---为复选菜单项(翻转状态位)---
 xor ss_mi.fState,MFS_CHECKED
 mov ss_mi.fMask,MIIM_STATE
 invoke SetMenuItemInfoW,uhMenu,Id,NULL,ADDR ss_mi
 mov eax,ss_mi.fState
 and eax,MFS_CHECKED
 ret
;---为单选菜单项(修改组内各项目状态)---
ss_radio:
 mov eax,Id
 mov edx,eax
 and edx,NOT MENU_RADIO_IIDMK  ;组内最小ID-1
 and eax,MENU_RADIO_GNMK
 mov ecx,MENU_RADIO_GNMK
 bsf ecx,ecx
 shr eax,cl  ;eax=组内项目数
 or eax,edx      ;组内最大ID
 or rdx,1
 invoke CheckMenuRadioItem,uhMenu,edx,eax,Id,MF_BYCOMMAND
ss_out:
 mov eax,MFS_CHECKED
 ret
ss_no:  ;不是单(复)选菜单项
 xor eax,eax
 ret
Menu_ChangeCheck endp

这是一个通用的单(复)选菜单状态处理函数,如果用户点击某一个菜单,则窗口过程会收到WM_COMMAND消息,其中wParam的低16位值为菜单ID,用户无需区分该ID的菜单类型,直接调用Menu_ChangeCheck进行自动处理。

2. 识别菜单ID是否为主菜单

在绘制菜单中,主菜单与子菜单的绘制是有所区别的,但WM_MEASUREITEM消息没有提供菜单句柄参数,只提供了一个菜单ID,所以不能有针对性地确定菜单项绘制尺寸。因此必须编写一个函数,来确定该菜单ID是主菜单项还是子菜单项。如果调用以下函数Menu_isOwnerId,并将主菜单句柄和某一菜单ID传递给函数,就可以确定该菜单ID是否为主菜单ID。

但这样做还有一个小问题,就是菜单的分隔条问题,因为菜单分隔条ID都默认为0,这样就不能使用枚举方法来确定是子菜单的水平分隔条还是主菜单的垂直分隔条。为了解决这个问题,约定将主菜单的分隔条ID设置为0ffffh,将子菜单的分隔条ID设置为0000h,所幸的是分隔条也可以指定ID。

;=============================================
;分析一个菜单ID是否为指定的菜单条
;入: uhMenu=主菜单句柄
;    mId=菜单ID
;出: EAX=该菜单ID在菜单条中的序号(0...)
;        如果是分隔条则为第一个分隔条序号。
;       =-1: 该菜单ID不属于该菜单。
;-------------------------------------------
;例: 如果uhMenu为主菜单句柄,mId也是主菜单Id,
;    则返回所在的序号;如果mId为子菜单Id,则
;    返回-1。
;=============================================
Menu_isOwnerId proc uhMenu:QWORD,mId:DWORD
 LOCAL ss_mi:MENUITEMINFO
 LOCAL ss_n:DWORD
 LOCAL ss_i:DWORD
 invoke GetMenuItemCount,rcx
 cmp eax,0
 jle ss_no
 mov ss_n,eax
 mov ss_i,0
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_ID
ss_lp1:
 invoke GetMenuItemInfoW,uhMenu,ss_i,TRUE,ADDR ss_mi
 mov eax,ss_mi.wID
 cmp eax,mId
 jz ss_ok
 inc ss_i
 dec ss_n
 jnz ss_lp1
ss_no:
 mov eax,-1
 ret
ss_ok:
 mov eax,ss_i
 ret
Menu_isOwnerId endp

3. 绘图对象池

在编程中经常要用到绘图对象,如画笔、画刷、字体、位图、区域、调色板等句柄值,这些句柄统一使用DeleteObject函数释放。有些句柄需要在程序退出时才能删除,如果使用一对一的变量来存放这些句柄是不太方便的,而且容易造成资源遗留问题。使用对象池,将这些句柄都放入对象池中,当窗口过程收到WM_DESTROY消息时统一删除这些句柄,既方便也安全。

以下为一个简单的对象池函数,没有检索功能,只为满足简单的使用。

;-------------------------------------------------------
; 对象池。用于存放由DeleteObject函数释放的绘图对象句柄,
; 包括逻辑画笔、画刷、字体、位图、区域、调色板。 
; 对象池的结构如下:
;    dd ?     ;已保存的对象个数
;    dq ?,?.. ;对象
;--------------------------------------------------------
.data
pObj_Pool dq 0     ;对象池地址(须初始化为0)

.code
;=============================================
;将位图、画刷等句柄添加到对象池
;入: hObj=绘图对象句柄
;         使用DeleteObject函数删除的任可句柄。
;        =0: 不操作
;出: RAX=hObj: 成功
;注: 程序退出时调用ObjPool_Free删除所全对象。
;=============================================
ObjPool_AddItem proc hObj:QWORD
 xor rax,rax
 test rcx,rcx  ;hObj
 jz ss_0
 cmp pObj_Pool,0
 jnz ss_1
;---创建对象池---
 call GetProcessHeap  ;获取默认堆句柄
 invoke HeapAlloc,rax,0,12
 mov pObj_Pool,rax
 xor ecx,ecx
 mov [rax],ecx  ;对象个数置0
 jmp ss_2
ss_1:
;---扩展内存---
 call GetProcessHeap  ;获取默认堆句柄
 mov r8,pObj_Pool
 mov r9d,[r8]  ;已保存的对象个数
 inc r9d
 shl r9d,3      ;*8
 add r9d,4
 invoke HeapReAlloc,rax,0,r8,r9
 mov pObj_Pool,rax
;---将新的值置入对象池---
ss_2:
 mov rcx,pObj_Pool
 mov rax,hObj
 mov edx,[rcx]
 mov [rcx+rdx*8+4],rax
 inc edx
 mov [rcx],edx ;对象个数增1
ss_0:
 ret
ObjPool_AddItem endp
;====================================
;释放对象池中的全部绘图对象
;程序退出时调用
;====================================
ObjPool_Free proc
 LOCAL ss_rsi:QWORD
 LOCAL ss_n:DWORD
 mov ss_rsi,rsi  ;保护
 mov rsi,pObj_Pool
 test rsi,rsi
 jz ss_0
 lodsd
 test eax,eax
 jz ss_out
 mov ss_n,eax ;对象个数
ss_lp1:
 lodsq
 invoke DeleteObject,rax
 dec ss_n
 jnz ss_lp1
ss_out:
 call GetProcessHeap  ;获取默认堆句柄
 invoke HeapFree,rax,0,pObj_Pool
 mov pObj_Pool,0
ss_0:
 mov rsi,ss_rsi
 ret
ObjPool_Free endp

对象池可以作为静态库函数或源码模板函数。如果扩展对象池的概念,将其应用到程序环境的管理上,是很有用的。

4. 根据位图的位数据绘制位图

在绘制菜单或控件时,绘制最多的可能是单(复)选位图,为每一个菜单项创建一个单(复)选位图是很浪费的,当然也可通过共用位图句柄的方法解决这个问题,例如以下为装载系统预定义的复选位图:

;=======================================
;装载系统预定义的复选位图
;返回: RAX=位图句柄
;=======================================
.data
 hBmpCheck dq 0 ;初始化值必须为0
.code
LoadCheckBmp proc
 mov rax,hBmpCheck
 test rax,rax
 jnz ss_0
 invoke LoadImage,NULL,OBM_CHECK,IMAGE_BITMAP,0,0,LR_SHARED or LR_DEFAULTSIZE
 mov hBmpCheck,rax
 invoke ObjPool_AddItem,rax ;添加到对象池
 mov rax,hBmpCheck
ss_0:
 ret
LoadCheckBmp endp

下面介绍SetDIBitsToDevice函数的使用方法,即使用位图的位数据绘制位图,而不是通过位图句柄显示位图。这种方法很适用于小的单色位图。

先编写一个绘图函数CtrlBmp_Draw,该函数可以绘制任何单色位图。单色位图不一定是黑白位图,可以有颜色,颜色由事先设置到hdc环境中的颜色决定。

;========================================================
;绘制控件位图
;入: hdc=目标DC。使用当前设置的颜色。
;    pRc=RECT结构地址。包含显示区域坐标。
;    pBits=位图数据地址。格式如下:
;          dd ?   ; 以像素为单位的图像宽度
;          dd ?   ; 以像素为单位的图像高度
;          db ... ; 位图的位值数组(见BITMAPINFO结构说明)
;    zFlag=0: 保留
;========================================================
CtrlBmp_Draw proc hdc:QWORD,pRc:QWORD,pBits:QWORD,zFlag:QWORD
 LOCAL ss_rbx:QWORD
 LOCAL ss_bm[SIZEOF BITMAPINFOHEADER +RGBQUAD*2]:BYTE
 LOCAL ss_bwt:DWORD
 LOCAL ss_bht:DWORD
 mov ss_rbx,rbx
 lea rbx,ss_bm ;单色位图需要2个RGBQUAD结构
;---填写BITMAPINFOHEADER结构---
 mov [rbx.BITMAPINFOHEADER].biSize,SIZEOF BITMAPINFOHEADER
 mov eax,[r8]   ;pBits
 mov ss_bwt,eax ;位图宽度
 mov [rbx.BITMAPINFOHEADER].biWidth,eax
 mov eax,[r8+4]
 mov ss_bht,eax ;位图高度
 neg eax  ;位图扫描行从上到下,使用负值
 mov [rbx.BITMAPINFOHEADER].biHeight,eax      ;以像素为单位的图像高度。
 mov [rbx.BITMAPINFOHEADER].biPlanes,1        ;目标设备的位平面数
 mov [rbx.BITMAPINFOHEADER].biBitCount,1      ;每个像素的位数。
 mov [rbx.BITMAPINFOHEADER].biCompression,0   ;默认
;---计算位图数据字节长度---
 mov eax,ss_bwt
 shr eax,3          ;/8=行字节长度
 add eax,3h         ;31
 and eax,NOT 3h     ;每行4字节对齐
 mul ss_bht
 mov [rbx.BITMAPINFOHEADER].biSizeImage,eax   ;以字节为单位的图像数据的大小
 mov [rbx.BITMAPINFOHEADER].biXPelsPerMeter,0 ;默认
 mov [rbx.BITMAPINFOHEADER].biYPelsPerMeter,0 ;默认
 mov [rbx.BITMAPINFOHEADER].biClrUsed,0       ;默认
 mov [rbx.BITMAPINFOHEADER].biClrImportant,0  ;默认
 add rbx,SIZEOF BITMAPINFOHEADER              ;指向RGB颜色表
;---填写RGB颜色表---
 invoke GetTextColor,hdc ;取当前的前景色
;---转换为RGB---
 rol ax,8
 rol eax,16
 rol ax,8
 ror eax,8
 mov [rbx],eax          ;第1个RGBQUAD结构存放前景色
 invoke GetBkColor,hdc  ;取当前的背景色
 rol ax,8
 rol eax,16
 rol ax,8
 ror eax,8
 mov [rbx+4],eax        ;第2个RGBQUAD结构存放背景色
 add pBits,8            ;指向位图的位数据
;---计算显示居中坐标---
 mov rax,pRc
 mov edx,[rax.RECT].right
 sub edx,[rax.RECT].left
 sub edx,ss_bwt
 sar edx,1
 add edx,[rax.RECT].left  ;水平居中
 mov r8d,[rax.RECT].bottom
 sub r8d,[rax.RECT].top
 sub r8d,ss_bht
 sar r8d,1
 add r8d,[rax.RECT].top  ;垂直居中
 invoke SetDIBitsToDevice,hdc,edx,r8d,ss_bwt,ss_bht,0,0,0,ss_bht,pBits,\
                      ADDR ss_bm,DIB_RGB_COLORS
 mov rbx,ss_rbx 
 ret
CtrlBmp_Draw endp

下面定义一个复选位图的位数据。位图的位数据每行必须为4字节对齐,如宽度为16位的位图,每行2个字节,但后面须补填2字节,这补填的2字节可填入任意值。

;----------------------------------------
;复选位图数据。每行必须为4字节对齐。
;位值="1"为背景色。对应黑白位图的白底。
;    ="0"为前景色。对应黑白位图的图形。
;----------------------------------------
Check_data dd 16,16   ;位图的宽和高(这是自定义参数)
           db 0ffh,0ffh,0,0 ;第1行扫描行位值
           db 0ffh,0ffh,0,0 ;第2...
           db 0ffh,0ffh,0,0
           db 0ffh,0ffh,0,0
           db 0ffh,0ffh,0,0
           db 0ffh,0efh,0,0
           db 0ffh,0cfh,0,0
           db 0ffh,08fh,0,0
           db 0f7h,01fh,0,0
           db 0f2h,03fh,0,0
           db 0f0h,07fh,0,0
           db 0f8h,0ffh,0,0
           db 0fdh,0ffh,0,0
           db 0ffh,0ffh,0,0
           db 0ffh,0ffh,0,0
           db 0ffh,0ffh,0,0

下面是使用复选位图的位数据绘制复选位图的函数。

;===========================================
;绘制控件的复选位图
;入: hdc=目标DC
;    pRc=RECT结构地址。包含显示区域坐标。
;    zFlag=0: 保留
;===========================================
CtrlBmp_DrawCheck proc hdc:QWORD,pRc:QWORD,\
                  zFlag:DWORD
 invoke CtrlBmp_Draw,rcx,rdx,ADDR Check_data
 ret
CtrlBmp_DrawCheck endp

对于单选位图,也可用上述方法绘制。但因为单选位图是一个规则圆,直接使用椭圆函数Ellipse来绘制更利于控制。单选位图绘制函数如下。

;=============================================
;绘制控件的单选位图
;入: hdc=目标DC
;    pRc=RECT结构地址。包含显示区域坐标。
;    zFlag=0: 保留
;=============================================
CtrlBmp_DrawRadio proc hdc:QWORD,pRc:QWORD,\
                  zFlag:DWORD
 LOCAL ss_hOldBrush:QWORD
 LOCAL ss_hOldPen:QWORD
 LOCAL ss_color:DWORD
 invoke GetTextColor,hdc ;取当前的前景色
 mov ss_color,eax
 invoke CreateSolidBrush,rax
 invoke SelectObject,hdc,rax
 mov ss_hOldBrush,rax
 invoke CreatePen,0,1,ss_color
 invoke SelectObject,hdc,rax
 mov ss_hOldPen,rax
 mov rax,pRc
 mov edx,[rax.RECT].right
 sub edx,[rax.RECT].left
 sub edx,5  ;5=圆直径
 shr rdx,1
 add edx,[rax.RECT].left ;x
 mov r8d,[rax.RECT].bottom
 sub r8d,[rax.RECT].top
 sub r8d,5  ;5=圆直径
 shr r8d,1
 add r8d,[rax.RECT].top ;y
 mov r9d,edx
 add r9d,5
 mov r10d,r8d
 add r10d,5
 invoke Ellipse,hdc,edx,r8d,r9d,r10d 
 invoke SelectObject,hdc,ss_hOldBrush
 invoke DeleteObject,rax
 invoke SelectObject,hdc,ss_hOldPen
 invoke DeleteObject,rax
 ret
CtrlBmp_DrawRadio endp

5. 菜单项绘制的初始化消息

当一个菜单项被设置了MFT_OWNERDRAW志标,系统就会向窗口过程发送初始化消息WM_MEASUREITEM消息,要求用户填写指定菜单项的相关尺寸,对每一个菜单项只发送一次。如果是主菜单,则立即发送;如果是子菜单,则在子菜单打开时发送。

WM_MEASUREITEM消息的lParam参数是一个MEASUREITEMSTRUCT结构。该结构并不是菜单绘制专用,也用于其它控件。以下针对菜单应用中相关参数进行说明:

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

(1) CtlType=如果是菜单项,则该值必须ODT_MENU,否则与菜单无关。

(2) CtlID=控件ID。绘制菜单时不使用该值。

(3) itemID=菜单项ID。对于菜单分隔线或子菜单标题项也有ID值。

(4) itemWidth=要求用户填写的菜单项的文本绘制宽度(以像素为单位)。该宽度值不包括菜单项的位图宽度,系统会自动处理位图宽度。

(5) itemHeight=要求用户填写的菜单项的文本绘制高度(以像素为单位)。对于主菜单项,则不需要填写该成员,系统采用默认高度。

注意:对于主菜单,如果设置了hbmpItem位图,并且位图的高度超过了主菜条的默认高度,则整个主菜单条的高度将自动调整为该位图的高度。

(6) itemData=这是菜单项中用户所关联的自定义值,即MENUITEMINFO结构中的dwItemData成员值。如果想对每一个菜单项实现针对性的尺寸控制,则可以通过它来传递用户自定义结构,提供专用的字体或尺寸参数。

注意,如果某一菜单项使用了hbmpItem位图,则系统不会发送WM_MEASUREITEM消息,这样就不能准确地填写itemWidth和itemHeight成员。因此,如果自己绘制菜单,并通过自动计算来确定文本绘制尺寸,则不要将hbmpItem位图与文本同时用于一个菜单项中。如果文本绘制尺寸采用固定值,则就不存在该问题了。

这里顺便提一下,如果在资源中定义一个无文本的菜单项,则将文本串定义为"\nul":

MENUITEM "\nul", IDM_LOCK

不能使用为"\0"或"",这样设置就成为一个分隔条了。这是在Masm64编程环境下的情况。

以下为处理WM_MEASUREITEM消息,并自动计算菜单项尺寸参数的源码。为了使菜单绘制整齐美观,定义了3个常量,其中MENU_DEFWT和MENU_DEFHT是为了确保子菜单尽量有统一的尺寸;MENU_DEFBMPWT用于扩大菜单项左边的位图区域,因为系统默认提供的区域只有17,视觉上感到比较拥挤。

 MENU_DEFWT    EQU 124   ;子菜单默认宽度
 MENU_DEFHT    EQU 22    ;子菜单默认高度
 MENU_DEFBMPWT EQU 40    ;子菜单左边默认位图区宽度

;========================================================
;设置自绘项目的尺寸---在WM_MEASUREITEM消息处调用
;入: hWin=窗口句柄
;    uhMenu=窗口主菜单句柄
;    hfont=绘制菜单所用的字体句柄
;         =0: 系统默认
;    lParam=WM_MEASUREITEM消息参数
;出: EAX=TRUE: 消息已处理
;       =NULL: 不是菜单所属消息
;    位图尺寸系统自动处理,不需要考虑。
;========================================================
Menu_SetItemSize proc hWin:QWORD,uhMenu:QWORD,\
                      hfont:QWORD,lParam:QWORD
 LOCAL ss_mi:MENUITEMINFO
 LOCAL ss_buf[MAX_PATH+1]:WORD
 LOCAL ss_size:SIZEL
 LOCAL ss_hdc:QWORD
 LOCAL ss_oldfont:QWORD
 mov ss_size.x,0
 xor rax,rax
 cmp [r9.MEASUREITEMSTRUCT].CtlType,ODT_MENU
 jnz ss_0        ;不是菜单绘制
 mov eax,[r9.MEASUREITEMSTRUCT].itemID
 test eax,eax
 jz ss_hsp  ;水平分隔条
 cmp ax,-1
 jz ss_vsp  ;垂直分隔条
;---取菜单标题---
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_STRING
 lea rax,ss_buf
 mov ss_mi.dwTypeData,rax
 xor cx,cx
 mov [rax],cx
 mov ss_mi.cch,MAX_PATH
 mov edx,[r9.MEASUREITEMSTRUCT].itemID ;菜单ID
 invoke GetMenuItemInfoW,uhMenu,edx,0,ADDR ss_mi
 test eax,eax
 jz ss_2    ;这种情况一般为独立的快捷菜单
 cmp ss_mi.cch,0
 jz ss_2  ;无标题字符串
;---计算菜单标题文本所需绘制宽度(ss_size.x为返回值)---
 invoke GetDC,hWnd  ;默认DC
 mov ss_hdc,rax
 mov rax,hfont
 test rax,rax
 jnz ss_3
 invoke GetStockObject,SYSTEM_FONT  ;取菜单默认字体
ss_3:
 invoke SelectObject,ss_hdc,rax
 mov ss_oldfont,rax
 invoke GetTextExtentPoint32W,ss_hdc,ADDR ss_buf,ss_mi.cch,ADDR ss_size
;--恢复DC环境--
 invoke SelectObject,ss_hdc,ss_oldfont
 invoke ReleaseDC,hWnd,ss_hdc
ss_2:
 mov r9,lParam
 mov eax,ss_size.y  ;文本绘制高度
 cmp rax,MENU_DEFHT
 jnc ss_5
 mov eax,MENU_DEFHT
ss_5:
 mov [r9.MEASUREITEMSTRUCT].itemHeight,eax ;默认高度
;---是否为子菜单项目---
 mov edx,[r9.MEASUREITEMSTRUCT].itemID
 invoke Menu_isOwnerId,uhMenu,edx
 mov r9,lParam
 mov ecx,ss_size.x  ;文本绘制宽度
 cmp eax,-1
 jnz ss_ok   ;为主菜单项目
;---是子菜单项目---
 add ecx,MENU_DEFBMPWT
 cmp ecx,MENU_DEFWT
 jnc ss_ok
 mov ecx,MENU_DEFWT ;取默认宽度
ss_ok:
 mov [r9.MEASUREITEMSTRUCT].itemWidth,ecx
 mov rax,TRUE
 jmp ss_0
ss_vsp: ;垂直分隔条
 mov [r9.MEASUREITEMSTRUCT].itemWidth,5
 jmp ss_0
ss_hsp:  ;水平分隔条
 mov [r9.MEASUREITEMSTRUCT].itemHeight,9 ;水平分隔线高度
 mov [r9.MEASUREITEMSTRUCT].itemWidth,MENU_DEFWT
ss_0:
 ret
Menu_SetItemSize endp

6. 菜单栏的背景色

菜单栏的背景色可以使用SetMenuInfo函数设置。该函数还可设置菜单栏的其它一此信息,使用比较简单,具体可查阅Windows文档。

通常情况下将菜单栏的背景色设置为窗口背景色,这样整体会比较谐调。如果不设置菜单栏背景色,则默认为控件背景色。

;=================================================
;设置菜单条的背景色
;入:hWin=窗口句柄
;        =0: 不重绘
;    uhMenu=菜单句柄
;    bkColor=背景色
;    zFlag=0(保留)
;出: NO
;=================================================
Menu_SetBackColor proc hWin:QWORD,uhMenu:QWORD,\
                      bkColor:DWORD,zFlag:DWORD
 LOCAL ss_mu:MENUINFO
 mov ss_mu.cbSize,SIZEOF MENUINFO
 mov ss_mu.fMask,MIM_BACKGROUND or MIM_APPLYTOSUBMENUS
 invoke CreateSolidBrush,bkColor ;创建背景画刷句柄
 mov ss_mu.hbrBack,rax
 invoke ObjPool_AddItem,rax      ;投入对象池
 invoke SetMenuInfo,uhMenu,ADDR ss_mu
 cmp hWin,0
 jz ss_0
 invoke DrawMenuBar,hWin
ss_0:
 ret
Menu_SetBackColor endp

7. 菜单绘制函数

菜单项的绘制包括3个项目,即菜单位图、菜单文本和分隔条。以下共4个函数来完成绘制任务:

(1)Menu_DrawItem :主函数,处理绘制的DC环境。
(2)Menu_DrawBmp :绘制菜单位图。
(3)Menu_DrawTxt :绘制菜单文本。
(4)Menu_DrawSeparator:绘制菜单分隔条。

;=====================================================
;绘制菜单项位图
;入: pDrawItem=DRAWITEMSTRUCT结构地址
;    pMenuInfo=MENUITEMINFO结构地址
;    zFlag=0: 保留
;返回: EAX=位图绘制区的宽度
;         =0: 无位图
;=====================================================
Menu_DrawBmp proc pDrawItem:QWORD,pMenuInfo:QWORD,\
                  zFlag:DWORD
 LOCAL ss_rc:RECT
 LOCAL ss_bitmap:BITMAP
 LOCAL ss_hBmp:QWORD
 LOCAL ss_hTempDC:QWORD
 LOCAL ss_wt:DWORD
 mov ss_wt,MENU_DEFBMPWT  ;位图绘制区的最小宽度
;---输出区域---
 mov eax,[rcx.DRAWITEMSTRUCT].rcItem.left
 mov ss_rc.left,eax
 mov eax,[rcx.DRAWITEMSTRUCT].rcItem.top
 mov ss_rc.top,eax
 mov eax,[rcx.DRAWITEMSTRUCT].rcItem.bottom
 mov ss_rc.bottom,eax
 mov eax,[rcx.DRAWITEMSTRUCT].rcItem.right
 cmp [rdx.MENUITEMINFO].cch,0
 jz ss_2                ;无菜单标题字符串
 mov r10,[rdx.MENUITEMINFO].dwTypeData
 mov r10w,[r10]
 cmp r10w,20h
 jc ss_2  ;首个字符为不可见,作空串处理
 mov eax,ss_wt
 add eax,ss_rc.left
ss_2:
 mov ss_rc.right,eax
;---确定要绘制的位图---
 mov rax,[rdx.MENUITEMINFO].hbmpItem
 test rax,rax
 jnz ss_3
 mov rax,[rdx.MENUITEMINFO].hbmpChecked
 test [rdx.MENUITEMINFO].fState,MFS_CHECKED
 jnz ss_3
 mov rax,[rdx.MENUITEMINFO].hbmpUnchecked
ss_3:
 test rax,rax
 jnz ss_bmp
;---无位图时绘制单(复)选位图---
 xor eax,eax
 test [rdx.MENUITEMINFO].fState,MFS_CHECKED
 jz ss_0
 test [rdx.MENUITEMINFO].fType,MFT_RADIOCHECK
 jz ss_Check
 invoke CtrlBmp_DrawRadio,QWORD PTR [rcx.DRAWITEMSTRUCT].hdc,ADDR ss_rc,0
 mov eax,ss_wt
 jmp ss_0
ss_Check:
 invoke CtrlBmp_DrawCheck,QWORD PTR [rcx.DRAWITEMSTRUCT].hdc,ADDR ss_rc,0
 mov eax,ss_wt
 jmp ss_0
ss_bmp:
 mov ss_hBmp,rax
;---计算位图居中坐标---
 invoke GetObject,ss_hBmp,SIZEOF BITMAP,ADDR ss_bitmap ;获取位图尺寸
 mov eax,ss_rc.bottom
 sub eax,ss_rc.top
 sub eax,ss_bitmap.bmHeight
 sar eax,1
 add ss_rc.top,eax  ;垂直居中
 mov eax,ss_rc.right
 sub eax,ss_rc.left
 sub eax,ss_bitmap.bmWidth
 jl ss_5
 sar eax,1
 add ss_rc.left,eax  ;水平居中
ss_5:
;---显示位图(使用TransparentBlt函数实现背景透明)---
 invoke CreateCompatibleDC,NULL
 mov ss_hTempDC,rax
 invoke SelectObject,rax,ss_hBmp
 mov rcx,pDrawItem
 mov rcx,[rcx.DRAWITEMSTRUCT].hdc
 invoke TransparentBlt,rcx,ss_rc.left,ss_rc.top,\
                     ss_bitmap.bmWidth,ss_bitmap.bmHeight,\
                     ss_hTempDC,0,0,ss_bitmap.bmWidth,\
                     ss_bitmap.bmHeight,0ffffffh
 invoke DeleteDC,ss_hTempDC
 mov eax,ss_bitmap.bmWidth
 cmp eax,ss_wt
 jnbe ss_0
 mov eax,ss_wt
ss_0:
 ret
Menu_DrawBmp endp
;===================================================
;绘制菜单项标题字符串
;入: pDrawItem=DRAWITEMSTRUCT结构地址
;    pMenuInfo=MENUITEMINFO结构地址
;    bwt=位图区宽度
;    zFlag.p0=0: 子菜单项目
;            =1: 主菜单项目
;===================================================
Menu_DrawTxt proc pDrawItem:QWORD,pMenuInfo:QWORD,\
                  bwt:DWORD,zFlag:DWORD
 LOCAL ss_x:DWORD
 mov eax,[rcx.DRAWITEMSTRUCT].rcItem.left
 mov ss_x,eax  ;保护
 test zFlag,1
 jnz ss_1
;---是子菜单项---
 cmp r8d,MENU_DEFBMPWT
 jnc ss_1
 mov r8d,MENU_DEFBMPWT  ;子菜单位图绘制区的最小宽度
ss_1:
 add eax,r8d  ;bwt
 mov [rcx.DRAWITEMSTRUCT].rcItem.left,eax
 mov rdx,[rdx.MENUITEMINFO].dwTypeData ;文本地址
 test rdx,rdx
 jz ss_0
 mov ax,[rdx]
 cmp ax,20h
 jc ss_0  ;首个字符为不可见,作空串处理
 mov r10d,DT_VCENTER or DT_SINGLELINE or DT_CENTER ;文本绘制格式
 test zFlag,1
 jnz ss_2
 mov r10d,DT_VCENTER or DT_SINGLELINE
ss_2:
 lea r9,[rcx.DRAWITEMSTRUCT].rcItem
 mov rcx,[rcx.DRAWITEMSTRUCT].hdc
 invoke DrawTextW,rcx,rdx,-1,r9,r10d
;---恢复区域参数---
ss_0:
 mov rcx,pDrawItem
 mov eax,ss_x
 mov [rcx.DRAWITEMSTRUCT].rcItem.left,eax
 ret
Menu_DrawTxt endp
;===================================================
;绘制菜单项分隔条
;入: pDrawItem=DRAWITEMSTRUCT结构地址
;    zFlag.p0=0: 子菜单项目
;            =1: 主菜单项目
;===================================================
Menu_DrawSeparator proc pDrawItem:QWORD,zFlag:DWORD
 test edx,1
 jnz ss_vert
;---水平分隔条---
 lea rdx,[rcx.DRAWITEMSTRUCT].rcItem
 mov eax,[rdx.RECT].bottom
 sub eax,[rdx.RECT].top
 shr eax,1
 add [rdx.RECT].top,eax
 mov rcx,[rcx.DRAWITEMSTRUCT].hdc
 invoke DrawEdge,rcx,rdx,EDGE_ETCHED,BF_TOP
 ret
;---垂直分隔条---
ss_vert:
 lea rdx,[rcx.DRAWITEMSTRUCT].rcItem
 mov eax,[rdx.RECT].right
 sub eax,[rdx.RECT].left
 shr eax,1
 add [rdx.RECT].left,eax
 add [rdx.RECT].top,2
 sub [rdx.RECT].bottom,2
 mov rcx,[rcx.DRAWITEMSTRUCT].hdc
 invoke DrawEdge,rcx,rdx,EDGE_ETCHED,BF_LEFT
 ret
Menu_DrawSeparator endp
;==============================================
;绘制菜单项目---在WM_DRAWITEM消息处调用
;入: hWin=窗口句柄
;    uhMenu=窗口主菜单句柄
;    hfont=绘制菜单所用的字体句柄
;         =0: 系统默认
;    lParam=DRAWITEMSTRUCT消息参数
;==============================================
Menu_DrawItem proc hWin:QWORD,uhMenu:QWORD,\
                   hfont:QWORD,lParam:QWORD
 LOCAL ss_rbx:QWORD
 LOCAL ss_mi:MENUITEMINFO
 LOCAL ss_buf[MAX_PATH+1]:WORD
 LOCAL ss_OldhFont:QWORD
 LOCAL ss_OldTxColor:DWORD
 LOCAL ss_OldBkColor:DWORD
 LOCAL ss_OldMode:DWORD
 LOCAL ss_txColor:DWORD
 LOCAL ss_bkColor:DWORD
 LOCAL ss_hBkBrush:QWORD
 LOCAL ss_hPen:QWORD
 LOCAL ss_Flag:DWORD
 LOCAL ss_hdc:QWORD
 mov ss_rbx,rbx
 mov rbx,r9  ;DRAWITEMSTRUCT
 mov rax,[rbx.DRAWITEMSTRUCT].hdc
 mov ss_hdc,rax
;---是否为主菜单项目---
 mov ss_Flag,0
 cmp rdx,[rbx.DRAWITEMSTRUCT].hwndItem
 jnz ss_1
 or ss_Flag,1  ;为主菜单项目
ss_1:
;---取菜单信息---
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_STRING or MIIM_STATE or MIIM_CHECKMARKS \
                 or MIIM_BITMAP or MIIM_FTYPE or MIIM_DATA
 lea rax,ss_buf
 mov ss_mi.dwTypeData,rax
 xor cx,cx
 mov [rax],cx
 mov ss_mi.cch,MAX_PATH
 mov rcx,[rbx.DRAWITEMSTRUCT].hwndItem   ;菜单句柄
 mov edx,[rbx.DRAWITEMSTRUCT].itemID ;菜单ID
 invoke GetMenuItemInfoW,rcx,edx,0,ADDR ss_mi
 test ss_mi.fType,MFT_SEPARATOR
 jz SS_2
 invoke Menu_DrawSeparator,rbx,ss_Flag
 jmp ss_0
SS_2:
;------------------
;确定颜色
;------------------
 test [rbx.DRAWITEMSTRUCT].itemState,(ODS_GRAYED or ODS_DISABLED)
 jnz ss_gray
 test [rbx.DRAWITEMSTRUCT].itemState,ODS_SELECTED or ODS_FOCUS or ODS_HOTLIGHT
 jnz ss_hot
;---非选择状态---
 mov ss_txColor,MENU_TXCOLOR
 mov ss_bkColor,MENU_BKCOLOR
 jmp ss_setcolor
ss_hot:
;---热点状态---
 mov ss_txColor,MENU_HTXCOLOR
 mov ss_bkColor,MENU_HBKCOLOR 
 jmp ss_setcolor
ss_gray:
;---禁用项目---
 mov ss_txColor,MENU_UTXCOLOR
 mov ss_bkColor,MENU_BKCOLOR 
;---充填背景色和文本色---
ss_setcolor:
 invoke CreateSolidBrush,ss_bkColor
 mov ss_hBkBrush,rax
 invoke FillRect,ss_hdc,ADDR [rbx.DRAWITEMSTRUCT].rcItem,ss_hBkBrush;
 invoke SetTextColor,ss_hdc,ss_txColor
 mov ss_OldTxColor,eax
 invoke SetBkColor,ss_hdc,ss_bkColor
 mov ss_OldBkColor,eax
 invoke SetBkMode,ss_hdc,TRANSPARENT
 mov ss_OldMode,eax
;---设置字体---
 cmp hfont,0
 jz ss_3
 invoke SelectObject,ss_hdc,hfont
 mov ss_OldhFont,rax
ss_3:
;---绘制位图---
 invoke Menu_DrawBmp,rbx,ADDR ss_mi,0
;---绘制文本---
 mov r8d,eax
 invoke Menu_DrawTxt,rbx,ADDR ss_mi,r8d,ss_Flag
;---绘制热点矩形---
 test [rbx.DRAWITEMSTRUCT].itemState,ODS_SELECTED or ODS_FOCUS or ODS_HOTLIGHT
 jz ss_out
 test [rbx.DRAWITEMSTRUCT].itemState,(ODS_GRAYED or ODS_DISABLED)
 jnz ss_out
 invoke CreatePen,0,1,MENU_FOCUSCOLOR
 mov ss_hPen,rax
 invoke FrameRect,ss_hdc,ADDR [rbx.DRAWITEMSTRUCT].rcItem,ss_hPen
 invoke DeleteObject,ss_hPen
ss_out:
 invoke DeleteObject,ss_hBkBrush
;---恢复DC环境---
 invoke SetTextColor,ss_hdc,ss_OldTxColor
 invoke SetTextColor,ss_hdc,ss_OldBkColor
 invoke SetBkMode,ss_hdc,ss_OldMode
 cmp hfont,0
 jz ss_0
 invoke SelectObject,ss_hdc,ss_OldhFont
ss_0:
 mov rbx,ss_rbx
 ret
Menu_DrawItem endp

8. 实例

为了演示菜单项各要素与状态的绘制情况,本实例中的菜单定义了2组单选菜单项目、2个复选菜单项目、2个hbmpItem位图主菜单、2个hbmpItem位图子菜单、1个禁用项目、1条垂直分隔线和3条水平分隔线。

8.1 资源菜单

以下为菜单资源文件的内容。菜单定义中并没有指定绘制志标MFT_OWNERDRAW,由用户装入菜单后再进行设置。只所以不在资源文件中指定,是为了演示的需要,即便于观察绘制与不绘制的区别。

//菜单位图
700             BITMAP  Image\File.bmp
701             BITMAP  Image\New.bmp
702             BITMAP  Image\Save.bmp
703             BITMAP  Image\SaveAs.bmp
704             BITMAP  Image\Exit.bmp
710             BITMAP  Image\Edit.bmp
711             BITMAP  Image\Cut.bmp
712             BITMAP  Image\Copy.bmp
713             BITMAP  Image\Paste.bmp
714             BITMAP  Image\Delete.bmp
715             BITMAP  Image\Lock.bmp
716             BITMAP  Image\UnLock.bmp
721             BITMAP  Image\Help.bmp
722             BITMAP  Image\About.bmp

//菜单ID
#define   IDM_MAIN    10  //主菜单条ID

#define   IDM_FILE    0100  //文件子菜单
#define   IDM_NEW     0101
#define   IDM_SAVE    0102
#define   IDM_SAVEAS  0103
#define   IDM_EXIT    0104

#define   IDM_EDIT    0200  //编辑子菜单
#define   IDM_CUT     0201
#define   IDM_COPY    0202
#define   IDM_PASTE   0203
#define   IDM_DEL     0204
#define   IDM_LOCK    0x8205 //这是复选菜单ID

#define   IDM_LOOK    0300   //查看子菜单
#define   IDM_WRAP    0x8301 //这是复选菜单ID
#define   IDM_SIMPLE  0x1301 //由3个菜单组成的一组单选菜单
#define   IDM_TRAD    0x1302
#define   IDM_ENGLISH 0x1303

#define   IDM_ALIGN   0400   //对齐子菜单
#define   IDM_LEFT    0x2401 //由4个菜单组成的一组单选菜单
#define   IDM_TOP     0x2402
#define   IDM_DEFAULT 0x2403
#define   IDM_SNAP    0x2404

#define   IDM_THELP   500   //帮助子菜单
#define   IDM_HELP    501
#define   IDM_ABOUT   502

IDM_MAIN MENUEX  // 定义主菜单条
BEGIN
    POPUP "\nul",IDM_FILE,,,01
    BEGIN
        MENUITEM "新  建", IDM_NEW
        MENUITEM "保  存", IDM_SAVE
        MENUITEM "另存为", IDM_SAVEAS
        MENUITEM "退  出", IDM_EXIT
    END
   POPUP "\nul",IDM_EDIT,,,02   //helpID值
   {
      MENUITEM "Cut",          IDM_CUT
      MENUITEM "Copy",         IDM_COPY
      MENUITEM "Paste",        IDM_PASTE
      MENUITEM "", ,           MFT_SEPARATOR
      MENUITEM "Delete",       IDM_DEL
      MENUITEM "", ,           MFT_SEPARATOR
      MENUITEM "Lock",         IDM_LOCK,,MFS_CHECKED
   }

   POPUP "查 看",IDM_LOOK,,,03   //helpID值
   {
      MENUITEM "自动换行",     IDM_WRAP ,,MFS_CHECKED
      MENUITEM "", ,           MFT_SEPARATOR
      MENUITEM "简体字",       IDM_SIMPLE,MFT_RADIOCHECK,MFS_CHECKED
      MENUITEM "繁体字",       IDM_TRAD,MFT_RADIOCHECK
      MENUITEM "英  文",       IDM_ENGLISH,MFT_RADIOCHECK
   }

   POPUP "对 齐",IDM_ALIGN,,,04   //helpID值
   {
      MENUITEM "横向对齐", IDM_LEFT ,MFT_RADIOCHECK,MFS_CHECKED
      MENUITEM "纵向对齐", IDM_TOP,MFT_RADIOCHECK
      MENUITEM "默认对齐", IDM_DEFAULT,MFT_RADIOCHECK
      MENUITEM "网格对齐", IDM_SNAP,MFT_RADIOCHECK,MFS_GRAYED
   }

   MENUITEM "", 0xffff,   MFT_SEPARATOR

   POPUP "帮 助",IDM_THELP,,,05   //helpID值
    BEGIN
        MENUITEM "\nul", IDM_HELP
        MENUITEM "\nul",IDM_ABOUT
    END
END

8.2 菜单装载与位图设置

以下共有5个函数:

Load_RsrcMenu: 菜单装载处理主函数。
Menu_LoadBmp: hbmpItem位图装载。
Menu_LoadCheckBmp: hbmpChecked位图装载。
Menu_SetAllDraw: 将全部菜单项设置MFT_OWNERDRAW志标。
Menu_NewFont: 创建菜单字体。


;---菜单位图ID---
 IDB_FILE    EQU 700
 IDB_NEW     EQU 701
 IDB_SAVE    EQU 702
 IDB_SAVEAS  EQU 703
 IDB_EXIT    EQU 704

 IDB_EDIT    EQU 710
 IDB_CUT     EQU 711 
 IDB_COPY    EQU 712
 IDB_PASTE   EQU 713
 IDB_DEL     EQU 714
 IDB_LOCK    EQU 715
 IDB_UNLOCK  EQU 716

 IDB_HELP    EQU 721
 IDB_ABOUT   EQU 722

;--------------------------------------------------
;菜单ID志标: 单选组组内首个菜单的组内序号必须为01h。
;--------------------------------------------------
 MENU_CHECKITEM    EQU 8000h   ;复选菜单项志标位
 MENU_RADIO_GIDMK  EQU 0f000h  ;单选组组号掩码(0-f)
 MENU_RADIO_GNMK   EQU 0f00h   ;单选组组内项目数掩码(0-f)
 MENU_RADIO_IIDMK  EQU 0ffh    ;单选组组内序号掩码(1-ff)
;--------------------------------------------------

;---菜单ID---
 IDM_MAIN    EQU 10     ;主菜单条ID

 IDM_FILE    EQU 0100   ;文件子菜单
 IDM_NEW     EQU 0101
 IDM_SAVE    EQU 0102
 IDM_SAVEAS  EQU 0103
 IDM_EXIT    EQU 0104

 IDM_EDIT    EQU 0200   ;编辑子菜单
 IDM_CUT     EQU 0201
 IDM_COPY    EQU 0202
 IDM_PASTE   EQU 0203
 IDM_DEL     EQU 0204
 IDM_LOCK    EQU 8205h  ;复选菜单ID

 IDM_LOOK    EQU 0300   ;查看子菜单
 IDM_WRAP    EQU 8301h  ;复选菜单ID
 IDM_SIMPLE  EQU 1301h  ;第1组单选菜单ID(1=组别,3=项目数,01=起始序号)
 IDM_TRAD    EQU 1302h  ;第1组单选菜单ID
 IDM_ENGLISH EQU 1303h  ;第1组单选菜单ID

 IDM_ALIGN   EQU 0400   ;对齐子菜单
 IDM_LEFT    EQU 2401h  ;第2组单选菜单ID
 IDM_TOP     EQU 2402h  ;第2组单选菜单ID
 IDM_DEFAULT EQU 2403h  ;第2组单选菜单ID
 IDM_SNAP    EQU 2404h  ;第2组单选菜单ID

 IDM_THELP   EQU 0500   ;帮助子菜单
 IDM_HELP    EQU 0501
 IDM_ABOUT   EQU 0502

.data?
 hMenuFont dq ? ;菜单字体句柄
 hMenu     dq ? ;主菜单句柄
.code
;=============================================
;创建菜单字体
;入: pFontName=字体名(UTF-8码)
;    fht=字体高度
;       =0: 默认
;    fWeight=字体粗细度(见LOGFONT结构)。常用:
;           0: 默认
;           FW_BOLD: 粗体
;出: RAX=字体句柄
;注: 字体句柄已添加到对象池。
;=============================================
Menu_NewFont proc pFontName:QWORD,fht:DWORD,\
                  fWeight:DWORD
 LOCAL ss_lf:LOGFONTW
 invoke RtlZeroMemory,ADDR ss_lf,SIZEOF ss_lf
 mov eax,fht
 mov ss_lf.lfHeight,eax
 mov eax,fWeight
 mov ss_lf.lfWeight,eax
 invoke MultiByteToWideChar,CP_UTF8,0,pFontName,-1,ADDR ss_lf.lfFaceName,LF_FACESIZE
 invoke CreateFontIndirectW,ADDR ss_lf
 invoke ObjPool_AddItem,rax ;添加到对象池
 ret 
Menu_NewFont endp
;===============================================
;将菜单的全部项目(包括子菜单)都设置为自绘格式
;入: uhMenu=菜单句柄
;===============================================
Menu_SetAllDraw proc uhMenu:QWORD
 LOCAL ss_mi:MENUITEMINFO
 LOCAL ss_i:DWORD
 LOCAL ss_num:DWORD
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_ID or MIIM_SUBMENU or MIIM_FTYPE
 mov ss_i,0
 invoke GetMenuItemCount,uhMenu
 cmp eax,0
 jle ss_out
 mov ss_num,eax
ss_lp1:
 invoke GetMenuItemInfoW,uhMenu,ss_i,TRUE,ADDR ss_mi
 or ss_mi.fType,MFT_OWNERDRAW
 invoke SetMenuItemInfoW,uhMenu,ss_i,TRUE,ADDR ss_mi
 cmp ss_mi.hSubMenu,0
 jz ss_nt
 invoke Menu_SetAllDraw,ss_mi.hSubMenu ;递归
ss_nt:
 inc ss_i
 dec ss_num
 jnz ss_lp1
ss_out:
 ret
Menu_SetAllDraw endp
;==============================================================
;装载菜单状态位图,并关联到指定的菜单项
;入: uhMenu=菜单句柄
;    mId=位图要关联的菜单ID
;    CheckedId=选择状态的位图ID
;             =0: 无
;    UncheckedId=未选择状态的位图ID
;               =0: 无
;               =CheckedId: 状态位图不改变
;出: RAX=CheckedId位图句柄
;==============================================================
Menu_LoadCheckBmp proc uhMenu:QWORD,mId:DWORD,\
                  CheckedId:DWORD,UncheckedId:DWORD
 LOCAL ss_hInst:QWORD
 LOCAL ss_mi:MENUITEMINFO
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,0
 mov ss_mi.hbmpUnchecked,0
 mov ss_mi.hbmpChecked,0
 invoke GetModuleHandle,0 ;程序实例句柄
 mov ss_hInst,rax
 cmp CheckedId,0
 jz ss_2
 invoke LoadImage,ss_hInst,CheckedId,IMAGE_BITMAP,0,0,LR_SHARED or LR_DEFAULTSIZE
 mov ss_mi.hbmpChecked,rax
 invoke ObjPool_AddItem,rax ;添加到对象池,便于程序退出时释放
 or ss_mi.fMask,MIIM_CHECKMARKS
ss_2:
 mov edx,UncheckedId
 test edx,edx
 jz ss_4
 mov rax,ss_mi.hbmpChecked
 cmp edx,CheckedId
 jz ss_3
 invoke LoadImage,ss_hInst,UncheckedId,IMAGE_BITMAP,0,0,LR_SHARED or LR_DEFAULTSIZE
 invoke ObjPool_AddItem,rax
 or ss_mi.fMask,MIIM_CHECKMARKS
ss_3:
 mov ss_mi.hbmpUnchecked,rax
ss_4:
 invoke SetMenuItemInfoW,uhMenu,mId,NULL,ADDR ss_mi
 mov rax,ss_mi.hbmpChecked
 ret
Menu_LoadCheckBmp endp
;===========================================
;装载菜单无状态位图,并关联到指定的菜单项
;入: uhMenu=菜单句柄
;    mId=位图要关联的菜单ID
;    BmpId=位图ID
;出: RAX=位图句柄
;===========================================
Menu_LoadBmp proc uhMenu:QWORD,mId:DWORD,\
                  BmpId:DWORD
 LOCAL ss_hInst:QWORD
 LOCAL ss_mi:MENUITEMINFO
 LOCAL ss_buf[MAX_PATH+1]:WORD
 mov ss_mi.cbSize,SIZEOF MENUITEMINFO
 mov ss_mi.fMask,MIIM_BITMAP or MIIM_STRING
 lea rax,ss_buf
 mov ss_mi.dwTypeData,rax
 xor cx,cx
 mov [rax],cx
 mov ss_mi.cch,MAX_PATH
 invoke GetMenuItemInfoW,uhMenu,mId,0,ADDR ss_mi
 mov ax,ss_buf
 cmp ax,20h
 jnc ss_1
 mov ss_buf,0
ss_1:
 invoke GetModuleHandle,0 ;程序实例句柄
 mov ss_hInst,rax
 invoke LoadImage,ss_hInst,BmpId,IMAGE_BITMAP,0,0,LR_SHARED or LR_DEFAULTSIZE
 mov ss_mi.hbmpItem,rax
 invoke ObjPool_AddItem,rax ;添加到对象池,便于程序退出时释放
 invoke SetMenuItemInfoW,uhMenu,mId,NULL,ADDR ss_mi
 mov rax,ss_mi.hbmpItem
 ret
Menu_LoadBmp endp
;===========================================
;装入菜单---资源菜单
;入: hWin=窗口句柄。主菜单将关联到该窗口。
;返回: hMenu=主菜单句柄
;      hMenuFont=菜单字体句柄
;===========================================
Load_RsrcMenu proc hWin:QWORD
;---在菜单装载或创建前先创建菜单字体---
 invoke Menu_NewFont,"楷体",0,FW_BOLD
 mov hMenuFont,rax
;---装入资源菜单---
 invoke GetModuleHandle,0 ;程序实例句柄
 invoke LoadMenu,rax,IDM_MAIN
 mov hMenu,rax
 invoke SetMenu,hWin,hMenu
;---装入菜单资源位图---
;---设置主菜单的无状态位图---
 invoke Menu_LoadBmp,hMenu,IDM_FILE,IDB_FILE
 invoke Menu_LoadBmp,hMenu,IDM_EDIT,IDB_EDIT
;---设置子菜单左边的无状态位图---
 invoke Menu_LoadCheckBmp,hMenu,IDM_NEW,IDB_NEW,IDB_NEW
 invoke Menu_LoadCheckBmp,hMenu,IDM_SAVE,IDB_SAVE,IDB_SAVE
 invoke Menu_LoadCheckBmp,hMenu,IDM_SAVEAS,IDB_SAVEAS,IDB_SAVEAS
 invoke Menu_LoadCheckBmp,hMenu,IDM_EXIT,IDB_EXIT,IDB_EXIT
 invoke Menu_LoadCheckBmp,hMenu,IDM_CUT,IDB_CUT,IDB_CUT
 invoke Menu_LoadCheckBmp,hMenu,IDM_COPY,IDB_COPY,IDB_COPY
 invoke Menu_LoadCheckBmp,hMenu,IDM_PASTE,IDB_PASTE,IDB_PASTE
 invoke Menu_LoadCheckBmp,hMenu,IDM_DEL,IDB_DEL,IDB_DEL
;---设置子菜单左边的状态位图---
 invoke Menu_LoadCheckBmp,hMenu,IDM_LOCK,IDB_LOCK,IDB_UNLOCK
;---设置子菜单独立无状态位图---
 invoke Menu_LoadBmp,hMenu,IDM_ABOUT,IDB_ABOUT
 invoke Menu_LoadBmp,hMenu,IDM_HELP,IDB_HELP
;---如果要自己绘制菜单,则调用以下2个函数---
 invoke Menu_SetBackColor,NULL,hMenu,MENU_BKCOLOR,0
 invoke Menu_SetAllDraw,hMenu  ;全部项目设置为自绘格式
;---重绘菜单---
 invoke DrawMenuBar,hWin
 mov rax,hMenu
 ret
Load_RsrcMenu endp

8.3 窗口过程

以下为窗口消息过程。注意,必须在处理WM_DESTROY消息时调用ObjPool_Free函数释放对象池,对象池中已保存了14个位图句柄、1个字体句柄和1个画刷句柄,在窗口销毁时删除它们。

.code
;===========================================================
;窗口过程
;===========================================================
WndProc proc hWin:QWORD,uMsg:QWORD,wParam:QWORD,lParam:QWORD
 LOCAL ss_pt:POINT
 cmp rdx,WM_MEASUREITEM
 jz jWM_MEASUREITEM
 cmp rdx,WM_DRAWITEM
 jz jWM_DRAWITEM
 cmp rdx,WM_COMMAND    ;uMsg
 jz jWM_COMMAND
 cmp rdx,WM_RBUTTONDOWN
 jz jWM_RBUTTONDOWN
 cmp rdx,WM_CREATE
 jz jWM_CREATE
 cmp rdx,WM_DESTROY
 jz jWM_DESTROY
ss_def:
 invoke DefWindowProc,hWin,uMsg,wParam,lParam
 ret
jWM_MEASUREITEM: ;设置菜单绘制尺寸
 invoke Menu_SetItemSize,hWin,hMenu,hMenuFont,lParam
 ret
jWM_DRAWITEM:    ;绘制菜单
 invoke Menu_DrawItem,hWin,hMenu,hMenuFont,lParam
 ret
jWM_CREATE:
 invoke Load_RsrcMenu,hWin  ;装载菜单并设置位图
 mov hMenu,rax
 jmp ss_ok
jWM_DESTROY:    ;销毁窗口
 call ObjPool_Free  ;释放对象池
 rcall PostQuitMessage,NULL
 jmp ss_ok
jWM_RBUTTONDOWN:  ;弹出快捷菜单
 invoke GetCursorPos,addr ss_pt     ;获得当前鼠标的坐标
 add ss_pt.x,10
 add ss_pt.y,10
 invoke GetSubMenu,hMenu,1
 invoke TrackPopupMenuEx,rax,0,ss_pt.x,ss_pt.y,hWin,0
 jmp ss_ok
jWM_COMMAND:
 test r9,r9
 jnz ss_def
;---改变单(复)选菜单状态---
 invoke Menu_ChangeCheck,hMenu,r8d ;r8d=菜单ID
;---执行菜单命令---
 cmp wParam,IDM_EXIT
 jnz ss_ok
 rcall SendMessage,hWin,WM_CLOSE,NULL,NULL
ss_ok:
 xor rax,rax
 ret
WndProc endp

效果图如下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值