字符消息
前面讨论了利用位移状态信息把按键消息翻译为字符消息的方法,并且提到,仅利用转换状态信息还不够,因为还需要知道与国家/地区有关的键盘配置。由于这个原因,您不应该试图把按键消息翻译为字符代码。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就是这样的程序。
键盘消息和字符集
本章剩下的范例程序有缺陷。它们不能在所有版本的Windows下都正常执行。这些缺陷不是特意引过程序代码中的;事实上,您也许永远不会遇到这些缺陷。只有在不同的键盘语言和键盘布局间切换,以及在多字节字符集的远东版Windows下执行程序时,这些问题才会出现-所以我不愿将它们称为「错误」。
不过,如果程序使用Unicode编译并在Windows NT下执行,那么程序会执行得更好。我在第二章提到过这个问题,并且展示了Unicode对简化棘手的国际化问题的重要性。
KEYVIEW1程序
了解键盘国际化问题的第一步,就是检查Windows传递给窗口消息处理程序的键盘内容和字符消息。程序6-2所示的KEYVIEW1会对此有所帮助。该程序在显示区域显示Windows向窗口消息处理程序发送的8种不同键盘消息的全部信息。
KEYVIEW1.C /*--------------------------------------------------------------------- KEYVIEW1.C --Displays Keyboard and Character Messages (c) Charles Petzold, 1998 ---------------------------------------------------------------------*/ #includeLRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ; int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow) { static TCHAR szAppName[] = TEXT ("KeyView1") ; 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 ("Keyboard Message Viewer #1"), 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 cxClientMax, cyClientMax, cxClient, cyClient, cxChar, cyChar ; static int cLinesMax, cLines ; static PMSG pmsg ; static RECT rectScroll ; static TCHAR szTop[] = TEXT ("Message Key Char ") TEXT ("Repeat Scan Ext ALT Prev Tran") ; static TCHAR szUnd[] = TEXT ("_______ ___ ____ ") TEXT ("______ ____ ___ ___ ____ ____") ; static TCHAR * szFormat[2] = { TEXT ("%-13s %3d %-15s%c%6u %4d %3s %3s %4s %4s"), TEXT ("%-13s 0x%04X%1s%c %6u %4d %3s %3s %4s %4s") } ; static TCHAR * szYes = TEXT ("Yes") ; static TCHAR * szNo = TEXT ("No") ; static TCHAR * szDown = TEXT ("Down") ; static TCHAR * szUp = TEXT ("Up") ; static TCHAR * szMessage [] = { TEXT ("WM_KEYDOWN"), TEXT ("WM_KEYUP"), TEXT ("WM_CHAR"), TEXT ("WM_DEADCHAR"), TEXT ("WM_SYSKEYDOWN"),TEXT ("WM_SYSKEYUP"), TEXT ("WM_SYSCHAR"), TEXT ("WM_SYSDEADCHAR") } ; HDC hdc ; int i, iType ; PAINTSTRUCT ps ; TCHAR szBuffer[128], szKeyName [32] ; TEXTMETRIC tm ; switch (message) { case WM_CREATE: case WM_DISPLAYCHANGE: // Get maximum size of client area cxClientMax = GetSystemMetrics (SM_CXMAXIMIZED) ; cyClientMax = GetSystemMetrics (SM_CYMAXIMIZED) ; // Get character size for fixed-pitch font hdc = GetDC (hwnd) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; GetTextMetrics (hdc, &tm) ; cxChar = tm.tmAveCharWidth ; cyChar = tm.tmHeight ; ReleaseDC (hwnd, hdc) ; // Allocate memory for display lines if (pmsg) free (pmsg) ; cLinesMax = cyClientMax / cyChar ; pmsg = malloc (cLinesMax * sizeof (MSG)) ; cLines = 0 ; // fall through case WM_SIZE: if (message == WM_SIZE) { cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; } // Calculate scrolling rectangle rectScroll.left = 0 ; rectScroll.right = cxClient ; rectScroll.top = cyChar ; rectScroll.bottom = cyChar * (cyClient / cyChar) ; InvalidateRect (hwnd, NULL, TRUE) ; return 0 ; case WM_KEYDOWN: case WM_KEYUP: case WM_CHAR: case WM_DEADCHAR: case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_SYSCHAR: case WM_SYSDEADCHAR: // Rearrange storage array for (i = cLinesMax - 1 ; i > 0 ; i--) { pmsg[i] = pmsg[i - 1] ; } // Store new message pmsg[0].hwnd = hwnd ; pmsg[0].message = message ; pmsg[0].wParam = wParam ; pmsg[0].lParam = lParam ; cLines = min (cLines + 1, cLinesMax) ; // Scroll up the display ScrollWindow (hwnd, 0, -cyChar, &rectScroll, &rectScroll) ; break ; // i.e., call DefWindowProc so Sys messages work case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ; SetBkMode (hdc, TRANSPARENT) ; TextOut (hdc, 0, 0, szTop, lstrlen (szTop)) ; TextOut (hdc, 0, 0, szUnd, lstrlen (szUnd)) ; for (i = 0 ; i < min (cLines, cyClient / cyChar - 1) ; i++) { iType = pmsg[i].message == WM_CHAR || pmsg[i].message == WM_SYSCHAR || pmsg[i].message == WM_DEADCHAR || pmsg[i].message == WM_SYSDEADCHAR ; GetKeyNameText (pmsg[i].lParam, szKeyName, sizeof (szKeyName) / sizeof (TCHAR)) ; TextOut (hdc, 0, (cyClient / cyChar - 1 - i) * cyChar, szBuffer, wsprintf (szBuffer, szFormat [iType], szMessage [pmsg[i].message - WM_KEYFIRST], pmsg[i].wParam, (PTSTR) (iType ? TEXT (" ") : szKeyName), (TCHAR) (iType ? pmsg[i].wParam : ' '), LOWORD (pmsg[i].lParam), HIWORD (pmsg[i].lParam) & 0xFF, 0x01000000 & pmsg[i].lParam ? szYes : szNo, 0x20000000 & pmsg[i].lParam ? szYes : szNo, 0x40000000 & pmsg[i].lParam ? szDown : szUp, 0x80000000 & pmsg[i].lParam ? szUp : szDown)) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }
KEYVIEW1显示窗口消息处理程序接收到的每次按键和字符消息的内容,并将这些消息储存在一个MSG结构的数组中。该数组的大小依据最大化窗口的大小和等宽的系统字体。如果使用者在程序执行时调整了视讯显示的大小(在这种情况下KEYVIEW1接收WM_DISPLAYCHANGE消息),将重新分配此数组。KEYVIEW1使用标准C的malloc函数为数组配置内存。
图6-2给出了在键入「Windows」之后KEYVIEW1的屏幕显示。第一列显示了键盘消息;第二列在键名称的前面显示了按键消息的虚拟键代码,此代码是经由GetKeyNameText函数取得的;第三列(标注为「Char」)在字符本身的后面显示字符消息的十六进制字符代码。其余六列显示了lParam消息参数中六个字段的状态。
为便于以分行的方式显示此信息,KEYVIEW1使用了等宽字体。与前一章所讨论的一样,这需要呼叫GetStockObject和SelectObject:
SelectObject (hdc, GetStockObject (SYSTEM_FIXED_FONT)) ;
KEYVIEW1在显示区域上部画了一个标题以确定分成九行。此列文字带有底线。虽然可以建立一种带底线的字体,但这里使用了另一种方法。我定义了两个字符串变量szTop(有文字)和szUnd(有底线),并在WM_PAINT消息处理期间将它们同时显示在窗口顶部的同一位置。通常,Windows以一种「不透明」的方式显示文字,也就是说显示字符时Windows将擦除字符背景区。这将导致第二个字符串(szUnd)擦除掉前一个(szTop)。要防止这一现象的发生,可将设备内容切换到「透明」模式:
SetBkMode (hdc, TRANSPARENT) ;
这种加底线的方法只有在使用等宽字体时才可行。否则,底线字符将无法与显现在底线上面的字符等宽。