承接前文:
- win32窗口编程
- windows 开发基础
- win32-注册窗口类、创建窗口
- win32-显示窗口、消息循环、消息队列
本文目录
- 键盘消息
- 键盘消息的分类
- WM_CHAR 字符消息
- 鼠标消息
- 鼠标消息附带信息
- 定时器消息 WM_TIMER
- 创建销毁定时器
- 菜单资源
- 资源相关
- 菜单资源使用
- 命令消息的处理 WM_COMMAND
- 上下文菜单
键盘消息
键盘消息的分类
- WM_KEYDOWN :按键被按下时产生。
- WM_KEYUP :按键被放开时产生。
- WM_SYSKEYDOWN :系统按键被按下时产生,比如:ALT、F10等。
- WM_SYSKEYUP :系统按键放开时产生。
- 附带信息:
- WPARAM :按键的 Virtual Key Code(虚拟键码),虚拟键码是操作系统定义的一组唯一标识不同键的整数值,用来表示每个键盘键的抽象标识。虚拟键码与具体的物理键盘码(如扫描码)或字符不同,它不依赖于特定的键盘布局。应用程序通过检查wParam的值可以得知是哪个键被按下了,然后可以进行相应的处理逻辑。因为虚拟键码是统一的跨越不同键盘布局的标识符,这样设计帮助开发者编写跨平台和可移植的应用程序。
- LPARAM :按键的参数,例如按下的次数
- 附带信息:
- 按下一个按键并松开,会产生一个 WM_KEYDOWN 消息和一个 WM_KEYUP 消息(成对出现)。
- 按下一个按键不松开,会连续产生 WM_KEYDOWN 消息,知道松开时会产生 一个 WM_KEYUP 消息。
- 无论大小写锁定键是否被打开,按下相同的字母时键码值是一样的,意味着单凭键码值无法区分大小写,所以按键消息需要翻译,以区分大小写。
- 在Windows系统上,
Alt
键和F10
键,这俩按键是系统按键,需要用WM_SYSKEYDOWN
和WM_SYSKEYUP
。
WM_CHAR 字符消息
- TranslateMessage() 在转换
WM_KEYDOWN
消息时,对于可见字符可以产生WM_CHAR
消息,不可见字符无此消息。 - 附带信息:
- WPARAM :输入字符的 ASCII 码值。
- LPARAM :按键的相关参数。
//TranslateMessage伪代码
TranlateMessage(){if (nMsg.message != WM_KEYDOWN)return ...; //如果不是 WM_KEYDOWN 消息直接return//根据 nMsg.wParam (键码值) 可以获知哪个按键被按下if (不可见字符的按键)return ...;//查看CapsLock (大小写锁定键) 是否被打开if (打开)PostMessage(nMsg.hWnd, WM_CHAR, 按键的大写ASCII码值, ... );elsePostMessage(nMsg.hWnd, WM_CHAR, 按键的小写ASCII码值, ... );
}
- CapsLock 键没有打开时,按下键盘
A
键 产生的WM_CHAR
消息的 wParam 值为 小写字母a
的ASCII码值97
。按下 CapsLock 键(键码值20)后,再按下键盘A
键,产生的WM_CHAR
消息的 wParam 值为 大写字母A
的ASCII码值65
。
鼠标消息
鼠标基本消息:
- WM_LBUTTONDOWN :鼠标左键按下。
- WM_LBUTTONUP :鼠标左键抬起。
- WM_RBUTTONDOWN :鼠标右键按下。
- WM_RBUTTONUP :鼠标右键抬起。
- WM_MOUSEMOVE :鼠标移动消息。
双击消息:
- WM_LBUTTONDBLCLK :左键双击。
- WM_RBUTTONDBLCLK :鼠标右键双击。
使用双击消息需要在注册窗口类时增加 CS_DBLCLKS
风格,否则无论鼠标表点击得多么快都是多次单击。
滚轮消息:
- WM_MOUSEWHEEL
鼠标消息附带信息
-
wParam :其他按键的状态,例如Ctrl / Shift 等。
-
lParam :鼠标的位置,窗口客户区坐标系。
- LOWORD x坐标位置
- HIWORD y坐标位置
-
一般情况下鼠标按下 / 抬起 成对出现。在鼠标移动过程中,会根据移动速度产生一系列的 WM_MOUSEMOVE 消息。
case WM_LBUTTONDOWN:OnLButtonDown(hWnd, wParam, lParam);break;
case WM_LBUTTONUP:OnLButtonUp(hWnd, wParam, lParam);break;void OnLButtonDown(HWND hWnd, WPARAM wParam, LPARAM lParam) {char text[128] = { 0 };sprintf_s(text, "WM_LBUTTONDOWN:其他按键状态:%d,x = %d, y = %d\n", wParam, LOWORD(lParam), HIWORD(lParam));WriteConsole(g_dos_output, text, strlen(text), NULL, NULL);
}void OnLButtonUp(HWND hWnd, WPARAM wParam, LPARAM lParam) {char text[128] = { 0 };sprintf_s(text, "WM_LBUTTONUP:其他按键状态:%d,x = %d, y = %d\n", wParam, LOWORD(lParam), HIWORD(lParam));WriteConsole(g_dos_output, text, strlen(text), NULL, NULL);
}
鼠标左键按下时,wParam 值为1,按住 Ctrl 同时按下鼠标左键 wParam 值为 1+8 = 9,松开时 wParam 值为 9 - 1 = 8。
双击:
滚轮的附带信息:
- wParam
- LOWORD :其他键的状态。
- HIWORD :滚轮的偏移量,通过正负表示滚轮方向。
- lParam,坐标原点为屏幕原点下的鼠标当前位置。
- LOWORD :x 坐标。
- HIWORD :y坐标。
case WM_MOUSEWHEEL:OnWheel(hWnd, wParam, lParam);break;void OnWheel(HWND hWnd, WPARAM wParam, LPARAM lParam) {char text[128] = { 0 };short nDelta = HIWORD(wParam);sprintf_s(text, "WM_MOUSEWHEEL, 其他键状态:%d,偏移量 %d, x:%d,y:%d\n", LOWORD(wParam), nDelta, LOWORD(lParam), HIWORD(lParam));WriteConsole(g_dos_output, text, strlen(text), NULL, NULL);
}
滚轮的距离以 120 为单位,往前滚动是正数,往后滚动是负数。
定时器消息 WM_TIMER
- 产生时间:
在程序中创建定时器,当到达指定时间时,定时器会向程序发送一个WM_TIMER
消息。定时器的经度是毫秒级,但是准确度很低。例如设置的时间是1000ms后,但是可能会在非1000m后到达。 - 附带信息:
wParam :定时器ID。
lParam :定时器处理函数指针。
定时器消息其实由 GetMessage() 发送,在前面的文章中提到过,因为 GetMessage() 函数做的事情特别多,特别忙碌,所以定时器消息不是很准,但误差也不会太大,通常也就几毫秒。
WM_TIMER消息的应用范围如下:
定时器功能:WM_TIMER消息通常与SetTimer函数一起使用,用于创建定时器。当定时器到达设定的时间间隔时,系统会发送WM_TIMER消息给窗口,从而触发相应的处理逻辑。开发者可以在接收到WM_TIMER消息时进行特定的操作,比如更新UI界面、执行一段代码等。
动画效果:WM_TIMER消息可以用于实现一些简单的动画效果,例如平滑的滚动、闪烁等。通过定时器不断发送WM_TIMER消息,可以控制界面元素的变化,从而营造出动态的视觉效果。
定时任务:WM_TIMER消息也可以用于执行定时任务,比如定时检查网络连接状态、定时备份数据等。通过设置定时器并处理WM_TIMER消息,可以实现定时执行任务的功能。
总的来说,WM_TIMER消息主要用于定时器相关的功能,可以帮助开发者管理定时事件、实现动画效果以及执行定时任务等操作。
创建销毁定时器
- 创建定时器:
UINT_PTR SetTime(HWND hWnd, //定时器窗口句柄UINT_PTR nIDEvent, //定时器IDUINT uElapse, //时间间隔TIMERPROC lpTimerFunc //定时器处理函数指针(一般不使用,为NULL) }; //创建成功,返回非0
- 关闭定时器:
BOOL KillTimer(HWND hWnd, //定时器窗口句柄UINT_PTR uIDEvent //定时器ID };
实验:在窗口创建之后显示之前创建两个定时器,一个每隔1秒触发,一个每个两秒触发,每次触发打印定时器的ID。
case WM_CREATE: //WM_CREATE消息产生时间即窗口创建成功显示之前SetTimer(hWnd, 1, 1000, NULL);SetTimer(hWnd, 2, 2000, NULL); //定时器创建成功后开始计时,设定的时间之后就会触发WM_TIMER消息break;case WM_TIMER: //处理定时器触发OnTimer(hWnd, wParam); //调用OnTimer()函数处理break;//OnTimer()函数内容如下:
void OnTimer(HWND hWnd, WPARAM wParam) { //这里传进来的wParam是定时器的ID/*switch(wParam){case 1:break;case 2:break;}*/ //一般对定时器的ID wParam使用switch语句,对不同的定时器ID做不同的处理char txt[128] = { 0 };sprintf_s(txt, "WM_TIMER: 定时器ID=%d\n", wParam);WriteConsole(g_dos_output, txt, strlen(txt), NULL, NULL);
}
运行结果:每产生一个定时器2就会产生两个定时器1。
菜单资源
菜单分类:
- 窗口的顶层菜单(窗口顶层一长条,新建、编辑等)
- 弹出式菜单(右键点击弹出,顶层菜单被点击弹出)
- 系统菜单(点击窗口图标弹出)
HMENU 类型表示菜单句柄,ID表示菜单项。
资源相关
- 资源脚本文件 :*.rc 文件,描绘资源文件的脚本代码。
- 编译器:RC.exe,用来编译资源脚本文件。
.c
/ cpp
文件经过 CL.exe
编译器编译成 .obj
文件,.rc
文件经过 RC.exe
编译成 .res
文件,.obj
文件和 .res
文件经过链接器 LINK.exe
链接成最终可执行文件 .exe
。
菜单资源使用
-
添加菜单资源
-
加载菜单资源
- 注册窗口类时设置菜单
- 创建窗口传参设置菜单
- 在主窗口 WM_CREATE 消息中利用 SetMenu 函数设置菜单
加载菜单资源:
HMENU LoadMenu(HINSTANCE hInstance, //handle to moduleLPCTSTR lpMenuName //menu name or resource identifier );
添加资源文件:
添加资源:
编辑菜单:
顶层的 文件
编辑
视图
项目
帮助
这些并没有ID,因为点击它们就做一件事情——弹出下拉框,而下拉框里面的每一项都有一个 ID ,以区分鼠标点击的哪一个内容。
菜单资源ID:IDR_MENU1
。
Resource.rc 文件内容:
添加菜单到窗口中:
方法一:在注册窗口类时添加菜单。
#include "resource.h" //添加资源头文件//注册窗口类时添加菜单ID:
wc.lpszMenuName = (char*)IDR_MENU1;
wc.hCursor = LoadCursor(NULL, IDC_ARROW); //顺便添加一个默认鼠标资源,这样可以使鼠标在一些情况下正常显示
方法二:在创建窗口时添加菜单(倒数第三个参数,注意是菜单的句柄不是菜单ID)
//创建窗口时添加菜单:
HMENU hMenu = LoadMenu(hIns, (char*)IDR_MENU1);
HWND hWnd = CreateWindowEx(0, pClassName, "My Window",WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPEDWINDOW,200, 200, 640, 480, //窗口位置(200,200),大小长宽(640,480)。nullptr, hMenu, hIns, nullptr
);
使用 LoadMenu() 函数来加载菜单的句柄。项目编译成功后,菜单资源也就在可运行文件.exe 中,在运行该可执行文件时,可执行文件又被加载到内存,通过 hInstance 可以找到当前进程所在的内存,再用菜单资源的ID可以找到菜单所在内存的句柄。
方法三:在主窗口 WM_CREATE 消息中利用 SetMenu 函数设置菜单。
case WM_CREATE:{HMENU hMenu = LoadMenu(nullptr, (char*)IDR_MENU1);SetMenu(hWnd, hMenu);}break;
命令消息的处理 WM_COMMAND
附带信息:
LOWORD(wParam) 菜单项的ID。
//WndProc 的 switch语句添加以下代码:
case WM_COMMAND:OnCommand(hWnd, wParam);
break;//OnCommand() 函数代码:
void OnCommand(HWND hWnd, WPARAM wParam) {switch (LOWORD(wParam)) {case ID_40001:MessageBox(hWnd, "新建被点击", "Information", MB_OK);break;case ID_40002:MessageBox(hWnd, "打开被点击", "Information", MB_OK);break;case ID_40003:MessageBox(hWnd, "退出被点击", "Information", MB_OK);break;}
}
运行结果:
上下文菜单
- 显示上下文菜单:
BOOL TrackPopupMenu(HMENU hMenu, //菜单句柄UINT uFlags, //显示方式int x, //水平位置,屏幕坐标系下int y, //垂直位置,屏幕坐标系下int nReserved, //保留参数,必须为0HWND hWnd, //处理菜单消息的窗口句柄CONST RECT *prcRect //NULL 忽略 }; //TrackPopupMenu 是阻塞函数
使用:
case WM_RBUTTONUP: //右键弹起时显示上下文菜单OnRButtonUp(hWnd, lParam);break;void OnRButtonUp(HWND hWnd, LPARAM lParam) {HMENU hMenu = LoadMenu(nullptr, (char*)IDR_MENU1); //加载菜单资源,拿到菜单句柄HMENU hPopup = GetSubMenu(hMenu, 0); //获取顶层菜单的子菜单,0就是文件下的子菜单POINT clientPoint = { LOWORD(lParam), HIWORD(lParam) }; //创建客户区的坐标ClientToScreen(hWnd, &clientPoint); //将客户区的坐标位置转化为屏幕坐标系TrackPopupMenu(hPopup, TPM_CENTERALIGN | TPM_VCENTERALIGN, //菜单中心显示,水平居中、垂直居中clientPoint.x, clientPoint.y, 0, hWnd, NULL);
}
运行:
或者使用专门的显示上下文菜单的消息:
- WM_CONTEXTMENU :鼠标右键抬起来 WM_RBUTTONUP 消息之后产生。
- wParam :右键点击的窗口句柄。
- lParam :LOWORD,x 坐标,屏幕坐标系;HIWORD,y 坐标,屏幕坐标系。
#include <Windows.h>
#include <stdio.h>#include "resource.h"HANDLE g_dos_output = 0;void OnCommand(HWND hWnd, WPARAM wParam) {switch (LOWORD(wParam)) {case ID_40001:MessageBox(hWnd, "新建被点击", "Information", MB_OK);break;case ID_40002:MessageBox(hWnd, "打开被点击", "Information", MB_OK);break;case ID_40003:MessageBox(hWnd, "退出被点击", "Information", MB_OK);break;}
}void OnRButtonUp(HWND hWnd, LPARAM lParam) {HMENU hMenu = LoadMenu(nullptr, (char*)IDR_MENU1);HMENU hPopup = GetSubMenu(hMenu, 0);TrackPopupMenu(hPopup, TPM_LEFTALIGN | TPM_TOPALIGN, //左上角显示LOWORD(lParam), HIWORD(lParam), 0, hWnd, NULL);
}LRESULT CALLBACK WndProc(HWND hWnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{switch (msgID){case WM_CLOSE:PostQuitMessage(0);break;case WM_COMMAND:OnCommand(hWnd, wParam);break;case WM_CONTEXTMENU:OnRButtonUp((HWND)wParam, lParam);break;}return DefWindowProc(hWnd, msgID, wParam, lParam);
}int CALLBACK WinMain(HINSTANCE hIns, HINSTANCE hPreIns, LPSTR lpCmdLine, int nCmdShow)
{AllocConsole();g_dos_output = GetStdHandle(STD_OUTPUT_HANDLE); //获得标准输出句柄const auto pClassName = "Main";WNDCLASSEX wc = { 0 };wc.cbClsExtra = 0;wc.cbWndExtra = 0;wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);wc.hCursor = LoadCursor(NULL, IDC_ARROW);wc.hIcon = nullptr;wc.hInstance = hIns;wc.lpfnWndProc = WndProc;wc.lpszClassName = pClassName;wc.lpszMenuName = (char*)IDR_MENU1;wc.hIconSm = nullptr;wc.style = CS_HREDRAW | CS_VREDRAW |CS_DBLCLKS;wc.cbSize = sizeof(wc);RegisterClassEx(&wc);HWND hWnd = CreateWindowEx(0, pClassName, "My Window",WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU | WS_OVERLAPPEDWINDOW,200, 200, 640, 480, //窗口位置(200,200),大小长宽(640,480)。nullptr, nullptr, hIns, nullptr);ShowWindow(hWnd, SW_SHOW);UpdateWindow(hWnd);MSG nMsg = { 0 };BOOL gResult;while (1) //先进入死循环,循环体中进行判断是否退出循环{if (PeekMessage(&nMsg, NULL, 0, 0, PM_NOREMOVE)) //PeekMessage侦察兵侦察是否有消息,如果有消息{if ((gResult = GetMessage(&nMsg, NULL, 0, 0)) > 0) //gResult值大于零意味着GetMessage没有抓到 WM_QUIT(返回值为0) 也没有出错(出错返回值为-1){TranslateMessage(&nMsg);DispatchMessage(&nMsg);}else //如果抓到 WM_QUIT 或者 出错 退出while循环{break;}}else //如果PeekMessage()没有侦察到消息,空闲处理{//WriteConsole(g_dos_output, "No Message", strlen("No Message"), NULL, NULL);}}if (gResult == -1) //GetMessage()出错返回值 -1{//错误处理或直接退出程序 return -1;}else //抓到 WM_QUIT 消息,退出程序,返回 PostQuitMessage()的参数值{return nMsg.wParam; //此时 wParam 是 PostQuitMessage()的参数值}
}
后续更新… …