三、Windows字符消息
前面讨论了利用位移状态信息把按键消息翻译为字符消息的方法,并且提到,仅利用转换状态信息还不够,因为还需要知道与国家/地区有关的键盘配置。由于这个原因,您不应该试图把按键消息翻译为字符代码。Windows会为您完成这一工作,在前面我们曾看到过以下的程序代码:
while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; }
这是WinMain中典型的消息循环。GetMessage函数用队列中的下一个消息填入msg结构的字段。DispatchMessage以此消息为参数呼叫适当的窗口消息处理程序。
在这两个函数之间是TranslateMessage函数,它将按键消息转换为字符消息。如果消息为WM_KEYDOWN或者WM_SYSKEYDOWN,并且按键与位移状态相组合产生一个字符,则TranslateMessage把字符消息放入消息队列中。此字符消息将是GetMessage从消息队列中得到的按键消息之后的下一个消息。
四类字符消息
字符消息可以分为四类,如表6-9所示。
表6-9 |
字符 |
死字符 |
|
非系统字符 |
WM_CHAR |
WM_DEADCHAR |
系统字符 |
WM_SYSCHAR |
WM_SYSDEADCHAR |
WM_CHAR和WM_DEADCHAR消息是从WM_KEYDOWN得到的;而WM_SYSCHAR和WM_SYSDEADCHAR消息是从WM_SYSKEYDOWN消息得到的(我将简要地讨论一下什么是死字符)。
有一个好消息:在大多数情况下,Windows程序会忽略除WM_CHAR之外的任何消息。伴随四个字符消息的lParam参数与产生字符代码消息的按键消息之lParam参数相同。不过,参数wParam不是虚拟键码。实际上,它是ANSI或Unicode字符代码。
这些字符消息是我们将文字传递给窗口消息处理程序时遇到的第一个消息。它们不是唯一的消息,其它消息伴随以0结尾的整个字符串。窗口消息处理程序是如何知道该字符是8位的ANSI字符还是16位的Unicode宽字符呢?很简单:任何与您用RegisterClassA(RegisterClass的ANSI版)注册的窗口类别相联系的窗口消息处理程序,都会获得含有ANSI字符代码的消息。如果窗口消息处理程序用RegisterClassW(RegisterClass的宽字符版)注册,那么传递给窗口消息处理程序的消息就带有Unicode字符代码。如果程序用RegisterClass注册窗口类别,那么在UNICODE标识符被定义时就呼叫RegisterClassW,否则呼叫RegisterClassA。
除非在程序写作的时候混合了ANSI和Unicode的函数与窗口消息处理程序,用WM_CHAR消息(及其它三种字符消息)说明的字符代码将是:
(TCHAR) wParam
同一个窗口消息处理程序可能会用到两个窗口类别,一个用RegisterClassA注册,而另一个用RegisterClassW注册。也就是说,窗口消息处理程序可能会获得一些ANSI字符代码消息和一些Unicode字符代码消息。如果您的窗口消息处理程序需要晓得目前窗口是否处理Unicode消息,则它可以呼叫:
fUnicode = IsWindowUnicode (hwnd) ;
如果hwnd的窗口消息处理程序获得Unicode消息,那么变量fUnicode将为TRUE,这表示窗口是用RegisterClassW注册的窗口类别。
消息顺序
因为TranslateMessage函数从WM_KEYDOWN和WM_SYSKEYDOWN消息产生了字符消息,所以字符消息是夹在按键消息之间传递给窗口消息处理程序的。例如,如果Caps Lock未打开,而使用者按下再释放A键,则窗口消息处理程序将接收到如表6-10所示的三个消息:
表6-10 |
消息 |
按键或者代码 |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「a」的字符代码(0x61) |
WM_KEYUP |
「A」的虚拟键码(0x41) |
如果您按下Shift键,再按下A键,然后释放A键,再释放Shift键,就会输入大写的A,而窗口消息处理程序会接收到五个消息,如表6-11所示:
表6-11 |
消息 |
按键或者代码 |
WM_KEYDOWN |
虚拟键码VK_SHIFT (0x10) |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「A」的字符代码(0x41) |
WM_KEYUP |
「A」的虚拟键码(0x41) |
WM_KEYUP |
虚拟键码VK_SHIFT(0x10) |
Shift键本身不产生字符消息。
如果使用者按住A键,以使自动重复产生一系列的按键,那么对每条WM_KEYDOWN消息,都会得到一条字符消息,如表6-12所示:
表6-12 |
消息 |
按键或者代码 |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「a」的字符代码(0x61) |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「a」的字符代码(0x61) |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「a」的字符代码(0x61) |
WM_KEYDOWN |
「A」的虚拟键码(0x41) |
WM_CHAR |
「a」的字符代码(0x61) |
WM_KEYUP |
「A」的虚拟键码(0x41) |
如果某些WM_KEYDOWN消息的重复计数大于1,那么相应的WM_CHAR消息将具有同样的重复计数。
组合使用Ctrl键与字母键会产生从0x01(Ctrl-A)到0x1A(Ctrl-Z)的ASCII控制代码,其中的某些控制代码也可以由表6-13列出的键产生:
表6-13 |
按键 |
字符代码 |
产生方法 |
ANSI C控制字符 |
Backspace |
0x08 |
Ctrl-H |
\b |
Tab |
0x09 |
Ctrl-I |
\t |
Ctrl-Enter |
0x0A |
Ctrl-J |
\n |
Enter |
0x0D |
Ctrl-M |
\r |
Esc |
0x1B |
Ctrl-[ |
|
最右列给出了在ANSI C中定义的控制字符,它们用于描述这些键的字符代码。
有时Windows程序将Ctrl与字母键的组合用作菜单快捷键(我将在第十章讨论),此时,不会将字母键转换成字符消息。
处理控制字符
处理按键和字符消息的基本规则是:如果需要读取输入到窗口的键盘字符,那么您可以处理WM_CHAR消息。如果需要读取光标键、功能键、Delete、Insert、Shift、Ctrl以及Alt键,那么您可以处理WM_KEYDOWN消息。
但是Tab键怎么办?Enter、Backspace和Escape键又怎么办?传统上,这些键都产生表6-13列出的ASCII控制字符。但是在Windows中,它们也产生虚拟键码。这些键应该在处理WM_CHAR或者在处理WM_KEYDOWN期间处理吗?
经过10年的考虑(回顾这些年来我写过的Windows程序代码),我更喜欢将Tab、Enter、Backspace和Escape键处理成控制字符,而不是虚拟键。我通常这样处理WM_CHAR:
case WM_CHAR: //其它行程序 switch (wParam) { case '\b': // backspace //其它行程序 break ; case '\t': // tab //其它行程序 break ; case '\n': // linefeed //其它行程序 break ; case '\r': // carriage return //其它行程序 break ; default: // character codes //其它行程序 break ; } return 0 ;
死字符消息
Windows程序经常忽略WM_DEADCHAR和WM_SYSDEADCHAR消息,但您应该明确地知道死字符是什么,以及它们工作的方式。
在某些非U.S.英语键盘上,有些键用于给字母加上音调。因为它们本身不产生字符,所以称之为「死键」。例如,使用德语键盘时,对于U.S.键盘上的+/=键,德语键盘的对应位置就是一个死键,未按下Shift键时它用于标识锐音,按下Shift键时则用于标识抑音。
当使用者按下这个死键时,窗口消息处理程序接收到一个wParam等于音调本身的ASCII或者Unicode代码的WM_DEADCHAR消息。当使用者再按下可以带有此音调的字母键(例如A键)时,窗口消息处理程序会接收到WM_CHAR消息,其中wParam等于带有音调的字母「a」的ANSI代码。
因此,使用者程序不需要处理WM_DEADCHAR消息,原因是WM_CHAR消息已含有程序所需要的所有信息。Windows的做法甚至还设计了内部错误处理。如果在死键之后跟有不能带此音调符号的字母(例如「s」),那么窗口消息处理程序将在一行接收到两条WM_CHAR消息-前一个消息的wParam等于音调符号本身的ASCII代码(与传递到WM_DEADCHAR消息的wParam值相同),第二个消息的wParam等于字母s的ASCII代码。
当然,要感受这种做法的运作方式,最好的方法就是实际操作。您必须加载使用死键的外语键盘,例如前面讲过的德语键盘。您可以这样设定:在「控制台」中选择「键盘」,然后选择「语系」页面标签。然后您需要一个应用程序,该程序可以显示它接收的每一个键盘消息的详细信息。下面的KEYVIEW1就是这样的程序。