掘金 阅读 ( ) • 2024-03-27 17:21

108320734_p0.jpg

本文用于记录笔者在学习「Windows API」时碰到的疑难点。本文对应的实验环境为Visual Studio 2022。

为了方便代码调试,我们首先编写一个自定义类,用于在VS的控制台中打印调试信息:

#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中插入如下的代码:

        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控制台的打印结果如下:

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_KEYDOWMWM_CHAR。并且无论我实际想输入的是大写还是小写字母,接收WM_KEYDOWN消息时wParam参数的值都是对应大写字母的ASCII码。而在接下来接收WM_CHAR消息时,wParam参数中的值则根据大小写字母而有不同的取值。

另外通过这段输出,我们也可以知道对于VK_SHIFT等不存在于ASCII码表中的虚拟键,只能触发消息WM_KEYDOWNWM_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组合快捷键的检测:

        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_LPARAMGET_Y_LPARAM宏。

我们可以先看看怎么利用这两个宏编写代码,以实时获取鼠标的坐标。

    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系统是如何传递鼠标坐标的:

// 下面的代码仅代表在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中。

另外,教材还为我们提供了另外一种获取鼠标相对坐标的方法:

case WM_MOUSEMOVE: {
    POINT point;
    // 获取鼠标相对电脑屏幕左上角的坐标,存入point中
    GetCursorPos(&point);
    // 将point中的坐标取出,换算成鼠标相对于
    // 窗口用户区左上角的坐标,再存回point中
    ScreenToClient(hWnd, &point);
    mousePosX = point.x;
    mousePosY = point.y;
    InvalidateRect(hWnd, nullptr, true);
    break;
}

经过测试,我们发现这两种方法的效果是完全一致的