■为什么使用快捷键
你可能会问:为什么我应该使用快捷键?为什么我不能捕捉WM_KEYDOWN或 WM_CHAR消息并自己复制菜单功能?它有什么优点?对一个单窗口程序,你当然可以捕捉键盘消息。但是使用快捷键的一个简单优点是,你不需要重写菜单和快捷键处理逻辑。
对有多个窗口和窗口过程的应用程序,快捷键变得十分重要。正如我们所见,对拥有当前输入焦点的窗口,Windows会将键盘消息发送给它的窗口过程。然而,对快捷键而言,Windows会将WM_COMMAND消息发送给一个句柄在Windows函数 TranslateAccelerator中指定的窗口程序。通常,这会是你的主窗口,亦即拥有菜单的那个窗口,这意味着响应快捷键的逻辑不需要被复制到每一个窗口过程。
如果你使用非模态对话框(在第十章讨论)或在主窗口的客户区内使用子窗口,这个优 点就变得极其重要。如果给多个窗口定义了同一个快捷键,只需在其中一个窗口包含这个逻辑即可。子窗口不会接收到来自快捷键的WM_COMMAND消息。
■指定加速键的一些规则
理论上,你可以使用几乎任何虚拟键或字符键与Shift键、Ctrl键或Alt键的组合来定义快捷键。然而,你应该试图和其他应用程序保持一致,以避免干扰Windows对键盘的使用。你应该避免使用Tab、回车键、Esc和空格键作为快捷键,因为它们通常保留给系统功能。
快捷键最通常的用途是用于程序中Edit菜单的菜单项。对这些菜单项推荐使用的快捷键在Windows 3.0和Windows 3.1之间有变化,因此同时支持新旧两种快捷键很常见,如下表所示:
功能 | 旧快捷键 | 新快捷键 |
Undo(撤销) | Alt+Backspace | Ctrl+Z |
Cut(剪切) | Shilft+Del | Ctrl+X |
Copy(复制) | Ctrl+Ins | Ctrl+C |
Paste(粘贴) | Shilft+Ins | Ctrl+V |
Delete或Clear(删除) | Del | Del |
另一个很常见的快捷键是F1功能键,它用来激活帮助。应该避免使用F4、F5和F6 键,因为它们经常被多文档界面(Multiple Document Interface, MDI)程序用作特殊功能,我们会在第十八章中讨论MDI。
■加速键表
你可以在VS中定义快捷键表。为了方便在你的程序中加载快捷键表,可以使它与应用程序同名(这也适用于菜单和图标)。
每个快捷键有一个ID和一个击键组合,这些你可以在Accelerator Properties对话框中 定义。如果你已经定义了菜单,菜单ID会存在于组合框中,你不需要重新输入它们。
快捷键可以是虚拟键代码或ASCII字符与Shift、Ctrl或Alt键的组合。你可以在字母前加A来指定ASCII字符与Ctrl键的组合。你也可以从组合框中选择虚拟键代码。
在为菜单项定义快捷键时,应当在菜单文本中包含相应的键组合。Tab字符(\t)能将文本和快捷键分开,这样快捷键会排列在第二列。为了在菜单中标识快捷键,可以使用文字Ctrl、Shift或Alt,后面跟着一个加号和键名(例如Shift+F6或Ctrl+F6)。
■加载快捷键表
在应用程序中,应使用LoadAccelerators函数来把快捷键表加载到内存并获得它的句柄。LoadAccelerators 语句和 Loadlcon、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) ;
}
}
Translate Accelerator函数确定保存在msg消息结构中的消息是否是键盘消息。如果是,该函数在快捷键表中寻找句柄为hAccel的匹配值。如果找到匹配值,它会调用句柄为hwnd 的窗口过程。如果快捷键ID对应系统菜单的一个菜单项,则相应消息为 WM_SYSCOMMAND;否则,消息是 WM_COMMAND。
当TranslateAccelerator返回时,如果消息被翻译过(并且已被发送给窗口过程),则返回值为非零值,否则返回值为零。如果TranslateAccelerator返回非零值,你就不应该再调用 TranslateMessage 和 DispatchMessage,而是应该返回 GetMessage 循环。
TranslateAccelerator中的hwnd参数看起来不太合适,因为消息循环中的三个函数都不需要它。并且,消息结构自身(结构变量msg)有一个名字为hwnd的成员,它也是一个窗口句柄。
这个函数之所以有些不同的原因是:msg结构的字段是由GetMessage调用来填充的。 当GetMessage的第二个参数是NULL时,它检索属于该应用程序的所有窗口的消息。当 GetMessage返回时,msg结构的hwnd成员是将会得到该消息的窗口句柄。然而,当 TranslateAccelerator 将键盘消息翻译成 WM_COMMAND 或 WM_SYSCOMMAND 消息时, 它将msg.hwnd窗口句柄替换成该函数第一个参数所指定的窗口句柄。于是Windows会将所有快捷键消息发送给同一个窗口,即使程序中另外一个窗口当前拥有输入焦点。当模态对话框或消息框拥有输入焦点时,TranslateAccelerator不翻译键盘消息,因为这些窗口的消息不通过程序的消息循环。
某些情况下,当应用程序中另一个窗口(比如非模态对话框)拥有输入焦点时,你并不想翻译键盘消息。如何处理这种情况将在第十一章介绍。
■接收加速键消息
当一个快捷键对应系统菜单的一个菜单项时,TranslateAccelerator会向窗口过程发送一条WM_SYSCOMMAND消息;否则,TranslateAccelerator向窗口过程发送一条 WM_COMMAND消息。下表列出了对于快捷键、菜单命令和子窗口控件你会接收到的WM_COMMAND消息的类型:
快捷键 | 菜单 | 控件 | |
LOWORD(wParam) | 快捷键ID | 菜单ID | 控件ID |
HIWORD(wParam) | 1 | 0 | 通知码 |
lParam | 0 | 0 | 子窗口句柄 |
如果快捷键对应某个菜单项,那么窗口过程还会接收到WM_INITMENU,M_INITMENUPOPUP和WM_MENUSELECT消息,就像菜单项被选择了一样。在处理 WM_INITMENUPOPUP时,程序通常可以启用或禁用弹出菜单的菜单项。使用快捷键时,你仍然拥有这种机制。当一个快捷键对应一个禁用或变灰的菜单项时, TranslateAccelerator 不会向窗口过程发送 WM_COMMAND 或 WM_SYSCOMMAND 消息。
如果当前窗口被最小化,对于映射到启用的系统菜单项的快捷键, TranslateAccelerator 将向窗口过程发送 WM_S YSCOMMAND 消息而不是 WM_COMMAND 消息。对于没有映射到任何菜单项的快捷键,TranslateAccelerator也会向窗口过程发送 WM_COMMAND 消息。
9.3.2 第62练:带有菜单和快捷键的简单记事本程序
/*------------------------------------------------------------------------
062 WIN32 API 每日一练
第62个例子POPPAD2.C:带有菜单和加速键的简单记事本程序
TranslateAccelerator函数
LoadAccelerators函数
自定义函数AskConfirmation函数
WM_INITMENUPOPUP消息
EnableMenuItem函数
IsClipboardFormatAvailable 函数
WM_QUERYENDSESSION消息
(c) www.bcdaren.com 编程达人
-----------------------------------------------------------------------*/
#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, LPSTR lpCmdLine, int nShowCmd)
{
HACCEL hAccel;
..(略)
//获取加速键资源句柄
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_AUTOVSCROLL |
ES_AUTOHSCROLL,
0, 0, 0, 0, hwnd, (HMENU)ID_EDIT,
((LPCREATESTRUCT)lparam)->hInstance, NULL);
SendMessage(hwndEdit, EM_SETLIMITTEXT, 0, 1);//设置最大字符数
return 0;
case WM_SETFOCUS:
SetFocus(hwndEdit);
return 0;
case WM_SIZE:
MoveWindow(hwndEdit, 0, 0, LOWORD(lparam), HIWORD(lparam), TRUE);
return 0;
//下拉菜单或子菜单即将变为活动状态时发送,wParam下拉菜单或子菜单的句柄。
//lParam低序位字指定打开下拉菜单或子菜单的菜单项的从零开始的相对位置。
//高阶字指示下拉菜单是否为 "窗口" 菜单。 如果菜单为 "窗口" 菜单,
//则此参数为 TRUE;否则为 FALSE。
case WM_INITMENUPOPUP:
if (LOWORD(lparam) == 1) //Edit = 1 弹出菜单将要被 显示
{
//撤消(UNDO)菜单设置——向编辑控件查询EM_CANUNDO是否可以撤消,
//SendMessage非零启用,零变灰
EnableMenuItem((HMENU)wparam, ID_EDIT_UNDO,
SendMessage(hwndEdit, EM_CANUNDO, 0, 0) ? //确定编辑控件的撤消
队列中是否有任何操作
MF_ENABLED : MF_GRAYED);
//Paste菜单项:检查剪切板是否有文本,有启用,无变灰
EnableMenuItem((HMENU)wparam, ID_EDIT_PASTE,
IsClipboardFormatAvailable(CF_TEXT) ? //剪切板是否有文字
MF_ENABLED : MF_GRAYED);
//检查是否有文本被选中
iSelect = SendMessage(hwndEdit, EM_GETSEL, 0, 0);
//LOWORD(iSelect) :第1个被选中的字符
//HIWORD(iSelect) : 结束位置,选中文本后面的第一个字符。
if (HIWORD(iSelect) == LOWORD(iSelect))//没有被选中的文本
iEnable = MF_GRAYED;//变灰
else
iEnable = MF_ENABLED;//启用
//启用,禁用或显示菜单项
EnableMenuItem((HMENU)wparam, ID_EDIT_CUT, iEnable);
EnableMenuItem((HMENU)wparam, ID_EDIT_COPY, iEnable);
EnableMenuItem((HMENU)wparam, ID_EDIT_CLEAR, iEnable);
return 0;
}
break;
case WM_COMMAND:
if (lparam)//控件消息
{
if (LOWORD(wparam) == ID_EDIT &&
(HIWORD(wparam) == EN_ERRSPACE ||
HIWORD(wparam) == EN_MAXTEXT))
MessageBox(NULL, TEXT("Edit control out of space."),
szAppName, MB_OK | MB_ICONSTOP);
return 0;
}
else switch (LOWORD(wparam))
{
case ID_FILE_NEW:
case ID_FILE_OPEN:
case ID_FILE_SAVE:
case ID_FILE_SAVE_AS:
case ID_FILE_PRINT:
MessageBeep(0);
return 0;
case ID_FILE_EXIT:
SendMessage(hwnd, WM_CLOSE, 0, 0);//向窗口过程发送WM_CLOSE
return 0;
case ID_EDIT_UNDO:
SendMessage(hwndEdit, WM_UNDO, 0, 0);
return 0;
case ID_EDIT_CUT:
SendMessage(hwndEdit, WM_CUT, 0, 0);
return 0;
case ID_EDIT_COPY:
SendMessage(hwndEdit, WM_COPY, 0, 0);
return 0;
case ID_EDIT_PASTE:
SendMessage(hwndEdit, WM_PASTE, 0, 0);
return 0;
case ID_EDIT_CLEAR:
SendMessage(hwndEdit, WM_CLEAR, 0, 0);
return 0;
case ID_EDIT_SELECT_ALL:
SendMessage(hwndEdit, EM_SETSEL, 0, -1);
return 0;
case ID_HELP_HELP:
MessageBox(hwnd, TEXT("Help not yer implemented!"),
szAppName, MB_OK | MB_ICONEXCLAMATION);
return 0;
case ID_HELP_ABOUT:
MessageBox(hwnd, TEXT("POPPAD2 (C) www.bcdaren.com 编程达人"),
szAppName, MB_OK | MB_ICONEXCLAMATION);
return 0;
}
break;
//选择窗口关闭按钮时收到该消息
case WM_CLOSE:
if (IDYES == AskConfirmation(hwnd))
DestroyWindow(hwnd);
return 0;
//系统关机或注销时收到该消息
case WM_QUERYDRAGICON:
if (IDYES == AskConfirmation(hwnd))
return 1;//Windows会关机
else
return 0;//Windows不会关机
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wparam, lparam);
}
/*****************************************************************************
TranslateAccelerator函数:处理菜单命令的加速键。
该函数将WM_KEYDOWN或WM_SYSKEYDOWN消息转换为WM_COMMAND或WM_SYSCOMMAND消息
(如果指定的加速器表中有键的条目),然后将WM_COMMAND或WM_SYSCOMMAND
消息直接发送到指定的窗口过程。在窗口过程处理完消息之前,TranslateAccelerator不会返回。
int TranslateAcceleratorA(
HWND hWnd, //要转换其消息的窗口的句柄
HACCEL hAccTable,//加速器资源句柄。
LPMSG lpMsg//指向MSG结构的指针,该结构包含使用GetMessage或PeekMessage函数从调用线程的消息队列中检索到的消息信息。
);
返回值
类型:int
如果函数成功,则返回值为非零。
如果函数失败,则返回值为零。
*****************************************************************************
WM_INITMENUPOPUP消息:当下拉菜单或子菜单即将变为活动状态时发送。这允许应用程序在菜单显示之前对其进行修改,而无需更改整个菜单。
参数
wParam:下拉菜单或子菜单的句柄。
lParam:
低位字指定打开下拉菜单或子菜单的菜单项的从零开始的相对位置。
高位字表示下拉菜单是否为窗口菜单。如果菜单是窗口菜单,则此参数为TRUE;否则为FALSE。
*****************************************************************************
LoadAccelerators函数:加载指定的加速键表
HACCEL LoadAcceleratorsA(
HINSTANCE hInstance,//模块的句柄,其可执行文件包含要加载的加速器表
LPCSTR lpTableName//加速键表名称
);
自定义函数AskConfirmation函数:显示一个消息框,要求用户对关闭程序进 行确认
当Yes按钮被选中时,消息框(以及AskConfirmation函数)返回IDYES。
只有这时POPPAD2 才调用Destroy Window。否则,这个程序不会终止。
如果想在结束程序前得到确认,还必须处理WM_QUERYENDSESSION消息。当用户选择关闭Windows时,
Windows开始向每一个窗口过程发送WM_QUERYENDSESSION 消息。
如果有任何一个窗口过程对此消息返回0, Windows将不会被终止。
*****************************************************************************
EnableMenuItem函数:启用,禁用或显示指定的菜单项
BOOL EnableMenuItem(
HMENU hMenu, //菜单句柄
UINT uIDEnableItem,//确定的要启用,禁用或呈灰色的菜单项
UINT uEnable //控制uIDEnableItem参数的解释,并指示菜单项是启用,禁用还是灰色
);
*****************************************************************************
IsClipboardFormatAvailable 函数:确定剪贴板是否包含指定格式的数据
BOOL IsClipboardFormatAvailable(
UINT format//标准或注册剪贴板格式
);
*****************************************************************************
WM_QUERYENDSESSION消息:Windows在关机的时候会想(向)所有顶层窗口广播一个消息WM_QUERYENDSESSION,
其lParam参数可以区分是关机还是注销用户(注销用 户时lParam是ENDSESSION_LOGOFF)。
然后Windows会等到所有的应用程序都对这个消息返回TRUE才会关机,
因此,只要我们的应用程序对这个消息的处理返回FALSE,Windows就不会关机了。
*/
062_POPPAD2.rc资源脚本文件中包含快捷键和菜单两个资源:
/
//
// Menu
//
POPPAD2 MENU
BEGIN
POPUP "FILE"
BEGIN
MENUITEM "NEW", ID_FILE_NEW
MENUITEM "OPEN", ID_FILE_OPEN
MENUITEM "SAVE", ID_FILE_SAVE
MENUITEM "SAVE_AS", ID_FILE_SAVE_AS
MENUITEM "PRINT", ID_FILE_PRINT
MENUITEM "EXIT", ID_FILE_EXIT
END
POPUP "EDIT"
BEGIN
MENUITEM "UNDO\tCtrl+Z", ID_EDIT_UNDO
MENUITEM "CUT\tCtrl+X", ID_EDIT_CUT
MENUITEM "COPY\tCtrl+C", ID_EDIT_COPY
MENUITEM "PASTE\tCtrl+V", ID_EDIT_PASTE
MENUITEM "SELECT_ALL\tCtrl+S", ID_EDIT_SELECT_ALL
MENUITEM "CLEAR\t DEL", ID_EDIT_CLEAR
END
POPUP "HELP"
BEGIN
MENUITEM "HELP", ID_HELP_HELP
MENUITEM "ABOUT", ID_HELP_ABOUT
END
END
/
//
// Accelerator
//
POPPAD2 ACCELERATORS
BEGIN
VK_DELETE, ID_EDIT_CLEAR, VIRTKEY, NOINVERT
"C", ID_EDIT_COPY, VIRTKEY, CONTROL, NOINVERT
"X", ID_EDIT_CUT, VIRTKEY, CONTROL, NOINVERT
"V", ID_EDIT_PASTE, VIRTKEY, CONTROL, NOINVERT
"S", ID_EDIT_SELECT_ALL, VIRTKEY, CONTROL, NOINVERT
"Z", ID_EDIT_UNDO, VIRTKEY, CONTROL, NOINVERT
END
【注意】添加快捷键时,刚好和菜单ID对应。
运行结果:
图9-16 带有菜单和快捷键的记事本
总结
实例POPPAD2.C在实例POPPAD1.C的基础上增加了菜单和快捷键操作。我们来看一下具体实现过程。
在WinMain主程序中:
●初始化窗口类的时候,添加菜单项。
wndclass.lpszMenuName = szAppName;
●创建完窗口之后,加载快捷键资源。
//获取加速键资源句柄
hAccel = LoadAccelerators(hInstance,szAppName);
●消息循环中,过滤快捷键:
while (GetMessage(&msg,NULL,0,0))
{
//处理键盘加速键
if (!TranslateAccelerator(hwnd,hAccel,&msg))
{
//非键盘加速键消息的处理
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
TranslateAccelerator函数会将WM_KEYDOWN或WM_SYSKEYDOWN ( 消息转换为WM_COMMAND或WM_SYSCOMMAND消息。通过TranslateAccelerator函数来判断(返回值为0,表示非快捷键消息,返回值非0,表示是快捷键消息),如果是快捷键消息则直接转换为WM_COMMAND菜单消息。如果不是快捷键消息,则还是走原来的流程,调用TranslateMessage函数和DispatchMessage函数。
窗口过程:
●WM_CREATE消息:创建一个edit编辑控件,并设定最大接收字符数。
WM_INITMENUPOPUP消息:下拉菜单或子菜单即将变为活动状态时发送WM_INITMENUPOPUP消息。
wParam下拉菜单或子菜单的句柄。
lParam低序位字指定打开下拉菜单或子菜单的菜单项的从零开始的相对位置。
高阶字指示下拉菜单是否为“窗口”菜单。 如果菜单为“窗口”菜单,则此参数为TRUE;否则为 FALSE。
如果LOWORD(lparam) == 1,表示为Edit菜单项(主菜单从0开始排序,0为FILE菜单)。调用SendMessage函数向edit控件发送EM_CANUNDO消息,查询是否有撤销操作,如果有,则EnableMenuItem函数启用ID_EDIT_UNDO菜单项,否则显灰禁用该子菜单。
IsClipboardFormatAvailable函数查询当前剪切板是否有内容,如果有,则将ID_EDIT_PASTE子菜单启用,如果没有,则禁用。
最后调用SendMessage函数项edit控件发送EM_GETSEL消息,查询是否当前有选中字符,如果有则启用ID_EDIT_CUT、ID_EDIT_COPY和ID_EDIT_CLEAR菜单,否则禁用。
●WM_COMMAND消息:如果该消息的lParam参数非0,则表示为控件发送的WM_COMMAND消息,如果edit控件控件不足或字符已满,则弹出提示信息。
如果是菜单发送的WM-command消息,则通过调用SendMessage函数向edit控件发送WM_UNDO、WM_COPY、WM_CUT 、WM_PASTE、WM_CLEAR、EM_SETSEL消息,分别对应UNDO 、CUT 、COPY、PASTE、CLEAR、SETSEL_ALL菜单,HELP和ABOUT菜单弹出MessageBox框提示信息。
●WM_CLOSE消息:调用自定义的AskConfirmation函数询问用户是否关闭窗口。
●WM_QUERYDRAGICON消息:当系统关机或注销时收到该消息,然后同样是调用自定义的AskConfirmation函数询问用户是否关闭窗口。