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,该函数可以绘制任何单色位图。单色位图不一定是黑白位图,可以有颜色,颜色由事先设置到h