见招拆招《Windows程序设计》(九)
相关的例子:下载>>> 作者:Zoologist 于2008-7-19上传 

菜单及其它资源

大多数Windows程序都包含一个自订的图标,Windows将该图标显示在应用程序窗口标题列的左上角。当程序显示在屏幕底部的任务栏,显示在Windows Explorer中,或者作为快捷方式显示在桌面上时,Windows也显示该程序的图标。有些程序--比如很多类似于画板一类的图形绘制工具--也使用自订鼠标光标来表示程序的不同操作。还有许多Windows程序使用菜单和对话框。菜单、对话框加上滚动条,这是标准Windows使用者接口。

图标、光标、菜单和对话框都是相互关联的,它们是Windows的全部资源型态。资源即数据,它们被储存在程序的.EXE文件中,但是它们并非驻留在程序的数据区域中。也就是说,资源不能从程序源程序中定义的变量直接存取,Windows提供函数直接或间接地把它们加载内存以备使用。我们已经遇到了两个这样的函数,即LoadIcon和LoadCursor,它们出现在范例程序,定义窗口类别结构的内容。它们从Windows中加载二进制图标和光标映象,并传回该图标或光标的句柄。在本期中,我们先建立自己的图标,它会从程序自己的.EXE文件中载入。

在本书中,我们将讨论这些资源:

前六个资源在本期讨论,对话框和位图将在后面几期中讨论。

图标、光标、字符串和自订资源

使用资源的好处之一,在于程序的许多组件能够连结编译进程序的.EXE文件中。如果没有资源这一个概念,如图标图像之类的二进制文件可能会存放在单独的文件中,.EXE会把它读入内存中使用。或者图标不得不在程序中以字节数组的形式定义(这样就无法看到实际的图标图像了)。作为资源,图标储存在开发者计算机上可单独编辑的文件中,但在编译程序中被连结编译进.EXE文件中。

从简单的角度理解,我们需要编写一个 *.rc 文件,是文本文件格式,里面描述我们要使用什么文件或者菜单格式等等,通过编译后生成 *.res文件,这是二进制格式的文件,我们在Win32汇编语言中直接使用了。我们在 rc 文件中给资源编号,然后在程序中说“我要使用xx号资源”,系统就会自动将它读取出来供你使用。

将图标添加到程序

MasmPlus 处理资源非常简单。比如,我们想更换生成 exe 的图标。

程序10-1 ICONDEMO

        
ICONDEMO.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	ICO_MAIN equ 1000h
	IDI_ICON equ 101
.DATA
	szAppName	db "IconDemo",0
.DATA?
        hIcon	 	HICON	?
        cxIcon		DD	?
        cyIcon		DD	?
        cxClient 	DD	?
        cyClient 	DD	?

.CODE
START:

	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0

WinMain proc hInst:DWORD,hPrevInst:DWORD,CmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	local hWnd :HWND

	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0

	push hInst
	pop wndclass.hInstance

	invoke LoadIcon,hInst,IDI_ICON
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	mov wndclass.lpszClassName,offset szAppName

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (EAX==0)
		 invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),addr szAppName,MB_ICONERROR 		
		 ret
	.endif
        
	invoke CreateWindowEx,
					NULL,
					ADDR szAppName, 					;window class name
					ADDR szAppName, 
					WS_OVERLAPPEDWINDOW,					;window style
					CW_USEDEFAULT,						;initial x position
					CW_USEDEFAULT,						;initial y position
					CW_USEDEFAULT, 						;initial x size
					CW_USEDEFAULT,						;initial y size
					NULL,							;parent window handle
					NULL,							;window menu handle
					hInst,							;program instance handle
					NULL										;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	hdc:HDC
        LOCAL   hInstance:HINSTANCE
        LOCAL   ps:PAINTSTRUCT
        LOCAL	x, y:DWORD

	.if uMsg==WM_CREATE
		mov	esi,lParam
		mov	eax,[esi+4]
		mov	hInstance,eax
		invoke	LoadIcon,hInstance,IDI_ICON
		mov	hIcon,eax
		
		invoke	GetSystemMetrics,SM_CXICON
                mov	cxIcon,eax
                invoke  GetSystemMetrics,SM_CYICON
                mov	cyIcon,eax

	        xor	eax,eax
	        ret
	.elseif uMsg == WM_SIZE
		mov	eax,lParam
		and	eax,0FFFFh
		mov	cxClient,eax
        
		mov	eax,lParam
		shr	eax,16
		mov	cyClient,eax

	        xor	eax,eax
	        ret
	.elseif uMsg == WM_PAINT
		invoke	BeginPaint,hwnd, addr ps
		mov	hdc,eax

                mov	y,0
	LoopY:               
		mov	x,0
	  LoopX:
	  	invoke	DrawIcon,hdc,x,y,hIcon
	  	mov	eax,cxIcon
	  	add	x,eax
	  	mov	eax,x
	  	cmp	eax,cxClient
	  	jl	LoopX
	
		mov	eax,cyIcon
                add	y,eax
                mov	eax,y
                cmp	eax,cyClient
                jl	LoopY
                
		invoke	EndPaint,hwnd, addr ps	        
	        xor	eax,eax
	        ret		
	.elseif uMsg == WM_DESTROY

	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START
        

        

如果您马上试着编译该程序,运行结果同普通的程序别无二致。这是因为我们还没有定义好程序要的图标资源。

这里推荐Aogo裁剪出来的,VC版的资源编辑器。可以用如下的方法建立一个资源文件:从「文件」菜单中选择「新建」,选择「文件」页面标签,单击「资源脚本」,在「文件名」栏中键入「ICONDEMO」,单击OK。之后,资源编辑器就会建立会建立两个文本文件:CONDEMO.RC(资源描述档)和RESOURCE.H。对于我们来说,只需要前者。

wpe3.jpg (33670 字节)

资源描述档是文本文件。它包括这些资源的可用文字形式表达的描述,例如菜单和对话框。资源描述文件也包括对非文字资源的二进制档案的引用,例如图标和自订的鼠标光标。

对于上面这个程序,您可以直接使用编辑好的图标文件,也可以尝试着手动在资源编辑器中创建一个新图标。如果你用Windows XP的图片和传真查看器直接查看这个图标的话,你会发现放大之后看到的图片似乎和“原始图片”不一样。这是因为ICONDEMO.ICO实际上包含了2个ICON。

我们在文件的开头定义了 IDI_ICON equ 101,在程序中用如下方法使用:

	invoke LoadIcon,hInst,IDI_ICON
	mov wndclass.hIcon,eax

面的几期中,是这样写的:

wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
        

之所以改变叙述,是因为以前我们为应用程序使用的是标准的图示,而这里我们的目的是使用自订图示。

现在呈现的是一个空白的32×32图素的图示,您可以在其中填入颜色。您会看到带有一组绘图工具和可用颜色的浮动工具列。注意颜色工具列中包括两个与颜色无关的选项,这两种颜色选项有时被称为「屏幕颜色」跟「反屏幕颜色」。当一个图素在着色时选择了「屏幕颜色」时,它实际上是透明的。不管图标在什么表面上显示,图示未着色的部分会显示出底色。这样我们就可以建立非矩形的图示。

双击围绕图标的区域,会出现「Icon Properties」对话框,该对话框使您能够更改图标的ID和文件名称。Developer Studio可能已经将ID设定为IDI_ICON1,将它改为IDI_ICON,这样ICONDEMO就可以引用图标(前缀IDI代表「图标的ID」)。同样地,将文件名改为ICONDEMO.ICO。

现在选择一种有特色的颜色(如红色)并在图示上画一个大的B(代表BIG),请注意不必像图10-1那么整齐。


 

wpe1.jpg (17131 字节)

图10-1 显示在Developer Studio中的标准(32×32)ICONDEMO文件

此时程序应该能够编译并执行得很好了。Developer Studio将在ICONDEMO.RC资源描述档中划一条横线,表示下面是带有标识符(IDI_ICON)的图示文件(ICONDEMO.ICO)。RESOURCE.H表头文件中会包含IDI_ICON标识符的定义。

编译过程中,我们会使用RC.EXE编译资源。文字资源描述文件被转化为二进制形式,也就是具有扩展名.RES的文件。然后,该已编译的资源文件随同.OBJ和.LIB文件一起在LINK步骤中被指定连结。这就是资源被添加到最后产生出来的.EXE文件中的方式。

当您执行ICONDEMO时,程序图标显示在标题列的左上角和工作列中。如果您将程序添加到「开始」菜单中,或在桌面上放置快捷方式,您也会在那儿看到该图示。

ICONDEMO也在显示区域水平和垂直地重复显示该图示。程序使用叙述

hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;
        

取得图示的句柄。使用叙述

cxIcon = GetSystemMetrics (SM_CXICON) ;
        
cyIcon = GetSystemMetrics (SM_CYICON) ;
        

取得图示的大小。然后,程序通过多次呼叫

DrawIcon (hdc, x, y, hIcon) ;
        

显示图标,其中x和y是被显示图示其左上角的坐标。

在目前使用的大多数显卡上,带有SM_CXICON和SM_CYICON索引的GetSystemMetrics会回报图示的大小为32×32图素。这是我们在资源编辑器中建立的图示大小,它也是图示出现在桌面上和显示在ICONDEMO程序显示区域的大小。然而,这个大小并非显示在程序的标题列或工作栏中的图示大小。小图示的大小可以由带有SM_CXSMSIZE和SM_CYSMSIZE索引的GetSystemMetrics获得(第一个SM表示「system metrics(系统度量)」,被包含的SM表示「small(小)」)。对于目前使用的大多数显示卡来说,小图示的大小为16×16图素。

这会产生问题。当Windows将32×32的图示缩小为16×16的图示时,必需减少图素的行和列。这样,对于某些比较复杂的图示,就会失真。因此,我们应该为那些图像缩小就会变形的图示建立特殊的16×16图素的图示。在Developer Studio中图标图像的上面是标识为「Device」的下拉式清单方块,在它的右边有一个按钮,按下该按钮会弹出「New Icon Image」对话框,此时选择「Small(16×16)」。现在您可以画另一个图示。如图10-2所示,画一个「S」(表示「小」)。


 

wpe2.jpg (6905 字节)

图10-2 在Developer Studio中显示的小(16×16)ICONDEMO文件

在该程序中您不必做任何事情。第二个图标图像被储存在相同的ICONDEMO.ICO文件中,并以相同的IDI_ICON标识符引用。在适当的时候,Windows会自动使用该较小的图示,例如在标题列或工作列中。当在桌面上显示快捷方式,以及程序呼叫DrawIcon装饰显示区域时,Windows会使用大图示。

在掌握这些知识之后,让我们看一看使用图示的详细情况。

取得图示句柄

如果您仔细阅读ICONDEMO.RC和RESOURCE.H文件,会看到由Developer Studio产生用于维护文件的一些标记。然而,当编译资源描述档时,只有少数几行是重要的。这些从ICONDEMO.RC和RESOURCE.H文件中摘录下来的关键部分被列在程序10-2中。

ICONDEMO.RC (摘录)
        
#define IDI_ICON 101
IDI_ICON ICON "icondemo.ico"

程序10-2 ICONDEMO.RC文件的摘录

程序10-2所显示的ICONDEMO.RC和RESOURCE.H文件与您在普通的文字编辑器中手动建立的很相似,80年代的Windows程序写作者就是这样做的。

ICONDEMO.RC中的这行

IDI_ICON ICON DISCARDABLE "icondemo.ico"
        

是资源描述档的ICON叙述。该图示有一个数值标识符IDI_ICON,等于101。由Developer Studio添加的DISCARDABLE关键词指出,必要时Windows可以从内存中丢弃图标,以获得额外的空间。之后不需要程序任何特定的操作,Windows就能够重新加载图示。DISCARDABLE属性是内定的,不需要指定。只有在名称和目录路径包含空格时,Developer Studio才将文件名加上引号。

当资源编译程序将编译的资源储存在ICONDEMO.RES中,并且由连结程序将资源添加到ICONDEMO.EXE中以后,该资源就可以经由一个资源型态(RT_ICON)和一个标识符(IDI_ICON或101)来标识。程序可以通过呼叫LoadIcon函数取得此图示的句柄:

		invoke	LoadIcon,hInstance,IDI_ICON
		mov	hIcon,eax        

请注意ICONDEMO在两个地方呼叫这个函数,一次在定义窗口类别时,另一次在窗口消息处理程序中取得图标的句柄用于绘制。LoadIcon传回HICON型态的值,它是图示的句柄。

LoadIcon的第一个参数,是指出资源来自哪个文件的执行实体句柄。使用hInstance表示它来自程序自己的.EXE文件。LoadIcon的第二个参数实际上被定义为指向字符串的指针。待会将会看到,可以使用字符串而不是用数值标识符标识资源。

LoadIcon知道,如果第二个参数的高字组为0,那么低字组就为图示的数值标识符。图标的标识符必须为16位值。

本书前面的范例程序使用了预先定义的图示:

LoadIcon (NULL, IDI_APPLICATION) ;
        

hInstance参数被设定为NULL,因此Windows知道这是预先定义的图示。IDI_APPLICATION也在WINDOWS.INC中定义.

LoadIcon的第二个参数带来了一个有趣的问题:图标的标识符能可以为字符串吗?答案是可以。方法如下:在 Developer Studio中,在ICONDEMO项目的文件列表上,选择 IDONDEMO.RC。您会看到顶端为「IconDemo Resource」的树状结构,然后是资源型态「Icon」,再下来是「IDI_ICON」。如果用鼠标右键单击图标标识符,并从菜单上选择「 Properties」,您就能改变ID。实际上,您可以把名称放在引号内将其更改为字符串。我用这种方法指定资源名称,并在本书的其它地方也使用该方法。

我喜欢为图示(以及一些其它资源)使用文字名称,因为名称可以是程序的名称。例如,假定文件被命名为MYPROG。如果您使用「Icon Properties」对话框将图标的ID指定为「MyProg」(包括引号),资源描述档将包含下列叙述:

MYPROG ICON DISCARDABLE myprog.ico
        

然而,在RESOURCE.H中并没有#define叙述,来指出MYPROG是数值标识符。资源描述文件将假定MYPROG是字符串标识符。

在C程序中,使用LoadIcon函数来取得图示句柄。您可能已经有了表示程序名的字符串:

static TCHAR szAppName [] = TEXT ("MyProg") ;
        

这意味着程序可以使用叙述:

hIcon = LoadIcon (hInstance, szAppName) ;
        

来加载图标,这比宏MAKEINTRESOURCE更清晰一些。

但是如果您确实想用数字来命名,那么您可以用数字代替标识符或字符串。在「Icon Properties」对话框中,在ID栏中输入数字。资源描述档将有一个类似下面的ICON叙述:

125 ICON DISCARDABLE myprog.ico
        

可以使用两种方法之一引用图示。明显易读的方式是:

hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (125)) ;
        

另一个不易阅读的方式是:

hIcon = LoadIcon (hInstance, TEXT ("#125")) ;
        

Windows识别初始字符#作为ASCII形式中字符数值的开头。

在程序中使用图标

虽然Windows以几种方式用图标来代表程序,但是许多Windows程序仅在用WNDCLASS结构和RegisterClass定义窗口类别时指定一个图示。如我们所看到的,这样作用得很好,尤其当图示文件包含标准和较小的图像大小时,更是如此。Windows在显示图标图像时,它会在图示文件中选择最合适的图像大小。

RegisterClass有一个改进版本叫做RegisterClassEx,它使用名为WNDCLASSEX的结构。WNDCLASSEX有两个附加的字段:cbSize和hIconSm。cbSize字段指出了WNDCLASSEX结构的大小,假设hIconSm被设定为小图标的图标句柄。这样,在WNDCLASSEX结构中,您可以设定与两个图示文件相关的两个图示句柄-一个用于标准图示,一个用于小图示。

有这种必要吗?没有。正如我们看到的,Windows已经从单个图示文件中提取了大小合适的图标图像。RegisterClassEx似乎没有RegisterClass聪明。如果hIconSm字段使用了包含多个图像的图标文件,则只有第一个图像能被利用。它可能是标准大小的图示,使用时才被缩小。RegisterClassEx似乎是为了使用多个图标图像而设计的,每个图像只包含一种图标大小。因为现在可以将多个图示大小包括在同一个图示文件中,所以我建议使用WNDCLASS和RegisterClass。

如果您想在程序执行的时候,动态地更改程序的图标,可以使用SetClassLong来达到目的。例如,如果您有与标识符IDI_ALTICON相关的第二个图示文件,则您可以使用以下的叙述将其切换到那个图示:

SetClassLong (hwnd, GCL_HICON,
        
    LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ALTICON))) ;
        

如果不想储存程序图标的句柄,但要使用DrawIcon函数在别处显示它,可以使用GetClassLong获得句柄。例如:

DrawIcon (hdc, x, y, GetClassLong (hwnd, GCL_HICON)) ;
        

在Windows文件的某些部分,LoadIcon被称为「过时的」,并推荐使用LoadImage(LoadIcon在/Platform SDK/User Interface Services/Resources/Icons中说明,LoadImage在/Platform SDK/User Interface Services/Resources/Resources中说明)。当然LoadImage更为灵活,但它没有LoadIcon简单。您会注意到,在ICONDEMO中对同一个图示呼叫了LoadIcon两次。这不会产生问题,也没有使用额外的内存。LoadIcon是取得句柄但不需要清除句柄的少数几个函数之一。实际上有一个DestroyIcon函数,但它与CreateIcon、CreateIconIndirect和CreateIconFromResource连在一起使用。这些函数使程序能够动态地建立图标图像。

使用自订光标

在程序中使用自订的鼠标光标与使用自订的图示相似,只是大多数程序写作者总是使用Windows提供的光标。自订游标一般为单色,大小为32×32图素。在Developer Studio中建立光标与建立图标的方法相同(从「Insert」菜单上选择「 Resource」,然后单击「Cursor」),但不要忘记定义热点。

可以在对象类别定义中设定自订光标,叙述为:

wndclass.hCursor = LoadCursor (hInstance, MAKEINTRESOURCE (IDC_CURSOR)) ;
        

如果光标用文字名称定义,则为:

wndclass.hCursor = LoadCursor (hInstance, szCursor) ;
        

每当鼠标位于根据这个类别建立的窗口上时,就会显示与IDC_CURSOR或szCursor相对应的鼠标光标。

如果使用了子窗口,那么您可能希望光标随着所在窗口的不同而有所区别。如果程序为这些子窗口定义了窗口类别,就可以在每个窗口类别中适当地设定hCursor字段,让每个窗口类别使用不同的光标。如果使用了预先定义的子窗口控件,就可以使用以下方法改变窗口类别的hCursor字段:

SetClassLong (hwndChild, GCL_HCURSOR,
        
    LoadCursor (hInstance, TEXT ("childcursor")) ;
        

如果您将显示区域划分为较小的逻辑区域而不使用子窗口,就可以使用SetCursor来改变鼠标光标:

SetCursor (hCursor) ;
        

在处理WM_MOUSEMOVE消息处理期间,您应该呼叫SetCursor;否则,当光标移动时,Windows将使用窗口类别中定义的光标来重画光标。文件指出,如果没有改变光标,则SetCursor速度将会很快。

字符串资源

把字符串当成资源的观念一开始可能令人觉得诡异。因为我们在使用原始码中定义为变量的一般字符串时,并没有碰到任何问题。

字符串资源主要是为了让程序转换成其它语言时更为方便。正如后面两章中将看到的一样,菜单和对话框也是资源描述文件的一部分。如果使用字符串资源而不是将字符串直接放入原始码中,那么程序所使用的所有文字将在同一文件-资源描述档中。如果转换了资源描述文件中的文字,那么建立程序的另一种语言版本所需做的一切就是重新连结程序。这种方法比重新组织原始码安全得多(然而,除了下一个范例程序,我在本书的其它程序中不使用字符串表,原因是字符串表使程序代码看起来更为模糊和复杂)。

您可以在「Insert」菜单中选择「Resource」,再选择「 String Table」,建立一个字符串表。字符串会显示在屏幕右边的列表中。通过双击字符串就可以选中它。针对每个字符串,您可以指定标识符和字符串的内容。

在资源描述中,字符串显示在一个多行的叙述中,如下所示:

STRINGTABLE DISCARDABLE
        
BEGIN
        
    IDS_STRING1, "character string 1"
        
    IDS_STRING2, "character string 2"
        
    其它字符串定义
        
END
        

如果您在替早期版本的Windows写程序,并在文字编辑器中手动建立这个字符串表(用Developer Studio来做这件事当然更容易得多了),您可以用左右大括号代替BEGIN和END叙述。

资源描述可以包含多个字符串表,但是每个ID必须唯一表示一个字符串。每个字符串占一行,最多4097个字符。\t可以作为制表符,\n则作为linefeed字符号。DrawText和MessageBox函数能够识别这些控制符号。

您的程序可以使用LoadString呼叫把字符串复制到程序数据段的缓冲区中:

LoadString (hInstance, id, szBuffer, iMaxLength) ;
        

参数id是ID,它加在资源描述文件中每个字符串的前面;szBuffer是指向接收字符串的字符数组的指针;iMaxLength是送入szBuffer中的最大字符数。函数传回字符串中的字符数。

每个字符串前面的ID一般是定义在表头文件中的宏标识符。许多Windows程序写作者使用前缀IDS_ 来表示字符串的ID。有时,文件名称或其它信息需要在字符串显示时插入到字符串中。在这种情况下,您可以将C的格式化字符放入字符串,并把它用于wsprintf中作为一个格式化字符串。

所有资源文字-包括字符串表中的文字-以Unicode格式储存在.RES编译资源文件以及最终的.EXE文件中。LoadStringW函数直接加载Unicode文字。LoadStringA函数(仅在Windows 98下有效)完成由Unicode到本地代码页的文字转换。

让我们来看一个程序,它使用三个字符串,在消息框中显示三条错误信息。RESOURCE.H表头文件为这些信息定义了三个标识符:

#define IDS_FILENOTFOUND           1
        
#define IDS_FILETOOBIG                2
        
#define IDS_FILEREADONLY           3
        

资源描述文件具有此字符串表:

STRINGTABLE
        
BEGIN
        
    IDS_FILENOTFOUND,                "File %s not found."
        
    IDS_FILETOOBIG,                      "File %s too large to edit."
        
    IDS_FILEREADONLY,                   "File %s is read-only."
        
END
        

C原始码文件也包含这个表头文件,并定义了一个显示消息框的函数(我假定szAppName是一个包含程序名称的整体变量)。

OkMessage (HWND hwnd, int iErrorNumber, TCHAR *szFileName)
        
{
        
    TCHAR szFormat [40] ;
        
    TCHAR szBuffer [60] ;
        
    LoadString (hInst, iErrorNumber, szFormat, 40) ;
        
    wsprintf (szBuffer, szFormat, szFilename) ;
        

    return MessageBox (   hwnd, szBuffer, szAppName,
        
                                                          MB_OK | MB_ICONEXCLAMATION) ;
        
}
        

为了显示包含「file not found」信息的消息框,程序呼叫:

OkMessage (hwnd, IDS_FILENOTFOUND, szFileName) ;
        

自订的资源

Windows也定义了「自订资源」,这又称为「使用者定义的资源」(使用者就是您-程序写作者,而不是那个使用您程序的幸运者)。自订资源让连结.EXE文件中的各种数据更为方便,对取得程序中的数据也是如此。资料可以是您需要的任何格式。程序用于存取自订资源的Windows函数促使Windows将数据加载内存并传回指向它的指标。然后您就可以对程序做任何操作。您会发现对于储存和存取各种自己的数据,这要比把数据储存在外部文件中,再使用文件输入函数存取它要方便得多。

例如,您有一个文件叫做BINDATA.BIN,它包含程序需要显示的一些数据。您可以选择这个文件的格式。如果在MYPROG项目中有MYPROG.RC资源描述档,您就可以在Developer Studio中从「Insert」菜单中选择「Resource」并按「 Custom」按钮,来建立自订的资源。键入表示资源的名称:例如,BINTYPE。然后,Developer Studio会生成资源名称(在这种情况下是IDR_BINTYPE1)并显示让您输入二进制数据的窗口。但是您不必输入什么,用鼠标右键单击IDR_BINTYPE1名称,并选择 Properties,然后就可以输入一个文件名称:例如,BINDATA.BIN。

资源描述档就会包含以下的一行叙述:

IDR_BINTYPE1 BINTYPE BINDATA.BIN
        

除了我们刚刚生成的BINTYPET资源型态外,这个叙述与ICONDEMO中的ICON叙述一样。有了图示后,您可以对资源名称使用文字的名称,而不是数字的标识符。

当您编译并连结程序,整个BINDATA.BIN文件会被并入MYPROG.EXE文件中。

在程序的初始化(比如,在处理WM_CREATE消息时)期间,您可以获得资源的句柄:

hResource = LoadResource (hInstance,
        
                   FindResource (hInstance, TEXT ("BINTYPE"),
        
                                                                 MAKEINTRESOURCE (IDR_BINTYPE1))) ;
        

变量hResource定义为HGLOBAL型态,它是指向内存区块的句柄。不管它的名称是什么,LoadResource不会立即将资源加载内存。把LoadResource和FindResource函数如上例般合在一起使用,在实质上就类似于LoadIcon和LoadCursor函数的做法。事实上,LoadIcon和LoadCursor函数就用到了LoadResource和FindResource函数。

当您需要存取文字时,呼叫LockResource:

pData = LockResource (hResource) ;
        

LockResource将资源加载内存(如果还没有加载的话),然后它会传回一个指向资源的指标。当结束对资源的使用时,您可以从内存中释放它:

FreeResource (hResource) ;
        

当您的程序终止时,也会释放资源,即使您没有呼叫FreeResource.。

让我们看一个使用三种资源-一个图标、一个字符串表和一个自订的资源-的范例程序。程序10-3所示的POEPOEM程序在其显示区域显示Edgar Allan Poe的「Annabel Lee」文字。自订的资源是文件POEPOEM.TXT,它包含了一段诗文,此文本文件以反斜线(\)结束。

程序10-3  POEPOEM
        
POEPOEM.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	IDS_APPNAME	equ	1001
	IDS_CAPTION	equ	1002
	IDS_ERRMSG	equ	1003
	
.DATA
.DATA?
	hInst	    HINSTANCE   ?
        pText 	    DD		?
	hResource   HGLOBAL	?
        hScroll     HWND        ?
        iPosition   DD		?
        cxChar      DD		?
        cyChar      DD		?
        cyClient    DD		?
        iNumLines   DD		?
        xScroll     DD		? 
.CODE
START:

	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0

WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND
	LOCAL szAppName[16],szCaption[64], szErrMsg[64]:TCHAR
	
	xor	edx,edx
	mov	eax,sizeof szAppName
	mov	ecx,sizeof TCHAR
	div	ecx
	mov	ebx,eax
	invoke	LoadString,hInstance,IDS_APPNAME,addr szAppName,ebx

	xor	edx,edx
	mov	eax,sizeof szCaption
	mov	ecx,sizeof TCHAR
	div	ecx
	mov	ebx,eax
	invoke	LoadString,hInstance,IDS_CAPTION,addr szCaption,ebx
	
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,addr szAppName
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	mov wndclass.lpszMenuName,NULL
	lea eax,szAppName
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   mov		ebx,sizeof szAppName
           invoke	LoadString,hInstance, IDS_APPNAME, addr szAppName,ebx
           mov		ebx,sizeof szErrMsg
           invoke	LoadString,hInstance, IDS_ERRMSG, addr szErrMsg,ebx
           invoke	MessageBox,NULL, addr szErrMsg,addr szAppName, MB_ICONERROR
           ret
	.endif
        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			ADDR szAppName, 
			WS_OVERLAPPEDWINDOW or WS_CLIPCHILDREN,			;window style
			CW_USEDEFAULT,						;initial x position
			CW_USEDEFAULT,						;initial y position
			CW_USEDEFAULT, 						;initial x size
			CW_USEDEFAULT,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,						;program instance handle
			NULL							;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	hdc:HDC
        LOCAL   ps:PAINTSTRUCT
        LOCAL	rect:RECT
        LOCAL	tm:TEXTMETRIC

	.if uMsg==WM_CREATE
		invoke	GetDC,hwnd
		mov	hdc,eax
		invoke	GetTextMetrics,hdc,addr tm
		mov	eax,tm.tmAveCharWidth
		mov	cxChar,eax
		mov	eax,tm.tmHeight
		add	eax,tm.tmExternalLeading
		mov	cyChar,eax
		invoke	ReleaseDC,hwnd,hdc
                invoke	  GetSystemMetrics,SM_CXVSCROLL
                mov	xScroll,eax
		invoke	CreateWindowEx,
					NULL,
					CTXT("scrollbar"), 
					NULL,
                                	WS_CHILD or WS_VISIBLE or SBS_VERT,
                                	0, 0, 0, 0,
               				hwnd, 1, hInst, NULL		
		mov	hScroll,eax

		invoke	FindResource,hInst,CTEXT("AnnabelLee"),CTEXT ("TEXT")
		mov	ebx,eax
                invoke	LoadResource,hInst,ebx
		mov	hResource,eax
                invoke	LockResource,hResource
                mov	pText,eax
		
		xor	eax,eax        
	        mov	iNumLines,eax
	        
                mov	esi,pText
                mov	ax,[esi]
                .while	(al!='\')&&(ax!=0)
                	.if	al==10
                		inc	iNumLines
                	.endif
                	inc	pText
                	inc	esi
                	mov	ax,[esi]
                .endw
                mov	BYTE ptr [esi],0
        	invoke  SetScrollRange,hScroll, SB_CTL, 0, iNumLines, FALSE
		invoke	SetScrollPos,hScroll, SB_CTL, 0, FALSE
                  
	        xor	eax,eax
	        ret
	.elseif uMsg == WM_SIZE
        	push	TRUE
        	mov	eax,lParam
        	shr	eax,16
        	push	eax
        	mov	cyClient,eax
        	push	xScroll
        	push	0
        	mov	eax,lParam
        	and	eax,0FFFFh
        	sub	eax,xScroll
        	push	eax
        	push	hScroll
        	call	MoveWindow
        	
                invoke  SetFocus,hwnd

	        xor	eax,eax
	        ret
	.elseif uMsg == WM_SETFOCUS
                invoke	SetFocus,hScroll
	        xor	eax,eax
	        ret
	.elseif uMsg == WM_VSCROLL
		mov	eax,wParam
		.if	eax == SB_TOP
			xor	eax,eax
			mov	iPosition,eax
		.elseif	eax == SB_BOTTOM
			mov	eax,iNumLines
			mov	iPosition,eax
		.elseif	eax == SB_LINEUP 
			dec	iPosition
		.elseif	eax == SB_LINEDOWN 
			inc	iPosition
		.elseif	eax == SB_PAGEUP
			xor	edx,edx
			mov	eax,cyClient
			mov	ecx,cyChar
			div	ecx
			mov	ebx,iPosition
			sub	ebx,eax
			mov	iPosition,eax
		.elseif	eax == SB_PAGEDOWN 	
			xor	edx,edx
			mov	eax,cyClient
			mov	ecx,cyChar
			div	ecx
			mov	ebx,iPosition
			add	ebx,eax
			mov	iPosition,eax			
		.elseif	eax == SB_THUMBPOSITION 	
			mov	eax,lParam
			and	eax,0FFFFh
			mov	iPosition,eax
		.endif	
		mov	eax,iPosition
		cmp	eax,iNumLines
		jl	@f
		mov	eax,iNumLines
	@@:	
		cmp	eax,0
		jg	@f
		xor	eax,eax
	@@:	
		mov	iPosition,eax
		invoke	GetScrollPos,hScroll, SB_CTL
		.if	eax!=iPosition
                        invoke	SetScrollPos,hScroll, SB_CTL, iPosition, TRUE
                       	invoke	InvalidateRect,hwnd, NULL, TRUE
		.endif			
		
	        xor	eax,eax
	        ret	        
	.elseif uMsg == WM_PAINT
		invoke	BeginPaint,hwnd, addr ps
		mov	hdc,eax
 		invoke	LockResource,hResource
 		mov	pText,eax
 		invoke	GetClientRect,hwnd,addr rect
 		mov	eax,cxChar
 		add	rect.left,eax
 		mov	eax,1
 		sub	eax,iPosition
 		mov	ecx,cyChar
 		mul	ecx
 		add	rect.top,eax
 		invoke	DrawText,hdc, pText, -1,addr rect, DT_EXTERNALLEADING
 		invoke	EndPaint,hwnd,addr ps

	        xor	eax,eax
	        ret		
	.elseif uMsg == WM_DESTROY

		invoke	FreeResource,hResource
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START
POEPOEM.RC
#define IDM_FILE_NEW                    40001
#define IDM_FILE_OPEN                   40002
#define IDM_FILE_SAVE                   40003
#define IDM_FILE_SAVE_AS                40004
#define IDM_APP_EXIT                    40005
#define IDM_EDIT_UNDO                   40006
#define IDM_EDIT_CUT                    40007
#define IDM_EDIT_COPY                   40008
#define IDM_EDIT_PASTE                  40009
#define IDM_EDIT_CLEAR                  40010
#define IDM_BKGND_WHITE                 40011
#define IDM_BKGND_LTGRAY                40012
#define IDM_BKGND_GRAY                  40013
#define IDM_BKGND_DKGRAY                40014
#define IDM_BKGND_BLACK                 40015
#define IDM_TIMER_START                 40016
#define IDM_TIMER_STOP                  40017
#define IDM_APP_HELP                    40018
#define IDM_APP_ABOUT                   40019
#define ID_MENUITEM40020                40020

POPMENU MENU DISCARDABLE 
BEGIN
    POPUP "MyMenu"
    BEGIN
        POPUP "&File"
        BEGIN
            MENUITEM "&New",                        IDM_FILE_NEW
            MENUITEM "&Open",                       IDM_FILE_OPEN
            MENUITEM "&Save",                       IDM_FILE_SAVE
            MENUITEM "Save &As",                    IDM_FILE_SAVE_AS
            MENUITEM SEPARATOR
            MENUITEM "E&xit",                       IDM_APP_EXIT
        END
        POPUP "&Edit"
        BEGIN
            MENUITEM "&Undo",                       IDM_EDIT_UNDO
            MENUITEM SEPARATOR
            MENUITEM "Cu&t",                        IDM_EDIT_CUT
            MENUITEM "&Copy",                       IDM_EDIT_COPY
            MENUITEM "&Paste",                      IDM_EDIT_PASTE
            MENUITEM "De&lete",                     IDM_EDIT_CLEAR
        END
        POPUP "&Background"
        BEGIN
            MENUITEM "&White",                      IDM_BKGND_WHITE, CHECKED
            MENUITEM "&Light Gray",                 IDM_BKGND_LTGRAY
            MENUITEM "&Gray",                       IDM_BKGND_GRAY
            MENUITEM "&Dark Gray",                  IDM_BKGND_DKGRAY
            MENUITEM "&Black",                      IDM_BKGND_BLACK
        END
        POPUP "&Help"
        BEGIN
            MENUITEM "&Help...",                    IDM_APP_HELP
            MENUITEM "&About PopMenu...",           IDM_APP_ABOUT
        END
    END
END
POEPOEM.TXT
        
It was many and many a year ago,
        
  In a kingdom by the sea,
        
That a maiden there lived whom you may know
        
By the name of Annabel Lee;
        
And this maiden she lived with no other thought
        
  Than to love and be loved by me.
        
I was a child and she was a child
        
  In this kingdom by the sea,
        
But we loved with a love that was more than love --
        
  I and my Annabel Lee --
        
With a love that the winged seraphs of Heaven
        
  Coveted her and me.
        
And this was the reason that, long ago,
        
  In this kingdom by the sea,
        
A wind blew out of a cloud, chilling
        
  My beautiful Annabel Lee;
        
So that her highborn kinsmen came
        
  And bore her away from me,
        
To shut her up in a sepulchre
        
In this kingdom by the sea.
        
The angels, not half so happy in Heaven,
        
  Went envying her and me --
        
Yes! that was the reason (as all men know,
        
  In this kingdom by the sea)
        
That the wind came out of the cloud by night,
        
  Chilling and killing my Annabel Lee.
        
But our love it was stronger by far than the love Of those who were older than we -- Of many far wiser than we --
        
And neither the angels in Heaven above
        
  Nor the demons down under the sea
        
Can ever dissever my soul from the soul
        
  Of the beautiful Annabel Lee:
        
For the moon never beams, without bringing me dreams
        
  Of the beautiful Annabel Lee;
        
And the stars never rise, but I feel the bright eyes
        
  Of the beautiful Annabel Lee:
        
And so, all the night-tide, I lie down by the side
        
Of my darling -- my darling -- my life and my bride,
        
  In her sepulchre there by the sea --
        
  In her tomb by the sounding sea.
        
  [May, 1849]
        
\
        

POEPOEM.ICO


 

wpe1.jpg (2742 字节)

wpe3.jpg (54377 字节)

在POEPOEM.RC资源描述档中,使用者定义的资源被定义为TEXT型态,取名为AnnabelLee:

ANNABELLEE  TEXT  POEPOEM.TXT
        

在WndProc处理WM_CREATE时,使用FindResource和LoadResource取得资源句柄。使用LockResource锁定资源,并且使用一个小程序将文件末尾的反斜线(\)换成0,这有利于后面WM_PAINT消息处理期间使用的DrawText函数。

注意,这里使用的是子窗口的滚动条,而不是窗口滚动条,这是因为子窗口滚动条有一个自动的键盘接口,因此在POEPOEM中没有处理WM_KEYDOWN。

POEPOEM还使用三个字符串,它们的ID在RESOURCE.H表头文件中定义。在程序的开始,IDS_APPNAME和IDS_CAPTIONPOEPOEM字符串由LoadString加载内存:

LoadString (hInstance, IDS_APPNAME, szAppName,    sizeof (szAppName) /
        
                                          sizeof (TCHAR)) ;
        
LoadString (hInstance, IDS_CAPTION, szCaption,    sizeof (szCaption) /
        
                                          sizeof (TCHAR)) ;
        

注意RegisterClass前面的两个呼叫。如果您在Windows 98下执行Unicode版本的POEPOEM,这两个呼叫就都会失败。因此,LoadStringA比LoadStringW要复杂得多(LoadStringA必须将资源字符串由Unicode转化为ANSI,而LoadStringW仅是直接加载它),LoadStringW在Windows 98下不被支持。这意味着在Windows 98下,当RegisterClassW函数失败时,MessageBoxW函数(Windows 98支持)就不能使用LoadStringW加载程序的字符串。由于这个原因,程序使用LoadStringA加载IDS_APPNAME和IDS_ERRMSG字符串,并使用MessageBoxA显示自订的消息框:

if (!RegisterClass (&wndclass))
        
{
        
           LoadStringA (hInstance, IDS_APPNAME, (char *) szAppName,
        
                    sizeof (szAppName)) ;
        
           LoadStringA (hInstance, IDS_ERRMSG, (char *) szErrMsg,
        
                    sizeof (szErrMsg)) ;
        
           MessageBoxA (NULL, (char *) szErrMsg,
        
                    (char *) szAppName, MB_ICONERROR) ;
        
           return 0 ;
        
}
        

注意,TCHAR字符串变量是指向char的指针。

既然我们已经定义了用于POEPOEM的所有字符串资源,那么翻译者将程序转换成外语版本就很容易了。当然,它们将不得不翻译「Annabel Lee」这个名字-我想,这会是一项困难得多的工作。

菜单

您还记得Monty Python有关奶酪店的幽默短剧吗?那故事内容是这样的:一个客人走进奶酪店想买某种奶酪。当然,店里没有这种奶酪。因此他又问有没有另一种奶酪,然后再问另一种,再问另一种,不断的问店家有没有另一种奶酪(最后总共问了40种的奶酪),回答仍然是没有,没有,没有,没有,没有。

这个不幸的事件可以通过菜单的使用来避免。一个菜单是一列可用的选项,它告诉饥饿的用餐者,厨房可以提供哪些服务,并且-对于Windows程序来说-还告诉使用者一个应用程序能够执行哪些操作。

菜单可能是Windows程序提供的一致使用者接口中最重要的部分,而在您的程序中增加菜单,是Windows程序设计中相对简单的部分。您在Developer Studio中定义菜单。每个可选的菜单项被赋予唯一的ID。您在窗口类别结构中指定菜单名称。当使用者选择一个菜单项时,Windows给您的程序发送包含该ID的WM_COMMAND消息。

讨论完菜单后,我还将讨论键盘快捷键,它们是一些键的组合,主要用于启动菜单功能。

菜单概念

窗口的菜单列紧接在标题列的下方显示,这个菜单列有时被称为「主菜单」或「顶层菜单」。列在顶层菜单的项目通常是下拉式菜单,也叫做「弹出式菜单」或「子菜单」。您也可以定义多重嵌套的弹出式菜单,也就是说,在弹出式菜单上的项目可以存取另一个弹出式菜单。有时弹出式菜单上的项目呼叫对话框以获得更多的信息。在标题列的最左端,很多父窗口都显示程序的小图标,这个图标可以启动系统菜单。它实际上是另一个弹出式菜单。

弹出式菜单的各项可以是「被选中的」,这意味着Windows在菜单文字的左端显示一个小的选中标记,选中标记让使用者知道从菜单中选中了哪些选项。这些选项之间可以是互斥的,也可以不互斥。顶层菜单项不能被选中。

顶层菜单或弹出式菜单项可以被「启用」、「禁用」或「无效化」。「启动」和「不启动」有时候被当作「启用」和「禁用」的同义词。被启用或禁用的菜单项在使用者看来是一样的,但是无效化的菜单项是使用灰色文字来显示的。

从使用者的角度来看,启用、禁用和无效化的菜单项都是可以「选择的」(被选择的菜单项目会被加高亮度显示),也就是说,使用者可以使用鼠标选择被禁用的菜单项,将反相显示光标列移动到禁用的菜单项上,或者使用菜单项的关键词母来选择该菜单项。然而,从程序写作者的角度来看,启用、禁用和无效化菜单项的功能是不同的。Windows只为启用的菜单项向程序发送WM_COMMAND消息。要让选项变得无效,可以把那些菜单项禁用和无效化。如果您想让使用者知道选择是无效的,那么您可以让一个菜单项无效化。

菜单结构

当您建立或改变程序中的菜单时,把顶层菜单和每一个弹出式菜单想象成各自独立的菜单是有用的。顶层菜单有一个菜单句柄,在顶层菜单中的每一个弹出式菜单也有它自己的菜单句柄。系统菜单(也是一个弹出式菜单)也有菜单句柄。

菜单中的每一项都有三个特性。第一个特性是菜单中显示什么,它可以是字符串或位图。第二个特性是WM_COMMAND消息中Windows发送给程序的菜单ID,或者是在使用者选择菜单项时Windows显示的弹出式菜单的句柄。第三个特性是菜单项的属性,包括是否被禁用、无效化或被选中。

定义菜单

要使用Developer Studio来给程序资源描述文件添加菜单,可以从Insert菜单中选择 Resource并选择Menu(或者您可能已经知道了)。然后,您可以用交谈式的方式定义菜单。菜单中每一项都有一个相关的 Menu Item Properties对话框,指出该项目的字符串。如果选中了Pop-up复选框,该项目就会呼叫一个弹出式菜单,并且没有ID与此项目相联系。如果没有选中 Pop-up复选框,该项目被选中时就会产生带有特定ID的WM_COMMAND消息。这两类菜单项分别出现在资源描述档的POPUP和MENUITEM叙述中。

当您为菜单中的项目键入文字时,可以键入一个「&」符号,指出后面一个字符在Windows显示菜单时要加底线。这种底线字符是在您使用Alt键选择菜单项时Windows要寻找的比对字符。如果在文字中不包括「&」符号,就不显示任何底线,Windows会将菜单项文字的第一个字母用于Alt键查找。

如果在Menu Items Properties对话框中选中Grayed选项,则菜单项是不能启动的,它的文字是灰色的,该项不产生WM_COMMAND消息。如果选中 Inactive选项,则菜单项也是不能启动的,也不产生WM_COMMAND消息,但是它的文字显示正常。 Checked选项在菜单项边上放置一个选中标记。Separator选项在弹出式菜单上产生一个分栏的横线。

在弹出式菜单的项目上,可以在字符串中使用制表符\t。紧接着\t的文字被放置在距离弹出式菜单的第一列右边新的一列上。在本章后面,会看到在使用键盘快捷键时它起的作用。字符串中的\a使跟着它的文字向右对齐。

您指定的ID值是Windows发送给窗口消息处理程序中菜单消息中的数值。在菜单中ID值应该是唯一的。按照惯例,我使用以IDM(「ID for a Menu」)开头的标识符。

在程序中引用菜单

大多数Windows应用程序在资源描述文件中只有一个菜单。您可以给菜单起一个与程序名称相同的文字的名称。程序写作者经常将程序名用于菜单名称,以便相同的字符串可以用于窗口类别、程序的图标名称和菜单名称。然后,程序在窗口的定义中为菜单引用该名称:

wndclass.lpszMenuName = szAppName ;
        

虽然存取菜单资源的最常用方法是在窗口类别中指定菜单,您也可以使用其它方法。Windows应用程序可以使用LoadMenu函数将菜单资源加载内存中,如同LoadIcon和LoadCursor函数一样。LoadMenu传回一个菜单句柄。如果您在资源描述档中为菜单使用了名称,叙述如下:

hMenu = LoadMenu (hInstance, TEXT ("MyMenu")) ;
        

如果使用了数值,那么LoadMenu呼叫采用如下的形式:

hMenu = LoadMenu (hInstance, MAKEINTRESOURCE (ID_MENU)) ;
        

然后,您可以将这个菜单句柄作为CreateWindow的第九个参数:

hwnd = CreateWindow (      TEXT ("MyClass"), TEXT ("Window Caption"),
        
   WS_OVERLAPPEDWINDOW,
        
   CW_USEDEFAULT, CW_USEDEFAULT,
        
   CW_USEDEFAULT, CW_USEDEFAULT,
        
   NULL, hMenu, hInstance, NULL) ;
        

在这种情况下,CreateWindow呼叫中指定的菜单可以覆盖窗口类别中指定的任何菜单。如果CreateWindow的第九个参数是NULL,那么您可以把窗口类别中的菜单看作是这种窗口类别的窗口内定使用的菜单。这样,您可以为依据同一窗口类别建立的几个窗口使用不同的菜单。

您也可以在窗口类别中指定NULL菜单,并且在CreateWindow呼叫中也指定NULL菜单,然后在窗口被建立后再给窗口指定一个菜单:

SetMenu (hwnd, hMenu) ;
        

这种形式使您可以动态地修改窗口的菜单。在本期后面的NOPOPUPS程序中我们将会看到这方面的例子。

当窗口被清除时,与窗口相关的所有菜单都将被清除。与窗口不相关的菜单在程序结束前通过呼叫DestroyMenu主动清除。

菜单和消息

当使用者选择一个菜单项时,Windows通常向窗口消息处理程序发送几个不同的消息。在大多数情况下, 您的程序可以忽略大部分消息,只需把它们传递给DefWindowProc即可。WM_INITMENU就是这一类的消息,它具有下列参数:

wParam: 主菜单句柄

lParam: 0

wParam值是您的主菜单句柄,即使使用者选择的是系统菜单中的项目。Windows程序通常忽略WM_INITMENU消息。尽管在选中该项之前的消息已经给程序提供了修改菜单的机会,但是我们觉得此刻改变顶层菜单是会扰乱使用者的。

程序也会接收到WM_MENUSELECT消息。随着使用者在菜单项中移动光标或者鼠标,程序会收到许多WM_MENUSELECT消息。这对实作那些包含对菜单项的文字描述的状态列是很有帮助的。WM_MENUSELECT的参数如下所示:

LOWORD (wParam):被选中项目:菜单ID或者弹出式菜单句柄

HIWORD (wParam):选择旗标

lParam: 包含被选中项目的菜单句柄

WM_MENUSELECT是一个菜单追踪消息,wParam的值告诉您目前选择的是菜单中的哪一项(加高亮度显示的那个),wParam的高字组中的「选择旗标」可以是下列这些旗标的组合:MF_GRAYED、MF_DISABLED、MF_CHECKED、MF_BITMAP、MF_POPUP、MF_HELP、MF_SYSMENU和MF_MOUSESELECT。如果您需要根据对菜单项的选择来改变窗口显示区域的内容,那么您可以使用WM_MENUSELECT消息。许多程序把该消息发送给DefWindowProc。

当Windows准备显示一个弹出式菜单时,它给窗口消息处理程序发送一个WM_INITMENUPOPUP消息,参数如下:

wParam: 弹出式菜单句柄

LOWORD (lParam):弹出式菜单索引

HIWORD (lParam): 系统菜单为1,其它为0

如果您需要在显示弹出式菜单之前启用或者禁用菜单项,那么这个消息就很重要。例如,假定程序使用弹出式菜单上的 Paste命令从剪贴簿复制文字,当您收到弹出式菜单中的WM_INITMENUPOPUP消息时,应确定剪贴簿内是否有文字存在。如果没有,那么应该使 Paste菜单项无效化。我们将在本章后面修改的POPPAD程序中看到这样的例子。

最重要的菜单消息是WM_COMMAND,它表示使用者已经从菜单中选中了一个被启用的菜单项。第八章中的WM_COMMAND消息也可以由子窗口控件产生。如果您碰巧为菜单和子窗口控件使用同一ID码,那么您可以通过lParam的值来区别它们,菜单项的lParam其值为0,请参见表10-1。

表10-1

 

菜单

控件

LOWORD (wParam): 菜单ID 控件ID
HIWORD (wParam): 0 通知码
lParam: 0 子窗口句柄

WM_SYSCOMMAND消息类似于WM_COMMAND消息,只是WM_SYSCOMMAND表示使用者从系统菜单中选择一个启用的菜单项:

wParam: 菜单ID

lParam: 0

然而,如果WM_SYSCOMMAND消息是由按鼠标按键产生的,LOWORD(lParam)和HIWORD(lParam)将包含鼠标光标位置的x和y屏幕坐标。

对于WM_SYSCOMMAND,菜单ID指示系统菜单中的哪一项被选中。对于预先定义的系统菜单项,较低的那四个位应该和0xFFF0进行AND运算来屏蔽掉,结果值应该为下列之一:SC_SIZE、SC_MOVE、SC_MINIMIZE、SC_MAXIMIZE、SC_NEXTWINDOW、SC_PREVWINDOW、SC_CLOSE、SC_VSCROLL、SC_HSCROLL、SC_ARRANGE、SC_RESTORE和SC_TASKLIST。此外,wParam可以是SC_MOUSEMENU或SC_KEYMENU。

如果您在系统菜单中添加菜单项,那么wParam的低字组将是您定义的菜单ID。为了避免与预先定义的菜单ID相冲突,应用程序应该使用小于0xF000的值,这对于将一般的WM_SYSCOMMAND消息发送给DefWindowProc是很重要的。如果您不这样做,那么您实际上就是禁用了正常的系统菜单命令。

我们将讨论的最后一个消息是WM_MENUCHAR。实际上,它根本不是菜单消息。在下列两种情况之一发生时,Windows会把这个消息发送到窗口消息处理程序:如果使用者按下Alt和一个与菜单项不匹配的字符时,或者在显示弹出式菜单而使用者按下一个与弹出式菜单里的项目不匹配的字符键时。随WM_MENUCHAR消息一起发送的参数如下所示:

LOWORD (wParam): 字符代码(ASCII或Unicode)

HIWORD (wParam): 选择码

lParam: 菜单句柄

选择码是:

Windows程序通常把该消息传递给DefWindowProc,它一般给Windows传回0,这会使Windows发出哔声。在后面的一期中会看到WM_MENUCHAR消息的使用。

范例程序

让我们来看一个简单的例子。程序10-4所示的MENUDEMO程序,在主菜单中有五个选择项-File、Edit、Background、Timer和Help,每一项都与一个弹出式菜单相连。MENUDEMO只完成了最简单、最通用的菜单处理操作,包括拦截WM_COMMAND消息和检查wParam的低字组。

程序10-4 MENUDEMO

        
MENUDEMO.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	ID_TIMER	equ	1
	IDM_FILE_NEW    equ     40001
	IDM_FILE_OPEN   equ     40002
	IDM_FILE_SAVE   equ     40003
	IDM_FILE_SAVE_AS equ    40004
	IDM_APP_EXIT    equ     40005
	IDM_EDIT_UNDO   equ     40006
	IDM_EDIT_CUT    equ     40007
	IDM_EDIT_COPY   equ     40008
	IDM_EDIT_PASTE  equ     40009
	IDM_EDIT_CLEAR  equ     40010
	IDM_BKGND_WHITE equ     40011
	IDM_BKGND_LTGRAY equ    40012
	IDM_BKGND_GRAY  equ     40013
	IDM_BKGND_DKGRAY equ    40014
	IDM_BKGND_BLACK equ     40015
	IDM_TIMER_START equ     40016
	IDM_TIMER_STOP  equ     40017
	IDM_APP_HELP    equ     40018
	IDM_APP_ABOUT   equ     40019
	ID_MENUITEM40020 equ    40020	
	
.DATA
	szAppName	TCHAR	"MenuDemo",0
	idColor 	DD	 WHITE_BRUSH,  LTGRAY_BRUSH, GRAY_BRUSH,DKGRAY_BRUSH, BLACK_BRUSH 
	iSelection	DD	IDM_BKGND_WHITE
.DATA?
.CODE
START:

	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0

WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND
	
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX

	lea eax,szAppName	
	mov wndclass.lpszMenuName,eax
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),szAppName, MB_ICONERROR
           ret
	.endif
        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			CTXT("Menu Demonstration"), 
			WS_OVERLAPPEDWINDOW,					;window style
			CW_USEDEFAULT,						;initial x position
			CW_USEDEFAULT,						;initial y position
			CW_USEDEFAULT, 						;initial x size
			CW_USEDEFAULT,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,							;program instance handle
			NULL										;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	hMenu:HMENU

	.if 	uMsg==WM_COMMAND
		invoke	GetMenu,hwnd
		mov	hMenu,eax
		
		mov	eax,wParam
		.if	(eax==IDM_FILE_NEW)  || \
			(eax==IDM_FILE_OPEN) || \
			(eax==IDM_FILE_SAVE) || \
			(eax==IDM_FILE_SAVE_AS)
			invoke	MessageBeep,0
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_EXIT)
			invoke	SendMessage,hwnd,WM_CLOSE,0,0
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_EDIT_UNDO) || \
			(eax==IDM_EDIT_CUT) ||  \                
			(eax==IDM_EDIT_COPY) || \
			(eax==IDM_EDIT_PASTE) ||\
			(eax==IDM_EDIT_CLEAR) 
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_BKGND_WHITE) || \		        
			(eax==IDM_BKGND_LTGRAY) ||\			
			(eax==IDM_BKGND_GRAY)  || \
			(eax==IDM_BKGND_DKGRAY) ||\ 
			(eax==IDM_BKGND_BLACK)
			invoke	CheckMenuItem,hMenu,iSelection,MF_UNCHECKED
			mov	eax,wParam
			and	eax,0FFFFh
			mov	iSelection,eax
			invoke	CheckMenuItem,hMenu,iSelection,MF_CHECKED
			mov	eax,wParam
			and	eax,0FFFFh			
			sub	eax,IDM_BKGND_WHITE
			mov	ebx,idColor[eax*4]
			invoke	GetStockObject,ebx
			invoke	SetClassLong,hwnd,GCL_HBRBACKGROUND,eax
			invoke	InvalidateRect,hwnd,NULL,TRUE
		        xor	eax,eax
		        ret
		.elseif	(eax== IDM_TIMER_START)        
			invoke	SetTimer,hwnd,ID_TIMER,1000,NULL
			.if	(eax!=0)
                   		invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_GRAYED
		                invoke  EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_ENABLED
			.endif	
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_TIMER_STOP)        			
			invoke	KillTimer,hwnd,ID_TIMER
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_ENABLED
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_GRAYED
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_HELP)        			
			invoke	MessageBox,hwnd,CTXT("Help not yet implemented!"),addr szAppName, MB_ICONEXCLAMATION or MB_OK
		        xor	eax,eax
		        ret			
		.elseif	(eax==IDM_APP_ABOUT)
                        invoke	MessageBox,hwnd,CTXT("Menu Demonstration Program"),CTXT("(c) Charles Petzold, 1998"),MB_ICONINFORMATION or MB_OK
		        xor	eax,eax
		        ret	
		.endif  
	.elseif uMsg == WM_TIMER
		invoke	MessageBeep,0
		xor	eax,eax
		ret			
	.elseif uMsg == WM_DESTROY
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START

        
MENUDEMO.RC
        
#define IDM_FILE_NEW                    40001
#define IDM_FILE_OPEN                   40002
#define IDM_FILE_SAVE                   40003
#define IDM_FILE_SAVE_AS                40004
#define IDM_APP_EXIT                    40005
#define IDM_EDIT_UNDO                   40006
#define IDM_EDIT_CUT                    40007
#define IDM_EDIT_COPY                   40008
#define IDM_EDIT_PASTE                  40009
#define IDM_EDIT_CLEAR                  40010
#define IDM_BKGND_WHITE                 40011
#define IDM_BKGND_LTGRAY                40012
#define IDM_BKGND_GRAY                  40013
#define IDM_BKGND_DKGRAY                40014
#define IDM_BKGND_BLACK                 40015
#define IDM_TIMER_START                 40016
#define IDM_TIMER_STOP                  40017
#define IDM_APP_HELP                    40018
#define IDM_APP_ABOUT                   40019
#define ID_MENUITEM40020                40020

MENUDEMO MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New",                        ID_MENUITEM40020
        MENUITEM "&Open",                       IDM_FILE_OPEN
        MENUITEM "&Save",                       IDM_FILE_SAVE
        MENUITEM "Save &As...",                 IDM_FILE_SAVE_AS
        MENUITEM SEPARATOR
        MENUITEM "E&xit",                       IDM_APP_EXIT
    END
    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo",                       IDM_EDIT_UNDO
        MENUITEM SEPARATOR
        MENUITEM "C&ut",                        IDM_EDIT_CUT
        MENUITEM "&Copy",                       IDM_EDIT_COPY
        MENUITEM "&Paste",                      IDM_EDIT_PASTE
        MENUITEM "De&lete",                     IDM_EDIT_CLEAR
    END
    POPUP "&Background"
    BEGIN
        MENUITEM "&White",                      IDM_BKGND_WHITE, CHECKED
        MENUITEM "&Light Gray",                 IDM_BKGND_LTGRAY
        MENUITEM "&Gray",                       IDM_BKGND_GRAY
        MENUITEM "&Dark Gray",                  IDM_BKGND_DKGRAY
        MENUITEM "&Black",                      IDM_BKGND_BLACK
    END
    POPUP "&Timer"
    BEGIN
        MENUITEM "&Start",                      IDM_TIMER_START
        MENUITEM "S&top",                       IDM_TIMER_STOP, GRAYED
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&Help...",                    IDM_APP_HELP
        MENUITEM "&About MenuDemo...",          IDM_APP_ABOUT
    END
END

wpe4.jpg (20448 字节)

MENUDEMO.RC资源描述档给了您定义菜单的提示。菜单的名称为「MenuDemo」。大多数项目有底线字母,这就是说您必须在字母前键入『&』。MENUITEM SEPARATOR叙述是在「Menu Item Properties」对话框中选中「Separator」框产生的。注意菜单中有一个项目具有「 Checked」选项,另一个具有「Grayed」选项。还有,「 Background」弹出式菜单中的五个项目应该按顺序输入,确保标识符是以数值的顺序,本程序需要这样。所有菜单项的标识符定义在RESOURCE.H中。

当收到弹出式菜单「File」和「Edit」各项有关的WM_COMMAND消息时,MENUDEMO程序只使系统发出哔声。「 Background」弹出式菜单列出MENUDEMO用来给背景着色的五种现有画刷。在MENUDEMO.RC资源描述档中,「 White」菜单项(菜单ID为IDM_BKGND_WHITE)被标以「 CHECKED」,它在菜单项旁边设定选中标记。在MENUDEMO.C中,iSelection的值被初始化为IDM_BKGND_WHITE。

Background」弹出式菜单上的五种画刷相互排斥。当MENUDEMO.C收到一个WM_COMMAND消息,而该消息中的wParam是「 Background」弹出式菜单上的五项之一时,它必须从先前选中的背景颜色中除掉选中标记,并把标记加到新的背景颜色上。为此,首先要得到菜单句柄:

hMenu = GetMenu (hwnd) ;
        

CheckMenuItem函数用来取消目前被选中的项目:

CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ;
        

iSelection的值被设定为wParam的值,新的背景颜色被选中:

iSelection = wParam ;
        
CheckMenuItem (hMenu, iSelection, MF_CHECKED) ;
        

窗口类别中的背景颜色于是被替换为新的背景颜色,窗口显示区域变为无效状态,Windows使用新的背景颜色清除窗口。

Timer弹出式菜单列出了两个选项-「Start」和「Stop」。开始时,「Stop」选项变为灰色的(就像在资源描述档中的菜单定义一样)。当您选择「Start」选项时,MENUDEMO试图启动一个定时器,如果成功,则无效化「Start」选项,并启用「Stop」选项:

EnableMenuItem (hMenu, IDM_TIMER_START, MF_GRAYED) ;
        
EnableMenuItem (hMenu, IDM_TIMER_STOP,  MF_ENABLED) ;
        

当收到一条WM_COMMAND消息,并且wParam等于IDM_TIMER_STOP时,MENUDEMO程序会停止计数,启用「 Start」项,然后无效化「Stop」选项:

EnableMenuItem (hMenu, IDM_TIMER_START, MF_ENABLED) ;
        
EnableMenuItem (hMenu, IDM_TIMER_STOP,  MF_GRAYED) ;
        

请注意,在定时器执行时,MENUDEMO程序不可能收到wParam等于IDM_TIMER_START的WM_COMMAND消息。同样地,在定时器关闭时,MENUDEMO程序也不可能收到wParam等于IDM_TIMER_STOP的WM_COMMAND消息。

当MENUDEMO收到一个WM_COMMAND消息,而该消息的参数wParam等于IDM_APP_ABOUT或IDM_APP_HELP时,MENUDEMO程序显示一个消息框(在下一期中,我们将把消息框变为对话框)。

当MENUDEMO程序收到一个WM_COMMAND消息,其参数wParam等于IDM_APP_EXIT时,它给自己发送一个WM_CLOSE消息。这个消息与DefWindowProc收到WM_SYSCOMMAND消息且wParam等于SC_CLOSE时发送给窗口消息处理程序的消息相同。我们将在本期后面介绍 POPPAD2时再仔细研究这个问题。

菜单设计规范

在MENUDEMO中的「File」和「Edit」弹出式菜单的格式与其它Windows程序中的格式非常类似。Windows的目的之一是为使用者提供一种易懂的接口,而不要求使用者为每个程序重新学习基本操作方式。如果「 File」和「Edit」菜单在每个Windows程序中看起来都一样,并且都使用同样的字母和Alt键来进行选择,那么当然有助于减轻使用者的学习负担。

除了「File」和「Edit」弹出式菜单外,大多数Windows程序的菜单都是不同的。当设计一个菜单时,您应该看一看现有的Windows程序以尽量保持一致。当然,如果您认为别的程序是不对的,而您知道正确的方法,那么没有人能够阻止您。同时记住,修改一个菜单,通常只需要修改资源描述档而不必修改您的程序代码。即使以后要改变菜单项的位置,也不会有多大的问题。

虽然您的程序菜单在顶层可以有MENUITEM叙述,但这是不合规范的,因为这样会很容易导致错误的选择。如果您要这样做,那么请在字符串后面加一个惊叹号,表示菜单项不会启动弹出式菜单。

较难的一种菜单定义方法

在程序的资源描述文件中定义菜单,通常是在您的窗口中添加菜单的最简单方法,但不是唯一的方法。如果您没有使用资源描述档,那么可以使用CreateMenu和AppendMenu两个函数在程序中建立菜单。在您定义完菜单后,您可以将菜单句柄发送给CreateWindow,或者使用SetMenu来设定窗口的菜单。

以下是具体的做法。CreateMenu简单地把一个句柄传回给新菜单:

hMenu = CreateMenu () ;
        

菜单一开始为空。AppendMenu将菜单项插入菜单中。您必须为顶层菜单项和每一个弹出式菜单提供不同的菜单句柄。弹出式菜单是单独构成的,然后将弹出式菜单句柄插入顶层菜单。程序10-5中所示的程序代码就是用这种方法建立菜单的,实际上,这个菜单与MENUDEMO程序中的菜单相同。为了简化说明,代码使用ASCII字符串。

程序10-5  不使用资源描述文件建立与MENUDEMO程序相同菜单的C程序代码
        
hMenu = CreateMenu () ;
        
hMenuPopup = CreateMenu () ;
        
AppendMenu         (hMenuPopup, MF_STRING, IDM_FILE_NEW, "&New");
        
AppendMenu         (hMenuPopup, MF_STRING, IDM_FILE_OPEN, "&Open...");
        
AppendMenu         (hMenuPopup, MF_STRING, IDM_FILE_SAVE, "&Save");
        
AppendMenu         (hMenuPopup,  MF_STRING, IDM_FILE_SAVE_AS, "Save &As...");
        
AppendMenu         (hMenuPopup, MF_SEPARATOR, 0, NULL) ;
        
AppendMenu         (hMenuPopup, MF_STRING, IDM_APP_EXIT, "E&xit") ;
        
AppendMenu         (hMenu, MF_POPUP, hMenuPopup, "&File") ;
        

hMenuPopup = CreateMenu () ;
        
AppendMenu         (hMenuPopup, MF_STRING, IDM_EDIT_UNDO,"&Undo") ;
        
AppendMenu         (hMenuPopup, MF_SEPARATOR, 0, NULL) ;
        
AppendMenu         (hMenuPopup, MF_STRING,IDM_EDIT_CUT, "Cu&t") ;
        
AppendMenu         (hMenuPopup, MF_STRING,IDM_EDIT_COPY,"&Copy") ;
        
AppendMenu         (hMenuPopup,  MF_STRING,IDM_EDIT_PASTE,"&Paste") ;
        
AppendMenu         (hMenuPopup, MF_STRING,IDM_EDIT_CLEAR,"De&lete") ;
        
AppendMenu         (hMenu,       MF_POPUP,     hMenuPopup, "&Edit") ;
        

hMenuPopup = CreateMenu () ;
        
AppendMenu (hMenuPopup, MF_STRING| MF_CHECKED,        IDM_BKGND_WHITE, "&White");
        
AppendMenu (hMenuPopup, MF_STRING,          IDM_BKGND_LTGRAY, "&Light Gray");
        
AppendMenu (hMenuPopup, MF_STRING,                    IDM_BKGND_GRAY,   "&Gray") ;
        
AppendMenu (hMenuPopup, MF_STRING,          IDM_BKGND_DKGRAY, "&Dark Gray");
        
AppendMenu (hMenuPopup,    MF_STRING,          IDM_BKGND_BLACK,  "&Black") ;
        

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Background") ;
        
hMenuPopup = CreateMenu () ;
        
AppendMenu (hMenuPopup, MF_STRING,          IDM_TIMER_START, "&Start") ;
        
AppendMenu (hMenuPopup, MF_STRING | MF_GRAYED, IDM_TIMER_STOP, "S&top") ;
        

AppendMenu (hMenu,       MF_POPUP, hMenuPopup, "&Timer") ;
        

hMenuPopup =      CreateMenu () ;
        

AppendMenu (hMenuPopup, MF_STRING,    IDM_HELP_HELP,        "&Help") ;
        
AppendMenu (hMenuPopup, MF_STRING,    IDM_APP_ABOUT,        "&About MenuDemo...") ;
        

AppendMenu (hMenu, MF_POPUP, hMenuPopup, "&Help") ;
        

我认为您会同意底下这个观点:使用资源描述档菜单模板来制作菜单,会更容易而且更清楚。我并不鼓励您使用这里的方法定义菜单,而只是提供了一种实作菜单的方法。当然,您可以使用包含所有菜单项字符串、ID和旗标等的结构数组来压缩程序代码大小。不过,如果您这么做了,那么您还可以利用Windows定义菜单的第三种方法。LoadMenuIndirect函数接受一个指向MENUITEMTEMPLATE型态的结构指针,并传回菜单的句柄,该函数在载入资源描述档中的常规菜单模板后,在Windows中构造菜单,读者不妨自己尝试一下。

浮动弹出式菜单

您还可以在没有顶层菜单列的情况下使用菜单,也就是说,您可以使弹出式菜单出现在屏幕顶层的任何位置。一种方法是使用鼠标右键来启动弹出式菜单。程序10-6所示的POPMENU说明了这种方法。

程序10-6 POPMENU

        
POPMENU.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	ID_TIMER	equ	1
	IDM_FILE_NEW    equ     40001
	IDM_FILE_OPEN   equ     40002
	IDM_FILE_SAVE   equ     40003
	IDM_FILE_SAVE_AS equ    40004
	IDM_APP_EXIT    equ     40005
	IDM_EDIT_UNDO   equ     40006
	IDM_EDIT_CUT    equ     40007
	IDM_EDIT_COPY   equ     40008
	IDM_EDIT_PASTE  equ     40009
	IDM_EDIT_CLEAR  equ     40010
	IDM_BKGND_WHITE equ     40011
	IDM_BKGND_LTGRAY equ    40012
	IDM_BKGND_GRAY  equ     40013
	IDM_BKGND_DKGRAY equ    40014
	IDM_BKGND_BLACK equ     40015
	IDM_TIMER_START equ     40016
	IDM_TIMER_STOP  equ     40017
	IDM_APP_HELP    equ     40018
	IDM_APP_ABOUT   equ     40019
	ID_MENUITEM40020 equ    40020	
	
.DATA
	szAppName	TCHAR	"PopMenu",0
	idColor 	DD	WHITE_BRUSH,  LTGRAY_BRUSH, GRAY_BRUSH,DKGRAY_BRUSH, BLACK_BRUSH 
	iSelection	DD	IDM_BKGND_WHITE
.DATA?
	hInst		HINSTANCE ?
	hMenu		HMENU	?
.CODE
START:

	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0

WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND
	
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX

	mov wndclass.lpszMenuName,NULL
	lea eax,szAppName	
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),szAppName, MB_ICONERROR
           ret
	.endif
        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			CTXT("Popup Menu Demonstration"), 
			WS_OVERLAPPEDWINDOW,					;window style
			CW_USEDEFAULT,						;initial x position
			CW_USEDEFAULT,						;initial y position
			CW_USEDEFAULT, 						;initial x size
			CW_USEDEFAULT,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,							;program instance handle
			NULL										;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	point:POINT
	.if	uMsg == WM_CREATE
		invoke	LoadMenu,hInst,addr szAppName
		mov	hMenu,eax
		invoke	GetSubMenu,hMenu,0
		mov	hMenu,eax
		xor	eax,eax
		ret			
	.elseif uMsg==WM_RBUTTONUP
		mov	eax,lParam
		and	eax,0FFFFh
		mov	point.x,eax
		mov	eax,lParam
		shr	eax,16
		mov	point.y,eax
		invoke	ClientToScreen,hwnd,addr point
		invoke	TrackPopupMenu,hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL

		xor	eax,eax
		ret	
	.elseif uMsg==WM_COMMAND
		mov	eax,wParam
		.if	(eax==IDM_FILE_NEW)  || \
			(eax==IDM_FILE_OPEN) || \
			(eax==IDM_FILE_SAVE) || \
			(eax==IDM_FILE_SAVE_AS)
			invoke	MessageBeep,0
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_EXIT)
			invoke	SendMessage,hwnd,WM_CLOSE,0,0
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_EDIT_UNDO) || \
			(eax==IDM_EDIT_CUT) ||  \                
			(eax==IDM_EDIT_COPY) || \
			(eax==IDM_EDIT_PASTE) ||\
			(eax==IDM_EDIT_CLEAR) 
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_BKGND_WHITE) || \		        
			(eax==IDM_BKGND_LTGRAY) ||\			
			(eax==IDM_BKGND_GRAY)  || \
			(eax==IDM_BKGND_DKGRAY) ||\ 
			(eax==IDM_BKGND_BLACK)
			invoke	CheckMenuItem,hMenu,iSelection,MF_UNCHECKED
			mov	eax,wParam
			and	eax,0FFFFh
			mov	iSelection,eax
			invoke	CheckMenuItem,hMenu,iSelection,MF_CHECKED
			mov	eax,wParam
			and	eax,0FFFFh			
			sub	eax,IDM_BKGND_WHITE
			mov	ebx,idColor[eax*4]
			invoke	GetStockObject,ebx
			invoke	SetClassLong,hwnd,GCL_HBRBACKGROUND,eax
			invoke	InvalidateRect,hwnd,NULL,TRUE
		        xor	eax,eax
		        ret
		.elseif	(eax== IDM_TIMER_START)        
			invoke	SetTimer,hwnd,ID_TIMER,1000,NULL
			.if	(eax!=0)
                   		invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_GRAYED
		                invoke  EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_ENABLED
			.endif	
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_TIMER_STOP)        			
			invoke	KillTimer,hwnd,ID_TIMER
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_ENABLED
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_GRAYED
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_HELP)        			
			invoke	MessageBox,hwnd,CTXT("Help not yet implemented!"),addr szAppName, MB_ICONEXCLAMATION or MB_OK
		        xor	eax,eax
		        ret			
		.elseif	(eax==IDM_APP_ABOUT)
                        invoke	MessageBox,hwnd,CTXT("Menu Demonstration Program"),CTXT("(c) Charles Petzold, 1998"),MB_ICONINFORMATION or MB_OK
		        xor	eax,eax
		        ret	
		.endif  
	.elseif uMsg == WM_TIMER
		invoke	MessageBeep,0
		xor	eax,eax
		ret			
	.elseif uMsg == WM_DESTROY
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START        

POPMENU.RC

#define IDM_FILE_NEW                    40001
#define IDM_FILE_OPEN                   40002
#define IDM_FILE_SAVE                   40003
#define IDM_FILE_SAVE_AS                40004
#define IDM_APP_EXIT                    40005
#define IDM_EDIT_UNDO                   40006
#define IDM_EDIT_CUT                    40007
#define IDM_EDIT_COPY                   40008
#define IDM_EDIT_PASTE                  40009
#define IDM_EDIT_CLEAR                  40010
#define IDM_BKGND_WHITE                 40011
#define IDM_BKGND_LTGRAY                40012
#define IDM_BKGND_GRAY                  40013
#define IDM_BKGND_DKGRAY                40014
#define IDM_BKGND_BLACK                 40015
#define IDM_TIMER_START                 40016
#define IDM_TIMER_STOP                  40017
#define IDM_APP_HELP                    40018
#define IDM_APP_ABOUT                   40019
#define ID_MENUITEM40020                40020

POPMENU MENU DISCARDABLE 
BEGIN
    POPUP "MyMenu"
    BEGIN
        POPUP "&File"
        BEGIN
            MENUITEM "&New",                        IDM_FILE_NEW
            MENUITEM "&Open",                       IDM_FILE_OPEN
            MENUITEM "&Save",                       IDM_FILE_SAVE
            MENUITEM "Save &As",                    IDM_FILE_SAVE_AS
            MENUITEM SEPARATOR
            MENUITEM "E&xit",                       IDM_APP_EXIT
        END
        POPUP "&Edit"
        BEGIN
            MENUITEM "&Undo",                       IDM_EDIT_UNDO
            MENUITEM SEPARATOR
            MENUITEM "Cu&t",                        IDM_EDIT_CUT
            MENUITEM "&Copy",                       IDM_EDIT_COPY
            MENUITEM "&Paste",                      IDM_EDIT_PASTE
            MENUITEM "De&lete",                     IDM_EDIT_CLEAR
        END
        POPUP "&Background"
        BEGIN
            MENUITEM "&White",                      IDM_BKGND_WHITE, CHECKED
            MENUITEM "&Light Gray",                 IDM_BKGND_LTGRAY
            MENUITEM "&Gray",                       IDM_BKGND_GRAY
            MENUITEM "&Dark Gray",                  IDM_BKGND_DKGRAY
            MENUITEM "&Black",                      IDM_BKGND_BLACK
        END
        POPUP "&Help"
        BEGIN
            MENUITEM "&Help...",                    IDM_APP_HELP
            MENUITEM "&About PopMenu...",           IDM_APP_ABOUT
        END
    END
END

       

资源描述档POPMENU.RC定义的菜单与MENUDEMO.RC中的菜单非常相似。不同的是,在顶层菜单中只包含一项-一个弹出式菜单「MyMenu」,它呼叫「File」、「Edit」、「Background」和「Help」选项。这四个选项垂直一行地出现在弹出式菜单上,而不是水平一列地出现在主菜单上。

wpe2.jpg (23266 字节)

运行结果,点鼠标右键出现菜单

在WndProc中的WM_CREATE处理期间,POPMENU取得此弹出式菜单的句柄,就是带有文字「MyMenu」的那个弹出式菜单:

hMenu = LoadMenu (hInst, szAppName) ;
        
hMenu = GetSubMenu (hMenu, 0) ;
        

在WM_RBUTTONUP消息处理期间,POPMENU提供了鼠标指针的位置,将此位置转换为屏幕坐标,再将坐标值传递给TrackPopupMenu:

point.x = LOWORD (lParam) ;
        
point.y = HIWORD (lParam) ;
        
ClientToScreen (hwnd, &point) ;
        
TrackPopupMenu (hMenu, TPM_RIGHTBUTTON, point.x, point.y,
        
                                                 0, hwnd, NULL) ;
        

然后,Windows显示出具有「File」、「Edit」、「Background」和「Help」项的弹出式菜单。选择其中任何一项都可以使嵌套的弹出式菜单显示在右边,菜单函数与一般的菜单一样。

使用系统菜单

使用WS_SYSMENU样式建立的父窗口,在其标题列的左侧有一个系统菜单按钮。如果您愿意,可以修改这个菜单。在Windows程序设计的早期,程序写作者一般把「About」菜单项放入系统菜单。虽然这种方法不常见,但是修改系统菜单往往是一种在短程序中添加菜单的快速偷懒方法。这里唯一的限制是:在系统菜单中增加的命令其ID值必须小于0xF000;否则它们将会与Windows系统菜单命令所使用的ID值相冲突。还要记住,当您为这些新菜单项在窗口消息处理程序中处理WM_SYSCOMMAND消息时,您必须把其它的WM_SYSCOMMAND消息发送给DefWindowProc。如果您不这样做,那么实际上是禁用了系统菜单上的所有正常选项。

程序10-7中所示的POORMENU(「设计不当的个人菜单」)在系统菜单中加入了一个分隔条和三个命令,最后一个命令将删除这些附加的菜单项。

程序10-7  POORMENU
        
POORMENU.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	ID_TIMER	equ	1
	IDM_FILE_NEW    equ     40001
	IDM_FILE_OPEN   equ     40002
	IDM_FILE_SAVE   equ     40003
	IDM_FILE_SAVE_AS equ    40004
	IDM_APP_EXIT    equ     40005
	IDM_EDIT_UNDO   equ     40006
	IDM_EDIT_CUT    equ     40007
	IDM_EDIT_COPY   equ     40008
	IDM_EDIT_PASTE  equ     40009
	IDM_EDIT_CLEAR  equ     40010
	IDM_BKGND_WHITE equ     40011
	IDM_BKGND_LTGRAY equ    40012
	IDM_BKGND_GRAY  equ     40013
	IDM_BKGND_DKGRAY equ    40014
	IDM_BKGND_BLACK equ     40015
	IDM_TIMER_START equ     40016
	IDM_TIMER_STOP  equ     40017
	IDM_APP_HELP    equ     40018
	IDM_APP_ABOUT   equ     40019
	ID_MENUITEM40020 equ    40020	
	
.DATA
	szAppName	TCHAR	"PopMenu",0
	idColor 	DD	WHITE_BRUSH,  LTGRAY_BRUSH, GRAY_BRUSH,DKGRAY_BRUSH, BLACK_BRUSH 
	iSelection	DD	IDM_BKGND_WHITE
.DATA?
	hInst		HINSTANCE ?
	hMenu		HMENU	?
.CODE
START:

	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0

WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND
	
	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX

	mov wndclass.lpszMenuName,NULL
	lea eax,szAppName	
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),szAppName, MB_ICONERROR
           ret
	.endif
        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			CTXT("Popup Menu Demonstration"), 
			WS_OVERLAPPEDWINDOW,					;window style
			CW_USEDEFAULT,						;initial x position
			CW_USEDEFAULT,						;initial y position
			CW_USEDEFAULT, 						;initial x size
			CW_USEDEFAULT,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,							;program instance handle
			NULL										;creation parameters
	mov hWnd,eax
	
	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	point:POINT
	.if	uMsg == WM_CREATE
		invoke	LoadMenu,hInst,addr szAppName
		mov	hMenu,eax
		invoke	GetSubMenu,hMenu,0
		mov	hMenu,eax
		xor	eax,eax
		ret			
	.elseif uMsg==WM_RBUTTONUP
		mov	eax,lParam
		and	eax,0FFFFh
		mov	point.x,eax
		mov	eax,lParam
		shr	eax,16
		mov	point.y,eax
		invoke	ClientToScreen,hwnd,addr point
		invoke	TrackPopupMenu,hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL

		xor	eax,eax
		ret	
	.elseif uMsg==WM_COMMAND
		mov	eax,wParam
		.if	(eax==IDM_FILE_NEW)  || \
			(eax==IDM_FILE_OPEN) || \
			(eax==IDM_FILE_SAVE) || \
			(eax==IDM_FILE_SAVE_AS)
			invoke	MessageBeep,0
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_EXIT)
			invoke	SendMessage,hwnd,WM_CLOSE,0,0
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_EDIT_UNDO) || \
			(eax==IDM_EDIT_CUT) ||  \                
			(eax==IDM_EDIT_COPY) || \
			(eax==IDM_EDIT_PASTE) ||\
			(eax==IDM_EDIT_CLEAR) 
		        xor	eax,eax
		        ret
		.elseif (eax==IDM_BKGND_WHITE) || \		        
			(eax==IDM_BKGND_LTGRAY) ||\			
			(eax==IDM_BKGND_GRAY)  || \
			(eax==IDM_BKGND_DKGRAY) ||\ 
			(eax==IDM_BKGND_BLACK)
			invoke	CheckMenuItem,hMenu,iSelection,MF_UNCHECKED
			mov	eax,wParam
			and	eax,0FFFFh
			mov	iSelection,eax
			invoke	CheckMenuItem,hMenu,iSelection,MF_CHECKED
			mov	eax,wParam
			and	eax,0FFFFh			
			sub	eax,IDM_BKGND_WHITE
			mov	ebx,idColor[eax*4]
			invoke	GetStockObject,ebx
			invoke	SetClassLong,hwnd,GCL_HBRBACKGROUND,eax
			invoke	InvalidateRect,hwnd,NULL,TRUE
		        xor	eax,eax
		        ret
		.elseif	(eax== IDM_TIMER_START)        
			invoke	SetTimer,hwnd,ID_TIMER,1000,NULL
			.if	(eax!=0)
                   		invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_GRAYED
		                invoke  EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_ENABLED
			.endif	
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_TIMER_STOP)        			
			invoke	KillTimer,hwnd,ID_TIMER
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_START, MF_ENABLED
                        invoke	EnableMenuItem,hMenu, IDM_TIMER_STOP,  MF_GRAYED
		        xor	eax,eax
		        ret
		.elseif	(eax==IDM_APP_HELP)        			
			invoke	MessageBox,hwnd,CTXT("Help not yet implemented!"),addr szAppName, MB_ICONEXCLAMATION or MB_OK
		        xor	eax,eax
		        ret			
		.elseif	(eax==IDM_APP_ABOUT)
                        invoke	MessageBox,hwnd,CTXT("Menu Demonstration Program"),CTXT("(c) Charles Petzold, 1998"),MB_ICONINFORMATION or MB_OK
		        xor	eax,eax
		        ret	
		.endif  
	.elseif uMsg == WM_TIMER
		invoke	MessageBeep,0
		xor	eax,eax
		ret			
	.elseif uMsg == WM_DESTROY
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START        

wpe5.jpg (13932 字节)

程序运行结果

三个菜单ID在POORMENU.ASM 的开始部分定义:

#define IDM_ABOUT          1
        
#define IDM_HELP           2
        
#define IDM_REMOVE         3
        

在程序窗口建立之后,POORMENU得到一个系统菜单的句柄:

hMenu = GetSystemMenu (hwnd, FALSE) ;
        

第一次呼叫GetSystemMenu时,您应该为修改菜单作准备,将第二个参数设定为FALSE。

使用四个AppendMenu呼叫来实作对菜单的修改:

AppendMenu (hMenu,       MF_SEPARATOR, 0,                                                NULL) ;
        
AppendMenu (hMenu,       MF_STRING, IDM_SYS_ABOUT,     TEXT ("About...")) ;
        
AppendMenu (hMenu,       MF_STRING, IDM_SYS_HELP,             TEXT ("Help...")) ;
        
AppendMenu (hMenu,       MF_STRING, IDM_SYS_REMOVE,    TEXT ("Remove Additions"));
        

第一个AppendMenu呼叫是添加分隔条。选择「Remove Additions」菜单项将使POORMENU删除这些附加的菜单项,这只要把第二个参数设定为TRUE,再次呼叫GetSystemMenu即可:

GetSystemMenu (hwnd, TRUE) ;
        

标准系统菜单有下列选项:Restore、Move、Size、Minimize、Maximize和Close。它们产生wParam分别等于SC_RESTORE、SC_MOVE、SC_SIZE、SC_MINIMUM、SC_MAXIMUM和SC_CLOSE的WM_SYSCOMMAND消息。尽管Windows程序一般不这样做,但是您可以自己处理这些消息,而不把它们留给DefWindowProc。您也可以使用下面所述的方法来禁止或者除掉系统菜单的标准选项。Windows文件中还介绍了一些系统菜单的标准附加项目,这些附加项目使用标识符SC_NEXTWINDOW、SC_PREVWINDOW、SC_VSCROLL、SC_HSCROLL和SC_ARRANGE。您也许会发现,在一些应用程序中将这些命令加入系统菜单是合适的。

改变菜单

我们已经看到了如何使用AppendMenu函数为程序定义菜单以及将菜单项加入到系统菜单中。在Windows 3.0之前,您不得不被迫使用ChangeMenu函数来完成这种工作。ChangeMenu函数有很多功能,至少在当时,整个Windows中它是最复杂的函数之一。现在,许多函数都比ChangeMenu函数还要复杂,并且ChangeMenu的功能被分解为五个新的函数:

如果菜单项是一个弹出式菜单,那么DeleteMenu和RemoveMenu之间的区别就很重要。DeleteMenu清除弹出式菜单,但RemoveMenu不清除它。

其它菜单命令

下面是在使用菜单时一些有用的函数。

当您改变顶层菜单项时,直到Windows重画菜单列时才显示所做的改变。您可以通过下列呼叫来强迫执行菜单更新:

DrawMenuBar (hwnd) ;
        

注意,DrawMenuBar的参数是窗口句柄而不是菜单句柄。

您可以使用下列命令来获得弹出式菜单的句柄:

hMenuPopup = GetSubMenu (hMenu, iPosition) ;
        

其中iPosition是hMenu指示的顶层菜单中弹出式菜单项的索引(开始为0)。然后您可以在其它函数中使用弹出式菜单句柄(例如在AppendMenu函数中)。

您可以使用下列命令获得顶层菜单或者弹出式菜单中目前的项数:

iCount = GetMenuItemCount (hMenu) ;
        

您可以取得弹出式菜单项的菜单ID:

id = GetMenuItemID (hMenuPopup, iPosition) ;
        

其中iPosition是菜单项在弹出式菜单中的位置(以0开始)。

在MENUDEMO中您已经看到如何选中、或者取消选中弹出式菜单中的某一项:

CheckMenuItem (hMenu, id, iCheck) ;
        

在MENUDEMO中,hMenu是顶层菜单的句柄,id是菜单ID,而iCheck的值是MF_CHECKED或MF_UNCHECKED。如果hMenu是弹出式菜单句柄,那么参数id是位置索引而不是菜单ID。如果使用索引会更方便的话,那么您可以在第三个参数中包含MF_BYPOSITION,例如:

CheckMenuItem (hMenu, iPosition, MF_CHECKED | MF_BYPOSITION) ;
        

除了第三个参数是MF_ENABLED、MF_DISABLED或MF_GRAYED外,EnableMenuItem函数与CheckMenuItem函数所完成的工作类似。如果您在具有弹出式菜单的顶层菜单项上使用EnableMenuItem,那么必须在第三个参数中使用MF_BYPOSITION标识符,因为菜单项没有菜单ID。我们将在后面所示的POPPAD2程序中看到EnableMenuItem的一个例子。 HiliteMenuItem也类似于CheckMenuItem和EnableMenuItem,但是它使用的是MF_HILITE和MF_UNHILITE。当您在菜单项之间移动时,Windows使用反白显示方式加亮显示菜单项。您通常不需要使用HiliteMenuItem。

您还需要对您的菜单做些什么呢?还记得我们在菜单中使用了哪些字符串吗?您可以透过下面的呼叫来回顾一下:

iCharCount = GetMenuString (hMenu, id, pString, iMaxCount, iFlag) ;
        

iFlag可以是MF_BYCOMMAND(其中id是菜单ID),也可以是MF_BYPOSITION(其中的id是位置索引)。函数将字符串的iMaxCount个字节复制到pString中,并传回复制的字节数。

或许您也想知道菜单项目前的属性是什么:

iFlags = GetMenuState (hMenu, id, iFlag) ;
        

同样地,iFlag可以是MF_BYCOMMAND或MF_BYPOSITION。传回值iFlags是目前所有属性的组合,您可以通过对MF_DISABLED、MF_GRAYED、MF_CHECKED、MF_MENUBREAK、MF_MENUBARBREAK和MF_SEPARATOR标识符的检测来决定目前的属性。

也许现在您对菜单有了一些了解。这时您可能想知道,如果您不再需要菜单时又应该如何处理。您可以使用下面的命令来清除菜单:

DestroyMenu (hMenu) ;
        

从而使菜单句柄无效。

建立菜单的非正统方法

现在让我们稍微偏离我们所讨论的主题。如果在您的程序中没有下拉式菜单,而是建立了多个没有弹出式菜单的顶层菜单,并呼叫SetMenu在顶层菜单之间切换,那会是什么样的情形呢?就像Lotus 1-2-3中老式的文字模式菜单那样。程序10-8中的NOPOPUPS程序展示了处理这种情况。在这个程序中,「File」和「Edit」项与MENUDEMO程序中的类似,但是却以另一种顶层菜单显示出来。

程序10-8 NOPOPUPS

        
NOPOPUPS.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	IDM_FILE  		equ  	40001
	IDM_EDIT                equ     40002
	IDM_FILE_NEW            equ     40003
	IDM_FILE_OPEN           equ     40004
	IDM_FILE_SAVE           equ     40005
 	IDM_FILE_SAVE_AS        equ     40006
 	IDM_MAIN                equ     40007
 	IDM_EDIT_UNDO           equ     40008
 	IDM_EDIT_CUT            equ     40009
 	IDM_EDIT_COPY           equ     40010
 	IDM_EDIT_PASTE          equ     40011
 	IDM_EDIT_CLEAR          equ     40012	
.DATA
	szAppName	TCHAR	"NoPopUps",0
.DATA?
	hMenuMain	HMENU 	?
	hMenuEdit	HMENU 	?
	hMenuFile 	HMENU 	?

.CODE
START:
	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0
WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND

	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,IDI_APPLICATION
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX

	mov wndclass.lpszMenuName,NULL
	lea eax,szAppName	
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),szAppName, MB_ICONERROR
           ret
	.endif
        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			CTXT("No-Popup Nested Menu Demonstration"), 
			WS_OVERLAPPEDWINDOW,					;window style
			CW_USEDEFAULT,						;initial x position
			CW_USEDEFAULT,						;initial y position
			CW_USEDEFAULT, 						;initial x size
			CW_USEDEFAULT,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,							;program instance handle
			NULL										;creation parameters
	mov hWnd,eax

  	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke TranslateMessage, ADDR msg
				invoke DispatchMessage,  ADDR msg
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret

WinMain endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	hInstance:HINSTANCE
	
	.if	uMsg == WM_CREATE
		invoke	GetWindowLong,hwnd,GWL_HINSTANCE
		mov	hInstance,eax
		invoke	LoadMenu,hInstance, CTXT("MenuMain")
                mov	hMenuMain,eax
       		invoke	LoadMenu,hInstance, CTXT("MenuFile")
                mov	hMenuFile,eax
        	invoke	LoadMenu,hInstance, CTXT("MenuEdit")
                mov	hMenuEdit,eax
                invoke  SetMenu,hwnd, hMenuMain
		xor	eax,eax
		ret			
	.elseif	uMsg == WM_COMMAND
		mov	eax,wParam
		and	eax,0FFFFh
		.if	(eax== IDM_MAIN)
			invoke	SetMenu,hwnd,hMenuMain
			xor	eax,eax
			ret			
		.elseif	(eax== IDM_FILE)   
			invoke	SetMenu,hwnd,hMenuFile
 			xor	eax,eax
			ret			
		.elseif	(eax== IDM_EDIT)   
			invoke	SetMenu,hwnd,hMenuEdit
			xor	eax,eax
			ret		
		.elseif	(eax== IDM_FILE_NEW) ||\
			(eax== IDM_FILE_OPEN)||\
			(eax== IDM_FILE_SAVE)||\
			(eax== IDM_FILE_SAVE_AS)||\
			(eax== IDM_EDIT_UNDO)||\
			(eax== IDM_EDIT_CUT)||\
			(eax== IDM_EDIT_COPY)||\
			(eax== IDM_EDIT_PASTE)||\
			(eax== IDM_EDIT_CLEAR)
			invoke	MessageBeep,0
			xor	eax,eax
			ret			
		.endif
	.elseif uMsg == WM_DESTROY
		invoke	SetMenu,hwnd, hMenuMain
		invoke  DestroyMenu,hMenuFile
		invoke	DestroyMenu,hMenuEdit
		
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START
        
NOPOPUPS.RC 
#define IDM_FILE_NEW                    40001
#define IDM_FILE_OPEN                   40002
#define IDM_FILE_SAVE                   40003
#define IDM_FILE_SAVE_AS                40004
#define IDM_APP_EXIT                    40005
#define IDM_EDIT_UNDO                   40006
#define IDM_EDIT_CUT                    40007
#define IDM_EDIT_COPY                   40008
#define IDM_EDIT_PASTE                  40009
#define IDM_EDIT_CLEAR                  40010
#define IDM_BKGND_WHITE                 40011
#define IDM_BKGND_LTGRAY                40012
#define IDM_BKGND_GRAY                  40013
#define IDM_BKGND_DKGRAY                40014
#define IDM_BKGND_BLACK                 40015
#define IDM_TIMER_START                 40016
#define IDM_TIMER_STOP                  40017
#define IDM_APP_HELP                    40018
#define IDM_APP_ABOUT                   40019
#define ID_MENUITEM40020                40020

MENUDEMO MENU DISCARDABLE 
BEGIN
    POPUP "&File"
    BEGIN
        MENUITEM "&New",                        ID_MENUITEM40020
        MENUITEM "&Open",                       IDM_FILE_OPEN
        MENUITEM "&Save",                       IDM_FILE_SAVE
        MENUITEM "Save &As...",                 IDM_FILE_SAVE_AS
        MENUITEM SEPARATOR
        MENUITEM "E&xit",                       IDM_APP_EXIT
    END
    POPUP "&Edit"
    BEGIN
        MENUITEM "&Undo",                       IDM_EDIT_UNDO
        MENUITEM SEPARATOR
        MENUITEM "C&ut",                        IDM_EDIT_CUT
        MENUITEM "&Copy",                       IDM_EDIT_COPY
        MENUITEM "&Paste",                      IDM_EDIT_PASTE
        MENUITEM "De&lete",                     IDM_EDIT_CLEAR
    END
    POPUP "&Background"
    BEGIN
        MENUITEM "&White",                      IDM_BKGND_WHITE, CHECKED
        MENUITEM "&Light Gray",                 IDM_BKGND_LTGRAY
        MENUITEM "&Gray",                       IDM_BKGND_GRAY
        MENUITEM "&Dark Gray",                  IDM_BKGND_DKGRAY
        MENUITEM "&Black",                      IDM_BKGND_BLACK
    END
    POPUP "&Timer"
    BEGIN
        MENUITEM "&Start",                      IDM_TIMER_START
        MENUITEM "S&top",                       IDM_TIMER_STOP, GRAYED
    END
    POPUP "&Help"
    BEGIN
        MENUITEM "&Help...",                    IDM_APP_HELP
        MENUITEM "&About MenuDemo...",          IDM_APP_ABOUT
    END
END
    

wpe6.jpg (7611 字节)

在Microsoft Developer Studio中,您建立了三个菜单,而不是一个。从「Insert」中选择「Resource」三次,每个菜单有一个不同的名称。当窗口消息处理程序处理WM_CREATE消息时,Windows将每个菜单资源加载内存:

hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ;
        
hMenuFile = LoadMenu (hInstance, TEXT ("MenuFile")) ;
        
hMenuEdit = LoadMenu (hInstance, TEXT ("MenuEdit")) ;
        

开始时,程序只显示主菜单:

SetMenu (hwnd, hMenuMain) ;
        

主菜单使用字符串「MAIN:」、「File...」和「Edit...」列出这三个选项。然而,「MAIN:」是禁用的,因此它不能使WM_COMMAND消息被发送到窗口消息处理程序。「File」和「Edit」菜单项以「FILE:」和「EDIT:」开始,表示它们是子菜单。每个菜单的最后一项都是字符串「(Main)」,表示传回到主菜单。在这三个菜单之间进行切换是很简单的:

case        WM_COMMAND :
        
           switch (wParam)
        
           {
        
           case   IDM_MAIN :
        
            SetMenu (hwnd, hMenuMain) ;
        
                  return 0 ;
        
           case   IDM_FILE :
        
                  SetMenu (hwnd, hMenuFile) ;
        
                  return 0 ;
        

           case   IDM_EDIT :
        
                 SetMenu (hwnd, hMenuEdit) ;
        
                  return 0 ;
        

   其它行程序
        
   }
        
          break ;
        

在WM_DESTROY消息处理期间,NOPOPUPS将程序的菜单设定为主菜单,并呼叫DestroyMenu来清除「File」和「Edit」菜单。当窗口被清除时,主菜单将被自动清除。

键盘快捷键

快捷键是产生WM_COMMAND消息(有些情况下是WM_SYSCOMMAND)的键组合。许多时候,程序使用快捷键来重复常用菜单项的动作(然而,快捷键还可以用于执行非菜单功能)。例如,许多Windows程序都有一个包含「Delete」或「Clear」选项的「Edit」菜单,这些程序习惯上都将Del键指定为该选项的快捷键。使用者可以通过「 Alt键」从菜单中选择「Delete」选项,或者只需按下快捷键 Del。当窗口消息处理程序收到一个WM_COMMAND消息时,它不必确定使用的是菜单还是快捷键。

为什么要使用快捷键

您也许会问:为什么我应该使用快捷键?为什么不能直接拦截WM_KEYDOWN或WM_CHAR消息而自己实作同样的菜单功能呢?好处又在哪里呢?对于一个单窗口应用程序,您当然可以拦截键盘消息,但是使用快捷键可以得到一些好处:您不需要把菜单和快捷键的处理方式重写一遍。

对于有多个窗口和多个窗口消息处理程序的应用程序来说,快捷键是非常重要的。正如我们所看到的,Windows将键盘消息发送给目前活动窗口的窗口消息处理程序。然而对于快捷键,Windows把WM_COMMAND消息发送给窗口消息处理程序,该窗口消息处理程序的句柄在Windows函数TranslateAccelerator中给出。通常这是主窗口,也是拥有菜单的窗口,这意味着无须每个窗口消息处理程序都把快捷键的操作处理程序重写一遍。

如果您在主窗口的显示区域中,使用了非系统模态对话框(在下一章中会讨论)或者子窗口,那么这种好处就变得非常重要。如果定义一个特定的快捷键以便在不同的窗口之间移动,那么,只需要一个窗口消息处理程序有这个处理程序。子窗口就不会收到快捷键引发的WM_COMMAND消息。

安排快捷键的几条规则

理论上,您可以使用任何虚拟键或者字符键连同Shift键、Ctrl键或Alt键来定义快捷键。然而,您应该尽力使应用程序之间协调一致,并且尽量避免干扰Windows的键盘使用。在快捷键中,应该避免使用Tab、Enter、Esc和Spacebar键,因为这些键常常用于完成系统功能。

快捷键最经常的用途是操作程序的「Edit」菜单中的各项。为这些菜单项推荐的快捷键在Windows 3.0和Windows 3.1之间已有不同,因此通常都要支持如下所列的新旧两套快捷键:

表10-2

功能

旧快捷键

新快捷键

Undo Alt+Backspace Ctrl+Z
Cut Shift+Del Ctrl+X
Copy Ctrl+Ins Ctrl+C
Paste Shift+Ins Ctrl+V
Delete或Clear Del Del

另一种常用的虚拟键是启动辅助信息的功能键F1。应该避免使用F4、F5和F6键,因为这些键常用在多重文件接口(MDI)程序中来完成特殊的功能。

快捷键表

您可以在Developer Studio中定义快捷键表。为了让程序中加载加速键表更为容易,给它和程序名相同的名称(与菜单和图示名也相同)。

每个快捷键都有在Accel Properties对话框中定义的ID和按键组合。如果您已经定义了菜单,则菜单ID会出现在下拉式清单方块中,因此不需要键入它们。

快捷键可以是虚拟键或ASCII字符与Shift、Ctrl或Alt键的组合。可以通过在字母前键入『^』来指定带有Ctrl键的ASCII字符。也可以从下拉式清单方块中选取虚拟键。

当您为菜单项定义快捷键时,应该将键的组合包含到菜单项的文字中。制表符(\t)将文字与快捷键分割开,将快捷键列在第二列。为了在菜单中为快捷键做上标记,可以在文字「Ctrl」、「Shift」或「Alt」之后跟上一个「+」号和一个键名(例如,「Shift+F6」或「Ctrl+F6」)。

快捷键表的加载

在您的程序中,您使用LoadAccelerators函数把快捷键表加载内存,并获得该表的句柄。 LoadAccelerators叙述非常类似于LoadIcon、LoadCursor和LoadMenu叙述。

首先,把快捷键表的句柄定义为型态HANDLE:

HANDLE hAccel ;
        

然后加载加速键表:

hAccel = LoadAccelerators (hInstance, TEXT ("MyAccelerators")) ;
        

正如图标、光标和菜单一样,您可以使用一个数值代替快捷键表的名称,然后在LoadAccelerators叙述中和MAKEINTRESOURCE宏一起使用该数值,或者把它放在双引号内,前面冠以字符「#」。

键盘代码转换

现在我们将讨论底下这三行程序代码,在本书中,截至目前为止建立的所有Windows程序中都使用过它们。这些程序代码是标准的消息循环:

while       (GetMessage (&msg, NULL, 0, 0))
        
{
        
  TranslateMessage (&msg) ;
        
    DispatchMessage (&msg) ;
        
}
        

下面把上头那段程序代码加以修改,以便使用加速键:

while       (GetMessage (&msg, NULL, 0, 0))
        
{
        
   if (!TranslateAccelerator (hwnd, hAccel, &msg))
        
    {
        
                  TranslateMessage (&msg) ;
        
                  DispatchMessage (&msg) ;
        
           }
        
}
        

TranslateAccelerator函数确认存放在msg消息结构中的消息是否为键盘消息。如果是,该函数将找寻句柄为hAccel的快捷键表。如果找到了一个符合的,则呼叫句柄为hwnd的窗口消息处理程序。如果快捷键ID与系统菜单的菜单项一致,则消息就是WM_SYSCOMMAND;否则,消息为WM_COMMAND。

当TranslateAccelerator传回时,如果消息已经被转换(并且已经被发送给窗口消息处理程序),那么传回值为非零;否则,传回值为0。如果TranslateAccelerator传回一个非零值,则不呼叫TranslateMessage和DispatchMessage,而是经过循环回到GetMessage呼叫中。

TranslateMessage中的参数hwnd看起来有点累赘,因为消息循环中的其它三个函数都没有要求这个参数。此外,消息结构本身(结构变量msg)有一个叫做hwnd的成员,它是窗口句柄。

该函数有些不同的原因在于:msg结构的字段由GetMessage呼叫填入。当GetMessage的第二个参数为NULL时,函数会找寻应用程序所有窗口的消息。当GetMessage传回时,msg结构的hwnd是将要获得消息之窗口的窗口句柄。然而,当TranslateAccelerator把键盘消息转换为WM_COMMAND或WM_SYSCOMMAND消息时,它使用函数的第一个参数指定的窗口句柄hwnd来代替窗口代号msg.hwnd。Windows就是这样把所有快捷键消息发送给同一窗口消息处理程序的,即使另一个应用窗口目前拥有输入焦点。当系统模态对话框或者消息框拥有输入焦点时,TranslateAccelerator不会转换键盘消息,因为这些窗口的消息是不经过程序的消息循环的。

在某些情况下,当您程序的另一个窗口(比如一个非系统模态对话框)拥有输入焦点时,您也许不想转换快捷键。您将在后面一期中看到如何处理这种情况。

接收快捷键消息

当快捷键与系统菜单中的菜单项相对应时,TranslateAccelerator给窗口消息处理程序发送一个WM_SYSCOMMAND消息,否则,TranslateAccelerator给窗口消息处理程序发送一个WM_COMMAND消息。下表所示为几种可能接收到的WM_COMMAND消息,这些消息用于快捷键、菜单命令以及子窗口控件:

表10-3

 

快捷键

菜单

控件

LOWORD (wParam) 快捷键ID 菜单ID 控件ID
HIWORD (wParam) 1 0 通知码
lParam 0 0 子窗口句柄

如果快捷键与一个菜单项对应,那么窗口消息处理程序还会收到WM_INITMENU、WM_INITMENUPOPUP和WM_MENUSELECT消息,就好像选中了菜单选项一样。在处理WM_INITMENUPOPUP时,程序往往启用和禁用弹出式菜单中的菜单项,因此,在使用快捷键时,您仍然能够实作这类功能。如果快捷键与一个禁用或者无效化的菜单项相对应,那么,TranslateAccelerator函数就不会向窗口消息处理程序发送WM_COMMAND或WM_SYSCOMMAND消息。

如果活动窗口已经被最小化,那么TranslateAccelerator将为与启用的系统菜单项相对应的快捷键向窗口消息处理程序发送WM_SYSCOMMAND消息,而不是WM_COMMAND消息。TranslateAccelerator也会为没有任何菜单项与之对应的快捷键,来向窗口消息处理程序发送WM_COMMAND消息。

菜单与快捷键应用程序POPPAD

在前面一期,我们建立了一个叫做POPPAD1的程序,它使用了子窗口编辑控件来实作基本的笔记本功能。在这一章中,我们将加入「File」和「Edit」菜单,并称此程序为POPPAD2。「Edit」菜单的菜单项的功能全部可用。

程序10-9 POPPAD2

        
POPPAD2.ASM
;MASMPlus 代码模板 - 普通的 Windows 程序代码

.386
.Model Flat, StdCall
Option Casemap :None

Include windows.inc
Include user32.inc
Include kernel32.inc
Include gdi32.inc

includelib gdi32.lib
IncludeLib user32.lib
IncludeLib kernel32.lib
include macro.asm
	
	WinMain PROTO :DWORD,:DWORD,:DWORD,:DWORD
	IDM_FILE_NEW  		equ  	40001
	IDM_FILE_OPEN           equ     40002
	IDM_FILE_SAVE           equ     40003
	IDM_FILE_SAVE_AS        equ     40004
	IDM_FILE_PRINT          equ     40005
	IDM_APP_EXIT            equ     40006
	IDM_EDIT_UNDO           equ     40007
	IDM_EDIT_CUT            equ     40008
	IDM_EDIT_COPY           equ     40009
	IDM_EDIT_PASTE          equ     40010
	IDM_EDIT_CLEAR          equ     40011
	IDM_EDIT_SELECT_ALL     equ     40012
	IDM_HELP_HELP           equ     40013
	IDM_APP_ABOUT           equ     40014 	
	ID_EDIT                 equ	   1


.DATA
	szAppName	TCHAR	"PopPad2",0
.DATA?
	hwndEdit	HWND	?

.CODE
START:
	invoke GetModuleHandle,NULL
	invoke WinMain,eax,NULL,NULL,SW_SHOWDEFAULT
	invoke ExitProcess,0
WinMain proc hInstance:DWORD,hPrevInst:DWORD,szCmdLine:DWORD,iCmdShow:DWORD
	LOCAL hAccel	 :HACCEL
	LOCAL wndclass   :WNDCLASSEX
	LOCAL msg  :MSG
	LOCAL hWnd :HWND
	LOCAL posW:RECT

	mov wndclass.cbSize,sizeof WNDCLASSEX	
	mov wndclass.style,CS_HREDRAW or CS_VREDRAW	
	mov wndclass.lpfnWndProc,offset WndProc

	mov wndclass.cbClsExtra,0
	mov wndclass.cbWndExtra,0
	
	push hInstance
	pop wndclass.hInstance
	
	invoke LoadIcon,hInstance,addr szAppName
	mov wndclass.hIcon,eax	
	
	invoke LoadCursor,NULL,IDC_ARROW
	mov wndclass.hCursor,eax	
	
	invoke GetStockObject,WHITE_BRUSH
	mov wndclass.hbrBackground,EAX
	
	lea eax,szAppName
	mov wndclass.lpszMenuName,eax
	mov wndclass.lpszClassName,eax

	mov wndclass.hIconSm,0
	
	invoke RegisterClassEx, ADDR wndclass
	.if (eax==0)
	   invoke MessageBox,NULL,CTXT("This program requires Windows NT!"),szAppName, MB_ICONERROR
           ret
	.endif
        
        invoke GetSystemMetrics,SM_CXSCREEN
        shr    eax,1
        mov    posW.right,eax
        shr    eax,1
        mov    posW.left,eax
        invoke GetSystemMetrics,SM_CYSCREEN
        shr    eax,1
        mov    posW.bottom,eax
        shr    eax,1
        mov	posW.top,eax
        mov    edx,eax        
	invoke CreateWindowEx,NULL,
			ADDR szAppName, 					;window class name
			CTXT("No-Popup Nested Menu Demonstration"), 
			WS_OVERLAPPEDWINDOW,					;window style
			posW.left,						;initial x position
			posW.top,						;initial y position
			posW.right, 						;initial x size
			posW.bottom,						;initial y size
			NULL,							;parent window handle
			NULL,							;window menu handle
			hInstance,						;program instance handle
			NULL							;creation parameters
	mov hWnd,eax


  	invoke ShowWindow,hWnd,iCmdShow
	invoke UpdateWindow,hWnd
	
	invoke	LoadAccelerators,hInstance,addr szAppName
	mov	hAccel,eax

	StartLoop:
		invoke GetMessage,ADDR msg,NULL,0,0
			cmp eax, 0
			je ExitLoop
				invoke	TranslateAccelerator,hWnd,hAccel,addr msg
			 	.if eax==0 
					invoke TranslateMessage, ADDR msg
					invoke DispatchMessage,  ADDR msg
				.endif
			jmp StartLoop
	ExitLoop:
	
	mov eax,msg.wParam
	ret
WinMain endp


AskConfirmation proc	hwnd:HWND
        invoke	MessageBox,hwnd, CTXT ("Really want to close PopPad2?"),addr szAppName, MB_YESNO or MB_ICONQUESTION
	ret                          
AskConfirmation	endp

WndProc proc hwnd:DWORD,uMsg:DWORD,wParam :DWORD,lParam :DWORD
	LOCAL	iSelect,iEnable:DWORD
	
	.if	uMsg == WM_CREATE
		mov	esi,lParam
		mov	ebx,[esi+4]
                invoke CreateWindowEx,NULL,CTXT("edit"), NULL,\
                          WS_CHILD or WS_VISIBLE or WS_HSCROLL or WS_VSCROLL or  WS_BORDER or ES_LEFT or ES_MULTILINE or ES_AUTOHSCROLL or ES_AUTOVSCROLL,\
	                  0, 0, 0, 0, hwnd, ID_EDIT,\
	                  ebx, NULL
                mov	hwndEdit,eax

		xor	eax,eax
		ret			
	.elseif	uMsg == WM_SETFOCUS
		invoke	SetFocus,hwndEdit
		xor	eax,eax
		ret			
	.elseif	uMsg == WM_SIZE
		mov	eax,lParam
		and	eax,0FFFFh
		mov	ebx,eax
		mov	eax,lParam
		shr	eax,16
		mov	ecx,eax
		invoke	MoveWindow,hwndEdit,0,0,ebx,ecx,TRUE
		xor	eax,eax
		ret			
	.elseif	uMsg == WM_INITMENUPOPUP
		.if	lParam == 1
			invoke	SendMessage,hwndEdit,EM_CANUNDO,0,0
			.if	eax!=0
				mov	eax,MF_ENABLED
			.else
				mov	eax,MF_GRAYED
			.endif
			invoke	EnableMenuItem,wParam,IDM_EDIT_UNDO,eax
			
			invoke	IsClipboardFormatAvailable,CF_TEXT
			.if	eax!=0
				mov	eax,MF_ENABLED
			.else
				mov	eax,MF_GRAYED
			.endif
			invoke	EnableMenuItem,wParam,IDM_EDIT_PASTE,eax
			
			invoke	SendMessage,hwndEdit, EM_GETSEL, 0, 0
			mov	iSelect,eax

		mov	eax,iSelect
		shr	eax,16
		mov	ebx,iSelect
		and	ebx,0FFFFh
		.if	eax==ebx
			mov	eax,MF_GRAYED
		.else
			mov	eax,MF_ENABLED
		.endif	
		mov	iEnable,eax
              	invoke	EnableMenuItem,wParam, IDM_EDIT_CUT,  iEnable
		invoke	EnableMenuItem,wParam, IDM_EDIT_COPY, iEnable
		invoke	EnableMenuItem,wParam, IDM_EDIT_CLEAR, iEnable
		;invoke	MessageBox,hwnd,CTXT("pp"),NULL,MB_APPLMODAL
		xor	eax,eax
		ret	
		.endif	
	.elseif uMsg == WM_COMMAND
		.if	lParam!=0
			mov	eax,lParam
			and	eax,0FFFFh
			mov	ebx,wParam
			shr	ebx,16
			.if 	(eax==ID_EDIT) || (ebx==EN_ERRSPACE) || (ebx==EN_MAXTEXT)
				invoke	 MessageBox,hwnd, CTXT("Edit control out of space."), addr szAppName, MB_OK or MB_ICONSTOP
			.endif
		.else
			mov	eax,wParam
			and	eax,0FFFFh
			.if	(eax==IDM_FILE_NEW) || \
				(eax==IDM_FILE_OPEN) || \
				(eax==IDM_FILE_SAVE) || \
				(eax==IDM_FILE_SAVE_AS) || \
				(eax==IDM_FILE_PRINT)
				invoke	MessageBeep,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_APP_EXIT)
				invoke	SendMessage,hwnd,WM_CLOSE,0,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_EDIT_UNDO)
				invoke	SendMessage,hwndEdit,WM_UNDO,0,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_EDIT_CUT)
				invoke	SendMessage,hwndEdit,WM_CUT,0,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_EDIT_COPY)
				invoke	SendMessage,hwndEdit,WM_COPY,0,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_EDIT_PASTE)
				invoke	SendMessage,hwndEdit,WM_PASTE,0,0
				xor	eax,eax
	        		ret

			.elseif	(eax==IDM_EDIT_CLEAR)
				invoke	SendMessage,hwndEdit,WM_CLEAR,0,0
				xor	eax,eax
	        		ret
			.elseif	(eax==IDM_EDIT_SELECT_ALL)
				invoke	SendMessage,hwndEdit,EM_SETSEL,0,-1
				xor	eax,eax
	        		ret
			.elseif	(eax==  IDM_HELP_HELP)
				invoke	MessageBox,hwnd,CTXT("Help not yet implemented!"),addr szAppName, MB_OK or MB_ICONEXCLAMATION
				xor	eax,eax
	        		ret	        		
			.elseif	(eax==IDM_APP_ABOUT)
				invoke	MessageBox,hwnd, CTXT("POPPAD2 (c) Charles Petzold, 1998"),addr szAppName, MB_OK or MB_ICONINFORMATION
				xor	eax,eax
	        		ret	        		
			.endif	
		.endif
	.elseif uMsg == WM_CLOSE
		invoke	AskConfirmation,hwnd
		.if	eax==IDYES
			invoke	DestroyWindow,hwnd
		.endif	
	        xor	eax,eax
	        ret
	.elseif uMsg == WM_QUERYENDSESSION
		invoke	AskConfirmation,hwnd
		.if	eax==IDYES
			invoke	DestroyWindow,hwnd
			mov	eax,1
		.else
			xor	eax,eax
		.endif	
	        ret		
	.elseif uMsg == WM_DESTROY
		
	        invoke 	PostQuitMessage,NULL
	        xor	eax,eax
	        ret
	.endif

	invoke DefWindowProc,hwnd,uMsg,wParam,lParam
	ret
WndProc endp
END START

POPPAD2.RC

#define IDM_FILE_NEW 40001
#define IDM_FILE_OPEN 40002
#define IDM_FILE_SAVE 40003
#define IDM_FILE_SAVE_AS 40004
#define IDM_APP_EXIT 40005
#define IDM_EDIT_UNDO 40006
#define IDM_EDIT_CUT 40007
#define IDM_EDIT_COPY 40008
#define IDM_EDIT_PASTE 40009
#define IDM_EDIT_CLEAR 40010
#define IDM_BKGND_WHITE 40011
#define IDM_BKGND_LTGRAY 40012
#define IDM_BKGND_GRAY 40013
#define IDM_BKGND_DKGRAY 40014
#define IDM_BKGND_BLACK 40015
#define IDM_TIMER_START 40016
#define IDM_TIMER_STOP 40017
#define IDM_APP_HELP 40018
#define IDM_APP_ABOUT 40019
#define ID_MENUITEM40020 40020

POPMENU MENU DISCARDABLE
BEGIN
POPUP "MyMenu"
BEGIN
POPUP "&File"
BEGIN
MENUITEM "&New", IDM_FILE_NEW
MENUITEM "&Open", IDM_FILE_OPEN
MENUITEM "&Save", IDM_FILE_SAVE
MENUITEM "Save &As", IDM_FILE_SAVE_AS
MENUITEM SEPARATOR
MENUITEM "E&xit", IDM_APP_EXIT
END
POPUP "&Edit"
BEGIN
MENUITEM "&Undo", IDM_EDIT_UNDO
MENUITEM SEPARATOR
MENUITEM "Cu&t", IDM_EDIT_CUT
MENUITEM "&Copy", IDM_EDIT_COPY
MENUITEM "&Paste", IDM_EDIT_PASTE
MENUITEM "De&lete", IDM_EDIT_CLEAR
END
POPUP "&Background"
BEGIN
MENUITEM "&White", IDM_BKGND_WHITE, CHECKED
MENUITEM "&Light Gray", IDM_BKGND_LTGRAY
MENUITEM "&Gray", IDM_BKGND_GRAY
MENUITEM "&Dark Gray", IDM_BKGND_DKGRAY
MENUITEM "&Black", IDM_BKGND_BLACK
END
POPUP "&Help"
BEGIN
MENUITEM "&Help...", IDM_APP_HELP
MENUITEM "&About PopMenu...", IDM_APP_ABOUT
END
END
END

        

wpe7.jpg (15615 字节)

运行结果,中间可以输入

POPPAD2.RC资源描述文件包含菜单和快捷键。您将注意到,所有快捷键都表示在制表符(\t)后的「Edit」弹出式菜单的字符串中。

启用菜单项

窗口消息处理程序的工作包括启用和无效化「Edit」菜单中的选项,这项工作在处理WM_INITMENUPOPUP时完成。首先,程序检查是否要显示「Edit」弹出式菜单。因为菜单里「Edit」的位置索引(「File」从0开始)是1,因此如果即将显示「Edit」弹出式菜单,那么lParam应该等于1。

为了确定是否启用「Undo」选项,POPPAD2给编辑控件发送一条EM_CANUNDO消息。如果编辑控件能够执行「Undo」动作,那么SendMessage呼叫传回非零值。在这种情况下,选项被启用;否则,选项无效化:

EnableMenuItem (wParam, IDM_UNDO,
        
           SendMessage (hwndEdit, EM_CANUNDO, 0, 0) ?
        
                                                 MF_ENABLED : MF_GRAYED) ;
        

只有当剪贴簿中包含文字时,「Paste」选项才能够被启用。我们可以使用CF_TEXT标识符通过IsClipboardFormatAvailable呼叫来确定这一点:

EnableMenuItem (wParam, IDM_PASTE,
        
           IsClipboardFormatAvailable (CF_TEXT) ? MF_ENABLED : MF_GRAYED) ;
        

只有选择了编辑控件中的文字,「Cut」、「Copy」和「Delete」选项才能够被启用。给编辑控件发送一条EM_GETSEL消息,并传回包含此信息的整数:

iSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0) ;
        

iSelect的低位字是第一个被选中字符的位置,iSelect的高字组是下一个被选中字符的位置。如果这两个字相等,则表示没有选中文字:

if (HIWORD (iSelect) == LOWORD (iSelect))
        
           iEnable = MF_GRAYED ;
        
else
        
           iEnable = MF_ENABLED ;
        

然后可以将iEnable的值用于「Cut」、「Copy」和「Delete」选项:

EnableMenuItem (wParam, IDM_CUT,  iEnable) ;
        
EnableMenuItem (wParam, IDM_COPY, iEnable) ;
        
EnableMenuItem (wParam, IDM_DEL,  iEnable) ;
        

处理菜单项

当然,如果POPPAD2程序不使用子窗口编辑控件,那么我们将面临一些问题,这涉及如何完成「Edit」菜单中的「Undo」、「Cut」、「Copy」、「Paste」、「Clear」和「Select All」选项。正是编辑控件使得这种处理变得容易,因为对于每一个选项我们只需向编辑控件发送一个消息即可:

case        IDM_UNDO :
        
           SendMessage (hwndEdit, WM_UNDO, 0, 0) ;
        
           return 0 ;
        
case        IDM_CUT :
        
           SendMessage (hwndEdit, WM_CUT, 0, 0) ;
        
           return 0 ;
        

case        IDM_COPY :
        
           SendMessage (hwndEdit, WM_COPY, 0, 0) ;
        
           return 0 ;
        

case        IDM_PASTE :
        
           SendMessage (hwndEdit, WM_PASTE, 0, 0) ;
        
           return 0 ;
        

case        IDM_DEL :
        
           SendMessage (hwndEdit, WM_DEL, 0, 0) ;
        
           return 0 ;
        
case        IDM_SELALL :
        
           SendMessage (hwndEdit, EM_SETSEL, 0, -1) ;
        
           return 0 ;
        

注意,我们可以更进一步简化这些处理-只要使IDM_UNDO、IDM_CUT等等的值等于相对应的窗口消息WM_UNDO、WM_CUT的值。

「File」弹出式菜单上的「About」选项启动一个简单的消息框:

case        IDM_ABOUT :
        
           MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"),
        
                                                 szAppName, MB_OK | MB_ICONINFORMATION) ;
        
           return 0 ;
        

在下一期中,我们将把它变成一个对话框。当您从菜单中选择「Help」选项或者按下F1快捷键时,同样可以启动一个消息框。

「Exit」选项向窗口消息处理程序发送一个WM_CLOSE消息:

case        IDM_EXIT :
        
           SendMessage (hwnd, WM_CLOSE, 0, 0) ;
        
           return 0 ;
        

这正是DefWindowProc收到一个wParam等于SC_CLOSE的WM_SYSCOMMAND消息时所完成的工作。

在前面的那些程序中,我们没有在窗口消息处理程序中处理WM_CLOSE消息,而只是简单地把它送给DefWindowProc。DefWindowProc对WM_CLOSE的处理非常简单:呼叫DestroyWindow函数。可以不把WM_CLOSE消息送给DefWindowProc,而让POPPAD2来处理它。这个事实到目前为止并不重要,但是当POPPAD可以真正编辑文字时,它会变得非常重要了。

case WM_CLOSE :
        
           if (IDYES == AskConfirmation (hwnd))
        
                  DestroyWindow (hwnd) ;
        
  return 0 ;
        

AskConfirmation是POPPAD2中的一个函数,它显示一个请求确认关闭程序的消息框:

AskConfirmation (HWND hwnd)
        
{
        
          return MessageBox (hwnd, TEXT ("Really want to close Poppad2?"),
        
                          szAppName, MB_YESNO | MB_ICONQUESTION) ;
        
}
        

如果选择了Yes按钮的话,消息框(以及AskConfirmation函数)将传回IDYES。只有这样,程序才会呼叫DestroyWindow,否则,程序不会结束。

如果要在程序结束之前确认使用者真的要结束程序,那么您还必须处理WM_QUERYENDSESSION消息。当使用者要关闭Windows时,Windows开始向每个窗口消息处理程序发送一个WM_QUERYENDSESSION消息。如果有任何一个窗口消息处理程序处理这个消息后传回0,那么Windows将不会结束。我们如下处理了WM_QUERYENDSESSION:

case        WM_QUERYENDSESSION :
        
           if (IDYES == AskConfirmation (hwnd))
        
                 return 1 ;
        
           else
        
                  return 0 ;
        

如果要在程序结束之前要求使用者的确认,必须处理WM_CLOSE和WM_QUERYENDSESSION这两个消息,这就是为什么我们使POPPAD2中的「Exit」菜单选项只向窗口消息处理程序发送一个WM_CLOSE消息的原因。这样做,我们避免了在别处进行请求确认的动作。

如果要处理WM_QUERYENDSESSION消息,那么您也许还会对WM_ENDSESSION消息感兴趣。Windows把这个消息发送给先前收到WM_QUERYENDSESSION消息的每个窗口消息处理程序。如果由于另一个程序从WM_QUERYENDSESSION传回了0而不能结束Windows的执行,那么WM_ENDSESSION的wParam参数为0。WM_ENDSESSION消息实际上回答了这个问题:我告诉过Windows可以把我结束掉,但是我真的被结束掉了吗?

尽管在POPPAD2的「File」菜单中我加上了常见的「New」、「Open」、「Save」和「Save As」选项,但是它们现在并不起作用。要处理这些命令,我们需要使用对话框。现在是讨论对话框的时机,也是您准备学习它们的时候了。



<<<上一篇
欢迎访问AoGo汇编小站:http://www.aogosoft.com