likes
comments
collection
share

【专业课学习】「Windows API」疑难点汇编

作者站长头像
站长
· 阅读数 45

【专业课学习】「Windows API」疑难点汇编

本文用于记录笔者在学习「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;
}

经过测试,我们发现这两种方法的效果是完全一致的。那么什么时候该选用GetCursorPosScreenToClient函数来获取鼠标的坐标呢?一个简单的答案是,当我们需要在接收WM_PAINT或者其他消息时,需要根据用户当前的光标位置进行某些计算时,由于我们无法再通过lParam来实现,这时就只能使用这两个函数了。

Caret问题

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    static char text[] = "Hello World";
    static int lineHeight;
    static int caretPosX = 0;
    static int caretPosY = 0;
    switch (message) {
        case WM_CREATE: {
            HDC hDC = GetDC(hWnd);
            TEXTMETRIC tm;
            // 获取文本的度量信息
            GetTextMetrics(hDC, &tm);
            // 计算行高
            lineHeight = tm.tmHeight + tm.tmExternalLeading;
            // 创建光标
            CreateCaret(hWnd, nullptr, 1, lineHeight);
            // 显示光标
            ShowCaret(hWnd);
            ReleaseDC(hWnd, hDC);
            break;
        }
        case WM_PAINT: {
            PAINTSTRUCT ps;
            HDC hDC = BeginPaint(hWnd, &ps);
            TextOut(hDC, 0, 0, text, strlen(text));
            TextOut(hDC, 0, lineHeight, text, strlen(text));

            SIZE size;
            GetTextExtentPoint(hDC, text, caretPosX, &size);
            
            // 设置光标位置
            SetCaretPos(size.cx, caretPosY * lineHeight);
            EndPaint(hWnd, &ps);
            break;
        }
        case WM_KEYDOWN: {
            switch (wParam) {
                case VK_LEFT: {
                    if (caretPosX > 0) --caretPosX;
                    break;
                }
                case VK_RIGHT: {
                    if (caretPosX < strlen(text)) ++caretPosX;
                    break;
                }
                case VK_UP: {
                    if (caretPosY > 0) --caretPosY;
                    break;
                }
                case VK_DOWN: {
                    if (caretPosY < 1) ++caretPosY; // 假定只有两行文本
                    break;
                }
            }
            InvalidateRect(hWnd, nullptr, true);
            break;
        }
        case WM_DESTROY:
            PostQuitMessage(0);
            break;
        default:
            return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

禁(启)用菜单(项)问题

Windows API支持编写代码动态禁(启)用Menu Bar中的某个菜单(submenu),或者点开某个菜单后的某个菜单项(menu item)。

禁、启用操作对应的宏分别为MF_DISABLEDMF_ENABLED

禁用菜单项-代码示例:

/* 写法一 */
HMENU hMenu = GetMenu(hWnd);
// 调用GetSubMenu函数获取特定菜单的句柄,
// 该函数的第二个参数表示目标菜单在Menu Bar中的位置(从0开始数)
HMENU hSubMenu = GetSubMenu(hMenu, 0);
// 宏MF_BYPOSITION表示调用EnableMenuItem时第二个参数的含义为
// 目标菜单项在菜单中的位置(从0开始数)
EnableMenuItem(hSubMenu, 1, MF_DISABLED | MF_BYPOSITION);

/* 写法二 */
HMENU hMenu = GetMenu(hWnd);
HMENU hSubMenu = GetSubMenu(hMenu, 0);
// 宏MF_BYCOMMAND表示函数第二个参数的含义为目标菜单项的ID号
EnableMenuItem(hSubMenu, IDM_EXIT, MF_DISABLED | MF_BYCOMMAND);

/* 写法三 */
HMENU hMenu = GetMenu(hWnd);
// 经测试,只要知道目标菜单项的ID号,在调用函数时直接传入hMenu句柄即可
EnableMenuItem(hMenu, IDM_EXIT, MF_DISABLED | MF_BYCOMMAND);

禁用菜单-效果示例:

【专业课学习】「Windows API」疑难点汇编

至于禁用菜单,写法与禁用菜单项基本一致,不过要注意两点:

  1. 由于在Windows SDK中菜单并没有独立的ID号,因此只能通过指定位置的方式来对其进行操作。
  2. 对菜单进行的操作最终需要反映到菜单栏上,因此需要调用DrawMenuBar函数来强制重绘菜单栏。

禁用菜单-代码示例

HMENU hMenu = GetMenu(hWnd);
EnableMenuItem(hMenu, 1, MF_DISABLED | MF_BYPOSITION);
DrawMenuBar(hWnd);

禁用菜单-效果示例

【专业课学习】「Windows API」疑难点汇编

插入菜单(项)问题

我们先来看插入菜单项的问题。

插入菜单项-代码示例:

HMENU hMenu = GetMenu(hWnd);
HMENU hSubMenu = GetSubMenu(hMenu, 0);
static char loadItemText[] = "导入(&L)";
MENUITEMINFOA menuInfo;

#define IDM_FILE_LOAD 233
        
// 字段cbSize必须设置为sizeof(MENUITEMINFO)
menuInfo.cbSize = sizeof(MENUITEMINFO);
// 字段fMask用于声明系统在创建菜单项时应读取结构体中的哪些字段
// MIIM_ID表示系统应读取字段wID
// MIIM_STRING表示应读取字段dwTypeData,且该指针指向一个字符串
menuInfo.fMask = MIIM_ID | MIIM_STRING;
// 字段wID用于设定菜单项的ID号
menuInfo.wID = IDM_FILE_LOAD;
// 字段dwTypeData指向资源的首地址
menuInfo.dwTypeData = reinterpret_cast<LPSTR>(loadItemText);
// 字段cch用于设定dwTypeData指向资源在内存中的长度
menuInfo.cch = strlen(menuInfo.dwTypeData);

// 写法一
// 第三个参数为true,表示将新的菜单项插入到菜单中的指定位置
// 原先位于该位置的菜单项及位居其后者,则顺次后移
InsertMenuItem(hSubMenu, 1, true, &menuInfo);

//写法二
// 第三个参数为false,表示将新的菜单插入到原先由第二个参数指定的菜单项的位置
// 同样地,原有的相关菜单项也会自动顺次后移
InsertMenuItem(hSubMenu, IDM_EXIT, false, &menuInfo);

插入菜单项-效果示例:

【专业课学习】「Windows API」疑难点汇编

与禁用菜单的问题类似,我们也可以将上述代码推广至"插入菜单问题"。

插入菜单-代码示例

// 获取菜单栏的句柄
HMENU hMenu = GetMenu(hWnd);
// 为准备要新插入的菜单创建句柄
HMENU hNewMenu = CreatePopupMenu();

static char loadItemText[] = "颜色(&C)";

MENUITEMINFOA menuInfo;
menuInfo.cbSize = sizeof(MENUITEMINFO);
// 宏MIIM_SUBMENU表示系统应读取字段hSubMenu,以将新菜单与其句柄绑定
menuInfo.fMask = MIIM_SUBMENU | MIIM_STRING;
menuInfo.hSubMenu = hNewMenu;
menuInfo.dwTypeData = reinterpret_cast<LPSTR>(loadItemText);
menuInfo.cch = strlen(menuInfo.dwTypeData);

// 将新的菜单插入到菜单栏上
InsertMenuItem(hMenu, 1, true, &menuInfo);
DrawMenuBar(hWnd);

插入菜单-效果示例

【专业课学习】「Windows API」疑难点汇编

追加菜单(项)问题

"追加菜单(项)问题"实际上是"插入菜单(项)问题"的一个子问题。显然,我们仍然可以使用InsertMenuItem函数来实现。当然,这就意味着我们需要创建一揽子繁琐的MENUITEMINFOA结构体。幸运的是,Windows SDK为我们提供了AppendMenu函数,来便捷地实现追加操作。

我们来看看如何向菜单栏追加一个菜单,并在这个菜单中添加若干个菜单项。

追加菜单(项)-代码示例:

// 获取窗口菜单栏的句柄
HMENU hMenu = GetMenu(hWnd);
// 为新追加的菜单创建句柄
HMENU hNewMenu = CreatePopupMenu();
static char loadItemText[] = "颜色(&C)";

#define IDM_COLOR_RED 101
#define IDM_COLOR_YELLOW 102
#define IDM_COLOR_GREEN 103

// 通过句柄,为新菜单追加菜单项
// 宏MF_STRING与AppendMenu第四个参数对应,表示菜单项作为一个字符串显示
// AppendMenu第三个参数即为菜单项的ID号
AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_RED, "Red");
AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_YELLOW, "Yellow");
AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_GREEN, "Green");
        
// 将新菜单追加到菜单栏上
// 宏MF_POPUP用于声明要追加的资源是一个(弹出式)菜单,
// 并且此时第三个参数即为要追加菜单的句柄
AppendMenu(hMenu, MF_POPUP | MF_STRING, (UINT_PTR)hNewMenu, loadItemText);
DrawMenuBar(hWnd);

追加菜单(项)-效果示例:

【专业课学习】「Windows API」疑难点汇编

勾选(撤销)菜单项问题

勾选菜单项通过CheckMenuItem函数实现。诚然,这个函数对菜单栏中的菜单本身并不会有任何效果。

在指定对哪个菜单项产生作用的问题上,CheckMenuItem函数与先前介绍的一系列函数存在着高度雷同,这里就不再赘述了。我们直接来看代码:

勾选菜单项-代码示例:

/* 写法一 */
CheckMenuItem(hMenu, IDM_COLOR_YELLOW, MF_CHECKED | MF_BYCOMMAND);

/* 写法二 */
CheckMenuItem(hNewMenu, 1, MF_CHECKED | MF_BYPOSITION);

勾选菜单项-效果示例:

【专业课学习】「Windows API」疑难点汇编

EnableMenuItem函数类似,CheckMenuItem函数也支持"反向操作",即撤销对某个菜单项的勾选。我们只需要将前述代码中的宏MF_CHECKED替换为MF_UNCHECKED即可。

删除菜单(项)问题

删除菜单项一般可以通过RemoveMenu或者DeleteMenu函数实现。

与先前介绍的函数一样,它们同样支持MF_BYCOMMANDMF_BYPOSITION模式,这里就不再赘述了。

当从菜单中移除菜单项时,这两个函数的执行效果一致。但当从菜单栏中移除菜单时,RemoveMenu函数只是将令目标菜单暂时不在菜单栏中显示了,后续还可通过该菜单的句柄再次动态地将其添加回菜单栏。而RemoveMenu除了在视觉意义上,将目标菜单从菜单栏中移除之外,还会自动释放目标菜单绑定的句柄(也就是说从内存中释放了目标菜单及其所有的菜单项),因此在此之后程序员就无法再对该菜单进行任何操作了。

HMENU hMenu = GetMenu(hWnd);
HMENU hNewMenu = CreatePopupMenu();
static char loadItemText[] = "颜色(&C)";

#define IDM_COLOR_RED 101
#define IDM_COLOR_YELLOW 102
#define IDM_COLOR_GREEN 103

AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_RED, "Red");
AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_YELLOW, "Yellow");
AppendMenu(hNewMenu, MF_STRING, IDM_COLOR_GREEN, "Green");
AppendMenu(hMenu, MF_POPUP | MF_STRING, (UINT_PTR)hNewMenu, loadItemText);

// 从菜单中移除菜单项
RemoveMenu(hMenu, IDM_COLOR_RED, MF_BYCOMMAND);
// 从窗口菜单栏中移除菜单
RemoveMenu(hMenu, 2, MF_BYPOSITION);
// 在内存中彻底销毁菜单句柄。
DeleteMenu(hMenu, 2, MF_BYPOSITION);
转载自:https://juejin.cn/post/7350860703639011366
评论
请登录