菜单
资源代码讲解
虽说纯代码自己写资源的定义是非常不靠谱和浪费大好青春的举动,但是看懂代码是展现我们良好技术功底的基本需求,所以这节课小甲鱼通过示例代码给大家简单的讲解下构造和如何调用。
菜单的定义框架
在资源脚本文件中菜单的定义格式是:
菜单ID MENU [DISCARDABLE]
BEGIN
菜单项定义
…
END
菜单的定义框架
“菜单ID MENU [DISCARDABLE]”语句用来指定菜单的ID值和内存属性,菜单ID可以是16位的整数,范围是1~65 535,但菜单ID也可以用字符串表示,如下面的定义:
MainMenu menu
表示菜单的ID是字符串型的”MainMenu”,但这样定义的话,在程序中引用的时候就要用字符串指针代替16进制的菜单ID值,显得相当不便,所以在实际应用中通常使用16进制数值当做菜单ID。
数值型ID的范围限制在1~65 535之间的原因是字符串在内存中的线性地址总是大于10000h,API函数检测参数时发现小于10000h时就可以把它认为是数值型的,大于10000h时就当做字符串指针处理。
你就承认吧,因为这一句,百分之八十以上的鱼油不知道作者在说啥呢?
小甲鱼友情提示:10000h == 65536
menu关键字后面的DISCARDABLE是菜单的内存属性,表示菜单在不再使用的时候可以暂时从内存中释放以节省内存,这是一个可选属性。
另外,菜单项的定义语句必须包含在 BEGIN 和 END 关键字之内,这两个关键字也可以用花括号{ 和 } 来代替。
菜单项目的定义方法有3类:双层牛肉巨无霸,板烧鸡腿堡,老北京鸡肉卷(据说智商超过160才能理解。。。。。。)
方法一:
MENUITEM 菜单文字 命令ID [,选项列表]
方法二:
MENUITEM SEPARATOR
方法三:
POPUP 菜单文字 [,选项列表] (用法3)
BEGIN
item-definitions
...
END
下面分别就这3类详细说明
方法一组成部分如下:
菜单文字
命令ID
选项:CHECKED, GRAYED, INACTIVE, MENUBREAK 或 MENUBARBREAK
实验:当把例子中的“详细资料”及以后的菜单项都另起一列。
方法二定义的是菜单项之间的分隔线,显然,分隔线是不需要字符串和选项的。
方法三定义的是弹出式菜单( popup 菜单),顶层菜单是由多个弹出式子菜单组成的。
popup菜单的选项列表可以是以下的值:
GRAYED, INACTIVE, HELP
注:popup菜单项选中的时候会自动将弹出式菜单弹出来,不需要向程序发送消息,所以在定义的参数中不需要命令ID。
加速键的定义框架
和菜单的定义相比,加速键的定义要简单得多,具体的语法如下:
加速键ID ACCELERATORS
BEGIN
键名, 命令ID [,类型] [,选项]
...
END
加速键ID同样可以是一个字符串或者是1~65 535之间的数字,整个定义内容也是用begin和end(或花括号)包含起来。
每个键占据一行,各字段的含义如下:
键名:表示加速键对应的按键,可以有3种方式定义
"^字母":表示Ctrl键加上字母键
"字母":表示字母,这时类型必须指明是VIRTKEY
数值:表示ASCII码为该数值的字母,这时类型必须指明为ASCII。
命令ID:按下加速键后,Windows向程序发送的命令ID。把加速键和菜单项关联起来,要做的就是关联菜单项的命令ID。
类型:用来指定键的定义方式,可以是VIRTKEY和ASCII,分别用来表示”键名”字段定义的是虚拟键还是ASCII码。
选项:可以是 Alt, Control 或 Shift 中的单个或多个,如果指定多个,则中间用逗号隔开,表示加速键是按键加上这些控制键的组合键。这些选项只能在类型是VIRTKEY的情况下才能使用。
动手时间!
注意为 Resedit 设置 include 路径。
在一个资源脚本文件中,可以定义多个菜单和多个加速键表,当然也有其他各式各样的资源,有位图、图标与对话框等,这就涉及为这些资源取ID值的问题,取值的时候要掌握的原则是:
(1)对于同类别的多个资源,资源ID必须为不同的值,如定义了两个菜单,那么它们的ID就必须用不同的数值表示,否则将无法分辨。
(2)对于不同类别的资源,资源ID在数值上可以是相同的,如可以将菜单和加速键的ID都定义为1,同时也可以有ID为1的位图或图标等,Windows并不会把它们搞混。
使用菜单和加速键
在完成资源文件所示的编写后,我们来看看如何在程序中使用菜单和加速键。
例子程序的运行界面:Menu.exe
接下来,小甲鱼将带大家逐步分析这些功能是如何实现的。
Menu.asm 代码是在 FirstWindow 程序的基础上改写的,这是编写Win32汇编程序的一个常用方法 —— 拷贝一个模板程序再进行修改会节省很多的时间。
加载菜单
在窗口中加载菜单的方法在第4章已经提及,方法有两个:
一是在注册窗口类的时候指定类的默认菜单;
二是在建立窗口的时候在参数中指定菜单句柄。
Menu.asm 程序中明显用的是第2种方法。
注意到参数中的 hMenu。也就是说不管用哪种方法,都必须先获取菜单句柄存入 hMenu,才能对菜单进行XXOO操作。
我们使用 LoadMenu 函数来得到菜单句柄。
invoke LoadMenu, hInstance, IDM_MAIN
mov hMenu, eax
猛然发现,这个函数需要用到两个参数,第一个是实例的句柄,第二个是准备打开的菜单。
第一个参数我们用 GetModuleHandle 获取的实例句柄,第二个参数指定需要装入的菜单资源ID,函数返回菜单句柄。
在得到菜单句柄以后,我们把它放入hMenu。
另外,当资源文件中用字符串为名称定义菜单而不是用数值的时候,例如:
MainMenu menu //菜单名为字符串“MainMenu”
begin
……
end
那么在程序中就必须用字符串指针代替菜单ID做参数,这就是之前说比较麻烦的一点。
szMenu "MainMenu", 0 ;在数据段中定义菜单名称字符串
……
invoke LoadMenu, hInstance, addr szMenu ;在程序中装载
mov hMenu,eax
用字符串为名称定义资源,在资源装载函数LoadXXXX 中用字符串指针做参数装入,这实际上是一个通用的方法,不仅适用于菜单资源,对于其他类别的资源也是适用的。在其他资源的介绍中就不再另外说明了。
加载加速键
和菜单一样,加速键在使用前也要装入,参数同样是在资源脚本文件中定义的加速键ID,程序中对应的语句是:
invoke LoadAccelerators, hInstance, IDA_MAIN
mov @hAccelerator, eax
其实我们自己在程序中也可以很方便地实现加速键功能,方法是:截获 WM_KEYDOWN 消息并判断键盘消息并按照自定义的逻辑进行处理,使用加速键实际上是让Windows替我们完成这个功能。
Windows实现的方法正是在消息循环中检查 WM_KEYDOWN 和 WM_SYSKEYDOWN 消息。
.while TRUE
invoke GetMessage,addr @stMsg,NULL,0,0
.break .if eax == 0
invoke TranslateAccelerator, hWinMain, @hAccelerator,addr \ @stMsg
.if eax == 0
invoke TranslateMessage,addr @stMsg
invoke DispatchMessage,addr @stMsg
.endif
.endw
没错,四楼的 TranslateAccelerator 亮了!
TranslateAccelerator 函数是实现加速键功能的核心,它的参数为目标窗口、加速键句柄和 GetMessage 取得的消息结构。
该函数检查消息结构中的消息,如果遇到 WM_KEYDOWN 和 WM_SYSKEYDOWN 消息则检测加速键资源,看按键是否符合某个加速键,符合的话则向目标窗口发送WM_COMMAND或WM_SYSCOMMAND 消息,并返回TRUE,不符合的话不进行任何处理并返回FALSE。
由于加速键的键码并不是用户真正想输入窗口的,比如用户在写字板中输入文字,按 Ctrl+C 键是为了拷贝,而并不是想把 Ctrl+C 键对应的字符输入文档。
所以这个 Ctrl+C 的键码在完成加速键的使命后就应该丢弃。
也就是说符合加速键的键盘消息不应该再发送给窗口,TranslateMessage 和 DispatchMessage 函数前的逻辑判断就是这样的意图:只有 TranslateAccelerator 没有转换的消息(返回值eax为0)才继续处理。
菜单和加速键消息
当用户选择了一个菜单项的时候,Windows 向菜单所属的窗口发送 WM_COMMAND 消息;而用户按下了一个加速键的时候,Windows 向 TranslateAccelerator 函数指定的目标窗口发送WM_COMMAND 消息。
一般这两种情况对应的窗口都是主窗口,所以可以在主窗口中的窗口过程中集中处理 WM_COMMAND 消息,而不必考虑它究竟是菜单引发的还是加速键引发的。
WM_COMMAND 消息的两个参数是这样定义的:
wParam 的高位 = wNotifyCode ;通知码
wParam 的低位 = wID ;命令ID
lParam = hwndCtl ;发送 WM_COMMAND 的子窗口句柄
除了菜单和加速键,WM_COMMAND 消息也可以由其他子窗口引发,如主窗口的按钮或工具栏等,lParam 参数指定了引发消息的子窗口句柄。
对于菜单和加速键引发的 WM_COMMAND 消息,lParam 的值为零。wParam 参数的低16位是命令ID,也就是资源脚本文件中菜单项的命令ID或加速键的命令ID,高16位是通知码,菜单消息的通知码是0,加速键消息的通知码为1。
在需要处理菜单和加速键消息的窗口过程中,一般需要增加一个 WM_COMMAND 分支来处理对应的消息,这个分支的一般结构为=>
.elseif eax == WM_COMMAND ;eax中为wMsg
mov eax, wParam
movzx eax, ax
.if eax == 命令ID1
...
.elseif eax == 命令ID2
...
.endif
其中 movzx eax, ax 指令将16位的 ax 扩展到32位的 eax,相当于将 eax 的高16位填零。
作用就是当消息由加速键引起时,将高16位中的1忽略,这样下面的分支就可以同时处理菜单和加速键消息。
当然我们也可以去掉这一句,这样下面的比较语句中就要使用 ax 而不是 eax。
敏锐的鱼油会发觉,资源文件中定义的“字体”菜单项的ID为Ox4201,当选中“字体”菜单项的时候,对话框中显示的 wParam 数值正是00004201。
而按下加速键 Alt+F 的时候,显示出来的值就是00014201了,它们的区别就是高16位中的通知码不同。
菜单项的修改
在程序的运行中也可以动态修改菜单项,包括添加、删除和修改操作,这些操作是通过几个API函数来完成的:
添加菜单项
invoke AppendMenu, hMenu, uFlags, \ uIDNewItem, lpNewItem;
AppendMenu 用来在一个菜单的最后添加菜单项。
在程序的运行中也可以动态修改菜单项,包括添加、删除和修改操作,这些操作是通过几个API函数来完成的:
添加菜单项
invoke AppendMenu, hMenu, uFlags, \ uIDNewItem, lpNewItem;
AppendMenu 用来在一个菜单的最后添加菜单项。
对于 AppendMenu 和 InsertMenu,会有一个新的菜单项产生。
uIDNewItem 就表示这个新菜单项的命令
IDlpNewItem 指向新菜单项的文字字符串
修改菜单项
invoke ModifyMenu, hMenu, uPosition, uFlags, \ uIDNewItem, lpNewItem;
由于 ModifyMenu 函数可以修改一个菜单项的命令ID或文字字符串,所以也有 uIDNewItem 和 lpNewItem 参数。
删除菜单项
invoke DeleteMenu, hMenu, uPosition, uFlags;
invoke RemoveMenu, hMenu, uPosition, uFlags;
DeleteMenu 和 RemoveMenu 的不同之处在于对 popup 菜单项的处理。
DeleteMenu 不仅删除菜单项,而且将这个 popup 菜单项的所有子项目全部删除,这样,这个 popup 菜单就不能在别的地方继续使用。
RemoveMenu 仅从菜单中移去这个 popup 菜单项,整个 popup 菜单在内存中还是存在的。
自己尝试下修改。
使用系统菜单
系统菜单指按下了标题栏图标后弹出的菜单,和窗口菜单不同,选中系统菜单的菜单项后,Windows 向窗口发送的是 WM_SYSCOMMAND 消息而非 WM_COMMAND 消息。
默认的系统菜单中已经有“还原”、“移动”、“大小”、“最大化”、“最小化”和“关闭”等菜单项,这些菜单项的命令ID已经预定义为 SC_RESTORE,SC_MOVE,SC_SIZE,SC_MAXIMIZE,SC_MINIMIZE 和 SC_CLOSE
如果鱼油们要自己处理它们,可以在WM_SYSCOMMAND 消息中建立一个比较分支对它们进行处理,一般在程序中并不自己处理WM_SYSCOMMAND 消息(没事找事除外),而是交给DefWindowProc 处理。
如何在系统菜单中添加自己的菜单项呢?方法就是使用上面介绍的AppendMenu(当然也可以用InsertMenu),在添加前必须用 GetSystemMenu函数首先获取系统菜单的句柄。
看例题源码去:Menu.asm
右键弹出菜单
例子程序的一个功能是当用户在窗口客户区按下鼠标右键的时候弹出一个菜单,这个功能是用TrackPopupMenu 函数实现的。
invoke TrackPopupMenu, hMenu, uFlags, x, y, nReserved, hWnd, lpRect;
这个函数本身很简单,执行后在参数指定的 x,y 位置弹出一个属于 hWnd 窗口(也就是说 WM_COMMAND 消息发到这个窗口)的菜单,菜单句柄是 hMenu。
使用 TrackPopupMenu 时要注意的是,弹出的菜单句柄必须是 popup 类型的,而在资源文件中定义并且可以用 LoadMenu 函数装入的菜单并不是 popup 类型的。
所以在程序中我们要用 GetSubMenu 得到的第二层子菜单的句柄才是 popup 类型的。
GetSubMenu函数的用法是:
invoke GetSubMenu,hMenu,nPos
.if eax
mov hSubMenu,eax
.endif
nPos 参数指定要获取的菜单的位置索引,GetSubMenu 的返回值是获取的子菜单句柄。
这里的 x, y 是要弹出右键菜单的坐标,一般的话我们让他跟鼠标一起。
要获取鼠标位置,可以用 GetCursorPos 函数:
invoke GetCursorPos, lpPoint
参数 lpPoint 指向一个 POINT 数据结构,这个结构只有两个字段:
POINT STRUCT
x DWORD ?
y DWORD ?
POINT ENDS
uFlags 参数指定一些和位置相关的选项,它可以是 PM_CENTERALIGN,TPM_LEFTALIGN 或 TPM_RIGHTALIGN 三者之一,表示(x,y)坐标是代表弹出菜单位置的中间、左上角还是右上角,一般的习惯是使用TPM_LEFTALIGN,这样菜单会在鼠标点击处的右边弹出。
uFlags 中同时还可以指定用鼠标左键还是右键选定菜单项,定义值可以是 TPM_LEFTBUTTON 或 TPM_RIGHTBUTTON。
lpRect 指向一个 RECT 结构,用来指定一个区域,当菜单弹出后,在这个区域外单击鼠标,菜单才会消失。
如果这个参数指定为 NULL 的话,在菜单之外单击鼠标,菜单就会消失。
hWnd 是设置拥有快捷菜单的窗口的句柄。此窗口接收来自菜单的所有消息。
最后,还有个 nReserved 是保留值,必须为零。
木有了。。。。。。
菜单状态的检测和设置
在程序中经常要对菜单项的状态进行设置。
如剪贴板中没有数据时,”粘贴”菜单项应该灰化,窗口中没有被选中的字符时,“拷贝”菜单项也应该灰化,这样可以给使用者一个善意提醒。
同样,对菜单的状态也常常需要检测,如查看菜单项的状态是否处于灰化状态或选中状态以便进行下一步操作等。
对菜单项状态的检测可以用GetMenuState函数来完成。
invoke GetMenuState, hMenu, uId, uFlags
参数 hMenu 是菜单的句柄,uId 用来定位要检测的菜单项
当 uFlags 是 MF_BYCOMMAND 的时候,uId 用菜单项的命令ID指定
当 uFlags 是MF_BYPOSITION的时候,uId 的值是位置索引
函数执行后的返回值为-1时表示行动失败了。
否则会是返回 MF_CHECKED(选中),MF_DISABLED(禁用),MF_GRAYED(灰化),MF_HILITE(高亮),MF_MENUBARBREAK(分割线),MF_MENUBREAK(分割线)和MF_SEPARATOR(分割线)的组合值。
没错,再次强调,这些都是宏定义的结果,因此可以通过测试相应的数据位来分辨菜单项处于哪种状态。
invoke GetMenuState,hMenu,IDM_XXX,MF_BYCOMMAND
.if eax & MF_CHECKED
; 表示IDM_XXX菜单项现在是选中状态
.endif
当然我们还可以设置菜单的状态。
设置菜单项的状态可以用下列3个函数来实现不同的功能:
invoke EnableMenuItem, hMenu, uIDEnableItem, uEnable
invoke CheckMenuItem, hMenu, uIDCheckItem, uCheck
invoke CheckMenuRadioItem, hMenu, idFirst, idLast, idCheck, \ uFlags
EnableMenuItem 函数将菜单项在禁用、可用和灰化状态之间切换,uEnable 可以取值为 MF_DISABLED, MF_ENABLED 和 MF_GRAYED,分别代表这3种状态。
CheckMenuItem 函数将菜单项在非互斥的选定状态和非选定状态之间切换(即前面是否有对钩),uCheck 的取值可以是 MF_CHECKED 或 MF_UNCHECKED,代表选定或非选定状态。
CheckMenuRadioItem 将菜单项在互斥的选定状态和非选定状态之间切换(即前面是否有圆点标志),由于互斥的菜单项在一个范围内只有一个是可以选定的,当选定另一个的时候,原来的选定应该撤销。
idFirst 和 idLast 就指定了这个互斥范围。
函数在选定 idCheck 指定的菜单项的同时将自动清除 idFirst 和 idLast 范围内的其他选定。
所以 uFlags 中无需指定状态,只需指定 MF_BYCOMMAND 或 MF_BYPOSITION定位方法。
在这些函数的参数中,uIDEnableItem,uIDCheckItem,idFirst,idLast和idCheck用来定位菜单项,同样,参数的取值可以是菜单项的命令ID或位置索引。
最后,修改菜单状态的时机是什么时候呢?在程序中似乎不应该随时去检测状态并设置,这显然是很浪费资源的。
Windows 考虑到了这一点:在菜单将要激活的时候,也就是用户在菜单上按动鼠标的时候,Windows 在菜单弹出之前会向窗口过程发送 WM_INITMENU 消息,我们可以从容不迫地在这里进行各种检测,并设置对应的菜单项。
鱼油们可以自行修改测试。
其他菜单函数
除了前面介绍的一些函数之外,还有一些不太常用的菜单函数,在这里作一个简单的介绍。
菜单不一定非要在资源文件中定义,在程序中也可以用代码来建立菜单,不过比较麻烦一点。
方法是先用 CreateMenu 建立一个菜单,CreateMenu 函数没有参数,调用后返回一个没有任何菜单项的菜单句柄。
接下来就可以用AppendMenu 在上面一条条地添加菜单项了。
同样,CreatePopupMenu 也可以建立一个没有任何菜单项的菜单句柄,但它建立的是 popup 类型的菜单句柄,可以在 TrackPopupMenu 中直接使用。
如果要获取一个窗口当前使用的菜单句柄,那么可以使用 GetMenu 函数:
invoke GetMenu,hWnd
mov hMenu,eax
一个菜单的总项数可以用 GetMenuItemCount 函数获取:
invoke GetMenuItemCount, hMenu
不过 GetMenuItemCount 函数的返回值不包括子菜单展开以后的项数,而是指最上层菜单的项数。
如果要统计全部展开后的项数,那么只好用GetSubMenu 一层层地统计下去了再加起来。
建立窗口时指定了菜单句柄后并不是不能改变的,我们常常见到一些编辑软件,没有打开文件之前菜单只有寥寥几项,一打开文件以后功能菜单就全部出来了,实际上这是用SetMenu函数完成的:
invoke SetMenu,hWnd,hMenu
可以在资源文件中预定义几个不同的菜单,在使用的时候根据不同情况用 SetMenu 设置不同的菜单句柄。
使用菜单后,要涉及清除的问题,和窗口相连的菜单句柄在窗口摧毁的时候会由 Windows 自动释放,不需要手工释放。
但没有和窗口相连的菜单就要由程序自己来释放了,方法是使用 DestroyMenu 函数,比如没有和窗口相连而仅用 TrackPopupMenu 弹出的菜单句柄:
invoke DestroyMenu, hMenu