菜单及其它资源 |
|
大多数Windows程序都包含一个自订的图标,Windows将该图标显示在应用程序窗口标题列的左上角。当程序被列在「开始」菜单中,被显示在屏幕底部的工作列中,被列在Windows Explorer中,或者作为快捷方式显示在桌面上时,Windows也显示该程序的图标。有些程序-大部分是像小画家一类的图形绘制工具-也使用自订鼠标光标来表示程序的不同操作。还有许多Windows程序使用菜单和对话框。菜单、对话框加上滚动条,这是标准Windows使用者接口的卖点。
图标、光标、菜单和对话框都是相互关联的,它们是Windows的全部资源型态。资源即数据,它们被储存在程序的.EXE文件中,但是它们并非驻留在程序的数据区域中。也就是说,资源不能从程序原始码中定义的变量直接存取,Windows提供函数直接或间接地把它们加载内存以备使用。我们已经遇到了两个这样的函数,即LoadIcon和LoadCursor,它们出现在范例程序,定义窗口类别结构的内容设定叙述中。它们从Windows中加载二进制图标和光标映象,并传回该图标或光标的句柄。在本章中,我们先建立自己的图标,它会从程序自己的.EXE文件中载入。
在本书中,我们将讨论这些资源:
- 图示
- 游标
- 字符串
- 自订资源
- 菜单
- 键盘快捷键
- 对话框
- 位图
前六个资源在本章讨论,对话框在第十一章讨论,而位图在第十四章讨论。
使用资源的好处之一,在于程序的许多组件能够连结编译进程序的.EXE文件中。如果没有资源这一个概念,如图标图像之类的二进制文件可能会存放在单独的文件中,.EXE会把它读入内存中使用。或者图标不得不在程序中以字节数组的形式定义(这样就无法看到实际的图标图像了)。作为资源,图标储存在开发者计算机上可单独编辑的文件中,但在编译程序中被连结编译进.EXE文件中。
将图标添加到程序
将资源添加到程序中需要Visual C++ Developer Studio的一些附加功能。对于图示来说,可以使用「Image Editor」(也称为「Graphics Editor」)来绘制图标的图像。该图像被储存在扩展名为.ICO的图示文件中。Developer Studio还产生一个资源描述档(扩展名为.RC的文件,有时也称作资源定义文件),它列出了程序的所有资源和一个让程序引用资源的表头文件(RESOURCE.H)。
因此,您可以看到这些新文件是如何组织在一起的,让我们以建立名为ICONDEMO的新项目开始。像往常一样,在Developer Studio中从File菜单中选择New,然后依次选择 项目页面标签和Win32 Application。在Project Name栏中键入ICONDEMO并单击OK。这时,Developer Studio建立了用于支持工作区和项目的五个文件。这些文件包括文本文件ICONDEMO.DSW、ICONDEMO.DSP和ICONDEMO.MAK(假设当您从 Tools菜单选择Open后,在显示的 Open对话框中,从Build页面标签中选中 Export makefile when saving project file)。现在,让我们像通常那样所做的建立C原始码文件。从 File菜单上选择New,选择Files页面标签,并单击 C++Source File。在File Name栏中键入ICONDEMO.C并单击OK。此时,Developer Studio就建立了一个空的ICONDEMO.C文件。键入程序10-1中的程序,或选择 Insert菜单,然后选择File As Text选项,从本书附上的光盘中复制原始码。
ICONDEMO.C /*-------------------------------------------------------------------------- ICONDEMO.C -- Icon Demonstration Program (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { TCHAR szAppName[] = TEXT ("IconDemo") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("Icon Demo"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static HICON hIcon ; static int cxIcon, cyIcon, cxClient, cyClient ; HDC hdc ; HINSTANCE hInstance ; PAINTSTRUCT ps ; int x, y ; switch (message) { case WM_CREATE : hInstance = ((LPCREATESTRUCT) lParam)->hInstance ; hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ; cxIcon = GetSystemMetrics (SM_CXICON) ; cyIcon = GetSystemMetrics (SM_CYICON) ; return 0 ; case WM_SIZE : cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; for (y = 0 ; y < cyClient ; y += cyIcon) for (x = 0 ; x < cxClient ; x += cxIcon) DrawIcon (hdc, x, y, hIcon) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
如果您试着编译该程序,因为在程序开头引用的RESOURCE.H文件并不存在,所以会产生错误。然而,您不必直接建立RESOURCE.H文件,而是由Developer Studio为您建立一个。
您可以通过将资源描述档添加到项目中来做到这一点。从「File」菜单中选择「New」,选择「Files」页面标签,单击「Resource Script」,在「File Name」栏中键入「ICONDEMO」,单击OK。此时,Developer Studio会建立两个文本文件:ICONDEMO.RC(资源描述档)和RESOURCE.H(允许C原始码文件和资源描述档引用相同的已定义标识符)。不必直接编辑这两个档案,只要让Developer Studio来维护它们就可以。如果您想查看资源描述档和RESOURCE.H而不希望对Developer Studio产生干扰,可以用记事本打开它们。除非您对所做的动作很有把握,否则不要轻易地更改它们。请记住,只有在您下达明确的操作命令或重新编译项目时,Developer Studio才会储存这些文件的新版本。
资源描述档是文本文件。它包括这些资源的可用文字形式表达的描述,例如菜单和对话框。资源描述文件也包括对非文字资源的二进制档案的引用,例如图标和自订的鼠标光标。
现在,已经存在RESOURCE.H文件,您可以试着重新编译一下ICONDEMO。现在会出现一条错误消息,指出IDI_ICON还没被定义。这个标识符第一次出现在下面的叙述中:
wndclass.hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;
在本书前面的程序中,这个叙述是由下面的叙述代替的:
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
之所以改变叙述,是因为以前我们为应用程序使用的是标准的图示,而这里我们的目的是使用自订图示。
那么让我们建立一个图示吧!在Developer Studio的「File View」窗口中,您会看到两个文件-ICONDEMO.C和ICONDEMO.RC。您开启CONDEMO.C后,就可以编辑原始码。开启ICONDEMO.RC后,就可以把资源添加到文件中或编辑已存在的资源。要添加图示的话,请从「 Insert」菜单上选择「Resource」选择您想添加的资源,也就是图示,然后再按下「 New」按钮。
现在呈现的是一个空白的32×32图素的图示,您可以在其中填入颜色。您会看到带有一组绘图工具和可用颜色的浮动工具列。注意颜色工具列中包括两个与颜色无关的选项,这两种颜色选项有时被称为「屏幕颜色」跟「反屏幕颜色」。当一个图素在着色时选择了「屏幕颜色」时,它实际上是透明的。不管图标在什么表面上显示,图示未着色的部分会显示出底色。这样我们就可以建立非矩形的图示。
双击围绕图标的区域,会出现「Icon Properties」对话框,该对话框使您能够更改图标的ID和文件名称。Developer Studio可能已经将ID设定为IDI_ICON1,将它改为IDI_ICON,这样ICONDEMO就可以引用图标(前缀IDI代表「图标的ID」)。同样地,将文件名改为ICONDEMO.ICO。
现在选择一种有特色的颜色(如红色)并在图示上画一个大的B(代表BIG),请注意不必像图10-1那么整齐。
图10-1 显示在Developer Studio中的标准(32×32)ICONDEMO文件 |
此时程序应该能够编译并执行得很好了。Developer Studio将在ICONDEMO.RC资源描述档中划一条横线,表示下面是带有标识符(IDI_ICON)的图示文件(ICONDEMO.ICO)。RESOURCE.H表头文件中会包含IDI_ICON标识符的定义。
Developer Studio通过资源编译器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图素。这是我们在Developer Studio中建立的图示大小,它也是图示出现在桌面上和显示在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」(表示「小」)。
图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 (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Icon IDI_ICON ICON DISCARDABLE "icondemo.ico"
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by IconDemo.rc #define IDI_ICON 101
程序10-2 ICONDEMO.RC和RESOURCE.H文件的摘录 |
程序10-2所显示的ICONDEMO.RC和RESOURCE.H文件与您在普通的文字编辑器中手动建立的很相似,80年代的Windows程序写作者就是这样做的。唯一不同的是AFXRES.H,它是个表头文件,包含了在建立由机器产生的MFC项目时由Developer Studio使用的常用标识符。在本书中,我们不会用到AFXRES.H。
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函数取得此图示的句柄:
hIcon = LoadIcon (hInstance, MAKEINTRESOURCE (IDI_ICON)) ;
请注意ICONDEMO在两个地方呼叫这个函数,一次在定义窗口类别时,另一次在窗口消息处理程序中取得图标的句柄用于绘制。LoadIcon传回HICON型态的值,它是图示的句柄。
LoadIcon的第一个参数,是指出资源来自哪个文件的执行实体句柄。使用hInstance表示它来自程序自己的.EXE文件。LoadIcon的第二个参数实际上被定义为指向字符串的指针。待会将会看到,可以使用字符串而不是用数值标识符标识资源。宏MAKEINTRESOURCE(把整数转换成资源字符串)生成指向非数字的指针,如下所示:
#define MAKEINTRESOURCE(i) (LPTSTR) ((DWORD) ((WORD) (i)))
LoadIcon知道,如果第二个参数的高字组为0,那么低字组就为图示的数值标识符。图标的标识符必须为16位值。
本书前面的范例程序使用了预先定义的图示:
LoadIcon (NULL, IDI_APPLICATION) ;
hInstance参数被设定为NULL,因此Windows知道这是预先定义的图示。IDI_APPLICATION也在WINUSER.H中用MAKEINTRESOURCE定义:
#define IDI_APPLICATION MAKEINTRESOURCE(32512)
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.C /*--------------------------------------------------------------------------- POEPOEM.C -- Demonstrates Custom Resource (c) Charles Petzold, 1998 ----------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { TCHAR szAppName [16], szCaption [64], szErrMsg [64] ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; LoadString ( hInstance, IDS_APPNAME, szAppName, sizeof (szAppName) / sizeof (TCHAR)) ; LoadString ( hInstance, IDS_CAPTION, szCaption, sizeof (szCaption) / sizeof (TCHAR)) ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; 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 ; } hwnd = CreateWindow (szAppName, szCaption, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) { static char * pText ; static HGLOBAL hResource ; static HWND hScroll ; static int iPosition, cxChar, cyChar, cyClient, iNumLines, xScroll ; HDC hdc ; PAINTSTRUCT ps ; RECT rect ; TEXTMETRIC tm ; switch (message) { case WM_CREATE : hdc = GetDC (hwnd) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight + tm.tmExternalLeading ; ReleaseDC (hwnd, hdc) ; xScroll = GetSystemMetrics (SM_CXVSCROLL) ; hScroll = CreateWindow (TEXT ("scrollbar"), NULL, WS_CHILD | WS_VISIBLE | SBS_VERT, 0, 0, 0, 0, hwnd, (HMENU) 1, hInst, NULL) ; hResource = LoadResource (hInst, FindResource (hInst, TEXT ("AnnabelLee"), TEXT ("TEXT"))) ; pText = (char *) LockResource (hResource) ; iNumLines = 0 ; while (*pText != '\\' && *pText != '\0') { if (*pText == '\n') iNumLines ++ ; pText = AnsiNext (pText) ; } *pText = '\0' ; SetScrollRange (hScroll, SB_CTL, 0, iNumLines, FALSE) ; SetScrollPos (hScroll, SB_CTL, 0, FALSE) ; return 0 ; case WM_SIZE : MoveWindow (hScroll, LOWORD (lParam) - xScroll, 0, xScroll, cyClient = HIWORD (lParam), TRUE) ; SetFocus (hwnd) ; return 0 ; case WM_SETFOCUS : SetFocus (hScroll) ; return 0 ; case WM_VSCROLL : switch (wParam) { case SB_TOP : iPosition = 0 ; break ; case SB_BOTTOM : iPosition = iNumLines ; break ; case SB_LINEUP : iPosition -= 1 ; break ; case SB_LINEDOWN : iPosition += 1 ; break ; case SB_PAGEUP : iPosition -= cyClient / cyChar ; break ; case SB_PAGEDOWN : iPosition += cyClient / cyChar ; break ; case SB_THUMBPOSITION : iPosition = LOWORD (lParam) ; break ; } iPosition = max (0, min (iPosition, iNumLines)) ; if (iPosition != GetScrollPos (hScroll, SB_CTL)) { SetScrollPos (hScroll, SB_CTL, iPosition, TRUE) ; InvalidateRect (hwnd, NULL, TRUE) ; } return 0 ; case WM_PAINT : hdc = BeginPaint (hwnd, &ps) ; pText = (char *) LockResource (hResource) ; GetClientRect (hwnd, &rect) ; rect.left += cxChar ; rect.top += cyChar * (1 - iPosition) ; DrawTextA (hdc, pText, -1, &rect, DT_EXTERNALLEADING) ; EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY : FreeResource (hResource) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
POEPOEM.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // TEXT ANNABELLEE TEXT DISCARDABLE "poepoem.txt" ///////////////////////////////////////////////////////////////////////////// // Icon POEPOEM ICON DISCARDABLE "poepoem.ico" ///////////////////////////////////////////////////////////////////////////// // String Table STRINGTABLE DISCARDABLE BEGIN IDS_APPNAME "PoePoem" IDS_CAPTION """Annabel Lee"" by Edgar Allan Poe" IDS_ERRMSG "This program requires Windows NT!" END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by PoePoem.rc #define IDS_APPNAME 1 #define IDS_CAPTION 2 #define IDS_ERRMSG 3
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 |
在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: 菜单句柄
选择码是:
- 0 不显示弹出式菜单
- MF_POPUP 显示弹出式菜单
- MF_SYSMENU 显示系统弹出式菜单
Windows程序通常把该消息传递给DefWindowProc,它一般给Windows传回0,这会使Windows发出哔声。在 第十四章GRAFMENU程序中会看到WM_MENUCHAR消息的使用。
范例程序
让我们来看一个简单的例子。程序10-4所示的MENUDEMO程序,在主菜单中有五个选择项-File、Edit、Background、Timer和Help,每一项都与一个弹出式菜单相连。MENUDEMO只完成了最简单、最通用的菜单处理操作,包括拦截WM_COMMAND消息和检查wParam的低字组。
MENUDEMO.C /*--------------------------------------------------------------------- MENUDEMO.C -- Menu Demonstration (c) Charles Petzold, 1998 -----------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_TIMER 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; TCHAR szAppName[] = TEXT ("MenuDemo") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static int idColor [5] = { WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, BLACK_BRUSH } ; static int iSelection = IDM_BKGND_WHITE ; HMENU hMenu ; switch (message) { case WM_COMMAND: hMenu = GetMenu (hwnd) ; switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: MessageBeep (0) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_BKGND_WHITE: // Note: Logic below case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE case IDM_BKGND_GRAY: // through IDM_BLACK are case IDM_BKGND_DKGRAY: // consecutive numbers in case IDM_BKGND_BLACK: // the order shown here. CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MF_CHECKED) ; SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (idColor [LOWORD (wParam) - IDM_BKGND_WHITE])) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_TIMER_START: if (SetTimer (hwnd, ID_TIMER, 1000, NULL)) { EnableMenuItem (hMenu, IDM_TIMER_START, MF_GRAYED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_ENABLED) ; } return 0 ; case IDM_TIMER_STOP: KillTimer (hwnd, ID_TIMER) ; EnableMenuItem (hMenu, IDM_TIMER_START, MF_ENABLED) ; EnableMenuItem (hMenu, IDM_TIMER_STOP, MF_GRAYED) ; return 0 ; case IDM_APP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd,TEXT ("Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_ICONINFORMATION | MB_OK) ; return 0 ; } break ; case WM_TIMER: MessageBeep (0) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
MENUDEMO.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu MENUDEMO MENU DISCARDABLE 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 "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
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by 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
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说明了这种方法。
POPMENU.C /*---------------------------------------------------------------------- POPMENU.C -- Popup Menu Demonstration (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; HINSTANCE hInst ; TCHAR szAppName[] = TEXT ("PopMenu") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hInst = hInstance ; hwnd = CreateWindow ( szAppName, TEXT ("Popup Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HMENU hMenu ; static int idColor [5] = {WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, BLACK_BRUSH } ; static int iSelection = IDM_BKGND_WHITE ; POINT point ; switch (message) { case WM_CREATE: hMenu = LoadMenu (hInst, szAppName) ; hMenu = GetSubMenu (hMenu, 0) ; return 0 ; case WM_RBUTTONUP: point.x = LOWORD (lParam) ; point.y = HIWORD (lParam) ; ClientToScreen (hwnd, &point) ; TrackPopupMenu (hMenu, TPM_RIGHTBUTTON, point.x, point.y, 0, hwnd, NULL) ; return 0 ; case WM_COMMAND: switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; case IDM_BKGND_WHITE: // Note: Logic below case IDM_BKGND_LTGRAY: // assumes that IDM_WHITE case IDM_BKGND_GRAY: // through IDM_BLACK are case IDM_BKGND_DKGRAY: // consecutive numbers in case IDM_BKGND_BLACK: // the order shown here. CheckMenuItem (hMenu, iSelection, MF_UNCHECKED) ; iSelection = LOWORD (wParam) ; CheckMenuItem (hMenu, iSelection, MF_CHECKED) ; SetClassLong (hwnd, GCL_HBRBACKGROUND, (LONG) GetStockObject (idColor [LOWORD (wParam) - IDM_BKGND_WHITE])) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd, TEXT ("Popup Menu Demonstration Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_ICONINFORMATION | MB_OK) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_APP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_ICONEXCLAMATION | MB_OK) ; return 0 ; } break ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
//Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu 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
// Microsoft Developer Studio generated include file. // Used by 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_APP_HELP 40016 #define IDM_APP_ABOUT 40017
资源描述档POPMENU.RC定义的菜单与MENUDEMO.RC中的菜单非常相似。不同的是,在顶层菜单中只包含一项-一个弹出式菜单「MyMenu」,它呼叫「File」、「Edit」、「Background」和「Help」选项。这四个选项垂直一行地出现在弹出式菜单上,而不是水平一列地出现在主菜单上。
在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」项的弹出式菜单。选择其中任何一项都可以使嵌套的弹出式菜单显示在右边,菜单函数与一般的菜单一样。
如果要使用与该程序的主菜单相同的菜单并带有TrackPopupMenu,您会遇到一些问题,因为函数需要弹出式菜单句柄。在「Microsoft Knowledge Base」文章ID Q99806有提供一些信息。
使用系统菜单
使用WS_SYSMENU样式建立的父窗口,在其标题列的左侧有一个系统菜单按钮。如果您愿意,可以修改这个菜单。在Windows程序设计的早期,程序写作者一般把「About」菜单项放入系统菜单。虽然这种方法不常见,但是修改系统菜单往往是一种在短程序中添加菜单的快速偷懒方法。这里唯一的限制是:在系统菜单中增加的命令其ID值必须小于0xF000;否则它们将会与Windows系统菜单命令所使用的ID值相冲突。还要记住,当您为这些新菜单项在窗口消息处理程序中处理WM_SYSCOMMAND消息时,您必须把其它的WM_SYSCOMMAND消息发送给DefWindowProc。如果您不这样做,那么实际上是禁用了系统菜单上的所有正常选项。
程序10-7中所示的POORMENU(「设计不当的个人菜单」)在系统菜单中加入了一个分隔条和三个命令,最后一个命令将删除这些附加的菜单项。
程序10-7 POORMENU POORMENU.C /*------------------------------------------------------------------------- POORMENU.C -- The Poor Person's Menu (c) Charles Petzold, 1998 --------------------------------------------------------------------------*/ #include <windows.h> #define IDM_SYS_ABOUT 1 #define IDM_SYS_HELP 2 #define IDM_SYS_REMOVE 3 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; static TCHAR szAppName[] = TEXT ("PoorMenu") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HMENU hMenu ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow ( szAppName, TEXT ("The Poor-Person's Menu"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; hMenu = GetSystemMenu (hwnd, FALSE) ; 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")) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc ( HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { switch (message) { case WM_SYSCOMMAND: switch (LOWORD (wParam)) { case IDM_SYS_ABOUT: MessageBox ( hwnd, TEXT ("A Poor-Person's Menu Program\n") TEXT ("(c) Charles Petzold, 1998"), szAppName, MB_OK | MB_ICONINFORMATION) ; return 0 ; case IDM_SYS_HELP: MessageBox ( hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; return 0 ; case IDM_SYS_REMOVE: GetSystemMenu (hwnd, TRUE) ; return 0 ; } break ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
三个菜单ID在POORMENU.C的开始部分定义:
#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的功能被分解为五个新的函数:
-
AppendMenu在菜单尾部添加一个新的菜单项目
-
DeleteMenu删除菜单中一个现有的菜单项并清除该项目
- InsertMenu在菜单中插入一个新项目
- ModifyMenu修改一个现有的菜单项目
- RemoveMenu从菜单中移走某一项目
如果菜单项是一个弹出式菜单,那么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程序中的类似,但是却以另一种顶层菜单显示出来。
NOPOPUPS.C /*------------------------------------------------------------------------- NOPOPUPS.C -- Demonstrates No-Popup Nested Menu (c) Charles Petzold, 1998 -------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("NoPopUps") ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, TEXT ("No-Popup Nested Menu Demonstration"), WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HMENU hMenuMain, hMenuEdit, hMenuFile ; HINSTANCE hInstance ; switch (message) { case WM_CREATE: hInstance = (HINSTANCE) GetWindowLong (hwnd, GWL_HINSTANCE) ; hMenuMain = LoadMenu (hInstance, TEXT ("MenuMain")) ; hMenuFile = LoadMenu (hInstance, TEXT ("MenuFile")) ; hMenuEdit = LoadMenu (hInstance, TEXT ("MenuEdit")) ; SetMenu (hwnd, hMenuMain) ; return 0 ; case WM_COMMAND: switch (LOWORD (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 ; case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_EDIT_UNDO: case IDM_EDIT_CUT: case IDM_EDIT_COPY: case IDM_EDIT_PASTE: case IDM_EDIT_CLEAR: MessageBeep (0) ; return 0 ; } break ; case WM_DESTROY: SetMenu (hwnd, hMenuMain) ; DestroyMenu (hMenuFile) ; DestroyMenu (hMenuEdit) ; PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
NOPOPUPS.RC (摘录) //Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Menu MENUMAIN MENU DISCARDABLE BEGIN MENUITEM "MAIN:", 0, INACTIVE MENUITEM "&File...", IDM_FILE MENUITEM "&Edit...", IDM_EDIT END MENUFILE MENU DISCARDABLE BEGIN MENUITEM "FILE:", 0, INACTIVE MENUITEM "&New", IDM_FILE_NEW MENUITEM "&Open...", IDM_FILE_OPEN MENUITEM "&Save", IDM_FILE_SAVE MENUITEM "Save &As", IDM_FILE_SAVE_AS MENUITEM "(&Main)", IDM_MAIN END MENUEDIT MENU DISCARDABLE BEGIN MENUITEM "EDIT:", 0, INACTIVE MENUITEM "&Undo", IDM_EDIT_UNDO MENUITEM "Cu&t", IDM_EDIT_CUT MENUITEM "&Copy", IDM_EDIT_COPY MENUITEM "&Paste", IDM_EDIT_PASTE MENUITEM "De&lete", IDM_EDIT_CLEAR MENUITEM "(&Main)", IDM_MAIN END
RESOURCE.H (摘录) // Microsoft Developer Studio generated include file. // Used by NoPopups.rc #define IDM_FILE 40001 #define IDM_EDIT 40002 #define IDM_FILE_NEW 40003 #define IDM_FILE_OPEN 40004 #define IDM_FILE_SAVE 40005 #define IDM_FILE_SAVE_AS 40006 #define IDM_MAIN 40007 #define IDM_EDIT_UNDO 40008 #define IDM_EDIT_CUT 40009 #define IDM_EDIT_COPY 40010 #define IDM_EDIT_PASTE 40011 #define IDM_EDIT_CLEAR 40012
在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」菜单的菜单项的功能全部可用;我们将在第十一章中完成「File」功能,在 第十三章中完成「Print」功能。POPPAD2如程序10-9所示。
POPPAD2.C /*--------------------------------------------------------------------------- POPPAD2.C -- Popup Editor Version 2 (includes menu) (c) Charles Petzold, 1998 ---------------------------------------------------------------------------*/ #include <windows.h> #include "resource.h" #define ID_EDIT 1 LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); TCHAR szAppName[] = TEXT ("PopPad2") ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { HACCEL hAccel ; HWND hwnd ; MSG msg ; WNDCLASS wndclass ; wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = szAppName ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) { MessageBox ( NULL, TEXT ("This program requires Windows NT!"), szAppName, MB_ICONERROR) ; return 0 ; } hwnd = CreateWindow (szAppName, szAppName, WS_OVERLAPPEDWINDOW, GetSystemMetrics (SM_CXSCREEN) / 4, GetSystemMetrics (SM_CYSCREEN) / 4, GetSystemMetrics (SM_CXSCREEN) / 2, GetSystemMetrics (SM_CYSCREEN) / 2, NULL, NULL, hInstance, NULL) ; ShowWindow (hwnd, iCmdShow) ; UpdateWindow (hwnd) ; hAccel = LoadAccelerators (hInstance, szAppName) ; while (GetMessage (&msg, NULL, 0, 0)) { if (!TranslateAccelerator (hwnd, hAccel, &msg)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } } return msg.wParam ; } AskConfirmation (HWND hwnd) { return MessageBox ( hwnd, TEXT ("Really want to close PopPad2?"), szAppName, MB_YESNO | MB_ICONQUESTION) ; } LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam,LPARAM lParam) { static HWND hwndEdit ; int iSelect, iEnable ; switch (message) { case WM_CREATE: hwndEdit = CreateWindow (TEXT ("edit"), NULL, WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL | WS_BORDER | ES_LEFT | ES_MULTILINE | ES_AUTOHSCROLL | ES_AUTOVSCROLL, 0, 0, 0, 0, hwnd, (HMENU) ID_EDIT, ((LPCREATESTRUCT) lParam)->hInstance, NULL) ; return 0 ; case WM_SETFOCUS: SetFocus (hwndEdit) ; return 0 ; case WM_SIZE: MoveWindow (hwndEdit, 0, 0, LOWORD (lParam), HIWORD (lParam), TRUE) ; return 0 ; case WM_INITMENUPOPUP: if (lParam == 1) { EnableMenuItem ((HMENU) wParam, IDM_EDIT_UNDO, SendMessage (hwndEdit, EM_CANUNDO, 0, 0) ? MF_ENABLED : MF_GRAYED) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_PASTE, IsClipboardFormatAvailable (CF_TEXT) ? MF_ENABLED : MF_GRAYED) ; iSelect = SendMessage (hwndEdit, EM_GETSEL, 0, 0) ; if (HIWORD (iSelect) == LOWORD (iSelect)) iEnable = MF_GRAYED ; else iEnable = MF_ENABLED ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_CUT, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_COPY, iEnable) ; EnableMenuItem ((HMENU) wParam, IDM_EDIT_CLEAR, iEnable) ; return 0 ; } break ; case WM_COMMAND: if (lParam) { if (LOWORD (lParam) == ID_EDIT && (HIWORD (wParam) == EN_ERRSPACE || HIWORD (wParam) == EN_MAXTEXT)) MessageBox (hwnd, TEXT ("Edit control out of space."), szAppName, MB_OK | MB_ICONSTOP) ; return 0 ; } else switch (LOWORD (wParam)) { case IDM_FILE_NEW: case IDM_FILE_OPEN: case IDM_FILE_SAVE: case IDM_FILE_SAVE_AS: case IDM_FILE_PRINT: MessageBeep (0) ; return 0 ; case IDM_APP_EXIT: SendMessage (hwnd, WM_CLOSE, 0, 0) ; return 0 ; case IDM_EDIT_UNDO: SendMessage (hwndEdit, WM_UNDO, 0, 0) ; return 0 ; case IDM_EDIT_CUT: SendMessage (hwndEdit, WM_CUT, 0, 0) ; return 0 ; case IDM_EDIT_COPY: SendMessage (hwndEdit, WM_COPY, 0, 0) ; return 0 ; case IDM_EDIT_PASTE: SendMessage (hwndEdit, WM_PASTE, 0, 0) ; return 0 ; case IDM_EDIT_CLEAR: SendMessage (hwndEdit, WM_CLEAR, 0, 0) ; return 0 ; case IDM_EDIT_SELECT_ALL: SendMessage (hwndEdit, EM_SETSEL, 0, -1) ; return 0 ; case IDM_HELP_HELP: MessageBox (hwnd, TEXT ("Help not yet implemented!"), szAppName, MB_OK | MB_ICONEXCLAMATION) ; return 0 ; case IDM_APP_ABOUT: MessageBox (hwnd, TEXT ("POPPAD2 (c) Charles Petzold, 1998"), szAppName, MB_OK | MB_ICONINFORMATION) ; return 0 ; } break ; case WM_CLOSE: if (IDYES == AskConfirmation (hwnd)) DestroyWindow (hwnd) ; return 0 ; case WM_QUERYENDSESSION: if (IDYES == AskConfirmation (hwnd)) return 1 ; else return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
//Microsoft Developer Studio generated resource script. #include "resource.h" #include "afxres.h" ///////////////////////////////////////////////////////////////////////////// // Icon POPPAD2 ICON DISCARDABLE "poppad2.ico" ///////////////////////////////////////////////////////////////////////////// // Menu POPPAD2 MENU DISCARDABLE 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 "&Print", IDM_FILE_PRINT MENUITEM SEPARATOR MENUITEM "E&xit", IDM_APP_EXIT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tCtrl+Z", IDM_EDIT_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tCtrl+X", IDM_EDIT_CUT MENUITEM "&Copy\tCtrl+C", IDM_EDIT_COPY MENUITEM "&Paste\tCtrl+V", IDM_EDIT_PASTE MENUITEM "De&lete\tDel", IDM_EDIT_CLEAR MENUITEM SEPARATOR MENUITEM "&Select All", IDM_EDIT_SELECT_ALL END POPUP "&Help" BEGIN MENUITEM "&Help...", IDM_HELP_HELP MENUITEM "&About PopPad2...", IDM_APP_ABOUT END END ///////////////////////////////////////////////////////////////////////////// // Accelerator POPPAD2 ACCELERATORS DISCARDABLE BEGIN VK_BACK, IDM_EDIT_UNDO, VIRTKEY, ALT, NOINVERT VK_DELETE, IDM_EDIT_CLEAR, VIRTKEY, NOINVERT VK_DELETE, IDM_EDIT_CUT, VIRTKEY, SHIFT, NOINVERT VK_F1, IDM_HELP_HELP, VIRTKEY, NOINVERT VK_INSERT, IDM_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT VK_INSERT, IDM_EDIT_PASTE, VIRTKEY, SHIFT, NOINVERT "^C", IDM_EDIT_COPY, ASCII, NOINVERT "^V", IDM_EDIT_PASTE, ASCII, NOINVERT "^X", IDM_EDIT_CUT, ASCII, NOINVERT "^Z", IDM_EDIT_UNDO, ASCII, NOINVERT END
// Microsoft Developer Studio generated include file. // Used by 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_FILE_PRINT 40005 #define IDM_APP_EXIT 40006 #define IDM_EDIT_UNDO 40007 #define IDM_EDIT_CUT 40008 #define IDM_EDIT_COPY 40009 #define IDM_EDIT_PASTE 40010 #define IDM_EDIT_CLEAR 40011 #define IDM_EDIT_SELECT_ALL 40012 #define IDM_HELP_HELP 40013 #define IDM_APP_ABOUT 40014
POPPAD2.ICO |
|
|
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」选项,但是它们现在并不起作用。要处理这些命令,我们需要使用对话框。现在是讨论对话框的时机,也是您准备学习它们的时候了。