
本文用于记录笔者在学习「Windows API」时碰到的疑难点。本文对应的实验环境为Visual Studio 2022。
为了方便代码调试,我们首先编写一个自定义类,用于在VS的控制台中打印调试信息:
c++
#include <sstream>
class MyDebugOutput {
private:
std::stringstream stream;
public:
~MyDebugOutput() {
OutputDebugString(stream.str().c_str());
}
template <typename T>
MyDebugOutput& operator<<(const T& msg) {
stream << msg;
return *this;
}
};
WM_KEYDOWN与WM_CHAR
为了说明问题,我们先在WndProc
中插入如下的代码:
c++
case WM_KEYDOWN: {
MyDebugOutput() << "Here is WM_KEYDOWN, wParam=" << wParam << '\n';
break;
}
case WM_CHAR: {
MyDebugOutput() << "Here is WM_CHAR, wParam=" << wParam << '\n';
break;
}
当我先后在键盘上输入小写的q
和大写的Q
后,VS控制台的打印结果如下:
python
Here is WM_KEYDOWN, wParam=81 #81对应大写字母Q的ASCII码
Here is WM_CHAR, wParam=113 #113对应小写字母q的ASCII码
Here is WM_KEYDOWN, wParam=16 #VK_SHIFT,用于输入大写字母Q
Here is WM_KEYDOWN, wParam=81
Here is WM_CHAR, wParam=81
我们注意到,对于输入字符(实际上可以是任何在ASCII码表中招到的可读字符或者控制符)的键盘操作,会先后触发WM_KEYDOWM
和WM_CHAR
。并且无论我实际想输入的是大写还是小写字母,接收WM_KEYDOWN
消息时wParam
参数的值都是对应大写字母 的ASCII码。而在接下来接收WM_CHAR
消息时,wParam
参数中的值则根据大小写字母而有不同的取值。
另外通过这段输出,我们也可以知道对于VK_SHIFT
等不存在于ASCII码表中的虚拟键,只能触发消息WM_KEYDOWN
,WM_CHAR
对此不会有任何响应。
Shift/Ctrl组合快捷键问题
从上个问题中我们可以发现,对于用户敲击Shift键的动作,WM_KEYDOWN
可以作出响应,而WM_CHAR
却不然。那么假如我们的应用中,规定某个快捷键组合为shift+q
,又该如何编写代码呢?
首先,根据前述的分析,我们肯定要将处理组合键的代码写在WM_KEYDOWN
中。于是现在关键的问题就是如何检测用户在按住shift键的同时敲击了q键。
这里先揭晓答案,我们需要使用short GetKeyState(int nVirtKey)
函数。根据微软官方的文档"If the high-order bit is 1, the key is down; otherwise, it is up. "的说明,GetKeyState
函数返回值中的最高位若为1
,则表示对应的虚拟键被按下;若为0
,则反之。
根据上面的分析,我们可以通过如下的代码实现Shift组合快捷键的检测:
c++
case WM_KEYDOWN: {
if (wParam == 'Q') {
// 掩码0x8000即0b1000_0000_0000_0000
// 这里的按位与操作是为了提取short型返回值的最高位
if (GetKeyState(VK_SHIFT) & 0x8000) {
MyDebugOutput() << "You hit shift+q!\n";
}
else {
MyDebugOutput() << "You hit q/Q!\n";
}
}
break;
}
经测试,代码可以输出正确的结果。当然,我们在编程时若不愿在我们的代码中留下0x8000
这么一个奇怪的magic number,我们也可以将GetKeyState
的返回结果与0
作比较,因为C/C++中整型的最高位恰好也是符号位!
另外一个好消息是,对基于Ctrl组合键的检测,也可以使用上述代码实现,只需将代码中的VK_SHIFT
替换成VK_CONTROL
即可。
获取鼠标坐标问题
为了获取鼠标的坐标,一种办法是使用<windowsx.h>
头文件中的GET_X_LPARAM
和GET_Y_LPARAM
宏。
我们可以先看看怎么利用这两个宏编写代码,以实时获取鼠标的坐标。
C++
static int mousePosX = 0;
static int mousePosY = 0;
switch (message) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hDC = BeginPaint(hWnd, &ps);
std::stringstream stream;
// 利用字符串流拼接数据,并生成一个临时的字符串
stream << mousePosX << ", " << mousePosY;
std::string mystr = stream.str();
TextOut(hDC, 0, 0, mystr.c_str(), strlen(mystr.c_str()));
EndPaint(hWnd, &ps);
break;
}
case WM_MOUSEMOVE: {
mousePosX = GET_X_LPARAM(lParam);
mousePosY = GET_Y_LPARAM(lParam);
InvalidateRect(hWnd, nullptr, true);
break;
}
// 后略...
}
在这段代码中,当我们移动鼠标时,屏幕上实时地显示当前鼠标相对于窗口用户区左上角的坐标。
此外透过这两个宏的定义,我们也可以注意到接收WM_MOUSEMOVE
消息时Windows系统是如何传递鼠标坐标的:
c++
// 下面的代码仅代表在64位版本Windows系统中的情况:
#define GET_X_LPARAM(lParam) ((int)(short)((WORD)(((DWORD_PTR)(lParam)) & 0xffff)))
#define GET_Y_LPARAM(lParam) ((int)(short)((WORD)((((DWORD_PTR)(lParam)) >> 16) & 0xffff)))
可见在接收WM_MOUSEMOVE
消息时,鼠标的相对坐标分别存放在lParam
参数的高2Byte和低2Byte中。
另外,教材还为我们提供了另外一种获取鼠标相对坐标的方法:
c++
case WM_MOUSEMOVE: {
POINT point;
// 获取鼠标相对电脑屏幕左上角的坐标,存入point中
GetCursorPos(&point);
// 将point中的坐标取出,换算成鼠标相对于
// 窗口用户区左上角的坐标,再存回point中
ScreenToClient(hWnd, &point);
mousePosX = point.x;
mousePosY = point.y;
InvalidateRect(hWnd, nullptr, true);
break;
}
经过测试,我们发现这两种方法的效果是完全一致的。