将加速键提取为人类可读的文本






4.77/5 (15投票s)
本文介绍了如何提取加速键并将其转换为人类可读的文本,使用了当前用户的语言设置。
引言
我最近遇到了在自定义绘制菜单中绘制加速键的问题。在这篇简短的文章中,我将介绍我解决这个问题的方法。
加速键和菜单
加速键本质上是键盘快捷键,可以映射到应用程序中的不同功能。加速键映射到的功能是控件 ID。与按钮、组合框、列表框以及菜单项相同的 ID。通过这种方式,可以使不同但功能相似的 GUI 对象表示同一个函数。
从生理学角度来看,使用菜单然后使用鼠标是一个坏主意,因此,我们必须让用户可以选择最小化鼠标的使用。这就是为什么在菜单中显示加速键是一个好主意;它告知用户存在快捷键,并且不一定非要使用鼠标。
加速键使用的极致是使用户可以自定义所有按键组合。但这超出了本文的范围。
加速键表
加速键表基本上是一个由三列组成的表,正如此结构定义所示
typedef struct tagACCEL { BYTE fVirt; WORD key; WORD cmd; } ACCEL, *LPACCEL;
fVirt
是一个位组,其中包含按键组合是否包含 Ctrl、Alt 或 Shift。它还指示 key
是虚拟键码还是 ASCII 码。cmd
成员是此加速键关联的 ID。
加速键通常存在于二进制文件的资源部分。在 MFC 应用程序中,您甚至很少看到它们。MFC 和 WTL 允许加速键表和主框架窗口共享同一个 ID。当主框架创建时,它会自动使用自身相同的 ID 加载加速键表。要手动加载加速键,您需要使用 Win32 函数
HACCEL LoadAccelerators(HINSTANCE hInstance, LPCTSTR lpTableName)
hInstance
通常是您可执行文件的实例句柄,但也可能是您动态加载的 DLL 的句柄。lpTableName
是资源名称。由于在典型的 MFC 项目中,您不会有任何名称可用,只有整数资源 ID,因此您需要使用宏 MAKEINTRESOURCE()
。该函数的返回值是加速键的句柄。您无法以文档记录的方式使用此句柄,而只能使用 Win32 提供的函数。
要访问该表本身,您需要将其内容复制到您提供的缓冲区中。该函数
int CopyAcceleratorTable( HACCEL hAccelSrc, LPACCEL lpAccelDst, int cAccelEntries );
将为您完成此操作。为了分配保存整个表的缓冲区,您必须首先确定表的行数。这可以通过调用 CopyAcceleratorTable(hAccelSrc, 0, 0)
来完成。然后,该函数将返回表的行数。然后分配缓冲区,再次调用该函数,但这次传递您的缓冲区地址及其表项中的长度。
这样,我们现在就掌握了如何从资源中提取加速键数据的知识。
虚拟键码、扫描码和名称
键使用虚拟键码或扫描码来识别。扫描码是一种低级代码,用于标识键盘上的键,精确到可以告诉您它在键盘上的确切位置。如果您按下数字键盘上的 insert 键,扫描码将表示该键,而不是箭头键上方的 insert 键。相比之下,虚拟键码并不总是区分这一点。由于这个事实,某些虚拟键码会转换为多个扫描码。当将键转换为文本时,这会成为一个小问题。
我们进行此练习的原因是,我们想将加速键表中的虚拟键码转换为人类可读的文本。我们也希望转换能够符合用户的键盘语言设置。在浏览 MSDN 文档后,您会发现没有虚拟键码到文本的函数可以帮助您完成这项任务。但是,有一个扫描码到文本的函数
int GetKeyNameText( LONG lParam, LPTSTR lpString, int nSize );
这会将原始问题转换为将虚拟键码映射到扫描码的问题。如前所述,这是一个问题。虚拟键码到扫描码的转换并非总是明确的。如果您调用该函数
UINT MapVirtualKey( UINT uCode, UINT uMapType );
将 insert 的虚拟键码映射到扫描码,然后使用扫描码与 GetKeyNameText()
,您将获得文本“NUM INS”。显然,这并非您希望在菜单中显示的内容。“NUM”部分会混淆用户,而且可以说是一种谎言,因为任何 insert 键都可以。
这个问题可以追溯到 AT 兼容键盘推出之时。较旧的 XT 兼容键盘在字母数字键盘和数字键盘之间没有键(并且只有 10 个功能键,但这对我们来说没问题)。如果您仔细查看键盘,您会发现在数字键盘上的键布局与“扩展”部分的键盘布局相同。为了区分这两组之间的扫描码,在扫描码中添加了一个 *扩展位*。
扩展位(28,256d,100h)将用于键盘扩展部分上的那些键。如果将此位与 GetKeyNameText()
一起使用,将从文本中删除任何“NUM”,一切看起来都很棒。
因此,要将加速键转换为非混淆的人类可读格式,您可以这样做
// pseudo code - see source code for real code String s; if(accel[i].fVirt & FALT) s += GetKeyNameText(MapVirtualKey(VK_MENU)); if(accel[i].fVirt & FCONTROL) { if(s) s += "+"; s += GetKeyNameText(MapVirtualKey(VK_CONTROL)); } if(accel[i].fVirt & FSHIFT) { if(s) s += "+"; s += GetKeyNameText(MapVirtualKey(VK_SHIFT)); } if(accel[i].fVirt & FVIRTKEY) { scancode = MapVirtualKey(accel[i].key); switch(accel[i].key) { case VK_INSERT: case VK_DELETE: case VK_HOME: case VK_END: case VK_NEXT: // Page down case VK_PRIOR: // Page up case VK_LEFT: case VK_RIGHT: case VK_UP: case VK_DOWN: scancode |= 0x100; // Add extended bit } s += GetKeyNameText(scancode); } else { // ASCII key code s += (char)accel[i].key; } // s is now Shift+Ctrl+Ins for instance
源代码
本文附带的源代码包含三个函数
bool GetAcceleratorTexts(HACCEL hAccel, std::map<UINT, CString>& mapId2AccelText);
bool GetAcceleratorTexts(HINSTANCE hInst, LPCTSTR lpszAccelRes, std::map<UINT, CString>& mapId2AccelText);
bool GetAcceleratorTexts(HINSTANCE hInst, int nId, std::map<UINT, CString>& mapId2AccelText);
如您所见,它依赖于 CString
,因此您需要 MFC、ATL 或 WTL 才能使用此代码。只需稍作修改,您就可以使其与 std::basic_string<>
一起正常工作。所有三个函数都做同样的事情,只是接受不同的输入。
第三个参数 mapId2AccelText
将在函数成功返回时保存文本。顾名思义,它将 ID 与每个加速键文本进行映射。