【专业课学习】「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_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组合快捷键的检测:
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
宏。
我们可以先看看怎么利用这两个宏编写代码,以实时获取鼠标的坐标。
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;
}
经过测试,我们发现这两种方法的效果是完全一致的。那么什么时候该选用GetCursorPos
和ScreenToClient
函数来获取鼠标的坐标呢?一个简单的答案是,当我们需要在接收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_DISABLED
、MF_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 SDK中菜单并没有独立的ID号,因此只能通过指定位置的方式来对其进行操作。
- 对菜单进行的操作最终需要反映到菜单栏上,因此需要调用
DrawMenuBar
函数来强制重绘菜单栏。
禁用菜单-代码示例
HMENU hMenu = GetMenu(hWnd);
EnableMenuItem(hMenu, 1, MF_DISABLED | MF_BYPOSITION);
DrawMenuBar(hWnd);
禁用菜单-效果示例
插入菜单(项)问题
我们先来看插入菜单项的问题。
插入菜单项-代码示例:
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);
插入菜单项-效果示例:
与禁用菜单的问题类似,我们也可以将上述代码推广至"插入菜单问题"。
插入菜单-代码示例
// 获取菜单栏的句柄
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);
插入菜单-效果示例
追加菜单(项)问题
"追加菜单(项)问题"实际上是"插入菜单(项)问题"的一个子问题。显然,我们仍然可以使用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);
追加菜单(项)-效果示例:
勾选(撤销)菜单项问题
勾选菜单项通过CheckMenuItem
函数实现。诚然,这个函数对菜单栏中的菜单本身并不会有任何效果。
在指定对哪个菜单项产生作用的问题上,CheckMenuItem
函数与先前介绍的一系列函数存在着高度雷同,这里就不再赘述了。我们直接来看代码:
勾选菜单项-代码示例:
/* 写法一 */
CheckMenuItem(hMenu, IDM_COLOR_YELLOW, MF_CHECKED | MF_BYCOMMAND);
/* 写法二 */
CheckMenuItem(hNewMenu, 1, MF_CHECKED | MF_BYPOSITION);
勾选菜单项-效果示例:
与EnableMenuItem
函数类似,CheckMenuItem
函数也支持"反向操作",即撤销对某个菜单项的勾选。我们只需要将前述代码中的宏MF_CHECKED
替换为MF_UNCHECKED
即可。
删除菜单(项)问题
删除菜单项一般可以通过RemoveMenu
或者DeleteMenu
函数实现。
与先前介绍的函数一样,它们同样支持MF_BYCOMMAND
和MF_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