键盘基础
Windows程序是如何获得键盘输入的:键盘输入以消息的形式传递给程序的窗口消息处理程序。
Windows用八种不同的消息来传递不同的键盘事件。在大多数情况下,这些消息中包含的键盘信息会多于程序所需要的。处理键盘的部分工作就是识别出哪些消息是重要的,哪些是不重要的。
忽略键盘
您可以忽略那些属于系统功能的按键,它们通常用到Alt键。程序不必监视这些按键,因为Windows会将按键的作用通知程序。
Windows将这些键盘快捷键转换为菜单命令消息,您不必自己去进行转换。
谁获得了焦点
接收特定键盘事件的窗口具有输入焦点。输入焦点的概念与活动窗口的概念很相近。有输入焦点的窗口是活动窗口或活动窗口的衍生窗口(活动窗口的子窗口,或者活动窗口子窗口的子窗口等等)。
有时输入焦点不在任何窗口中。这种情况发生在所有程序都是最小化的时候。这时,Windows将继续向活动窗口发送键盘消息,但是这些消息与发送给非最小化的活动窗口的键盘消息有不同的形式。
窗口消息处理程序通过拦截WM_SETFOCUS和WM_KILLFOCUS消息来判定它的窗口何时拥有输入焦点。WM_SETFOCUS指示窗口正在得到输入焦点,WM_KILLFOCUS表示窗口正在失去输入焦点。
队列和同步
Windows在所谓的「系统消息队列」中保存这些消息。系统消息队列是独立的消息队列,它由Windows维护,用于初步保存使用者从键盘和鼠标输入的信息。
首先在系统消息队列中保存消息,然后将它们放入应用程序的消息队列。
按键和字符
应用程序从Windows接收的关于键盘事件的消息可以分为按键和字符两类。
对产生可显示字符的按键组合,Windows不仅给程序发送按键消息,而且还发送字符消息。有些键不产生字符,这些键包括shift键、功能键、光标移动键和特殊字符键如Insert和Delete。对于这些键,Windows只产生按键消息。
按键消息
当您按下一个键时,Windows把WM_KEYDOWN或者WM_SYSKEYDOWN消息放入有输入焦点的窗口的消息队列;当您释放一个键时,Windows把WM_KEYUP或者WM_SYSKEYUP消息放入消息队列中。
通常「down(按下)」和「up(放开)」消息是成对出现的。不过,如果您按住一个键使得自动重复功能生效,那么当该键最后被释放时,Windows会给窗口消息处理程序发送一系列WM_KEYDOWN(或者WM_SYSKEYDOWN)消息和一个WM_KEYUP(或者WM_SYSKEYUP)消息。像所有放入队列的消息一样,按键消息也有时间信息。通过呼叫GetMessageTime,您可以获得按下或者释放键的相对时间。
系统按键与非系统按键
WM_SYSKEYDOWN和WM_SYSKEYUP消息经常由与Alt相组合的按键产生,这些按键启动程序菜单或者系统菜单上的选项,或者用于切换活动窗口等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统菜单快捷键(Alt键与一个功能键相结合,例如Alt-F4用于关闭应用程序)。程序通常忽略WM_SYSKEYUP和WM_SYSKEYDOWN消息,并将它们传送到DefWindowProc。
WM_KEYDOWN和WM_KEYUP消息通常是在按下或者释放不带Alt键的键时产生的,您的程序可以使用或者忽略这些消息,Windows本身并不处理这些消息。
对所有四类按键消息,wParam是虚拟键代码,表示按下或释放的键,而lParam则包含属于按键的其它数据。
虚拟键码
虚拟键码保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP消息的wParam参数中。此代码标识按下或释放的键。
您使用的大多数虚拟键码的名称在WINUSER.H表头文件中都定义为以VK_开头。
前四个虚拟键码中有三个指的是鼠标键:
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
1 | 01 | VK_LBUTTON |
| 鼠标左键 |
2 | 02 | VK_RBUTTON |
| 鼠标右键 |
3 | 03 | VK_CANCEL | ˇ | Ctrl-Break |
4 | 04 | VK_MBUTTON |
| 鼠标中键 |
您永远都不会从键盘消息中获得这些鼠标键代码。
下表中的键--Backspace、Tab、Enter、Escape和Spacebar-通常用于Windows程序。不过,Windows一般用字符消息(而不是键盘消息)来处理这些键。
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
8 | 08 | VK_BACK | ˇ | Backspace |
9 | 09 | VK_TAB | ˇ | Tab |
12 | 0C | VK_CLEAR |
| Num Lock关闭时的数字键盘5 |
13 | 0D | VK_RETURN | ˇ | Enter (或者另一个) |
16 | 10 | VK_SHIFT | ˇ | Shift (或者另一个) |
17 | 11 | VK_CONTROL | ˇ | Ctrl (或者另一个) |
18 | 12 | VK_MENU | ˇ | Alt (或者另一个) |
19 | 13 | VK_PAUSE |
| Pause |
20 | 14 | VK_CAPITAL | ˇ | Caps Lock |
27 | 1B | VK_ESCAPE | ˇ | Esc |
32 | 20 | VK_SPACE | ˇ | Spacebar |
另外,Windows程序通常不需要监视Shift、Ctrl或Alt键的状态。
下表列出的前八个码可能是与VK_INSERT和VK_DELETE一起最常用的虚拟键码:
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
33 | 21 | VK_PRIOR | ˇ | Page Up |
34 | 22 | VK_NEXT | ˇ | Page Down |
35 | 23 | VK_END | ˇ | End |
36 | 24 | VK_HOME | ˇ | Home |
37 | 25 | VK_LEFT | ˇ | 左箭头 |
38 | 26 | VK_UP | ˇ | 上箭头 |
39 | 27 | VK_RIGHT | ˇ | 右箭头 |
40 | 28 | VK_DOWN | ˇ | 下箭头 |
41 | 29 | VK_SELECT |
|
|
42 | 2A | VK_PRINT |
|
|
43 | 2B | VK_EXECUTE |
|
|
44 | 2C | VK_SNAPSHOT |
| Print Screen |
45 | 2D | VK_INSERT | ˇ | Insert |
46 | 2E | VK_DELETE | ˇ | Delete |
47 | 2F | VK_HELP |
|
|
许多名称(例如VK_PRIOR和VK_NEXT)都与键上的标志不同,而且也与滚动条中的标识符不统一。Print Screen键在平时都被Windows应用程序所忽略。Windows本身响应此键时会将视讯显示的位图影本存放到剪贴板中。假使有键盘提供了VK_SELECT、VK_PRINT、VK_EXECUTE和VK_HELP,大概也没几个人看过那样的键盘。
Windows也包括在主键盘上的字母和数字键的虚拟键码(数字键盘将单独处理)。
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
48-57 | 30-39 | 无 | ˇ | 主键盘上的0到9 |
65-90 | 41-5A | 无 | ˇ | A到Z |
数字和字母的虚拟键码是ASCII码。Windows程序几乎从不使用这些虚拟键码;实际上,程序使用的是ASCII码字符的字符消息。
下表所示的代码用于数字键盘上的键(如果有的话):
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
96-105 | 60-69 | VK_NUMPAD0到VK_ NUMPAD9 |
| NumLock打开时数字键盘上的0到9 |
106 | 6A | VK_MULTIPLY |
| 数字键盘上的* |
107 | 6B | VK_ADD |
| 数字键盘上的+ |
108 | 6C | VK_SEPARATOR |
|
|
109 | 6D | VK_SUBTRACT |
| 数字键盘上的- |
110 | 6E | VK_DECIMAL |
| 数字键盘上的. |
111 | 6F | VK_DIVIDE |
| 数字键盘上的/ |
虽然多数的键盘都有12个功能键,但Windows只需要10个,而位旗标却有24个。另外,程序通常用功能键作为键盘快捷键,这样,它们通常不处理下表所示的按键:
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
112-121 | 70-79 | VK_F1到VK_F10 | ˇ | 功能键F1到F10 |
122-135 | 7A-87 | VK_F11到VK_F24 |
| 功能键F11到F24 |
144 | 90 | VK_NUMLOCK |
| Num Lock |
145 | 91 | VK_SCROLL |
| Scroll Lock |
lParam信息
lParam消息参数则含有对了解按键非常有用的其它信息。lParam的32位分为6个字段,如下图所示:
重复计数
WM_KEYUP或WM_SYSKEYUP消息的重复计数总是为1。
因为重复计数大于1指示按键速率大于您程序的处理能力,所以您也可能想在处理键盘消息时忽略重复计数。
OEM扫描码
在此我们不需要更多的信息。除非需要依赖实际键盘布局的样貌,不然Windows程序可以忽略掉几乎所有的OEM扫描码信息。
扩充键旗标
Windows程序通常忽略扩充键旗标。
键的先前状态
如果在此之前键是释放的,则键的先前状态为0,否则为1。对WM_KEYUP或者WM_SYSKEYUP消息,它总是设定为1;但是对WM_KEYDOWN或者WM_SYSKEYDOWN消息,此位可以为0,也可以为1。如果为1,则表示该键是自动重复功能所产生的第二个或者后续消息。
转换状态
如果键正被按下,则转换状态为0;如果键正被释放,则转换状态为1。对WM_KEYDOWN或者WM_SYSKEYDOWN消息,此字段为0;对WM_KEYUP或者WM_SYSKEYUP消息,此字段为1。
位移状态
在处理按键消息时,您可能需要知道是否按下了位移键(Shift、Ctrl和Alt)或开关键(Caps Lock、Num Lock和ScrollLock)。通过呼叫GetKeyState函数,您就能获得此信息。例如:
iState = GetKeyState(VK_SHIFT) ;
如果按下了Shift,则iState值为负(即设定了最高位置位)。如果Caps Lock键打开,则从
iState = GetKeyState(VK_CAPITAL) ;
传回的值低位被设为1。此位与键盘上的小灯保持一致。
通常,您在使用GetKeyState时,会带有虚拟键码VK_SHIFT、VK_CONTROL和VK_MENU(在说明Alt键时呼叫)。使用GetKeyState时,您也可以用下面的标识符来确定按下的Shift、Ctrl或Alt键是左边的还是右边的:VK_LSHIFT、VK_RSHIFT、VK_LCONTROL、VK_RCONTROL、VK_LMENU、VK_RMENU。这些标识符只用于GetKeyState和GetAsyncKeyState。
GetKeyState并非实时检查键盘状态,而只是检查直到目前为止正在处理的消息的键盘状态。
GetKeyState不会让您获得独立于普通键盘消息的键盘信息。例如,您或许想暂停窗口消息处理程序的处理,直到您按下F1功能键为止:
while (GetKeyState (VK_F1)>= 0) ; // WRONG !!!
不要这么做 !这将让程序当死(除非在执行此叙述之前早就从消息队列中接收到了F1的WM_KEYDOWN)。如果您确实需要知道目前某键的状态,那么您可以使用GetAsyncKeyState。
使用按键消息
Windows程序通常为不产生字符的按键使用WM_KEYDOWN消息。虽然您可能认为借助按键消息和位移键状态信息能将按键消息转换为字符消息,但是不要这么做,因为您将遇到国际键盘间的差异所带来的问题。
对于光标移动键、功能键、Insert和Delete键,WM_KEYDOWN消息是最有用的。不过, Insert、Delete和功能键经常作为菜单快捷键。因为Windows能把菜单快捷键翻译为菜单命令消息,所以您就不必自己来处理按键。
可以归纳如下:多数情况下,您将只为光标移动键(有时也为Insert和Delete键)处理WM_KEYDOWN消息。
在使用这些键的时候,您可以通过GetKeyState来检查Shift键和Ctrl键的状态。例如,Windows程序经常使用Shift与光标键的组合键来扩大文书处理里选中的范围。Ctrl键常用于修改光标键的意义。例如,Ctrl与右箭头键相组合可以表示光标右移一个字。
为SYSMETS加上键盘处理功能
通过向窗口消息处理程序发送假冒消息,我们可能会让WndProc认为它获得了卷动信息。
caseWM_KEYDOWN:
switch(wParam)
{
case VK_HOME:
SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ;
break ;
case VK_END:
SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM,0) ;
break ;
case VK_PRIOR:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP,0) ;
break ;
}
#include <windows.h>
#include "sysmets.h"
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("Sysmets4");
HWND hwnd;
MSG msg;
WNDCLASS wndclass;
wndclass.cbClsExtra = 0;
wndclass.cbWndExtra = 0;
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hInstance = hInstance;
wndclass.lpfnWndProc = WndProc;
wndclass.lpszClassName = szAppName;
wndclass.lpszMenuName = NULL;
wndclass.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass(&wndclass))
{
MessageBox(NULL, TEXT("Program requires Windows 7!"),
szAppName, MB_ICONERROR
);
return 0;
}
hwnd = CreateWindow(
szAppName,
TEXT("Get System Metrics No.4"),
WS_OVERLAPPEDWINDOW | WS_VSCROLL | WS_HSCROLL,
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 cxChar, cxCaps, cyChar, cxClient, cyClient, iMaxWidth;
HDC hdc;
int i, x, y, iVertPos, iHorzPos, iPaintBeg, iPaintEnd;
PAINTSTRUCT ps;
SCROLLINFO si;
TCHAR szBuffer[10];
TEXTMETRIC tm;
switch (message)
{
case WM_CREATE:
hdc = GetDC(hwnd);
GetTextMetrics(hdc, &tm);
cxChar = tm.tmAveCharWidth;
cxCaps = (tm.tmPitchAndFamily & 1 ? 3 : 2) * cxChar / 2;
cyChar = tm.tmHeight + tm.tmExternalLeading;
ReleaseDC(hwnd, hdc);
iMaxWidth = 40 * cxChar + 22 * cxCaps;
return 0;
case WM_SIZE:
cxClient = LOWORD(lParam);
cyClient = HIWORD(lParam);
si.cbSize = sizeof(si);
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = NUMLINES - 1;
si.nPage = cyClient / cyChar;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
si.cbSize = sizeof(si);
si.fMask = SIF_RANGE | SIF_PAGE;
si.nMin = 0;
si.nMax = 2 + iMaxWidth / cxChar;
si.nPage = cxClient / cxChar;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
return 0;
case WM_VSCROLL:
si.cbSize = sizeof(si);
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_TOP:
si.nPos = si.nMin;
break;
case SB_BOTTOM:
si.nPos = si.nMax;
break;
case SB_LINEUP:
si.nPos -= 1;
break;
case SB_PAGEUP:
si.nPos -= si.nPage;
break;
case SB_LINEDOWN:
si.nPos += 1;
break;
case SB_PAGEDOWN:
si.nPos += si.nPage;
break;
case SB_THUMBTRACK:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_POS;
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
GetScrollInfo(hwnd, SB_VERT, &si);
if (si.nPos != iVertPos)
{
ScrollWindow(hwnd, 0, cyChar * (iVertPos - si.nPos),
NULL, NULL);
UpdateWindow(hwnd);
}
return 0;
case WM_HSCROLL:
si.cbSize = sizeof(si);
si.fMask = SIF_ALL;
GetScrollInfo(hwnd, SB_HORZ, &si);
iHorzPos = si.nPos;
switch (LOWORD(wParam))
{
case SB_LINELEFT:
si.nPos -= 1;
break;
case SB_LINERIGHT:
si.nPos += 1;
break;
case SB_PAGELEFT:
si.nPos -= si.nPage;
break;
case SB_PAGERIGHT:
si.nPos += si.nPage;
break;
case SB_THUMBPOSITION:
si.nPos = si.nTrackPos;
break;
default:
break;
}
si.fMask = SIF_POS;
SetScrollInfo(hwnd, SB_HORZ, &si, TRUE);
GetScrollInfo(hwnd, SB_HORZ, &si);
if (si.nPos != iHorzPos)
{
ScrollWindow(hwnd, cxChar * (iHorzPos - si.nPos), 0,
NULL, NULL);
//UpdateWindow(hwnd);
}
return 0;
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
SendMessage (hwnd, WM_VSCROLL, SB_TOP, 0) ;
break ;
case VK_END:
SendMessage (hwnd, WM_VSCROLL, SB_BOTTOM, 0) ;
break ;
case VK_PRIOR:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEUP, 0) ;
break ;
case VK_NEXT:
SendMessage (hwnd, WM_VSCROLL, SB_PAGEDOWN, 0) ;
break ;
case VK_UP:
SendMessage (hwnd, WM_VSCROLL, SB_LINEUP, 0) ;
break ;
case VK_DOWN:
SendMessage (hwnd, WM_VSCROLL, SB_LINEDOWN, 0) ;
break ;
case VK_LEFT:
SendMessage (hwnd, WM_HSCROLL, SB_PAGELEFT, 0) ;
break ;
case VK_RIGHT:
SendMessage (hwnd, WM_HSCROLL, SB_PAGERIGHT, 0) ;
break ;
}
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps);
si.cbSize = sizeof(si);
si.fMask = SIF_POS;
GetScrollInfo(hwnd, SB_VERT, &si);
iVertPos = si.nPos;
GetScrollInfo(hwnd, SB_HORZ, &si);
iHorzPos = si.nPos;
iPaintBeg = max(0, iVertPos + ps.rcPaint.top / cyChar);
iPaintEnd = min(NUMLINES - 1, iVertPos + ps.rcPaint.bottom / cyChar);
for (i = iPaintBeg; i <= iPaintEnd; i++)
{
x = cxChar * (1 - iHorzPos);
y = cyChar * (i - iVertPos);
TextOut(hdc, x, y,
sysmetrics[i].szLabel,
lstrlen(sysmetrics[i].szLabel));
TextOut(hdc, x + 22*cxCaps, y,
sysmetrics[i].szDesc,
lstrlen(sysmetrics[i].szDesc));
SetTextAlign(hdc, TA_RIGHT | TA_TOP);
TextOut(hdc, x + 22*cxCaps + 40*cxChar, y, szBuffer,
wsprintf(szBuffer, TEXT("%5d"),
GetSystemMetrics(sysmetrics[i].Index)));
SetTextAlign(hdc, TA_LEFT | TA_TOP);
}
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
字符消息
TranslateMessage函数,它将按键消息转换为字符消息。如果消息为WM_KEYDOWN或者WM_SYSKEYDOWN,并且按键与位移状态相组合产生一个字符,则TranslateMessage把字符消息放入消息队列中。此字符消息将是GetMessage从消息队列中得到的按键消息之后的下一个消息。
四类字符消息
| 字符 | 死字符 |
非系统字符 | WM_CHAR | WM_DEADCHAR |
系统字符 | WM_SYSCHAR | WM_SYSDEADCHAR |
在大多数情况下,Windows程序会忽略除WM_CHAR之外的任何消息。伴随四个字符消息的lParam参数与产生字符代码消息的按键消息之lParam参数相同。不过,参数wParam不是虚拟键码。实际上,它是ANSI或Unicode字符代码。
如果您的窗口消息处理程序需要晓得目前窗口是否处理Unicode消息,则它可以呼叫:
fUnicode = IsWindowUnicode(hwnd) ;
如果hwnd的窗口消息处理程序获得Unicode消息,那么变量fUnicode将为TRUE,这表示窗口是用RegisterClassW注册的窗口类别。
消息顺序
因为TranslateMessage函数从WM_KEYDOWN和WM_SYSKEYDOWN消息产生了字符消息,所以字符消息是夹在按键消息之间传递给窗口消息处理程序的。
如果您按下Shift键,再按下A键,然后释放A键,再释放Shift键,就会输入大写的A,而窗口消息处理程序会接收到五个消息,如表所示:
消息 | 按键或者代码 |
WM_KEYDOWN | 虚拟键码VK_SHIFT (0x10) |
WM_KEYDOWN | 「A」的虚拟键码(0x41) |
WM_CHAR | 「A」的字符代码(0x41) |
WM_KEYUP | 「A」的虚拟键码(0x41) |
WM_KEYUP | 虚拟键码VK_SHIFT(0x10) |
组合使用Ctrl键与字母键会产生从0x01(Ctrl-A)到0x1A(Ctrl-Z)的ASCII控制代码,其中的某些控制代码也可以由下表列出的键产生:
按键 | 字符代码 | 产生方法 | 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-[ |
|
最右列给出了在ANSIC中定义的控制字符,它们用于描述这些键的字符代码。
有时Windows程序将Ctrl与字母键的组合用作菜单快捷键,此时,不会将字母键转换成字符消息。
处理控制字符
处理按键和字符消息的基本规则是:如果需要读取输入到窗口的键盘字符,那么您可以处理WM_CHAR消息。如果需要读取光标键、功能键、Delete、Insert、Shift、Ctrl以及Alt键,那么您可以处理WM_KEYDOWN消息。
但是Tab键怎么办?Enter、Backspace和Escape键又怎么办?传统上,这些键都产生上表列出的ASCII控制字符。但是在Windows中,它们也产生虚拟键码。这些键应该在处理WM_CHAR或者在处理WM_KEYDOWN期间处理吗?
我更喜欢将Tab、Enter、Backspace和Escape键处理成控制字符,而不是虚拟键。我通常这样处理WM_CHAR:
caseWM_CHAR:
//其它行程序
switch(wParam)
{
case'\b': // backspace
//其它行程序
break;
case'\t': // tab
//其它行程序
break;
case'\n': // linefeed
//其它行程序
break;
case'\r': // carriage return
//其它行程序
break;
default:// character codes
//其它行程序
break;
}
return0 ;
死字符消息
在某些非U.S.英语键盘上,有些键用于给字母加上音调。因为它们本身不产生字符,所以称之为「死键」。例如,使用德语键盘时,对于U.S.键盘上的+/=键,德语键盘的对应位置就是一个死键,未按下Shift键时它用于标识锐音,按下Shift键时则用于标识抑音。
#include <windows.h>
LRESULT 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 = (MSG*)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],//WM_KEDOWN WM_CHAR
pmsg[i].wParam, //49 0031
(PTSTR) (iType ? TEXT (" ") : szKeyName),//1 " "
(TCHAR) (iType ? pmsg[i].wParam : ' '),//' ' 1
LOWORD (pmsg[i].lParam),//1
HIWORD (pmsg[i].lParam) & 0xFF,//2
0x01000000 & pmsg[i].lParam ? szYes : szNo,//NO
0x20000000 & pmsg[i].lParam ? szYes : szNo,//NO
0x40000000 & pmsg[i].lParam ? szDown : szUp,//UP 如果在此之前键是释放的,则键的先前状态为0,否则为1。
0x80000000 & pmsg[i].lParam ? szUp : szDown)) ;//DOWN 如果键正被按下,则转换状态为0;如果键正被释放,则转换状态为1。
}
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
字符集和字体
Windows支持三类字体-点阵字体、向量字体和(从Windows 3.1开始的)TrueType字体。
事实上向量字体已经过时了。这些字体中的字符由简单的线段组成,但这些线段没有定义填入区域。向量字体可以较好地缩放到任意大小,但字符通常看上去有些单薄。
TrueType字体是定义了填入区域的文字轮廓字体。
在点阵字体中,每个字符都定义为与视讯显示器上的像素对应的位点阵。点阵字体通常被设计成方便在视讯显示器上阅读的字体Windows中的标题栏、菜单、按钮和对话框的显示文字都使用点阵字体。
在内定的设备内容下获得的点阵字体称为系统字体。您可通过呼叫带有SYSTEM_FONT标识符的GetStockObject函数来获得字体句柄。KEYVIEW1程序选择使用SYSTEM_FIXED_FONT表示的等宽系统字体。GetStockObject函数的另一个选项是OEM_FIXED_FON
对于许多标准控件和使用者接口组件,Windows不使用系统字体。相反地,使用名称为MS Sans Serif的字体(「MS」代表Microsoft),可在GetStockObject函数中使用DEFAULT_GUI_FONT标识符来得到该字体。
TrueType 和大字体
我们使用的点阵字体(在日文版Windows中带有附加字体)最多包括256个字符。
大字体解决方案的其它部分是WM_INPUTLANGCHANGE消息。一旦您使用桌面下端的弹出式菜单来改变键盘布局,Windows都会向您的窗口消息处理程序发送WM_INPUTLANGCHANGE消息。wParam消息参数是新键盘布局的字符集ID。
插入符号(不是光标)
主要有五个插入符号函数:
CreateCaret 建立与窗口有关的插入符号
SetCaretPos 在窗口中设定插入符号的位置
ShowCaret 显示插入符号
HideCaret 隐藏插入符号
DestroyCaret 撤消插入符号
另外还有取得插入符号目前位置(GetCaretPos)和取得以及设定插入符号闪烁时间(GetCaretBlinkTime和SetCaretBlinkTime)的函数。
在Windows中,插入符号定义为水平线、与字符大小相同的方框,或者与字符同高的竖线。
通过处理WM_SETFOCUS和WM_KILLFOCUS消息,程序就可以确定它是否有输入焦点。
使用插入符号的主要规则很简单:窗口消息处理程序在WM_SETFOCUS消息处理期间呼叫CreateCaret,在WM_KILLFOCUS消息处理期间呼叫DestroyCaret。
这里还有几条其它规则:插入符号刚建立时是隐蔽的。如果想使插入符号可见,那么您在呼叫CreateCaret之后,窗口消息处理程序还必须呼叫ShowCaret。另外,当窗口消息处理程序处理一条非WM_PAINT消息而且希望在窗口内绘制某些东西时,它必须呼叫HideCaret隐藏插入符号。在绘制完毕后,再呼叫ShowCaret显示插入符号。HideCaret的影响具有累积效果,如果多次呼叫HideCaret而不呼叫ShowCaret,那么只有呼叫ShowCaret相同次数时,才能看到插入符号。
TYPER程序
TYPER程序使用了本章讨论的所有内容,您可以认为TYPER是一个相当简单的文字编辑器。在窗口中,您可以输入字符,用光标移动键(也可以称为插入符号移动键)来移动光标(I型标),按下Escape键清除窗口的内容等。缩放窗口、改变键盘输入语言时都会清除窗口的内容。
#include <windows.h>
#define BUFFER(x,y) *(pBuffer + y * cxBuffer + x)
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("Typer") ;
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 ("Typing Program"),
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 DWORD dwCharSet = DEFAULT_CHARSET ;
static int cxChar, cyChar, cxClient, cyClient, cxBuffer, cyBuffer,
xCaret, yCaret ;
static TCHAR *pBuffer = NULL ;
HDC hdc ;
int x, y, i ;
PAINTSTRUCT ps ;
TEXTMETRIC tm ;
switch (message)
{
case WM_INPUTLANGCHANGE:
dwCharSet = wParam ;
// fall through
case WM_CREATE:
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
GetTextMetrics (hdc, &tm) ;
cxChar = tm.tmAveCharWidth ;
cyChar = tm.tmHeight ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
// fall through
case WM_SIZE:
// obtain window size in pixels
if (message == WM_SIZE)
{
cxClient = LOWORD (lParam) ;
cyClient = HIWORD (lParam) ;
}
// calculate window size in characters
cxBuffer = max (1, cxClient / cxChar) ;
cyBuffer = max (1, cyClient / cyChar) ;
// allocate memory for buffer and clear it
if (pBuffer != NULL)
free (pBuffer) ;
pBuffer = (TCHAR *) malloc (cxBuffer * cyBuffer * sizeof (TCHAR)) ;
for (y = 0 ; y < cyBuffer ; y++)
for (x = 0 ; x < cxBuffer ; x++)
BUFFER(x,y) = ' ' ;
// set caret to upper left corner
xCaret = 0 ;
yCaret = 0 ;
if (hwnd == GetFocus ())
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
InvalidateRect (hwnd, NULL, TRUE) ;
return 0 ;
case WM_SETFOCUS:
// create and show the caret
CreateCaret (hwnd, NULL, cxChar, cyChar) ;
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
ShowCaret (hwnd) ;
return 0 ;
case WM_KILLFOCUS:
// hide and destroy the caret
HideCaret (hwnd) ;
DestroyCaret () ;
return 0 ;
case WM_KEYDOWN:
switch (wParam)
{
case VK_HOME:
xCaret = 0 ;
break ;
case VK_END:
xCaret = cxBuffer - 1 ;
break ;
case VK_PRIOR:
yCaret = 0 ;
break ;
case VK_NEXT:
yCaret = cyBuffer - 1 ;
break ;
case VK_LEFT:
xCaret = max (xCaret - 1, 0) ;
break ;
case VK_RIGHT:
xCaret = min (xCaret + 1, cxBuffer - 1) ;
break ;
case VK_UP:
yCaret = max (yCaret - 1, 0) ;
break ;
case VK_DOWN:
yCaret = min (yCaret + 1, cyBuffer - 1) ;
break ;
case VK_DELETE:
for (x = xCaret ; x < cxBuffer - 1 ; x++)
BUFFER (x, yCaret) = BUFFER (x + 1, yCaret) ;
BUFFER (cxBuffer - 1, yCaret) = ' ' ;
HideCaret (hwnd) ;
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0,FIXED_PITCH, NULL)) ;
TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
& BUFFER (xCaret, yCaret),
cxBuffer - xCaret) ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
ShowCaret (hwnd) ;
break ;
}
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
return 0 ;
case WM_CHAR:
for (i = 0 ; i < (int) LOWORD (lParam) ; i++)
{
switch (wParam)
{
case '\b': // backspace
if (xCaret > 0)
{
xCaret-- ;
SendMessage (hwnd, WM_KEYDOWN, VK_DELETE, 1) ;
}
break ;
case '\t': // tab
do
{
SendMessage (hwnd, WM_CHAR, ' ', 1) ;
}
while (xCaret % 8 != 0) ;
break ;
case '\n': // line feed
if (++yCaret == cyBuffer)
yCaret = 0 ;
break ;
case '\r': // carriage return
xCaret = 0 ;
if (++yCaret == cyBuffer)
yCaret = 0 ;
break ;
case '\x1B': // escape
for (y = 0 ; y < cyBuffer ; y++)
for (x = 0 ; x < cxBuffer ; x++)
BUFFER (x, y) = ' ' ;
xCaret = 0 ;
yCaret = 0 ;
InvalidateRect (hwnd, NULL, FALSE) ;
break ;
default: // character codes
BUFFER (xCaret, yCaret) = (TCHAR) wParam ;
HideCaret (hwnd) ;
hdc = GetDC (hwnd) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
TextOut (hdc, xCaret * cxChar, yCaret * cyChar,
& BUFFER (xCaret, yCaret), 1) ;
DeleteObject (
SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
ReleaseDC (hwnd, hdc) ;
ShowCaret (hwnd) ;
if (++xCaret == cxBuffer)
{
xCaret = 0 ;
if (++yCaret == cyBuffer)
yCaret = 0 ;
}
break ;
}
}
SetCaretPos (xCaret * cxChar, yCaret * cyChar) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
SelectObject (hdc, CreateFont (0, 0, 0, 0, 0, 0, 0, 0,
dwCharSet, 0, 0, 0, FIXED_PITCH, NULL)) ;
for (y = 0 ; y < cyBuffer ; y++)
TextOut (hdc, 0, y * cyChar, & BUFFER(0,y), cxBuffer) ;
DeleteObject (SelectObject (hdc, GetStockObject (SYSTEM_FONT))) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}