Windows 编程/接口
当按下键盘上的某个键时,信号会传送到计算机,内核会接收到它。这些信号,或者称为“键码”,是需要转换为 ASCII 字符的原始数据。内核执行此转换,并获取有关按键的其他信息。必要的信息被编码到一条消息中,并发送到当前活动的窗口。同时,消息也会发送到当前活动的插入符。
在 Windows 内部,存在一个很大的术语问题。如此多的不同事物都需要拥有自己的名称,以便我们可以用它们进行编程,并保持一切井然有序。一个完美的例子是光标和插入符之间的区别。光标是表示鼠标的图形图像。它可以是用于指向的箭头、手形、沙漏或 I 形文本选择器。另一方面,插入符是用于输入文本的闪烁对象。当您键入时,字母会出现在插入符处,并且插入符会向前移动 1 个空格。务必区分这些术语,因为如果在程序中混淆了它们,则可能需要进行大量调试。
程序可以选择处理一些按键消息。需要注意的是,并非所有这些消息都需要在程序中处理,实际上,通常最好不要处理其中的许多消息。
- WM_KEYDOWN
- WM_KEYDOWN 消息指示已按下某个键,或已按下并按住该键。如果按住该键,键盘将进入“自动重复”模式,并以一定频率重复生成按键事件。内核将为每个事件生成 WM_KEYDOWN 消息,其中 LPARAM 消息组件包含内核连续发送的消息数量,以便程序可以选择忽略某些消息而不会错过任何信息。除了键计数之外,LPARAM 还将包含以下信息
LPARAM 的位 用途 0-15 键计数 16-23 扫描码 29 上下文代码 30 先前状态 31 键转换
- 扫描码是来自键盘的原始二进制信号,它可能与字符的 ASCII 值不对应。请注意,键盘上的所有按钮都会生成扫描码,包括操作按钮(Shift、ALT、CTRL)。除非您尝试以特殊方式与键盘交互,否则您需要忽略扫描码。上下文代码确定是否同时按下了 ALT 键。如果同时按下了 ALT 键,则上下文代码为 1。先前状态是生成消息之前按钮的状态。键转换确定是正在按下键还是正在释放键。对于大多数程序而言,WM_KEYDOWN 消息中的大多数字段都可以安全地忽略,除非您尝试使用自动重复功能,或尝试在低级与键盘交互。
- WM_KEYUP
- 当已按下某个键被释放时,会发送此消息。每个按键至少会生成两条消息:按键按下(按下按钮时)和按键释放(释放按钮时)。通常对于大多数文本处理应用程序,可以忽略按键释放消息。WPARAM 是虚拟字符代码的值,而 LPARAM 与 WM_KEYDOWN 消息中的相同。
Windows 用户无疑会熟悉一些与大型 Windows 程序一起使用的常见键组合。CTRL+C 将对象复制到剪贴板。CTRL+P 打印当前文档。CTRL+S 保存当前文档。还有数十种其他组合,并且每个程序似乎都有其自己的特定键组合。
在 Windows 世界中,这些键组合称为“加速键”。使用加速键的程序将定义一个加速键表。此表将包含所有不同的键组合以及它们各自映射到的命令标识符。当按下键盘上的加速键时,程序不会接收按键消息,而是接收 WM_COMMAND 消息,其中 WPARAM 字段包含命令标识符。
要将加速键组合转换为 WM_COMMAND 消息,需要在消息循环中结合使用 TranslateAccelerator 函数,如下所示
while(GetMessage(&msg, NULL, 0, 0)) { if(!TranslateAccelerator(&msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } }
TranslateAccelerator 函数将自动将 WM_COMMAND 消息分派到相应的窗口。
每个程序只能拥有 1 个活动插入符,更糟糕的是,整个系统一次只能在屏幕上拥有 1 个活动插入符。使用插入符时,程序员需要注意在不使用时销毁插入符,并在需要时重新创建插入符。这可以通过在 WM_SETFOCUS 消息(当窗口变为活动状态时)创建插入符,并在 WM_KILLFOCUS 和 WM_DESTROY 消息中销毁插入符来相对轻松地完成。
鼠标关联的消息更多,因为鼠标能够执行比键盘更多的独特任务。例如,鼠标至少有 2 个按钮(通常为 3 个或更多),它通常有一个轨迹球,并且可以悬停在屏幕上的对象上。鼠标的每个功能都可以通过消息处理,因此我们需要几种消息
程序可以处理许多鼠标消息。
- WM_LBUTTONDBLCLK
- 用户双击了鼠标左键。
- WM_LBUTTONDOWN
- 用户按下了鼠标左键。
- WM_LBUTTONUP
- 用户释放了鼠标左键。
- WM_MBUTTONDOWN
- 用户按下了鼠标中键。
- WM_MBUTTONUP
- 用户释放了鼠标中键。
- WM_MOUSEMOVE
- 用户在窗口的客户区内移动了鼠标光标。
- WM_MOUSEWHEEL
- 用户旋转或按下了鼠标滚轮。
- WM_RBUTTONDOWN
- 用户按下了鼠标右键。
- WM_RBUTTONUP
- 用户释放了鼠标右键。
此外,LPARAM 字段将包含有关光标位置的信息(以 X-Y 坐标表示),而 WPARAM 字段将包含有关 Shift 和 CTRL 键状态的信息。
系统将处理图形鼠标移动,因此您无需担心程序会锁定鼠标。但是,程序能够更改鼠标光标,并在需要时将鼠标消息发送到其他窗口。
计时器用于通过暂停来间隔程序的流程,以便在另一组与结果交互之前允许一组操作进行处理。从严格意义上讲,Windows 计时器不是用户输入设备,尽管计时器可以向窗口发送输入消息,因此通常以与鼠标和键盘相同的方式对其进行处理。具体来说,Charles Petzold 的著名著作“Programming Windows”将计时器视为输入设备。
计时器的常见用法是通知程序暂停结束,以便它可以擦除先前绘制在屏幕上的图像,例如在显示来自一个文件夹的各种图像的屏幕保护程序中。但是,本机计时器功能对于游戏或时间关键型响应而言并不被认为是准确的。DirectX API 适用于游戏。
每次分配给计时器的指定时间间隔过去时,系统都会向与计时器关联的窗口发送 WM_TIMER 消息。
新的计时器在由SetTimer函数创建后立即开始计时间隔。创建计时器时,您将检索一个唯一的标识符,该标识符可由KillTimer函数用于销毁计时器。此标识符也存在于 WM_TIMER 消息的第一个参数中。
让我们看看函数语法。
UINT_PTR SetTimer( HWND hWnd, //Handle of the window associated to the timer UINT nIDEvent, //an identifier for the timer UINT uElapse, //the time-out value, in ms TIMERPROC lpTimerFunc //the address of the time procedure (see below) ); BOOL KillTimer( HWND hWnd, // Handle of the window associated to the timer UINT_PTR uIDEvent // the identifier of the timer to destroy );
如果要重置现有计时器,则必须将第一个参数设置为 NULL,并将第二个参数设置为现有计时器 ID。
您可以通过两种不同的方式处理 WM_TIMER 消息
- 通过处理作为第一个参数传递的窗口的窗口过程中收到的 WM_TIMER 消息。
- 通过定义一个 TimerProc 回调函数(第四个参数)来处理消息,而不是使用窗口过程。
让我们看看第一个。
#define IDT_TIMER1 1001 #define IDT_TIMER2 1002 ... SetTimer(hwnd, //handle of the window associated to the timer IDT_TIMER1, //timer identifier 5000, // 5 seconds timeout (TIMERPROC)NULL); //no timer procedure, process WM_TIMER in the window procedure SetTimer(hwnd, IDT_TIMER2, 10000, (TIMERPROC)NULL); ... LRESULT CALLBACK WinProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam){ ... case WM_TIMER: switch(wParam) { case IDT_TIMER1: //process the 5 seconds timer break; case IDT_TIMER2: //process the 10 seconds timer break; } ... /* to destroy the timers */ KillTimer(hwnd, IDT_TIMER1); KillTimer(hwnd, IDT_TIMER2);
现在使用一个 TimerProc。
VOID CALLBACK TimerProc( HWND hwnd, // handle of window for timer messages UINT uMsg, // WM_TIMER message UINT idEvent, // timer identifier DWORD dwTime // current system time );
它由系统调用,以处理关联的 Timer 的 WM_TIMER 消息。让我们看一些代码。
#define IDT_TIMER1 1001 ... /* The Timer Procedure */ VOID CALLBACK TimerProc(HWND hwnd, UINT uMsg, UINT idEvent, DWORD dwTime) { MessageBox(NULL, "One second is passed, the timer procedure is called, killing the timer", "Timer Procedure", MB_OK); KillTimer(hwnd, idEvent); } ... /* Creating the timer */ SetTimer(hwnd, IDT_TIMER1, 1000, (TIMERPROC)TimerProc); ...
定时器只带有一个消息,WM_TIMER,并且 WPARAM 字段将包含定时器 ID 号。