谁获得了焦点
与所有的个人计算机硬件一样,键盘必须由在Windows下执行的所有应用程序共享。有些应用程序可能有多个窗口,键盘必须由该应用程序内的所有窗口共享。
回想一下,程序用来从消息队列中检索消息的MSG结构包括hwnd字段。此字段指出接收消息的窗口控件码。消息循环中的DispatchMessage函数向窗口消息处理程序发送该消息,此窗口消息处理程序与需要消息的窗口相联系。在按下键盘上的键时,只有一个窗口消息处理程序接收键盘消息,并且此消息包括接收消息的窗口控件码。
接收特定键盘事件的窗口具有输入焦点。输入焦点的概念与活动窗口的概念很相近。有输入焦点的窗口是活动窗口或活动窗口的衍生窗口(活动窗口的子窗口,或者活动窗口子窗口的子窗口等等)。
通常很容易辨别活动窗口。它通常是顶层窗口-也就是说,它的父窗口句柄是NULL。如果活动窗口有标题列,Windows将突出显示标题列。如果活动窗口具有对话框架(对话框中很常见的格式)而不是标题列,Windows将突出显示框架。如果活动窗口目前是最小化的,Windows将在工作列中突出显示该项,其显示就像一个按下的按钮。
如果活动窗口有子窗口,那么有输入焦点的窗口既可以是活动窗口也可以是其子窗口。最常见的子窗口有类似以下控件:出现在对话框中的下压按钮、单选钮、复选框、滚动条、编辑方块和清单方块。子窗口不能自己成为活动窗口。只有当它是活动窗口的衍生窗口时,子窗口才能有输入焦点。子窗口控件一般通过显示一个闪烁的插入符号或虚线来表示它具有输入焦点。
有时输入焦点不在任何窗口中。这种情况发生在所有程序都是最小化的时候。这时,Windows将继续向活动窗口发送键盘消息,但是这些消息与发送给非最小化的活动窗口的键盘消息有不同的形式。
窗口消息处理程序通过拦截WM_SETFOCUS和WM_KILLFOCUS消息来判定它的窗口何时拥有输入焦点。WM_SETFOCUS指示窗口正在得到输入焦点,WM_KILLFOCUS表示窗口正在失去输入焦点。我将在本章的后面详细说明这些消息。
队列和同步
当使用者按下并释放键盘上的键时,Windows和键盘驱动程序将硬件扫描码转换为格式消息。然而,这些消息并不保存在消息队列中。实际上,Windows在所谓的「系统消息队列」中保存这些消息。系统消息队列是独立的消息队列,它由Windows维护,用于初步保存使用者从键盘和鼠标输入的信息。只有当Windows应用程序处理完前一个使用者输入消息时,Windows才会从系统消息队列中取出下一个消息,并将其放入应用程序的消息队列中。
此过程分为两步:首先在系统消息队列中保存消息,然后将它们放入应用程序的消息队列,其原因是需要同步。就像我们刚才所学的,假定接收键盘输入的窗口就是有输入焦点的窗口。使用者的输入速度可能比应用程序处理按键的速度快,并且特定的按键可能会使焦点从一个窗口切换到另一个窗口,后来的按键就输入到了另一个窗口。但如果后来的按键已经记下了目标窗口的地址,并放入了应用程序消息队列,那么后来的按键就不能输入到另一个窗口。
按键和字符
应用程序从Windows接收的关于键盘事件的消息可以分为按键和字符两类,这与您看待键盘的两种方式一致。
首先,您可以将键盘看作是键的集合。键盘只有唯一的A键,按下该键是一次按键,释放该键也是一次按键。但是键盘也是能产生可显示字符或控制字符的输入设备。根据Ctrl、 Shift和Caps Lock键的状态,A键能产生几个字符。通常情况下,此字符为小写a。如果按下Shift键或者打开了Caps Lock,则该字符就变成大写A。如果按下了Ctrl,则该字符为Ctrl-A(它在ASCII中有意义,但在Windows中可能是某事件的键盘快捷键)。在一些键盘上,A按键之前可能有「死字符键(dead-character key)」或者Shift、Ctrl或者Alt的不同组合,这些组合可以产生带有音调标记的小写或者大写,例如,à、á、狻⒛、或拧?/p>
对产生可显示字符的按键组合,Windows不仅给程序发送按键消息,而且还发送字符消息。有些键不产生字符,这些键包括shift键、功能键、光标移动键和特殊字符键如Insert和Delete。对于这些键,Windows只产生按键消息。
当您按下一个键时,Windows把WM_KEYDOWN或者WM_SYSKEYDOWN消息放入有输入焦点的窗口的消息队列;当您释放一个键时,Windows把WM_KEYUP或者WM_SYSKEYUP消息放入消息队列中。
表6-1 |
键按下 | 键释放 | |
非系统键 | WM_KEYDOWN | WM_KEYUP |
系统键 | WM_SYSKEYDOWN | WM_SYSKEYUP |
通常「down(按下)」和「up(放开)」消息是成对出现的。不过,如果您按住一个键使得自动重复功能生效,那么当该键最后被释放时,Windows会给窗口消息处理程序发送一系列WM_KEYDOWN(或者WM_SYSKEYDOWN)消息和一个WM_KEYUP(或者WM_SYSKEYUP)消息。像所有放入队列的消息一样,按键消息也有时间信息。通过呼叫GetMessageTime,您可以获得按下或者释放键的相对时间。
系统按键与非系统按键
WM_SYSKEYDOWN和WM_SYSKEYUP中的「SYS」代表「系统」,它表示该按键对Windows比对Windows应用程序更加重要。WM_SYSKEYDOWN和WM_SYSKEYUP消息经常由与Alt相组合的按键产生,这些按键启动程序菜单或者系统菜单上的选项,或者用于切换活动窗口等系统功能(Alt-Tab或者Alt-Esc),也可以用作系统菜单快捷键(Alt键与一个功能键相结合,例如Alt-F4用于关闭应用程序)。程序通常忽略WM_SYSKEYUP和WM_SYSKEYDOWN消息,并将它们传送到DefWindowProc。由于Windows要处理所有Alt键的功能,所以您无需拦截这些消息。您的窗口消息处理程序将最后收到关于这些按键结果(如菜单选择)的其它消息。如果您想在自己的窗口消息处理程序中加上拦截系统按键的程序代码(如本章后面的KEYVIEW1和KEYVIEW2程序所作的那样),那么在处理这些消息之后再传送到DefWindowProc,Windows就仍然可以将它们用于通常的目的。
但是,请再考虑一下,几乎所有会影响使用者程序窗口的消息都会先通过使用者窗口消息处理程序。只有使用者把消息传送到DefWindowProc,Windows才会对消息进行处理。例如,如果您将下面几行叙述:
case WM_SYSKEYDOWN: case WM_SYSKEYUP: caseWM_SYSCHAR: return 0 ;
加入到一个窗口消息处理程序中,那么当您的程序主窗口拥有输入焦点时,就可以有效地阻止所有Alt键操作(我将在本章的后面讨论WM_SYSCHAR),其中包括Alt-Tab、Alt-Esc以及菜单操作。虽然我怀疑您会这么做,但是,我相信您会感到窗口消息处理程序的强大功能。
WM_KEYDOWN和WM_KEYUP消息通常是在按下或者释放不带Alt键的键时产生的,您的程序可以使用或者忽略这些消息,Windows本身并不处理这些消息。
对所有四类按键消息,wParam是虚拟键代码,表示按下或释放的键,而lParam则包含属于按键的其它数据。
虚拟键码
虚拟键码保存在WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP消息的wParam参数中。此代码标识按下或释放的键。
哈,又是「虚拟」,您喜欢这个词吗?虚拟指的是假定存在于思想中而不是现实世界中的一些事物,也只有熟练使用DOS汇编语言编写应用程序的程序写作者才有可能指出,为什么对Windows键盘处理如此基本的键码是虚拟的而不是真实的。
对于早期的程序写作者来说,真实的键码由实际键盘硬件产生。在Windows文件中将这些键码称为「扫描码(scan codes)」。在IBM兼容机种上,扫描码16是Q键,17是W键,18是E、19是R,20是T,21是Y等等。这时您会发现,扫描码是依据键盘的实际布局的。Windows开发者认为这些代码过于与设备相关了,于是他们试图通过定义所谓的虚拟键码,以便经由与设备无关的方式处理键盘。其中一些虚拟键码不能在IBM兼容机种上产生,但可能会在其它制造商生产的键盘中找到,或者在未来的键盘上找到。
您使用的大多数虚拟键码的名称在WINUSER.H表头文件中都定义为以VK_开头。表6-2列出了这些名称和数值(十进制和十六进制),以及与虚拟键相对应的IBM兼容机种键盘上的键。下表也标出了Windows执行时是否需要这些键。下表还按数字顺序列出了虚拟键码。
前四个虚拟键码中有三个指的是鼠标键:
表6-2 |
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
1 | 01 | VK_LBUTTON | 鼠标左键 | |
2 | 02 | VK_RBUTTON | 鼠标右键 | |
3 | 03 | VK_CANCEL | ˇ | Ctrl-Break |
4 | 04 | VK_MBUTTON | 鼠标中键 |
您永远都不会从键盘消息中获得这些鼠标键代码。在下一章可以看到,我们能够从鼠标消息中获得它们。VK_CANCEL代码是一个虚拟键码,它包括同时按下两个键(Ctrl-Break)。Windows应用程序通常不使用此键。
表6-3中的键--Backspace、Tab、Enter、Escape和Spacebar-通常用于Windows程序。不过,Windows一般用字符消息(而不是键盘消息)来处理这些键。
表6-3 |
十进制 | 十六进制 | 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键的状态。
表6-4列出的前八个码可能是与VK_INSERT和VK_DELETE一起最常用的虚拟键码:
表6-4 |
十进制 | 十六进制 | 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也包括在主键盘上的字母和数字键的虚拟键码(数字键盘将单独处理)。
表6-5 |
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
48-57 | 30-39 | 无 | ˇ | 主键盘上的0到9 |
65-90 | 41-5A | 无 | ˇ | A到Z |
注意,数字和字母的虚拟键码是ASCII码。Windows程序几乎从不使用这些虚拟键码;实际上,程序使用的是ASCII码字符的字符消息。
表6-6所示的代码是由Microsoft Natural Keyboard及其兼容键盘产生的:
表6-6 |
十进制 | 十六进制 | WINUSER.H标识符 | 必需? | IBM兼容键盘 |
91 | 5B | VK_LWIN | 左Windows键 | |
92 | 5C | VK_RWIN | 右Windows键 | |
93 | 5D | VK_APPS | Applications键 |
Windows用VK_LWIN和VK_RWIN键打开「开始」菜单或者(在以前的版本中)启动「工作管理员程序」。这两个都可以用于登录或注销Windows(只在Microsoft Windows NT中有效),或者登录或注销网络(在Windows for Applications中)。应用程序能够通过显示辅助信息或者当成快捷方式键看待来处理application键。
表6-7所示的代码用于数字键盘上的键(如果有的话):
表6-7 |
十进制 | 十六进制 | 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个。另外,程序通常用功能键作为键盘快捷键,这样,它们通常不处理表6-8所示的按键:
表6-8 |
十进制 | 十六进制 | 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 |
另外,还定义了一些其它虚拟键码,但它们只用于非标准键盘上的键,或者通常在大型主机终端机上使用的键。查看/ Platform SDK / User Interface Services / User Input / Virtual-Key Codes,可得到完整的列表。
lParam信息
在四个按键消息(WM_KEYDOWN、WM_KEYUP、WM_SYSKEYDOWN和WM_SYSKEYUP)中,wParam消息参数含有上面所讨论的虚拟键码,而lParam消息参数则含有对了解按键非常有用的其它信息。lParam的32位分为6个字段,如图6-1所示。
图6-1 lParam变量的6个按键消息字段 |
重复计数
重复计数是该消息所表示的按键次数,大多数情况下,重复计数设定为1。不过,如果按下一个键之后,您的窗口消息处理程序不够快,以致不能处理自动重复速率(您可以在「控制台」的「键盘」中进行设定)下的按键消息,Windows就把几个WM_KEYDOWN或者WM_SYSKEYDOWN消息组合到单个消息中,并相应地增加重复计数。WM_KEYUP或WM_SYSKEYUP消息的重复计数总是为1。
因为重复计数大于1指示按键速率大于您程序的处理能力,所以您也可能想在处理键盘消息时忽略重复计数。几乎每个人都有文书处理或执行电子表格时画面卷过头的经验,因为多余的按键堆满了键盘缓冲区,所以当程序用一些时间来处理每一次按键时,如果忽略您程序中的重复计数,就能够解决此问题。不过,有时可能也会用到重复计数,您应该尝试使用两种方法执行程序,并从中找出一种较好的方法。
OEM扫描码
OEM扫描码是由硬件(键盘)产生的代码。这对中古时代的汇编程序写作者来说应该很熟悉,它是从PC相容机种的ROM BIOS服务中所获得的值(OEM指的是PC的原始设备制造商(Original Equipment Manufacturer)及其与「IBM标准」同步的内容)。在此我们不需要更多的信息。除非需要依赖实际键盘布局的样貌,不然Windows程序可以忽略掉几乎所有的OEM扫描码信息,参见第二十二章的程序KBMIDI。
扩充键旗标
如果按键结果来自IBM增强键盘的附加键之一,那么扩充键旗标为1(IBM增强型键盘有101或102个键。功能键在键盘顶端,光标移动键从数字键盘中分离出来,但在数字键盘上还保留有光标移动键的功能)。对键盘右端的Alt和Ctrl键,以及不是数字键盘那部分的光标移动键(包括Insert和Delete键)、数字键盘上的斜线(/)和Enter键以及Num Lock键等,此旗标均被设定为1。Windows程序通常忽略扩充键旗标。
内容代码
右按键时,假如同时压下ALT键,那么内容代码为1。对WM_SYSKEYUP与WM_SYSKEYDOWN而言,此位总视为1;而对WM_SYSKEYUP与WM_KEYDOW消息而言,此位为0。除了两个之外:
- 如果活动窗口最小化了,则它没有输入焦点。这时候所有的按键都会产生WM_SYSKEYUP和WM_SYSKEYDOWN消息。如果Alt键未被按下,则内容代码字段被设定为0。Windows使用WM_SYSKEYUP和WM_SYSKEYDOWN消息,从而使最小化了的活动窗口不处理这些按键。
- 对于一些外国语文(非英文)键盘,有些字符是通过Shift、Ctrl或者Alt键与其它键相组合而产生的。这时内容代码为1,但是此消息并非系统按键消息。
键的先前状态
如果在此之前键是释放的,则键的先前状态为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和Scroll Lock)。通过呼叫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(下面将详细说明)。
使用虚拟键码VK_LBUTTON、VK_RBUTTON和VK_MBUTTON,您也可以获得鼠标键的状态。不过,大多数需要监视鼠标键与按键相组合的Windows应用程序都使用其它方法来做到这一点-即在接收到鼠标消息时检查按键。实际上,位移状态信息包含在鼠标信息中,正如您在下一章中将看到的一样。
请注意GetKeyState的使用,它并非实时检查键盘状态,而只是检查直到目前为止正在处理的消息的键盘状态。多数情况下,这正符合您的要求。如果您需要确定使用者是否按下了Shift-Tab,请在处理Tab键的WM_KEYDOWN消息时呼叫GetKeyState,带有参数VK_SHIFT。如果GetKeyState传回的值为负,那么您就知道在按下Tab键之前按下了Shift键。并且,如果在您开始处理Tab键之前,已经释放了Shift键也没有关系。您知道,在按下Tab键的时候Shift键是按下的。
GetKeyState不会让您获得独立于普通键盘消息的键盘信息。例如,您或许想暂停窗口消息处理程序的处理,直到您按下F1功能键为止:
while (GetKeyState (VK_F1) >= 0) ; // WRONG !!!
不要这么做!这将让程序当死(除非在执行此叙述之前早就从消息队列中接收到了F1的WM_KEYDOWN)。如果您确实需要知道目前某键的状态,那么您可以使用GetAsyncKeyState。
使用按键消息
如果程序能够获得每个按键的信息,这当然很理想,但是大多数Windows程序忽略了几乎所有的按键,而只处理部分的按键消息。WM_SYSKEYDOWN和WM_SYSKEYUP消息是由Windows系统函数使用的,您不必为此费心,就算您要处理WM_KEYDOWN消息,通常也可以忽略WM_KEYUP消息。
Windows程序通常为不产生字符的按键使用WM_KEYDOWN消息。虽然您可能认为借助按键消息和位移键状态信息能将按键消息转换为字符消息,但是不要这么做,因为您将遇到国际键盘间的差异所带来的问题。例如,如果您得到wParam等于0x33的WM_KEYDOWN消息,您就可以知道使用者按下了键3,到此为止一切正常。这时,如果用GetKeyState发现Shift键被按下,您就可能会认为使用者输入了#号,这可不一定。比如英国使用者就是在输入£。
对于光标移动键、功能键、Insert和Delete键,WM_KEYDOWN消息是最有用的。不过, Insert、Delete和功能键经常作为菜单快捷键。因为Windows能把菜单快捷键翻译为菜单命令消息,所以您就不必自己来处理按键。
在Windows之前的MS-DOS应用程序中大量使用功能键与Shift、Ctrl和Alt键的组合,同样地,您也可以在Windows程序中使用(实际上,Microsoft Word将大量的功能键用作命令快捷方式),但并不推荐这样做。如果您确实希望使用功能键,那么这些键应该是重复菜单命令。Windows的目标之一就是提供不需要记忆或者使用复杂命令流程的使用者接口。
因此,可以归纳如下:多数情况下,您将只为光标移动键(有时也为Insert和Delete键)处理WM_KEYDOWN消息。在使用这些键的时候,您可以通过GetKeyState来检查Shift键和Ctrl键的状态。例如,Windows程序经常使用Shift与光标键的组合键来扩大文书处理里选中的范围。Ctrl键常用于修改光标键的意义。例如,Ctrl与右箭头键相组合可以表示光标右移一个字。
决定您的程序中使用键盘方式的最佳方法之一是了解现有的Windows程序使用键盘的方式。如果您不喜欢那些定义,当然可以对其加以修改,但是这样做不利于其它人很快地学会使用您的程序。
为SYSMETS加上键盘处理功能
在编写第四章中三个版本的SYSMETS程序时,我们还不了解键盘,只能使用滚动条和鼠标来卷动文字。现在我们知道了处理键盘消息的方法,那么不妨在程序中加入键盘接口。显然,这是处理光标移动键的工作。我们将大多数光标键(Home、End、Page Up、Page Down、Up Arrow和Down Arrow)用于垂直卷动,左箭头键和右箭头键用于不太重要的水平卷动。
建立键盘接口的一种简单方法是在窗口消息处理程序中加入与WM_VSCROLL和WM_HSCROLL处理方式相仿,而且本质上相同的WM_KEYDOWN处理方法。不过这样子做是不聪明的,因为如果要修改滚动条的做法,就必须相对应地修改WM_KEYDOWN。
为什么不简单地将每一种WM_KEYDOWN消息都翻译成同等效用的WM_VSCROLL或者WM_HSCROLL消息呢?通过向窗口消息处理程序发送假冒消息,我们可能会让WndProc认为它获得了卷动信息。
在Windows中,这种方法是可行的。发送消息的函数叫做SendMessage,它所用的参数与传递到窗口消息处理程序的参数是相同的:
SendMessage (hwnd, message, wParam, lParam) ;
在呼叫SendMessage时,Windows呼叫窗口句柄为hwnd的窗口消息处理程序,并把这四个参数传给它。当窗口消息处理程序完成消息处理之后,Windows把控制传回到SendMessage呼叫之后的下一道叙述。您发送消息过去的窗口消息处理程序,可以是同一个窗口消息处理程序、同一程序中的其它窗口消息处理程序或者其它应用程序,中的窗口消息处理程序。
下面说明在SYSMETS程序中使用SendMessage处理WM_KEYDOWN代码的方法:
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 ;
至此,您已经有了大概观念了吧。我们的目标是为滚动条添加键盘接口,并且也正在这么做。通过把卷动消息发送到窗口消息处理程序,我们实作了用光标移动键进行卷动列的功能。现在您知道在SYSMETS3中为WM_VSCROLL消息加上SB_TOP和SB_BOTTOM处理码的原因了吧。在那里并没有用到它,但是现在处理Home和End键时就有用了。如程序6-1所示的SYSENTS4就加上了这些变化。编译这个程序时还需要用到第四章的SYSMETS.H文件。
SYSMETS4.C /*---------------------------------------------------------------------- SYSMETS4.C -- System Metrics Display Program No. 4 (c) Charles Petzold, 1998 ------------------------------------------------------------------------*/ #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.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 ("Program requires Windows NT!"), 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) ; // Save the width of the three columns iMaxWidth = 40 * cxChar + 22 * cxCaps ; return 0 ; case WM_SIZE: cxClient = LOWORD (lParam) ; cyClient = HIWORD (lParam) ; // Set vertical scroll bar range and page size 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) ; // Set horizontal scroll bar range and page size 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: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; GetScrollInfo (hwnd, SB_VERT, &si) ; // Save the position for comparison later on 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_LINEDOWN: si.nPos += 1 ; break ; case SB_PAGEUP: si.nPos -= si.nPage ; break ; case SB_PAGEDOWN: si.nPos += si.nPage ; break ; case SB_THUMBTRACK: si.nPos = si.nTrackPos ; break ; default: break ; } // Set the position and then retrieve it. Due to adjustments // by Windows it might not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_VERT, &si, TRUE) ; GetScrollInfo (hwnd, SB_VERT, &si) ; // If the position has changed, scroll the window and update it if (si.nPos != iVertPos) { ScrollWindow (hwnd, 0, cyChar * (iVertPos - si.nPos), NULL, NULL) ; UpdateWindow (hwnd) ; } return 0 ; case WM_HSCROLL: // Get all the vertical scroll bar information si.cbSize = sizeof (si) ; si.fMask = SIF_ALL ; // Save the position for comparison later on 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 ; } // Set the position and then retrieve it. Due to adjustments // by Windows it might not be the same as the value set. si.fMask = SIF_POS ; SetScrollInfo (hwnd, SB_HORZ, &si, TRUE) ; GetScrollInfo (hwnd, SB_HORZ, &si) ; // If the position has changed, scroll the window if (si.nPos != iHorzPos) { ScrollWindow (hwnd, cxChar * (iHorzPos - si.nPos), 0, NULL, NULL) ; } 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_PAGEUP, 0) ; break ; case VK_RIGHT: SendMessage (hwnd, WM_HSCROLL, SB_PAGEDOWN, 0) ; break ; } return 0 ; case WM_PAINT: hdc = BeginPaint (hwnd, &ps) ; // Get vertical scroll bar position si.cbSize = sizeof (si) ; si.fMask = SIF_POS ; GetScrollInfo (hwnd, SB_VERT, &si) ; iVertPos = si.nPos ; // Get horizontal scroll bar position GetScrollInfo (hwnd, SB_HORZ, &si) ; iHorzPos = si.nPos ; // Find painting limits 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].iIndex))) ; SetTextAlign (hdc, TA_LEFT | TA_TOP) ; } EndPaint (hwnd, &ps) ; return 0 ; case WM_DESTROY: PostQuitMessage (0) ; return 0 ; } return DefWindowProc (hwnd, message, wParam, lParam) ; }