读者可能注意到大多数Windows程序都包含图标。打开“我的电脑”以后,各个可执行程序显示的图标各不相同,当程序运行后,大多数程序有菜单。另外,当鼠标移动到窗口中后,光标也有可能变得不同,很多程序还使用对话框来提供用户界面。
菜单、图标与对话框都是可执行文件的组成部分,它们是以资源的形式存放在文件中的。但这些资源并不在源代码的数据段中定义,而是由链接程序放入文件的单独一个节区中,当运行中要用到资源的时候,必须借助API函数装入后才能使用。
除了菜单、图标与对话框,Windows中还有其他一些类型的资源,它们是:
● 菜单和加速键
● 光标和图标
● 位图
● 对话框
● 字符串资源
● 版本信息
● 自定义资源
资源文件的“源文件”是以rc为扩展名的脚本文件,由资源编译器Rc.exe编译成为以res为扩展名的二进制资源文件,最后在链接的时候由Link.exe链入可执行文件中,这在前面的内容中已经有所介绍,在本章中将介绍资源的定义方法,以及在程序中的使用方法。
本章的篇幅比较大,但是编写一个Win32程序,与界面有关的代码起码要占一半以上,而与界面相关的代码中,又有大部分涉及各种资源和控件的使用,所以仔细研究本章,以及第9章介绍的“通用控件”的内容绝不是浪费时间,了解了这两章的内容,写一个应用程序的界面就基本上不成问题了。
5.1 菜单和加速键
5.1.1 菜单和加速键的组成
如图5.1所示,在窗口中,菜单位于标题栏下面。这个菜单称为“主菜单”或“顶层菜单”,图中菜单的菜单项有“文件”、“查看”和“帮助”。单击主菜单上的项目后,可以弹出下一层菜单,叫做“弹出式菜单”或“子菜单”。子菜单中可以继续包含下一层子菜单。如单击“查看”弹出一个子菜单后,再单击其中的“工具栏”可以继续弹出一个子菜单。在子菜单中可以继续弹出下一层子菜单的菜单项最右边用一个三角箭头来表示。

图5.1 菜单示意图
有的程序在窗口的客户区单击鼠标右键也可以弹出一个菜单,单击标题栏图标也可以弹出一个系统菜单,这些菜单都属于弹出式菜单。
菜单中的菜单项有好几种,从资源定义的角度来看,分隔用的横线也是一个菜单项。除横线外其他菜单项可以供用户选择,也可以设置为“禁止”或“灰化”状态暂时停用,如图5.1中“被禁用的菜单项”和“被灰化的菜单项”所示。“禁用”的菜单项看上去和普通菜单项相同,但无法在上面单击鼠标,“灰化”的菜单项从外观上就已经表示是不可用的。菜单项也可以在左边显示选中标记,如图5.1中的“大图标”前的圆点和“状态栏”前的打钩。圆点表示选中标记是互斥的,打钩表示是不互斥的。
加速键就是菜单项的快捷键,图中的“字体”菜单项右边有个“Alt+F”,表示当窗口是激活的时候,不必打开菜单,直接按“Alt”加“F”的组合键就相当于选择了“字体”菜单项,同样,直接按下“Ctrl”加“Alt”加“B”键等于选择了“背景色”菜单项。加速键也是资源的组成部分,一般将最常用的菜单项定义为加速键,以减少打开菜单的操作。加速键的定义要遵循惯例,如“Ctrl+C”和“Ctrl+V”一般定义为“复制”和“粘贴”,“Ctrl+X”定义为“剪切”等。当然加速键的定义并不是必需的,不定义加速键并不会影响程序的功能。
不管程序中是否定义加速键,Windows总是定义了几个默认的加速键,如“F10”键会打开窗口的主菜单,“Alt+空格”会打开系统菜单,“Alt+F4”等于单击了“关闭”按钮等。
5.1.2 菜单和加速键的资源定义
先来看一个用于演示菜单和加速键使用的例子程序,所有的源文件包含在所附光盘的Chapter05\Menu目录下面,运行后产生如图5.1所示的菜单,目录中包括资源脚本文件Menu.rc,汇编源文件Menu.asm,makefile文件和图标、光标等文件,这里是资源脚本文件Menu.rc:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_MAIN 0x1000 //图标
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define IDM_MAIN 0x2000 //菜单
#define IDA_MAIN 0x2000 //加速键
#define IDM_OPEN 0x4101
#define IDM_OPTION 0x4102
#define IDM_EXIT 0x4103
#define IDM_SETFONT 0x4201
#define IDM_SETCOLOR 0x4202
#define IDM_INACT 0x4203
#define IDM_GRAY 0x4204
#define IDM_BIG 0x4205
#define IDM_SMALL 0x4206
#define IDM_LIST 0x4207
#define IDM_DETAIL 0x4208
#define IDM_TOOLBAR 0x4209
#define IDM_TOOLBARTEXT 0x4210
#define IDM_INPUTBAR 0x4211
#define IDM_STATUSBAR 0x4212
#define IDM_HELP 0x4301
#define IDM_ABOUT 0x4302
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_MAIN ICON "Main.ico"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDM_MAIN menu discardable
BEGIN
popup "文件(&F)"
BEGIN
menuitem "打开文件(&O)...", IDM_OPEN
menuitem "关闭文件(&C)...", IDM_OPTION
menuitem separator
menuitem "退出(&X)", IDM_EXIT
END
popup "查看(&V)"
BEGIN
menuitem "字体(&F)...\tAlt+F",IDM_SETFONT
menuitem "背景色(&B)...\tCtrl+Alt+B",IDM_SETCOLOR
menuitem separator
menuitem "被禁用的菜单项", IDM_INACT, INACTIVE
menuitem "被灰化的菜单项", IDM_GRAY, GRAYED
menuitem separator
menuitem "大图标(&G)", IDM_BIG
menuitem "小图标(&M)", IDM_SMALL
menuitem "列表(&L)", IDM_LIST
menuitem "详细资料(&D)", IDM_DETAIL
menuitem separator
popup "工具栏(&T)"
BEGIN
menuitem "标准按钮(&S)", IDM_TOOLBAR
menuitem "文字标签(&C)", IDM_TOOLBARTEXT
menuitem "命令栏(&I)", IDM_INPUTBAR
END
menuitem "状态栏(&U)", IDM_STATUSBAR
END
popup "帮助(&H)" ,HELP
BEGIN
menuitem "帮助主题(&H)\tF1", IDM_HELP
menuitem separator
menuitem "关于本程序(&A)...",IDM_ABOUT
END
END
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDA_MAIN accelerators
BEGIN
VK_F1, IDM_HELP, VIRTKEY
"B", IDM_SETCOLOR,VIRTKEY,CONTROL,ALT
"F", IDM_SETFONT,VIRTKEY,ALT
END
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
编译上述文件使用的makefile文件如下:
NAME = Menu
OBJS = $(NAME).obj
RES = $(NAME).res
LINK_FLAG = /subsystem:windows
ML_FLAG = /c /coff
$(NAME).exe: $(OBJS) $(RES)
Link $(LINK_FLAG) $(OBJS) $(RES)
.asm.obj:
ml $(ML_FLAG) $<
.rc.res:
rc $<
clean:
del *.obj
del *.res
为了编译资源文件,makefile中比以前多了一个资源编译的隐含规则:
.rc.res:
rc $< #依赖于与目标文件同名的 .rc 文件。这里就是 Menu.rc
同时,在EXE文件的依赖文件中增加了Menu.res文件。
在rc文件中,各种语句使用的是C语言的格式,因为资源编译器rc.exe根本上就是Visual C++附带的(这一点在第2章中就提及过),所以在定义等值语句的时候用的是#define,包含语句使用#include<文件名>,用到十六进制数值的时候并不是用汇编的语法在后面加h,而是用前面加0x的方法,如1234h写为0x1234,注释也要用前面加 // 的方法。这些在书写的时候一定要注意,以免引起语法错误。
在脚本文件的头部,首先要把MASM32 SDK软件包中的resource.h文件包含进来,这个文件中包括了资源定义中很多的预定义值,如窗口属性与加速键的键值等。资源在程序中的引用往往用一个数值来表示,称为资源的ID值,但在定义的时候直接使用数值不是很直观,所以往往用#define语句将数值定义为容易记忆的字符串。
1.菜单的定义
在资源脚本文件中菜单的定义格式是:
菜单ID MENU [DISCARDABLE]
BEGIN
菜单项定义
…
END
“菜单ID MENU [DISCARDABLE]”语句用来指定菜单的ID值和内存属性,菜单ID可以是16位的整数,范围是1~65535,在Menu.rc文件中,定义的菜单ID是2000h,但菜单ID也可以用字符串表示,如下面的定义:
MainMenu menu
begin
menuitem …
end
表示菜单的ID是字符串型的“MainMenu”,但这样定义的话,在程序中引用的时候就要用字符串指针代替十六进制的菜单ID值,显得相当不便,所以在实际应用中通常使用十六进制数值当做菜单ID。
数值型ID的范围限制在1~65535之间的原因是字符串在内存中的线性地址总是大于10000h,API函数检测参数时发现小于10000h时就可以把它认为是数值型的,大于10000h时就当做字符串指针处理。
menu关键字后面的DISCARDABLE是菜单的内存属性,表示菜单在不再使用的时候可以暂时从内存中释放以节省内存,这是一个可选属性。菜单项的定义语句必须包含在begin和end关键字之内,这两个关键字也可以用花括号{ 和 } 代替。
菜单项目的定义方法有3类:
MENUITEM菜单文字,命令ID [,选项列表] (用法1)
或 MENUITEM SEPARATOR (用法2)
或 POPUP菜单文字 [,选项列表] (用法3)
BEGIN
item-definitions
...
END
下面分别就这3类详细说明,用法1定义的是普通菜单项,图5.1中的“字体”与“背景色”等菜单项都是这样定义的,它的组成部分如下:
● 菜单文字——显示在菜单项中的字符串。如果需要字符串中某个字母带下划线,那么可以在字母前面加&符号,如“字体(F)...”就要写成“字体(&F)...”,带下划线的字母可以被系统自动当做快捷键:在这里,当菜单打开的时候按下F键,那么就相当于用鼠标选择了“字体”选项。在同一个弹出菜单中要注意不同的菜单项快捷键应该有所区别。另外,如果要把加速键的提示信息显示在菜单项的右边,如“字体”菜单项中的“Alt+F”字符,可以在两者中间加\t(表示插入一个Tab字符),写为“字体(&F)...\tAlt+F”,这样Tab后面的字符在显示的时候会右对齐。
● 命令ID——用来分辨不同的菜单项。当菜单被选中的时候,Windows会向窗口过程发送WM_COMMAND消息,消息的参数就是这个命令ID。用命令ID可以分辨用户究竟选中了哪个菜单项,所以不同的菜单项应该定义不同的ID值,除非想让两个菜单项的功能相同。
● 选项——用来定义菜单项的各种属性,它可以是下列数值:
■ CHECKED——表示打上选定标志(对钩)。
■ GRAYED——表示菜单项是灰化的。
■ INACTIVE——表示菜单项是禁用的。
■ MENUBREAK或MENUBARBREAK——表示将这个菜单项和以后的菜单项列到新的列中。
读者可以做个实验,把例子中的“详细资料”一句的定义语句改为:
menuitem "详细资料(&D)",IDM_DETAIL,MENUBARBREAK
那么显示出来的菜单如图5.2所示:“详细资料”及以后的菜单项都另起一列了!

图5.2 使用MENUBARBREAK的效果
用法2定义的是菜单项之间的分隔线,显然,分隔线是不需要字符串和选项的。
方法3定义的是弹出式菜单,顶层菜单是由多个弹出式子菜单组成的,所以在Menu.rc文件中,主菜单是由“文件”、“查看”和“帮助”3个顺序定义的弹出式菜单组成的,弹出式菜单的定义也可以嵌套,如“查看”菜单中的“工具栏”又是一个弹出式菜单,在嵌套的时候要注意像写C的源程序一样把begin和end(或者{ 和 })正确地配对。popup菜单的选项列表可以是以下的值:
● GRAYED——表示菜单项是灰化的。
● INACTIVE——表示菜单项是禁用的。
● HELP——表示本项和以后的菜单项是右对齐的,如图5.1中所示的“帮助”菜单。
由于popup菜单项选中的时候会自动将弹出式菜单弹出来,不需要向程序发送消息,所以在定义的参数中不需要命令ID。
有些选项是可以同时定义的,如果要指定超过一个的选项,中间要用逗号隔开,但是也有些小小的限制:GRAYED和INACTIVE不能同时使用,MENUBREAK和MENUBARBREAK也是不能同时使用的。
2.加速键的定义
与菜单的定义相比,加速键的定义要简单得多,具体的语法如下:
加速键ID ACCELERATORS
BEGIN
键名, 命令ID [,类型] [,选项]
...
END
加速键ID同样可以是一个字符串或者是1~65535之间的数字,整个定义内容也是用begin和end(或花括号)包含起来,中间是多个加速键的定义项目,每个键占据一行,各字段的含义如下所示。
● 键名——表示加速键对应的按键,可以有3种方式定义。
■ "^字母":表示Ctrl键加上字母键。
■ "字母":表示字母,这时类型必须指明是VIRTKEY。
■ 数值:表示ASCII码为该数值的字母,这时类型必须指明为ASCII。
● 命令ID——按下加速键后,Windows向程序发送的命令ID。如果想把加速键和菜单项关联起来,这里就是要关联菜单项的命令ID。
● 类型——用来指定键的定义方式,可以是VIRTKEY和ASCII,分别用来表示“键名”字段定义的是虚拟键还是ASCII码。
● 选项——可以是Alt,Control或Shift中的单个或多个,如果指定多个,则中间用逗号隔开,表示加速键是按键加上这些控制键的组合键。
在键名的定义中,系统按键如F1,F2,BackSpace和Esc等都是用虚拟键的方法定义的,Resource.h中已经包括所有的预定义,它们是以VK_带头的一些值,如VK_BACK,VK_TAB,VK_RETURN,VK_ESCAPE,VK_DELETE,VK_F1和VK_F2等,读者可以查看Resource.h文件。下面是加速键定义的一些例子:
"^C", ID ;Ctrl+C
"K", ID ;Shift+K
"k", ID,ALT ;Alt+k
98, ID,ASCII ;b(字符b的ASCII码为98)
66, ID,ASCII ;B (Shift b)
"g", ID ;g
VK_F1, ID,VIRTKEY ;F1
VK_F1, ID,VIRTKEY,CONTROL ;Ctrl+F1
VK_F2, ID,VIRTKEY,ALT,SHIFT ;Alt+Shift+F2
在一个资源脚本文件中,可以定义多个菜单和多个加速键表,当然也有其他各式各样的资源,有位图、图标与对话框等,这就涉及如何为这些资源取ID值的问题,取值的时候要掌握的原则是:
(1)对于同类别的多个资源,资源ID必须为不同的值,如定义了两个菜单,那么它们的ID就必须用不同的数值表示,否则将无法分辨。
(2)对于不同类别的资源,资源ID在数值上可以是相同的,如可以将菜单和加速键的ID都定义为1,同时也可以有ID为1的位图或图标等,Windows并不会把它们搞混。
5.1.3 使用菜单和加速键
在完成资源文件的编写后,来看看如何在程序中使用菜单和加速键,例子程序的运行界面如图5.1所示,这里先列出例子程序的功能说明,读者可以先尝试一下,以便在以下的程序分析中有所印象。程序功能如下。
● 程序在用户选择了任何一个菜单项以后,会弹出一个对话框,将接收到的菜单命令ID显示出来。
● 选择“大图标”“小图标”“列表”和“详细资料”菜单项后,选中的菜单项前面会出现一个圆点选中标记,4个菜单项的选择是互斥的。
● 在“状态栏”及“工具栏”菜单的3个菜单项中选择后,选中的菜单项前面会出现打钩标记,它们是不互斥的。
● 在窗口的客户区单击鼠标右键会弹出和“查看”菜单一致的弹出式菜单。
● 在标题栏图标上单击鼠标左键,会弹出系统菜单,注意上面比默认的菜单多了两项:“帮助主题”和“关于本程序”。
接下来,将逐步分析这些功能是如何实现的。下面是Menu.asm源代码,代码是在第4章的FirstWindow程序的基础上改写的,这是编写Win32汇编程序的一个常用方法——复制一个模板程序再进行修改会节省很多的时间。
;Menu.asm 菜单资源的使用例子
; 使用 nmake 或下列命令进行编译和链接:
; ml /c /coff Menu.asm
; rc Menu.rc
; Link /subsystem:windows Menu.obj Menu.res
.386
.model flat,stdcall
option casemap:none
;include 文件定义
;-------------------------------------------------------------
include windows.inc
include user32.inc
includelib user32.lib
include kernel32.inc
includelib kernel32.lib
;--------------------------------------------------------------
;Equ 等值定义
ICO_MAIN equ 1000h ;图标
IDM_MAIN equ 2000h ;菜单
IDA_MAIN equ 2000h ;加速键
IDM_OPEN equ 4101h
IDM_OPTION equ 4102h
IDM_EXIT equ 4103h
IDM_SETFONT equ 4201h
IDM_SETCOLOR equ 4202h
IDM_INACT equ 4203h
IDM_GRAY equ 4204h
IDM_BIG equ 4205h
IDM_SMALL equ 4206h
IDM_LIST equ 4207h
IDM_DETAIL equ 4208h
IDM_TOOLBAR equ 4209h
IDM_TOOLBARTEXT equ 4210h
IDM_INPUTBAR equ 4211h
IDM_STATUSBAR equ 4212h
IDM_HELP equ 4301h
IDM_ABOUT equ 4302h
;数据段
;-------------------------------------------------------------
.data? ;未初始化
hInstance dword ?
hWinMain dword ?
hMenu dword ?
hSubMenu dword ?
.const ;常量数据
szClassName byte 'Menu Example',0
szCaptionMain byte 'Menu', 0
szMenuHelp byte '帮助主题(&H)', 0
szMenuAbout byte '关于本程序(&A)...', 0
szCaption byte '菜单选择', 0
szFormat byte '您选择了菜单命令:%08x', 0
;-----------------------------------------------------------------
;代码段
;-------------------------------------------------------------
.code
_DisplayMenuItem proc _dwCommandId
local @szBuffer[256]:byte
pushad
invoke wsprintf, addr @szBuffer, addr szFormat, _dwCommandId
invoke MessageBox, hWinMain, addr @szBuffer, offset szCaption, MB_OK
popad
ret
_DisplayMenuItem endp
_Quit proc
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
ret
_Quit endp
;窗口过程
;-------------------------------------------------------------
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
local @stPos:POINT
local @hSysMenu
mov eax, uMsg
.if eax == WM_CREATE
invoke GetSubMenu, hMenu, 1
mov hSubMenu, eax
;在系统菜单中添加菜单项
invoke GetSystemMenu, hWnd, FALSE
mov @hSysMenu, eax
invoke AppendMenu, @hSysMenu, MF_SEPARATOR, 0, NULL
invoke AppendMenu, @hSysMenu, 0, IDM_HELP, offset szMenuHelp
invoke AppendMenu, @hSysMenu, 0, IDM_ABOUT, offset szMenuAbout
;处理菜单及加速键消息
.elseif eax == WM_COMMAND
invoke _DisplayMenuItem, wParam
mov eax, wParam
movzx eax, ax
.if eax == IDM_EXIT
call _Quit
.elseif eax >= IDM_TOOLBAR && eax <= IDM_STATUSBAR
mov ebx, eax
invoke GetMenuState, hMenu, ebx, MF_BYCOMMAND
.if eax == MF_CHECKED
mov eax, MF_UNCHECKED
.else
mov eax, MF_CHECKED
.endif
invoke CheckMenuItem, hMenu, ebx, eax
.elseif eax >= IDM_BIG && eax <= IDM_DETAIL
invoke CheckMenuRadioItem, hMenu, IDM_BIG, IDM_DETAIL, eax, MF_BYCOMMAND
.endif
;处理系统菜单消息
.elseif eax == WM_SYSCOMMAND
mov eax, wParam
movzx eax, ax
.if eax == IDM_HELP || eax == IDM_ABOUT
invoke _DisplayMenuItem, wParam
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
.endif
;按下右键时弹出一个POPUP菜单
.elseif eax == WM_RBUTTONDOWN
invoke GetCursorPos, addr @stPos
invoke TrackPopupMenu, hSubMenu, TPM_LEFTALIGN, @stPos.x, @stPos.y, NULL, hWnd, NULL
.elseif eax == WM_CLOSE
call _Quit
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
;--------------------------------------------------------------
xor eax, eax
ret
_ProcWinMain endp
;-------------------------------------------------------------------
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
local @hAccelerator
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke LoadMenu, hInstance, IDA_MAIN
mov hMenu, eax
invoke LoadAccelerators, hInstance, IDA_MAIN
mov @hAccelerator, eax
;注册窗口类
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
invoke LoadIcon, hInstance, ICO_MAIN
mov @stWndClass.hIcon, eax
mov @stWndClass.hIconSm, eax
invoke LoadCursor, 0, IDC_ARROW
mov @stWndClass.hCursor, eax
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW + 1
mov @stWndClass.lpszClassName, offset szClassName
invoke RegisterClassEx, addr @stWndClass
;建立并显示窗口
invoke CreateWindowEx, WS_EX_CLIENTEDGE, \
offset szClassName, offset szCaptionMain, \
WS_OVERLAPPEDWINDOW, \
100, 100, 400, 300, \
NULL, hMenu, hInstance, NULL
mov hWinMain, eax
invoke ShowWindow, hWinMain, SW_SHOWNORMAL
invoke UpdateWindow, hWinMain
;消息循环
.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
ret
_WinMain endp
main proc
call _WinMain
invoke ExitProcess, 0
main endp
end main
编译运行效果:

1.加载菜单
在窗口中加载菜单的方法在第4章已经提及,方法有两个:一是在注册窗口类的时候指定类的默认菜单;二是在建立窗口的时候在参数中指定菜单句柄。Menu.asm程序中用的是第2种方法:
invoke CreateWindowEx,WS_EX_CLIENTEDGE,\
offset szClassName,offset szCaptionMain,\
WS_OVERLAPPEDWINDOW,\
100,100,400,300,\
NULL,hMenu,hInstance,NULL
在参数中指出了hMenu。不管用哪种方法,首先都必须使用LoadMenu函数来获取菜单句柄hMenu,如下面的语句:
invoke LoadMenu,hInstance,IDM_MAIN
mov hMenu,eax
这个函数的第1个参数是用GetModuleHandle获取的实例句柄,第2个参数指定需要装入的菜单资源ID,函数返回菜单句柄。在得到菜单句柄以后,我们先把它放入hMenu变量保存起来以便后用。
当资源文件中用字符串为名称定义菜单而不是用数值的时候,例如:
MainMenu menu //定义菜单名为字符串“MainMenu”
begin
...
end
那么在程序中就必须用字符串指针代替菜单ID做参数:
szMenu "MainMenu",0 ;在数据段中定义菜单名称字符串
...
invoke LoadMenu,hInstance,addr szMenu ;在程序中装载
mov hMenu,eax
用字符串为名称定义资源,在LoadMenu、LoadCursor、LoadBitmap等资源装载函数中用字符串指针做参数装入,这实际上是一个通用的方法,不仅适用于菜单资源,对于其他类别的资源也是适用的。在后面的介绍中就不再另外说明了。
2.加载加速键
和菜单一样,加速键在使用前也要装入,参数同样是在资源脚本文件中定义的加速键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函数是实现加速键功能的核心,它的参数为目标窗口、加速键句柄和GetMessage取得的消息结构。该函数检查消息结构中的消息,如果遇到WM_KEYDOWN和WM_SYSKEYDOWN消息则检测加速键资源,看按键是否符合某个加速键,如果符合则向目标窗口发送WM_COMMAND或WM_SYSCOMMAND消息,并返回TRUE,如果不符合则不进行任何处理并返回FALSE。
由于加速键的键码并不是用户真正想输入窗口的,比如,用户在写字板中输入文字,按Ctrl+C键是为了“复制”,而并不是想把Ctrl+C键对应的字符输入文档,所以这个Ctrl+C的键码在完成加速键的使命后就应该丢弃,也就是说,符合加速键的键盘消息不应该再发送给窗口,TranslateMessage和DispatchMessage函数前的逻辑判断就是这样的意图:只有TranslateAccelerator没有转换的消息(返回值eax为0)才继续处理。
另外,TranslateAccelerator的参数中有个“目标窗口”,例子中是程序的主窗口hWinMain,为什么要设置这样一个参数而不像DispatchMessage函数一样使用MSG结构中的hwnd呢?这是因为键盘消息可以产生于不同窗口中——既可能是主窗口,也可能是其他子窗口,如果用@stMsg.hwnd做目标窗口,就必须在所有子窗口的窗口过程中都设置处理加速键消息的代码,这显然是一种浪费,所以一般把所有的加速键消息都发送到主窗口,然后集中在主窗口的窗口过程中处理WM_COMMAND消息,这样有利于精简代码。
3.菜单和加速键消息
当用户选择了一个菜单项的时候,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。
在例子程序中,mov eax,wParam前面还有一句invoke _DisplayMenuItem,wParam,作用是在处理WM_COMMAND消息前将wParam的值通过一个对话框显示出来,读者可以与资源脚本文件中定义的命令ID值对比一下,在正常使用的程序中可以去掉这一句。
读者可以发现,资源文件中定义的“字体”菜单项的ID为0x4201,当选中“字体”菜单项的时候,对话框中显示的wParam数值正是00004201,而按下加速键Alt+F的时候,显示出来的值就是00014201了,它们的区别就是高16位中的通知码不同。
4.菜单项的修改
在程序的运行中也可以动态修改菜单项,包括添加、删除和修改操作,这些操作是通过几个API函数来完成的:
invoke AppendMenu,hMenu,uFlags,uIDNewItem,lpNewItem ;添加菜单项
invoke InsertMenu,hMenu,uPosition,uFlags,uIDNewItem,lpNewItem;插入菜单项
invoke ModifyMenu,hMenu,uPosition,uFlags,uIDNewItem,lpNewItem;修改菜单项
invoke DeleteMenu,hMenu,uPosition,uFlags ;删除菜单项
invoke RemoveMenu,hMenu,uPosition,uFlags ;删除菜单项
其中AppendMenu用来在一个菜单的最后添加菜单项,InsertMenu则在中间插入菜单项,ModifyMenu可以修改一个菜单项的文字,DeleteMenu和RemoveMenu则可以删除一个菜单项。
这些函数中的参数都是雷同的,hMenu参数指要操作的菜单句柄;uPosition用来定位要操作的菜单项。定位的方法有两种:用命令ID定位或用位置索引。用哪一种方法取决于后面的uFlags参数:当uFlags为MF_BYCOMMAND时,uPosition为菜单项的命令ID;而当uFlags为MF_BYPOSITION的时候,uPosition表示菜单项的位置索引,索引是从0开始的,也就是说第一个菜单项的索引值为0。
由于AppendMenu函数总是在菜单的最后添加新的菜单项,所以不需要uPosition参数。
对于AppendMenu和InsertMenu,会有一个新的菜单项产生,uIDNewItem表示这个新菜单项的命令ID,lpNewItem指向新菜单项的文字字符串,ModifyMenu函数可以修改一个菜单项的命令ID或文字字符串,所以也有uIDNewItem和lpNewItem参数。而用来删除菜单项的DeleteMenu和RemoveMenu显然用不着uIDNewItem和lpNewItem参数。
uFlags参数除了指定MF_BYCOMMAND还是MF_BYPOSITION外,还可以组合指定菜单项的其他属性,如MF_CHECKED,MF_DISABLED,MF_ENABLED,MF_GRAYED,MF_MENUBARBREAK,MF_MENUBREAK,MF_SEPARATOR和MF_UNCHECKED等,从其字面上就可以看出这些属性的含义。
DeleteMenu和RemoveMenu的不同之处在于对popup菜单项的处理。当它们用于popup属性的菜单项时,DeleteMenu不仅删除菜单项,而且将这个popup菜单项的所有子项目全部删除,这样,这个popup菜单就不能在别的地方继续使用;而RemoveMenu仅从菜单中移去这个popup菜单项,整个popup菜单在内存中还是存在的。以Menu.asm程序为例,按鼠标右键弹出的菜单实际上是主菜单中的“查看”菜单项,假如用DeleteMenu删除主菜单中的“查看”项目,那么按右键也就弹不出菜单了,而用RemoveMenu删除主菜单中的“查看”项目,按鼠标右键仍然可以弹出菜单。对于非popup属性的菜单项,DeleteMenu和RemoveMenu的效果是相同的。
5.使用系统菜单
系统菜单指按下了标题栏图标后弹出的菜单,与窗口菜单不同,选中系统菜单的菜单项后,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函数首先获取系统菜单的句柄。例子程序在窗口初始化的时候在系统菜单尾添加了一个分隔线和两个菜单项:“帮助主题”和“关于本程序”:
.if eax == WM_CREATE
...
invoke GetSystemMenu,hWnd,FALSE
mov @hSysMenu,eax
invoke AppendMenu,@hSysMenu,MF_SEPARATOR,0,NULL
invoke AppendMenu,@hSysMenu,0,IDM_HELP,offset szMenuHelp
invoke AppendMenu,@hSysMenu,0,IDM_ABOUT,offset szMenuAbout
在窗口过程中处理系统菜单消息的分支结构为:
.elseif eax == WM_SYSCOMMAND
mov eax,wParam
.if ax == 自定义ID1
...
.elseif ax == 自定义ID2
...
.else
invoke DefWindowProc,hWnd,uMsg,wParam,lParam
ret
.endif
与处理WM_COMMAND消息不同的是,在WM_SYSCOMMAND消息中处理了自定义的菜单命令ID后,必须把其他命令ID交给DefWindowProc处理,并直接把返回值返回给Windows,不然的话会发现窗口不能移动,不能关闭,不能最小化……因为它相当于屏蔽了所有SC_RESTORE,SC_MOVE,SC_SIZE,SC_MAXIMIZE,SC_MINIMIZE和SC_CLOSE等消息的处理。
6.右键弹出菜单
例子程序的一个功能是当用户在窗口客户区按下鼠标右键的时候弹出一个菜单,这个功能是用TrackPopupMenu函数实现的。TrackPopupMenu函数的用法:
invoke TrackPopupMenu,hMenu,uFlags,x,y,nReserved,hWnd,lpRect
这个函数本身很简单,执行后在参数指定的x,y位置弹出一个属于hWnd窗口(也就是说WM_COMMAND消息发到这个窗口)的菜单,菜单句柄是hMenu。由于函数中的坐标是以整个屏幕左上角为基准的,所以弹出菜单的位置不一定在窗口的客户区内,它可以是屏幕的任何一个地方。
uFlags参数指定一些和位置相关的选项,它可以是PM_CENTERALIGN,TPM_LEFTALIGN或TPM_RIGHTALIGN三者之一,表示(x,y)坐标是代表弹出菜单位置的中间、左上角还是右上角,一般的习惯是使用TPM_LEFTALIGN,这样菜单会在鼠标点击处的右边弹出。uFlags中同时还可以指定用鼠标左键还是右键选定菜单项,定义值可以是TPM_LEFTBUTTON或TPM_RIGHTBUTTON,如果选择TPM_RIGHTBUTTON的话,对在菜单项上面按鼠标左键是没有反应的。
lpRect指向一个RECT结构,用来指定一个区域,当菜单弹出后,在这个区域外单击鼠标,菜单才会消失,如果这个参数指定为NULL的话,在菜单之外单击鼠标,菜单就会消失。
在使用TrackPopupMenu之前,有几个准备工作是要做的:为了在客户区中按下鼠标右键弹出菜单,我们当然要处理鼠标右键消息,也就是说在WM_RBUTTONDOWN消息中调用TrackPopupMenu函数,一般的习惯是在鼠标按下的地方弹出菜单,所以还要首先获取鼠标光标的位置,然后在此位置弹出菜单。
要获取鼠标位置,可以用GetCursorPos函数:
invoke GetCursorPos,lpPoint
参数lpPoint指向一个POINT数据结构,这个结构只有两个字段:
POINT STRUCT
x DWORD ?
y DWORD ?
POINT ENDS
该结构用来表示一个点的(x,y)坐标,GetCursorPos将当前的鼠标位置返回到这个结构中,程序中的相关代码是:
local @stPos:POINT ;首先定义一个POINT结构
...
invoke GetCursorPos,addr @stPos ;获取鼠标位置
invoke TrackPopupMenu,hSubMenu,\
TPM_LEFTALIGN,@stPos.x,@stPos.y,NULL,hWnd,NULL
用GetCursorPos获取的鼠标位置是一个POINT结构,但由于TrackPopupMenu输入坐标的方法是用x,y两个参数,而不是一个POINT结构,所以要用结构中的两个字段@stPos.x和@stPos.y分别输入。
使用TrackPopupMenu时要注意的是,弹出的菜单句柄必须是popup类型的,而在资源文件中定义并且可以用LoadMenu函数装入的菜单并不是popup类型的,popup菜单(如例子中的“文件”与“查看”等)只能在第二层中才能定义,在程序中用GetSubMenu得到的第二层子菜单的句柄才是popup类型的。GetSubMenu函数的用法是:
invoke GetSubMenu,hMenu,nPos
.if eax
mov hSubMenu,eax
.endif
nPos参数指定要获取的菜单项的位置索引,GetSubMenu的返回值是获取的子菜单句柄。
例如用invoke GetSubMenu,hMenu,1取得第二个子菜单(“文件”子菜单为0,“查看”子菜单为1,……)的句柄,然后在TrackPopupMenu中使用,这个菜单句柄就是主菜单中的“查看”菜单,所以按鼠标右键弹出的菜单和下拉菜单中的“查看”菜单是一模一样的。
7.菜单状态的检测和设置
在程序中经常要对菜单项的状态进行设置,比如,剪贴板中没有数据时,“粘贴”菜单项应该灰化,窗口中没有被选中的字符时,“拷贝”菜单项也应该灰化,这样可以给使用者一个善意的提醒。同样,对菜单的状态也常常需要检测,如查看菜单项的状态是否处于灰化状态或选中状态以便进行下一步操作等。
对菜单项状态的检测可以用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的组合值,它们分别表示菜单项的状态是选中、禁用、灰化、高亮显示,以及3种分隔线,读者可以用test指令测试相应的数据位来分辨菜单项处于哪种状态,一般的测试代码如下:
invoke GetMenuState,hMenu,IDM_XXX,MF_BYCOMMAND
.if eax & MF_CHECKED
;表示IDM_XXX菜单项现在是选中状态
.endif
同样,读者也可以用eax & MF_DISABLED和eax & MF_GRAYED等条件测试其他状态。设置菜单项的状态可以用下列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或位置索引,可以在状态参数(uEnable,uCheck,uFlags)中组合定义MF_BYCOMMAND或MF_BYPOSITION来决定使用哪种方法。
在例子程序中,当选中IDM_TOOLBAR和IDM_STATUSBAR之间的菜单项的时候,程序先用invoke GetMenuState, hMenu, ebx, MF_BYCOMMAND获取当前的状态,检查是否选定,并将选定状态反转后用CheckMenuItem重新设置:
.elseif eax >= IDM_TOOLBAR && eax <= IDM_STATUSBAR
mov ebx,eax
invoke GetMenuState,hMenu,ebx,MF_BYCOMMAND
.if eax == MF_CHECKED
mov eax,MF_UNCHECKED
.else
mov eax,MF_CHECKED
.endif
invoke CheckMenuItem,hMenu,ebx,eax
当选中IDM_BIG和IDM_DETAIL之间的菜单项的时候,程序用CheckMenuRadioItem将原先IDM_BIG和IDM_DETAIL范围内的互斥选定撤销并将当前选定的菜单项加圆点标记。
.elseif eax >= IDM_BIG && eax <= IDM_DETAIL
invoke CheckMenuRadioItem,hMenu,IDM_BIG,IDM_DETAIL,\
eax,MF_BYCOMMAND
最后,修改菜单状态的时机是什么时候呢?在程序中似乎不应该随时去检测状态并设置,这显然是很浪费资源的。Windows考虑到了这一点:在菜单将要激活的时候,也就是用户在菜单上按动鼠标的时候,Windows在菜单弹出之前会向窗口过程发送WM_INITMENU消息,我们可以从容不迫地在这里进行各种检测,并设置对应的菜单项。
读者可以注意到,在状态参数中指定MF_BYCOMMAND或MF_BYPOSITION将决定位置参数用命令ID还是位置索引表示,这个规则在所有的菜单函数中都是适用的,MF_BYCOMMAND是默认值(它的定义值是0),如果两者都不定义的话,位置参数代表的就是命令ID。
8.其他菜单函数
除了前面介绍的一些函数之外,还有一些不太常用的菜单函数,在这里做简单的介绍。
菜单不一定非要在资源文件中定义,在程序中也可以用代码来建立菜单,不过比较麻烦一点的方法是先用CreateMenu建立一个菜单,CreateMenu函数没有参数,调用后返回一个没有任何菜单项的菜单句柄,接下来就可以用AppendMenu在上面一条条地添加菜单项了。
同样,CreatePopupMenu也可以建立一个没有任何菜单项的菜单句柄,但它建立的是popup类型的菜单句柄,可以在TrackPopupMenu中直接使用。
如果要获取一个窗口当前使用的菜单句柄,那么可以使用GetMenu函数:
invoke GetMenu,hWnd
mov hMenu,eax
一个菜单的总项数可以用GetMenuItemCount函数获取:
invoke GetMenuItemCount,hMenu
不过GetMenuItemCount函数的返回值不包括子菜单展开以后的项数,而是指最上层菜单的项数。比如,在例子程序中对hMenu统计的结果是3,因为Menu.rc中定义的最上层的菜单项是“文件”、“查看”和“帮助”,总共3个,如果要统计全部展开后的项数,那么只好用GetSubMenu一层层地统计下去了。
对菜单中各个菜单项当前的文字和命令ID也是可以查询的,方法是用GetMenuString和GetMenuItemID,读者可以自行参考命令手册。
建立窗口时指定了菜单句柄后并不是不能改变的,我们常常见到一些编辑软件,没有打开文件之前菜单只有寥寥几项,一打开文件以后功能菜单就全部出来了,实际上这是用SetMenu函数完成的:
invoke SetMenu,hWnd,hMenu
可以在资源文件中预定义几个不同的菜单,在使用的时候根据不同情况随时用SetMenu设置不同的菜单句柄。
使用菜单后,要涉及清除的问题,与窗口相连的菜单句柄在窗口摧毁的时候会由Windows自动释放,不需要手工释放,但没有与窗口相连的菜单就要由程序自己来释放了,方法是使用DestroyMenu函数,比如没有与窗口相连而仅用TrackPopupMenu弹出的菜单句柄:
invoke DestroyMenu,hMenu
5.2 图标和光标
图标和光标是图形资源,图标通常用做应用程序的“形象代表”出现在文件浏览器、运行窗口左上角或程序的快捷方式等所有代表文件的地方,为自己写的应用程序选一个合适的图标会使程序变得引人注目;而光标就是鼠标移动时屏幕上那个指示位置的箭头,应用程序可以定义自己的光标,这样,光标移到程序的客户区中就会变成需要的形状。
5.2.1 图标和光标的资源定义
与菜单、加速键等资源不同,在资源脚本文件中定义图标和光标时并不是一个一个像素地定义,而是指定图标和光标的文件名,由资源编译器将像素数据读入再转换成二进制格式。由此可见,在资源定义之前要用其他工具先创建图标和光标文件。图标和静态光标文件的扩展名分别是ico和cur,还有一种扩展名为ani的动态光标文件。
光标和图标在资源文件中的定义语句是:
图标ID ICON [DISCARDABLE] 图标文件名 ;定义图标
光标ID CURSOR [DISCARDABLE] 光标文件名 ;定义光标
DISCARDABLE关键字是内存选项,表示在不用的时候可以从内存暂时卸掉,当文件名包含空格时,两边要用双引号引起来,图标ID和光标ID同样也可以用16位的整数或字符串表示,这里是几个定义的例子:
MyIcon icon “1.ico” ;把1.ico定义为ID为“MyIcon”的图标资源
1000 icon discardable 2.ico ;把2.ico定义为ID为1000的图标资源
1001 icon “big icon.ico” ;把big icon.ico定义为ID为1001的图标资源
1002 cursor “big arrow.ani” ;把big arrow.ani定义为ID为1002的光标资源
GoodCursor cursor arrow.cur ;把arrow.cur定义为ID为“GoodCursor”的光标资源
资源文件中定义的图标可以不止一个,但由于Windows在“我的电脑”中列出文件的时候总是使用资源中的第一个图标当做文件的图标,所以在资源脚本文件中要把想用做程序图标的图标定义语句排在最前面。
5.2.2 使用图标和光标
在这里,用一个例子来说明图标和光标的用法,程序是建立在FirstWindow.asm和Menu.asm程序的基础上的,为了节省篇幅,在这里就不列出全部源程序了,完整的源程序可以在所附光盘的Chapter05\Icon目录中找到。程序中创建了一个菜单,运行后可以在“图标和光标”菜单中选择不同的图标和光标,选择不同的图标以后,窗口标题栏左边的图标和桌面任务栏上的窗口图标都会变化;选择不同的光标后,当鼠标移动到窗口客户区中的时候,光标会变成程序指定的光标。具体的效果如图5.3所示,大图标对应“笑脸”,小图标对应“箭头”,而光标A和B分别是“小恐龙”光标和“手型”光标,其中“小恐龙”光标是ani类型的动态光标,在屏幕上显示为一个走动中的恐龙模样。

图5.3 不同的图标和光标
资源文件Icon.rc的定义如下:
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#include <resource.h>
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#define ICO_BIG 0x1000
#define ICO_SMALL 0x1001
#define CUR_2 0x1000
#define IDM_MAIN 0x2000
#define IDM_EXIT 0x2101
#define IDM_BIG 0x2201
#define IDM_SMALL 0x2202
#define IDM_CUR1 0x2203
#define IDM_CUR2 0x2204
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
ICO_SMALL ICON "Small.ico"
ICO_BIG ICON "Big.ico"
CUR_2 CURSOR "2.cur"
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
IDM_MAIN menu discardable
BEGIN
popup "文件(&F)"
BEGIN
menuitem "退出(&X)", IDM_EXIT
END
popup "图标和光标(&I)"
BEGIN
menuitem "大图标(&G)", IDM_BIG
menuitem "小图标(&M)", IDM_SMALL
menuitem separator
menuitem "光标A(&A)", IDM_CUR1
menuitem "光标B(&B)", IDM_CUR2
END
END
//>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
经过上一节的“洗礼”,读者对菜单的定义应该很熟悉了,这里就不再说明IDM_MAIN的定义了,脚本文件中定义了ICO_SMALL,ICO_BIG两套图标和CUR_2静态光标,磁盘上还有个动态光标文件1.ani。
Icon.asm的大部分是窗口模板程序的内容,与FirstWindow.asm是相同的,仅在窗口过程的WM_CREATE和WM_COMMAND增加了一些内容:
【学习笔记】:
;Icon.asm 光标资源和图标资源的使用例子
;使用 nmake 或下列命令进行编译和链接:
;ml /c /coff Icon.asm
;rc Icon.rc
;Link /subsystem:windows Icon.obj Icon.res
.386
.model flat,stdcall
option casemap:none
;include 文件定义
include C:/masm32/include/windows.inc
include C:/masm32/include/user32.inc
includelib C:/masm32/lib/user32.lib
include C:/masm32/include/kernel32.inc
includelib C:/masm32/lib/kernel32.lib
;Equ 等值定义
ICO_BIG equ 1000h
ICO_SMALL equ 1001h
CUR_2 equ 1000h
IDM_MAIN equ 2000h
IDM_EXIT equ 2101h
IDM_BIG equ 2201h
IDM_SMALL equ 2202h
IDM_CUR1 equ 2203h
IDM_CUR2 equ 2204h
; 数据段
.data?
hInstance dword ?
hWinMain dword ?
hMenu dword ?
hIcoBig dword ?
hIcoSmall dword ?
hCur1 dword ?
hCur2 dword ?
.const
szClassName byte 'Icon and Cursor Example', 0
szCursorFile byte '1.ani', 0
; 代码段
.code
_Quit proc
invoke DestroyWindow, hWinMain
invoke PostQuitMessage, NULL
ret
_Quit endp
_ProcWinMain proc uses ebx edi esi, hWnd, uMsg, wParam, lParam
mov eax, uMsg
.if eax == WM_CREATE
invoke LoadIcon, hInstance, ICO_BIG
mov hIcoBig, eax
invoke LoadIcon, hInstance, ICO_SMALL
mov hIcoSmall, eax
invoke LoadCursorFromFile, addr szCursorFile
mov hCur1, eax
invoke LoadCursor, hInstance, CUR_2
mov hCur2, eax
invoke SendMessage, hWnd, WM_COMMAND, IDM_BIG, NULL
invoke SendMessage, hWnd, WM_COMMAND, IDM_CUR1, NULL
.elseif eax == WM_COMMAND
mov eax, wParam
movzx eax, ax
.if eax == IDM_EXIT
call _Quit
.elseif eax == IDM_BIG
invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, hIcoBig
invoke CheckMenuRadioItem, hMenu, IDM_BIG, IDM_SMALL, IDM_BIG, MF_BYCOMMAND
.elseif eax == IDM_SMALL
invoke SendMessage, hWnd, WM_SETICON, ICON_BIG, hIcoSmall
invoke CheckMenuRadioItem, hMenu, IDM_BIG, IDM_SMALL, IDM_SMALL, MF_BYCOMMAND
.elseif eax == IDM_CUR1
invoke SetClassLong, hWnd, GCL_HCURSOR, hCur1
invoke CheckMenuRadioItem, hMenu, IDM_CUR1, IDM_CUR2, IDM_CUR1, MF_BYCOMMAND
.elseif eax == IDM_CUR2
invoke SetClassLong, hWnd, GCL_HCURSOR, hCur2
invoke CheckMenuRadioItem, hMenu, IDM_CUR1, IDM_CUR2, IDM_CUR2, MF_BYCOMMAND
.endif
.elseif eax == WM_CLOSE
call _Quit
.else
invoke DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.endif
xor eax, eax
ret
_ProcWinMain endp
_WinMain proc
local @stWndClass:WNDCLASSEX
local @stMsg:MSG
invoke GetModuleHandle, NULL
mov hInstance, eax
invoke LoadMenu, hInstance, IDM_MAIN
mov hMenu, eax
;注册窗口类
invoke RtlZeroMemory, addr @stWndClass, sizeof @stWndClass
push hInstance
pop @stWndClass.hInstance
mov @stWndClass.cbSize, sizeof WNDCLASSEX
mov @stWndClass.style, CS_HREDRAW or CS_VREDRAW
mov @stWndClass.lpfnWndProc, offset _ProcWinMain
mov @stWndClass.hbrBackground, COLOR_WINDOW + 1
mov @stWndClass.lpszClassName, offset szClassName
invoke RegisterClassEx, addr @stWndClass
;建立并显示窗口
invoke CreateWindowEx, WS_EX_CLIENTEDGE, \
offset szClassName, offset szClassName, \
WS_OVERLAPPEDWINDOW, \
100, 100, 400, 300, \
NULL, hMenu, hInstance, NULL
mov hWinMain, eax
invoke ShowWindow, hWinMain, SW_SHOWNORMAL
invoke UpdateWindow, hWinMain
;消息循环
.while TRUE
invoke GetMessage, addr @stMsg, NULL, 0, 0
.break .if eax == 0
invoke TranslateMessage, addr @stMsg
invoke DispatchMessage, addr @stMsg
.endw
ret
_WinMain endp
main proc
call _WinMain
invoke ExitProcess, NULL
main endp
end main
编译运行:

(截图截不到光标)
1.装入图标和光标
在WM_CREATE消息中,程序从资源节区中装入所有的图标和光标资源,装入图标是用LoadIcon函数来完成的:
invoke LoadIcon,hInstance,lpIconName
.if eax
mov hIcon,eax
.endif
hInstance参数指定实例句柄,表示图标资源定义在哪个可执行文件中,lpIconName参数指定图标资源的名称,它就是资源文件中定义的图标ID值,如果调用成功的话,函数返回图标句柄。
除了可以装入资源文件中定义的图标资源之外,当参数hInstance为NULL的时候,用LoadIcon还可以用预定义的lpIconName参数装入Windows预定义的图标,参数说明如表5.1所示。
表5.1 LoadIcon可以装入的预定义图标

装入光标的函数有两个。装入在资源中定义的光标的函数是LoadCursor,它的语法和LoadIcon几乎一样:
invoke LoadCursor,hInstance,lpCursorName
.if eax
mov hCursor,eax
.endif
LoadCursor的用法也和LoadIcon相似,lpCursorName是光标资源的ID,LoadCursor也可以用指定hInstance为NULL的办法装入表5.2所列的预定义光标,这时候lpCursorName参数的取值如表5.2所示。
表5.2 LoadCursor可以装入的预定义光标

读者可以注意到,预定义的图标和光标都是Windows系统中常用的,预定义图标常用在消息框中,预定义光标就是Windows鼠标属性中的光标。使用预定义图标和光标的好处是它们的形状会随着系统设置值的不同自动改变,如改变“控制面板”→“鼠标”→“指针”中的设置后,装入的光标会自动改变。
另一个光标装入函数是LoadCursorFromFile,这个函数从磁盘光标文件中装入光标:
invoke LoadCursorFromFile,lpCursorFileName.if eax
mov hCursor,eax
.endif
在Windows 9x中,静态光标文件*.cur既可以定义在资源文件中,也可以使用LoadCursorFromFile函数装入,但是动态光标文件*.ani只能通过文件方式装入。在Windows 2000及XP中,两种光标文件都可以通过资源装入。为了在不同的操作系统上都可以使用,例子文件使用LoadCursorFromFile函数来装入动态光标文件。
2.使用图标和光标
现在来看如何使用图标。图标一般使用在对话框中或者程序窗口的标题栏中,要在标题栏中设置图标可以用向窗口发送WM_SETICON消息的办法实现:
invoke SendMessage,hWnd,WM_SETICON,ICON_BIG,hIcon
消息的wParam参数可以是ICON_BIG或ICON_SMALL,用来指定图标的分辨率为32×32还是16×16。
要将窗口的光标设置为新的光标不能使用WM_SETCURSOR,这个消息是通知窗口重新刷新光标而不是让它设定指定的光标。Windows中有个SetCursor函数可以用来设置窗口光标,但这只能将新的光标维持很短一段时间,因为当Windows向窗口重新发送WM_SETCURSOR消息的时候,光标就会被设置为原来的样子,而不妙的是,Windows常常会自动向窗口过程发送WM_SETCURSOR消息,所以SetCursor并不能用来永久地改变窗口的光标。
如果要改变窗口的光标,正确的办法是用SetClassLong函数改变窗口类的属性,这个函数的使用方法如下:
invoke SetClassLong,hWnd,nIndex,dwNewLong
由于这个函数用来改变窗口类的属性,所以可以永久改变类中的光标设定,hWnd用来指定一个用这个类建立的某个窗口句柄,nIndex参数指定要改变窗口类的哪个属性,可以指定为GCL_HBRBACKGROUND,GCL_HCURSOR,GCL_HICON,GCL_HMODULE,GCL_MENUNAME,GCL_STYLE或GCL_WNDPROC等,它们分别表示要改变的窗口类的背景色、光标、图标、hInstance、菜单、风格或窗口过程地址,读者可以用这个函数来改变一个窗口类的几乎所有属性,程序中通过这个函数将窗口的光标在不同的光标句柄之间切换:
invoke SetClassLong,hWnd,GCL_HCURSOR,hCur1或hCur2
5.3 位图
5.3.1 位图简介
位图(Bitmap)是Windows操作系统存储图像的方式,图像中的每个像素对应存储器中的一个或多个数据位,如单色位图每个像素对应1位,16色位图每个像素对应4位,256色为8位,全彩色为24位等,所有的像素数据按照一行行的顺序排列在存储器中,每个像素对应的位数称为颜色深度。
使用位图的优越之处是操作的速度很快,计算机的屏幕显示是由硬件从视频缓冲区中的数据映射的,向视频缓冲区中拷贝数据就可以直接将图像显示在屏幕上,所以以位图的方式存储像素,显示图形的时间几乎就是向视频缓冲区拷贝数据的时间。
位图的不便之处一是尺寸问题:由于位图是不压缩的,它占用的空间很大,一个1024×768像素、24位色的图像的大小为1024×768×3,达2.3 MB;二是位图的缩放问题,读者都知道矢量和位图之间的关系,矢量图形(网上流行的Flash动画就是矢量格式的)可以无限制缩放而不变形,因为它是根据矢量实时计算出像素数据的,而位图缩放后要对原来的像素数据进行插值计算,不可避免地会有失真。
在使用Windows的位图之前,必须搞清楚几个概念:位图、设备无关位图和位图文件。
单纯意义上的“位图”指的就是存放在内存中、可以马上使用的位图,它的颜色深度总是对应当前设备(如屏幕或打印机等)的颜色深度。不与具体的设备对应,位图数据是没有意义的,因为无法知道要把数据中的多少位解释成一个像素。
对于存放在磁盘上或别的地方的位图数据来说,它的颜色深度有可能和屏幕颜色深度不同,为了准确描述它的颜色信息,必须有像素数据的属性说明,以及色彩表,在使用这个位图的时候,可以根据这些信息将像素数据转换到需要的颜色深度。色彩表和位图数据合在一起就叫做设备无关位图(DIB),因为它转换后可以用在不同颜色深度的设备上。Windows有函数专门用来处理DIB。使用DIB唯一的问题是当将高颜色数的DIB转换到低颜色数的设备上时,由于色彩只能被转换成设备所能表示的最相近的颜色,所以可能会有很大的颜色失真。
DIB可以存放在磁盘上的位图文件中,位图文件一般以bmp为扩展名,它的内容包括一个bitmap文件头和DIB数据,bitmap文件头可以用来验证整个文件的有效性。所以简单地讲,DIB是位图数据的超集,位图文件又是DIB的超集。
Windows支持的图形文件格式只有bmp,ico和cur等几种,可以广泛用在GDI操作中的只有bmp文件,其他格式文件,如jpg与tif等都是不能直接应用的,要使用这些文件,必须在代码中将它们转换到位图格式以后才行,所以要编一个仅支持bmp的图片浏览器是很简单的,而要支持其他格式麻烦就大了,仅jpg格式的解码就是个很复杂的问题!
5.3.2 在资源中定义位图
Windows对bmp文件的支持有两种方法,一种是打开bmp文件读入DIB部分的数据,然后用函数将DIB数据转换到位图数据;另一种方法就是在资源文件中用和ico,cur文件类似的方法定义位图资源,然后在程序中装入后使用。
在资源脚本文件中定义位图资源的语法是:
位图ID BITMAP [DISCARDABLE] 位图文件名
在程序中可以用LoadBitmap函数装入位图资源:
invoke LoadBitmap,hInstance,lpBitmapName
.if eax
mov hBitmap,eax
.endif
LoadBitmap函数返回一个位图句柄,在程序退出的时候,位图句柄必须用DeleteObject函数释放。对位图资源的大部分操作涉及GDI的内容,这方面的内容在第7章中详细介绍。
&spm=1001.2101.3001.5002&articleId=150530733&d=1&t=3&u=32b3affeda1a4f7f811d88d62dcc9b52)
947

被折叠的 条评论
为什么被折叠?



