Windows菜单位图与菜单绘制

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值