Windows下的消息循环机制(转载)

第一部分:什么是消息,以及为什么需要消息循环?

1. 什么是消息?

• 在 Windows 系统中,消息是一种小型数据包,用于操作系统与应用程序之间的通信,也可以用于应用程序内部不同部分之间的通信。

• 类似于通知或指令。

示例消息

• 用户点击鼠标左键,并提供点击坐标。

• 窗口需要重新绘制。

• 用户按下键盘上的某个按键。

• 定时器计时结束。

2. 消息的结构(简化版)

• HWND(窗口句柄):表示消息所指向窗口的唯一标识符,类似于地址。

• UINT(消息标识符):表示消息的类型,如 WM_LBUTTONDOWN(鼠标左键按下)、WM_PAINT(窗口重绘)、WM_KEYDOWN(按键按下)。

• WPARAM(消息参数):包含与消息相关的附加数据,例如鼠标按钮状态、按下的按键等。

• LPARAM(消息参数):通常包含更多数据,例如鼠标点击的 x 和 y 坐标。

3. 为什么需要消息循环?

• Windows 是事件驱动型操作系统,应用程序并非按线性方式执行,而是等待事件发生后作出响应。

• 消息循环就是应用程序接收并处理这些事件的机制。

• 如果没有消息循环,应用程序就像一个没有铃声的电话——无法知道外部事件的发生。

类比

把应用程序比作前台接待员。接待员坐在桌前不知道会接到什么任务。电话铃声响起(Windows 系统触发事件),接待员接听电话(处理消息),然后根据任务内容采取行动。消息循环就像是接待员不断检查电话和处理任务的过程。

第二部分:消息循环的核心组件

一个典型的 Windows 消息循环在 C++ 中看起来像这样:

MSG msg; *// 用于保存消息的结构*	
while (GetMessage(&msg, NULL, 0, 0)) { // 获取消息*
	TranslateMessage(&msg); // 进行键盘输入翻译*
	DispatchMessage(&msg); // 将消息分发到窗口过程*
}

逐步解析

  1. MSG 结构

用于存储消息的结构,包括 HWND、UINT、WPARAM、LPARAM 以及时间戳 time 和鼠标光标位置 pt。

  1. GetMessage()

从应用程序的消息队列中获取消息。

• &msg: 指向 MSG 结构的指针,用于存储消息信息。

• NULL: 指定要检索的窗口句柄,为 NULL 表示检索属于当前线程的所有窗口的消息。

• 0, 0: 消息过滤值,0, 0 表示不过滤任何消息。

返回值:

• 返回非零值:表示获取了非 WM_QUIT 的消息。

• 返回零:表示获取到 WM_QUIT 消息,退出循环。

• 返回 -1:表示发生错误。

  1. TranslateMessage()

处理与键盘输入相关的消息,例如将 WM_KEYDOWN 和 WM_KEYUP 转换为 WM_CHAR,表示字符输入。

  1. DispatchMessage()

将消息发送到相应的窗口过程处理。

第三部分:窗口过程 - 处理消息的地方

1. 什么是窗口过程?

• 窗口过程是应用程序中定义的一个函数,用于处理发送到特定窗口的消息。

• 每个窗口都有一个与之关联的窗口过程。

• 类似于事件处理程序,事件发生时,操作系统会发送消息给窗口,窗口过程处理这些消息。

2. 窗口过程的典型结构

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
	switch (uMsg) {
		case WM_CREATE:// 处理窗口创建
			return 0;
		case WM_PAINT:// 处理窗口重绘*
			return 0;
		case WM_LBUTTONDOWN:// 处理鼠标左键按下
			return 0;
		case WM_DESTROY:// 处理窗口销毁
			PostQuitMessage(0); // 发送 WM_QUIT 消息*	
      return 0;
		default:
			return DefWindowProc(hwnd, uMsg, wParam, lParam); // 默认消息处理
	}
}

第四部分:消息队列 - 消息的存储和传递

1. 消息队列的类型

系统消息队列: 整个系统只有一个队列,用于存储硬件输入事件(鼠标、键盘)和系统级事件。

线程消息队列: 每个 GUI 线程都有一个消息队列。系统会将系统消息队列中的消息分配到相应线程的消息队列。

2. 工作流程

  1. 输入事件: 用户与系统交互,例如点击鼠标或按键。
  2. 系统消息队列: Windows 将事件对应的消息存入系统消息队列。
  3. 线程消息队列: 系统确定消息所属的线程,并将其转移到该线程的消息队列。
  4. GetMessage(): 应用程序的消息循环从线程消息队列中取出消息。
  5. DispatchMessage(): 消息循环调用该函数,将消息发送给对应的窗口过程。
  6. 窗口过程: 处理消息并执行相关操作。
  7. 重复: 循环继续,直到应用程序终止。

视觉化

+---------------------+ +-----------------------+ +----------------------+

| 系统消息队列 | --> | 线程消息队列 | --> | 应用程序 |

+---------------------+ +-----------------------+ | +----------------+ |

| - 鼠标事件 | | - WM_MOUSEMOVE | | | 消息循环 | |

| - 键盘事件 | | - WM_LBUTTONDOWN | | | - GetMessage() | |

| - 其他事件 | | - WM_PAINT | | | - ... | |

+---------------------+ +-----------------------+ | +----------------+ |

| +----------------+ |

| | 窗口过程 | |

| | - case WM_... | |

| | - ... | |

| +----------------+ |

+----------------------+

类比

邮局(系统消息队列): 处理所有的邮件(事件)。

邮递员(线程消息队列): 每个线程像邮递员,负责特定的邮件。

信件(消息): 消息就是信件。

接待员(消息循环): 检查邮件并分发。

住户(窗口过程): 收信并采取行动。

Windows message loop mechanism | Shiori的博客


2 Windows消息循环机制

  • Windows消息循环机制是Windows操作系统中GUI编程的核心部分,它是事件驱动编程模型的基础。
  • 每个运行中的Windows应用程序都有与之关联的一个消息队列,当产生消息后(比如点击鼠标就会产生鼠标按下的消息),操作系统会将消息放入消息队列,应用程序会循环从消息队列中检索消息并处理。这就是Windows消息循环机制。
  • 模型示意图
    请添加图片描述

3 消息

  • 消息是如何产生的?当用户与应用程序窗口进行交互或系统事件发生时,硬件中断或者软件事件触发器会生成相应的消息,并将其放入应用程序的消息队列,这一步是由操作系统完成的。

  • Windows定义了成百上千个不同的消息类型,常见的有以下几种

    • WM_CHAR: 从键盘输入字符
    • WM_COMMAND: 用户选择菜单内的某项
    • WM_CREATE: 生成窗口
    • WM_DESTROY: 撤销窗口
    • WM_LBUTTONDOWN: 按下鼠标左键
    • WM_LBUTTONUP: 释放鼠标左键
    • WM_MOUSEMOVE: 移动鼠标
    • WM_PAINT: 窗口需要重新绘制
    • WM_QUIT: 应用程序将结束
    • WM_SIZE: 窗口尺寸被调整

4 相关API

4.1 入口函数

  • 函数原型

    •   int WinMain ( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nShowCmd)
      
  • 参数

    • hInstance: 模块实例句柄
    • hPrevInstance: Win16遗留,现在基本不使用
    • lpCmdLine: 命令行参数
    • nShowCmd: 主窗口初始化时的显示方式

4.2 注册窗口

  • 函数原型

    •   ATOM WINAPI RegisterClassEx(const WNDCLASSEXA *lpwcx);
      
  • 参数

    • lpwcx: 说明窗口属性的结构体
      • cbSize: 结构体大小
      • style: 窗口类样式
      • lpfnWndProc: 指向窗口过程函数的指针。
      • cbClsExtra: 类附加内存的字节数。
      • cbWndExtra: 窗口附加内存的字节数。
      • hInstance : 应用程序实例句柄。
      • hIcon : 窗口左上角图标的句柄
      • hCursor : 光标句柄
      • hbrBackground : 背景画刷句柄。
      • lpszMenuName : 菜单名
      • lpszClassName : 窗口类名
      • hIconSm : 小图标句柄
  • 返回值

    • 函数成功注册窗口类后,会返回一个 ATOM 类型的值,它是类名称在系统中的内部标识符。如果注册失败,则返回零。

4.3 创建窗口

  • 函数原型

    •   HWND WINAPI CreateWindowEx(
            _In_ DWORD dwExStyle,
            _In_opt_ LPCWSTR lpClassName,
            _In_opt_ LPCWSTR lpWindowName,
            _In_ DWORD dwStyle,
            _In_ int X,
            _In_ int Y,
            _In_ int nWidth,
            _In_ int nHeight,
            _In_opt_ HWND hWndParent,
            _In_opt_ HMENU hMenu,
            _In_opt_ HINSTANCE hInstance,
            _In_opt_ LPVOID lpParam
        );
      
  • 参数

    • dwExStyle: 指定窗口的扩展样式
    • lpClassName: 窗口类名
    • lpWindowName: 窗口标题
    • dwStyle: 指定窗口的基本样式
      • WS_BORDER: 创建一个单边框的窗口
      • WS_CAPTION: 创建一个有标题框的窗口
      • WS_CHILD: 创建一个子窗口
      • WS_DISABLED: 创建一个初始状态为禁止的子窗口,该窗口不能接收来自用户的输入信息
      • WS_DLGFRAME: 创建一个带对话框边框风格的窗口
      • WS_HSCROLL: 创建一个有水平滚动条的窗口
      • WS_VSCROLL: 创建一个有垂直滚动条的窗口
      • WS_ICONIC: 创建一个初始状态为最小化状态的窗口
      • WS_MAXIMIZE: 创建一个具有最大化按钮的窗口
      • WS_OVERLAPPED: 创建一个层叠的窗口
      • WS_POPUP: 创建一个弹出式窗口
      • WS_SIZEBOX: 创建一个可调边框的窗口
      • WS_SYSMENU: 创建一个在标题条上带有窗口菜单的窗口,必须同时设定WS_CAPTION风格
      • WS_THICKFRAME: 创建一个具有可调边框的窗口
      • WS_VISIBLE: 创建一个初始状态为可见的窗口
      • WS_MINIMIZEBOX: 创建一个具有显示最小化按钮的窗口
      • WS_MAXIMIZEBOX: 创建一个具有显示最大化按钮的窗口
      • WS_OVERLAPPEDWINDOW: 创建一个具有WS_OVERLAPPED、WS_CAPTION、WS_SYSMENU、WS_THICKFRAME、WS_MINIMIZEBOX、WS_MAXIMIZEBOX风格的层叠窗口
      • WS_POPUPWINDOW: 创建一个具有WS_BORDER、WS_POPUP、WS_SYSMENU风格的窗口。必须同时设定WS_CAPTION风格
    • X: 初始X坐标
    • Y: 初始Y坐标
    • nWidth: 窗口宽度
    • nHeight: 窗口高度
    • hWndParent: 父窗口句柄
    • hMenu: 菜单句柄
    • hInstance: 程序实例句柄
    • lpParam: 用户数据
  • 返回值: 窗口创建成功返回窗口句柄,创建失败返回NULL

4.4 显示窗口

  • 函数原型

    •   BOOL WINAPI ShowWindow(_In_ HWND hWnd, _In_ int nCmdShow);
      
  • 参数

    • hWnd: 窗口句柄
    • nCmdShow: 系统传递给WinMain函数的参数
  • 返回值

    • 成功返回TRUE,失败返回FALSE

4.5 更新窗口

  • 函数原型

    •   BOOL WINAPI UpdateWindow(_In_ HWND hWnd);
      
  • 参数

    • hWnd: 窗口句柄
  • 返回值

    • 成功返回TRUE,失败返回FALSE

4.6 从消息队列中检索消息

  • 函数原型

    •   BOOL WINAPI GetMessage(_Out_ LPMSG lpMsg, _In_opt_ HWND hWnd, _In_ UINT wMsgFilterMin, _In_ UINT wMsgFilterMax);
      
  • 参数

    • lpMsg: 指向 MSG 结构体的指针, 用于接收从消息队列取出的消息
    • hWnd: 窗口句柄,如果不为空,则只检索发给该窗口或其子窗口的消息;如果为 NULL,则检索所有线程的消息。
    • wMsgFilterMin, wMsgFilterMax: 定义了要检索的消息范围,通常设置为 0 到 0xFFFF,表示检索所有消息类型
  • 返回值

    • 从消息队列中获取的消息如果不是WM_QUIT, 则返回非零值

4.7 翻译消息

  • 函数原型

    •   BOOL WINAPI TranslateMessage(_In_ CONST MSG *lpMsg);
      
  • 参数

    • lpMsg: 指向一个 MSG 结构的指针
  • 说明

    • 它的主要作用是对来自消息队列的键盘消息(通常是 WM_KEYDOWN 和 WM_SYSKEYDOWN 消息)进行翻译,将其转换成字符消息(如 WM_CHAR 和 WM_SYSCHAR),以便应用程序能够更容易地识别和处理用户的字符输入。

4.8 分发消息

  • 函数原型

    •   LRESULT WINAPI DispatchMessage(_In_ CONST MSG *lpMsg);
      
  • 参数

    • lpMsg: 指向一个 MSG 结构的指针
  • 备注

    • 主要功能是将从消息队列中取出的消息分发给相应的窗口过程进行处理。

4.9 窗口过程函数

  • 函数原型

    •   LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
      
  • 参数

    • hWnd:窗口句柄,表示该消息是发送给哪个窗口的。
    • uMsg:消息标识符,用于指示需要处理的具体消息类型。
    • wParam, lParam:这两个参数的内容取决于消息类型,通常包含了消息的附加信息。
  • 说明

    • 窗口过程函数是每个窗口类注册时必须指定的一个回调函数,它负责处理发送给窗口的所有消息。

4.10 查找窗口

  • 函数原型

    •   HWND WINAPI FindWindow(LPCSTR lpClassName, LPCSTR lpWindowName);
      
  • 参数

    • lpClassName:指向一个以NULL结束的宽字符字符串,表示窗口类名。如果该参数为NULL,则函数仅根据窗口标题查找窗口。
    • lpWindowName:指向一个以NULL结束的宽字符字符串,表示窗口标题。如果该参数为NULL,则函数仅根据窗口类名查找窗口。
  • 返回值

    • 如果函数成功找到匹配的窗口,则返回该窗口的句柄。
    • 如果没有找到匹配的窗口,则返回 NULL 或 0。

4.11 发送消息-同步接口

  • 函数原型

    •   LRESULT WINAPI SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
      
  • 参数

    • hWnd:指向接收消息的窗口的句柄。
    • Msg:一个无符号整数,代表要发送的消息标识符,它可以是预定义的标准Windows消息(如 WM_PAINT、WM_KEYDOWN 等),也可以是自定义的消息(从 WM_USER 开始的范围)。
    • wParam 和 lParam:这两个参数根据具体的消息类型有不同的含义,通常用来携带额外的消息相关数据。

4.12 发送消息-异步接口

  • 函数原型

    •   BOOL WINAPI PostMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam);
      
  • 参数

    • hWnd:指向接收消息的窗口的句柄。
    • Msg:一个无符号整数,代表要发送的消息标识符,它可以是预定义的标准Windows消息(如 WM_PAINT、WM_KEYDOWN 等),也可以是自定义的消息(从 WM_USER 开始的范围)。
    • wParam 和 lParam:这两个参数根据具体的消息类型有不同的含义,通常用来携带额外的消息相关数据。
  • 备注

    • SendMessage 和 PostMessage 都是发送消息到消息队列中。但SendMessage必须等消息被处理后才会返回,而PostMessage执行后,不管消息是否被处理都会立即返回。

5 编程示例-处理系统消息

  • 创建工程时需要创建一个Win32项目

  • 源代码

  •   #include <windows.h>
      
      // 定义窗口过程函数
      LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
      {
      	switch (msg)
      	{
      	case WM_DESTROY:
      		//处理窗口销毁消息
      		PostQuitMessage(0); // 当窗口被销毁时发送WM_QUIT消息
      		break;
      	case WM_LBUTTONDOWN:
      		// 处理鼠标左键按下消息
      		MessageBox(NULL, L"Mouse Left Press", L"Info", MB_OK);
      		break;
      	case WM_KEYDOWN:
      		// 处理键盘按下消息
      		{
      			UINT keyCode = (UINT)wParam;
      			if (keyCode == VK_RETURN) {
      				// 回车键按下
      				MessageBox(NULL, L"Enter Press", L"Info", MB_OK);
      			}
      		}
      		break;
      	case WM_CHAR:
      		// 处理键盘输入的字符
      		{
      			UINT keyCode = (UINT)wParam;
      			if (keyCode == 0x41) {
      				//	大写字母A按下
      				MessageBox(NULL, L"A Press", L"Info", MB_OK);
      			}
      		}
      		break;
      	}
      	// 其他消息转交默认窗口过程处理
      	return DefWindowProc(hwnd, msg, wParam, lParam);
      }
      
      int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
      {
      	WNDCLASSW wc;
      
      	// 初始化窗口类结构体
      	wc.style = CS_HREDRAW | CS_VREDRAW;
      	wc.lpfnWndProc = WndProc;
      	wc.cbClsExtra = 0;
      	wc.cbWndExtra = 0;
      	wc.hInstance = hInstance;
      	wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
      	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
      	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); 
      	wc.lpszMenuName = NULL; // 如果没有菜单,则设为NULL
      	wc.lpszClassName = L"MyWndClass"; 
      
      	// 注册窗口类
      	if (!RegisterClass(&wc))
      	{
      		MessageBox(NULL, L"Failed to register window class.", L"Error", MB_OK | MB_ICONERROR);
      		return 0;
      	}
      
      	// 创建窗口
      	HWND hWnd = CreateWindowW(L"MyWndClass", L"Wnd", WS_OVERLAPPEDWINDOW,CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
      	if (!hWnd)
      	{
      		MessageBox(NULL, L"Failed to create window.", L"Error", MB_OK | MB_ICONERROR);
      		return 0;
      	}
      
      	// 显示并更新窗口
      	ShowWindow(hWnd, nCmdShow);
      	UpdateWindow(hWnd);
      
      	// 进入消息循环
      	MSG msg;
      	while (GetMessage(&msg, NULL, 0, 0))
      	{
      		// 翻译消息
      		TranslateMessage(&msg);
      		// 分发消息(由窗口过程函数处理)
      		DispatchMessage(&msg);
      	}
      
      	return msg.wParam;
      }
    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
    
  • 效果演示
    在这里插入图片描述

6 编程示例-发送和处理自定义消息

  • Win32编程时不仅可以处理系统消息,还可以发送和处理自定义消息。

  • 我们在进程A中创建一个窗口,并处理对应的自定义消息。在进程B中,查找进程A窗口,并向进程A发送自定义消息,实现两个窗口之间的通信。

  • 进程A

  •   #include <windows.h>
      
      // 定义自定义消息
      #define WM_MYCUSTOMMESSAGE (WM_USER + 100)
      
      // 定义窗口过程函数
      LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
      {
      	switch (msg)
      	{
      	case WM_MYCUSTOMMESSAGE:
      		// 处理自定义消息
      		MessageBox(NULL, L"WM_MYCUSTOMMESSAGE", L"Info", MB_OK);
      		break;
      	case WM_DESTROY:
      		//处理窗口销毁消息
      		PostQuitMessage(0); // 当窗口被销毁时发送WM_QUIT消息
      		break;
      	case WM_LBUTTONDOWN:
      		// 处理鼠标左键按下消息
      		MessageBox(NULL, L"Mouse Left Press", L"Info", MB_OK);
      		break;
      	case WM_KEYDOWN:
      		// 处理键盘按下消息
      	{
      		UINT keyCode = (UINT)wParam;
      		if (keyCode == VK_RETURN) {
      			// 回车键按下
      			MessageBox(NULL, L"Enter Press", L"Info", MB_OK);
      		}
      	}
      	break;
      	case WM_CHAR:
      		// 处理键盘输入的字符
      	{
      		UINT keyCode = (UINT)wParam;
      		if (keyCode == 0x41) {
      			//	大写字母A按下
      			MessageBox(NULL, L"A Press", L"Info", MB_OK);
      		}
      	}
      	break;
      	}
      	// 其他消息转交默认窗口过程处理
      	return DefWindowProc(hwnd, msg, wParam, lParam);
      }
      
      int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
      {
      	WNDCLASSW wc;
      
      	// 初始化窗口类结构体
      	wc.style = CS_HREDRAW | CS_VREDRAW;
      	wc.lpfnWndProc = WndProc;
      	wc.cbClsExtra = 0;
      	wc.cbWndExtra = 0;
      	wc.hInstance = hInstance;
      	wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
      	wc.hCursor = LoadCursor(NULL, IDC_ARROW);
      	wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
      	wc.lpszMenuName = NULL; // 如果没有菜单,则设为NULL
      	wc.lpszClassName = L"MyWndClass";
      
      	// 注册窗口类
      	if (!RegisterClass(&wc))
      	{
      		MessageBox(NULL, L"Failed to register window class.", L"Error", MB_OK | MB_ICONERROR);
      		return 0;
      	}
      
      	// 创建窗口
      	HWND hWnd = CreateWindowW(L"MyWndClass", L"Wnd", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL);
      	if (!hWnd)
      	{
      		MessageBox(NULL, L"Failed to create window.", L"Error", MB_OK | MB_ICONERROR);
      		return 0;
      	}
      
      	// 显示并更新窗口
      	ShowWindow(hWnd, nCmdShow);
      	UpdateWindow(hWnd);
      
      	// 进入消息循环
      	MSG msg;
      	while (GetMessage(&msg, NULL, 0, 0))
      	{
      		// 翻译消息
      		TranslateMessage(&msg);
      		// 分发消息(由窗口过程函数处理)
      		DispatchMessage(&msg);
      	}
      
      	return msg.wParam;
      }
      
    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
    
  • 进程B

  •   #include <windows.h>
      #include <stdio.h>
      // 定义自定义消息,ID值必须和进程A中的一致
      #define WM_MYCUSTOMMESSAGE (WM_USER + 100)
      
      int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
      	// 找到进程A的窗口句柄
      	HWND hwndTarget = FindWindow(TEXT("MyWndClass"), TEXT("Wnd"));
      	if (!hwndTarget) {
      		MessageBox(NULL, L"Failed to find target window.", L"Error", MB_OK | MB_ICONERROR);
      		return -1;
      	}
      
      	// 发送自定义同步消息
      	SendMessage(hwndTarget, WM_MYCUSTOMMESSAGE, 0, 0);
      	return 0;
      }
    

Windows编程模型-消息循环机制_windows消息循环机制-CSDN博客


Windows的应用程序一般包含窗口(Window),它主要为用户提供一种可视化的交互方式,窗口是由线程(Thread)创建的。Windows系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,一个线程会维护自己的一套消息队列(Message Queue),以保持线程间的独占性。队列的特点无非是先进先出,这种机制可以实现一种异步的需求响应过程。

消息的是什么样子的?

消息由一个叫MSG的结构体定义,包括窗口句柄(HWND),消息ID(UINT),参数(WPARAM, LPARAM)等等:

  1. struct MSG{ HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt;};

消息ID是消息的类型标识符,由系统或应用程序定义,消息ID为消息划分了类型。同时,也可以看出消息是对应于特定的窗口(窗口句柄)的。

**消息是如何分类的?其前缀都代表什么含义?

**消息ID只是一个整数,Windows系统预定义了很多消息ID,以不同的前缀来划分,比如WM_,CB_等等。
具体见下表:
**
**

Prefix Message category
ABM Application desktop toolbar
BM Button control
CB Combo box control
CBEM Extended combo box control
CDM Common dialog box
DBT Device
DL Drag list box
DM Default push button control
DTM Date and time picker control
EM Edit control
HDM Header control
HKM Hot key control
IPM IP address control
LB List box control
LVM List view control
MCM Month calendar control
PBM Progress bar
PGM Pager control
PSM Property sheet
RB Rebar control
SB Status bar window
SBM Scroll bar control
STM Static control
TB Toolbar
TBM Trackbar
TCM Tab control
TTM Tooltip control
TVM Tree-view control
UDM Up-down control
WM General window

应用程序可以定义自己的消息,其取值范围必须大于WM_USER。**

**如何通过消息传递任何参数?

Windows系统的消息机制都包含2个长整型的参数:WPARAM, LPARAM,可以存放指针,也就是说可以指向任何内容了。
传递的内容因消息各异,消息处理函数会根据消息的类型进行特别的处理,它知道传递的参数是什么含义。

消息在线程内传递时,由于在同一个地址空间中,指针的值是有效的。但是夸线程的情况就不能直接使用指针了,所以Windows系统提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等消息,用来特殊处理,指针的内容会被放到一个临时的内存映射文件(Memory-mapped File)里面,通过它实现线程间的共享数据。

消息队列和线程的关系是什么?****消息队列的结构是什么样子的?
**
Windows系统本身会维护一个唯一的消息队列,以便于发送给各个线程,这是系统内部的实现方式。
**而对于线程来说,每个线程可以拥有自己的消息队列,它和线程一一对应。在线程刚创建时,消息队列并不会被创建,而是当GDI的函数调用发生时,Windows系统才认为有必要为线程创建消息队列。
消息队列包含在一个叫THREADINFO的结构中,有四个队列:

Sent Message Queue
Posted Message Queue
Visualized Input Queue
Reply Message Queue

之所以维护多个队列,是因为不同消息的处理方式和处理顺序是不同的。

线程和窗口是一一对应的吗?如果想要有两个不同的窗口对消息作出不同反应,但是他们属于同一个线程,可能吗?

窗口由线程创建,一个线程可以创建多个窗口。窗口可由CreateWindow()函数创建,但前提是需要提供一个已注册的窗口类(Window Class),每一个窗口类在注册时需要指定一个窗口处理函数(Window Procedure),这个函数是一个回调函数,就是用来处理消息的。而由一个线程来创建对应于不同的窗口类的窗口是可以的。
由此可见,只要注册多个窗口类,每个窗口都可以拥有自己的消息处理函数,而同时,他们属于同一个线程。

**如何发送消息?

**消息的发送终归通过函数调用,比较常用的有PostMessage(),SendMessage(),另外还有一些Post或Send的函数。函数的调用者即发送消息的人。
这二者有什么不同呢?SendMessage()要求接收者立即处理消息,等处理完毕后才返回。而PostMessage()将消息发送到接收者队列中以后,立即返回,调用者不知道消息的处理情况。

他们的的原型如下:

  1. LRESULT SendMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);LRESULT PostMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

SendMessage()要求立即处理,所以它会直接调用窗口的消息处理函数(Window Procedure),完成后返回处理结果。
但这仅限于线程内的情况,夸线程时它调不到处理函数,只能把消息发送到接收线程的队列Sent Message Queue里。如果接收线程正在处理别的消息,那么它不会被打断,直到它主动去获取队列里的下一条消息时,它会拿到这一条消息,并开始处理,完成后他会通知发送线程结果(猜测是通过ReplyMessage()函数)。
在接收线程处理的过程中,发送线程会挂起等待SendMessage()返回。但是如果这时有其他线程发消息给这个发送线程,它可以响应,但仅限于非队列型(Non-queued)消息。

这种机制可能引起死锁,所以有其他函数比如SendMessageTimeout(), SendMessageCallback()等函数来避免这种情况。

PostMessage()并不需要同步,所以比较简单,它只是负责把消息发送到队列里面,然后马上返回发送者,之后消息的处理则再受控制。

**消息可以不进队列吗?什么消息不进队列?

可以。实际上MSDN把消息分为队列型(Queued Message)和非队列型(Non-queued Message),这只是不同的路由方式,但最终都会由消息处理函数来处理。
队列型消息包括硬件的输入(WM_KEY*等)、WM_TIMER消息、WM_PAINT消息等;非队列型的一些例子有WM_SETFOCUS, WM_ACTIVE, WM_SETCURSOR等,它们被直接发送给处理函数。

其实,按照MSDN的说法和消息的路由过程可以理解为,Posted Message Queue里的消息是真正的队列型消息,而通过SendMessage()发送到消息,即使它进入了Sent Message Queue,由于SendMessage要求的同步处理,这些消息也应该算非队列型消息。也许,Windows系统会特殊处理,使消息强行绕过队列。


谁来发送消息?硬件输入是如何被响应的?**
**
**消息可以由Windows系统发送,也可以由应用程序本身;可以向线程内发送,也可以夸线程。主要是看发送函数的调用者。

对于硬件消息,Windows系统启动时会运行一个叫Raw Input Thread的线程,简称RIT。这个线程负责处理System Hardware Input Queue(SHIQ)里面的消息,这些消息由硬件驱动发送。RIT负责把SHIQ里的消息分发到线程的消息队列里面,那RIT是如何知道该发给谁呢?如果是鼠标事件,那就看鼠标指针所指的窗口属于哪个线程,如果是键盘那就看哪个窗口当前是激活的。一些特殊的按键会有所不同,比如 Alt+Tab,Ctrl+Alt+Del等,RIT能保证他们不受当前线程的影响而死锁。RIT只能同时和一个线程关联起来。
有可能,Windows系统还维护了除SHIQ外地其他队列,分发给线程的队列,或者直接发给窗口的处理函数。

**
**

**消息循环是什么样子?线程何时挂起?何时醒来?
**
想象一个通常的Windows应用程序启动后,会显示一个窗口,它在等待用户的操作,并作出反应。
它其实是在一个不断等待消息的循环中,这个循环会不断去获取消息并作出处理,当没有消息的时候线程会挂起进入等待状态。这就是通常所说的消息循环。

一个典型的消息循环如下所示(注意这里没有处理GetMessage出错的情况):

  1. while(GetMessage(&msg, NULL, 0, 0 ) != FALSE)

这里GetMessage()从队列里取出一条消息,经过TranslateMessage(),主要是将虚拟按键消息(WM_KEYDOWN等)翻译成字符消息(WM_CHAR等)。
DispatchMessage()将调用消息处理函数。这里有一个灵活性,消息从队列拿出之后,也可以不分发,进行一些别的特殊操作。

下面在看看GetMessage()的细节:

  1. BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);

GetMessage()会从队列中取出消息,填到MSG结构中通过参数返回。如果此时的消息是WM_QUIT,也就标识线程需要结束,则 GetMessage()返回FALSE,那么while循环会终止。返回TRUE表示取到其他消息,可以继续循环并运行里面的内容。如果返回-1表示 GetMessage()出错。

其他几个参数是用来过滤消息的,可以指定接收消息的窗口,以及确定消息的类型范围。

这里还需要提到一个概念是线程的Wake Flag,这是一个整型值,保存在THREADINFO里面和4个消息队列平级的位置。它的每一位(bit)代表一个开关,比如QS_QUIT, QS_SENDMESSAGE等等,这些开关根据不同的情况会被打开或关闭。GetMessage()在处理的时候会依赖这些开关。

GetMessage()的处理流程如下:

  1. 处理Sent Message Queue里的消息,这些消息主要是由其他线程的SendMessage()发送,因为他们不能直接调用本线程的处理函数,而本线程调用 SendMessage()时会直接调用处理函数。一旦调用GetMessage(),所有的Sent Message都会被处理掉,并且GetMessage()不会返回;

  2. 处理Posted Message Queue里的消息,这里拿到一个消息后,GetMessage()将它拷贝到MSG结构中并返回TRUE。注意有三个消息WM_QUIT, WM_PAINT, WM_TIMER会被特殊处理,他们总是放在队列的最后面,直到没有其他消息的时候才被处理,连续的WM_PAINT消息甚至会被合并成一个以提高效率。从后面讨论的这三个消息的发送方式可以看出,使用Send或Post消息到队列里情况不多。

  3. 处理QS_QUIT开关,这个开关由PostQuitMessage()函数设置,表示线程需要结束。这里为什么不用Send或Post一个 WM_QUIT消息呢?据称:一个原因是处理内存紧缺的特殊情况,在这种情况下Send和Post很可能失败;其次是可以保证线程结束之前,所有Sent 和Posted消息都得到了处理,这是因为要保证程序运行的正确性,或者数据丢失?不得而知。

如果QS_QUIT打开,GetMessage()会填充一个WM_QUIT消息并返回FALSE。

  1. 处理Virtualized Input Queue里的消息,主要包括硬件输入和系统内部消息,并返回TRUE;

  2. 再次处理Sent Message Queue,来自MSDN却没有解释。难道在检查2、3、4步骤的时候可能出现新的Sent Message?或者是要保证推后处理后面两个消息;

  3. 处理QS_PAINT开关,这个开关只和线程拥有的窗口的有效性(Validated)有关,不受WM_PAINT的影响,当窗口无效需要重画的时候这个开关就会打开。当QS_PAINT打开的时候,GetMessage()会返回一个WM_PAINT消息。处理QS_PAINT放在后面,因为重绘一般比较慢,这样有助于提高效率;

  4. 处理QS_TIMER开关,和QS_PAINT类似,返回WM_TIMER消息,之所以它放在QS_PAINT之后是因为其优先级更低,如果Timer消息要求重绘但优先级又比Paint高,那么Paint就没有机会运行了。

如果GetMessage()中任何消息可处理,GetMessage()不会返回,而是将线程挂起,也就不会占用CPU时间了。
类似的WaitMessage()函数也是这个作用。

还有一个PeekMessage(),其原型为:

  1. BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg);

它的处理方式和GetMessage()一样,只是多了一个参数wRemoveMsg,可以指定是否移除队列里的消息。最大的不同应该是,当没有消息可处理时,PeekMessage()不是挂起等待消息的到来,而是立即返回FALSE。

**
WM_DESTROY, WM_QUIT, WM_CLOSE消息有什么不同?**

而其他两个消息是关于窗口的,WM_CLOSE会首先发送,一般情况程序接到该消息后可以有机会询问用户是否确认关闭窗口,如果用户确认后才调用 DestroyWindow()销毁窗口,此时会发送WM_DESTROY消息,这时窗口已经不显示了,在处理WM_DESTROY消息是可以发送 PostQuitMessage()来设置QS_QUIT开关,WM_QUIT消息会由GetMessage()函数返回,不过此时线程的消息循环可能也即将结束。

**窗口内的消息的路由是怎样的?窗口和其控件的关系是什么?

**一个窗口(Window)可以有一个Parent属性,对一个Parent窗口来说,属于它的窗口被称为子窗口(Child Window)。控件(Control)或对话框(Dialog)也是窗口,他们一般属于某个父窗口。
所有的窗口都有自己的句柄(HWND),消息被发送时,这个句柄就已经被指定了。所以当子窗口收到一个消息时,其父窗口不会也收到这个消息,除非子窗口手动的转发。
关于更详细的窗口和控件,会在另一篇中讨论。

谁来处理消息?****消息处理函数能发送消息么?

由消息处理函数(Window Procedure)来处理。消息处理函数是一个回调函数,其地址在注册窗口类的时候注册,只有在线程内才能调用。

其原型为:

  1. typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

处理函数内部一般是一个switch-case结构,来针对不同的消息类型进行处理。Windows系统还为所有窗口预定义了一个默认的处理函数 DefWindowProc(),它提供了最基本的消息处理,一般在不需要特殊处理的时候(即在switch的default分支)会调用这个函数。
由同一个窗口类创建的一组窗口共享一个消息处理函数,所以在编写处理函数的时候要小心处理窗口实例的局部变量。

处理函数里可以发送消息,但是可以想象有可能出现循环。另外处理函数还常常被递归调用,所以要减少局部变量的使用,以避免递归过深是栈溢出。

最后关于处理函数特化的问题将在另外的文章讨论。

--------------------------------------------------
参考资料:
Windows 游戏编程大师技巧 (第一卷)[André LaMothe]
Windows 核心编程 [Jeffery Richter]
Win32 and COM Development: User Interface: Windows User Interface: Windowing [MSDN]

一千个是什么 - Windows消息机制(Windows Messaging)-CSDN博客


windows编程笔记【二】windows类 WNDCLASSEX详解_wndclass.lpfnwndproc-CSDN博客

本笔记源自windows游戏编程大师技巧第二版2.6章,记做学习随笔。

使用vs2017环境编译。

Windows是一个面向对象的操作系统,Windows大量概念都源自于C++,其中一个概念就是Window类。

类描述了窗口的基本信息,比如窗口的图标,鼠标进入窗口的图标,和一些可定制的东西。

window的数据类有两个,WNDCLASS和WNDCLASSEX两个,WNDCLASS比较古老,可能要被弃用,我们使用WNDCLASSEX。

我们只需要创建一个这样的类,然后填写所有的字段。

#include <Windows.h>

#include <windowsx.h>

#define WIN32_LEAN_AND_MEAN

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline,int ncmdshow)

{

WNDCLASSEX winclass;

return 0;

}

来看WNDCLASSEX类的原型吧。

typedef struct WNDCLASSEX {

UINT cbSize;

UINT style;

WNDPROC lpfnWndProc;

int cbClsExtra;

int cbWndExtra;

HINSTANCE hInstance;

HICON hIcon;

HCURSOR hCursor;

HBRUSH hbrBackground;

LPCTSTR lpszMenuName;

LPCTSTR lpszClassName;

HICON hIconSm;

} WNDCLASSEX;

第一个:cbSize,填写WNDCLASSEX类本身的大小。因为这个结构被像指针一样传递,别的函数会检查他的大小,以确定数据块最低限度多大,这样你写入,别的函数就无需计算,我们这样写即可:

winclass.cbSize = sizeof(WNDCLASSEX);

第二个:style,包含描述窗口属性的样式(style)信息标志。

CS_HREDRAW若移动或改变窗口宽度,则刷新整个窗口
CS_VREDRAW若移动或改变窗口高度,则刷新整个窗口。
CS_OWNDC

为该类中每窗口分配一个单值的设备描述表(稍后再说)

CS_DBLCLKS当用户双击鼠标时向窗口程序发送一个双击的信息,同时光标位于该类的窗口里
CS_PARENTDC在父窗口中设定一个子窗口的剪切区,以便子窗口能够在父窗口中。
CS_SAVEBITS在一个窗口中保存用户图像,以便于该窗口被遮住,移动时不必每次刷新屏幕,但是会占用更多内存
CS_NOCLOSE禁用系统菜单上的关闭命令
winclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;

我们的窗口只需要这样选择参数即可,其他的参数后边再记录具体用途。

第三个:lpfnWndProc,他是一个回调函数,当你的窗口有消息后,比如用户要点击按钮,或者输入什么,win就会替你调用这个函数。

当然,这个回调函数是自己写的。

winclass.lpfnWndProc = WndProc; //WndProc不是window提供的东西,我们一会要自己写一个。

等到下一章来写WndProc回调函数,当然不喜欢WndProc这个名字的话,也可以取自己喜欢的回调函数名字。

第四五个:cbClsExtra,bWndExtra,为软件或窗口添加附件内存,绝大数人都是简单的设为0,默认值也是0。

winclass.cbClsExtra = 0;

winclass.cbWndExtra = 0;

第六个:hInstance,他就填写WinMain()函数中的hinstance,也就是应用程序的句柄。可以看上一篇笔记有对这个做出详细解释。

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline,int ncmdshow)

{

.

.

.

winclass.hInstance = hinstance;

return 0;

}

说明一下:句柄有很多,比如窗口句柄,上边的程序句柄,还有图标,菜单等等。。句柄起到类似指针的效果,任何前缀h的类型通常就是句柄,关于句柄的详细信息自行搜索。

第七个:hIcon,他装载一个标准程序的应用图标

winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

LoadIcon()原型如下:

HICON LoadIcon(HINSTANCE hInstance,LPCTSTR lpIconName);

第一个值是程序句柄类,他可以调用程序包含的图片资源,LPCTSTR是字符串,可以填入资源名字或字符串,我们这个用一个win提供的默认图标。

第八个:hCursor,是设置应用程序的鼠标样式

winclass.hCursor = LoadCursor(NULL, IDC_ARROW);

还是使用Ladlcom函数,这里用一个win提供的默认标准箭头。

第九个,hbrBackground,这是默认填充窗口,也就是设置窗口的背景颜色。

winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
GetStockObject()参数为预设画刷,之后强制转换为HBRUSH即可。

第十个,lpszMenuName,用于加载和选用窗口,暂时设为NULL,后边会用到。

winclass.lpszMenuName = NULL;

十一个,lpszClassName,他的作用是为你的WNDCLASSEX类取一个别名,是一个标识符,之后你就可以用这个新名字来引用这个类了。

winclass.lpszClassName = "WINCLASS1";

这里取名为“WINCLASS1”。

十二个, LoadIcon,他是小应用图标,用于在任务栏和窗口标题栏现实的图标。

winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

依然是使用LoadIcon()设置.

来回顾一下我们的设置:

#include <Windows.h>

#include <windowsx.h>

#define WIN32_LEAN_AND_MEAN

int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline,int ncmdshow)

{

WNDCLASSEX winclass;

winclass.cbSize = sizeof(WNDCLASSEX);

winclass.style = CS_VREDRAW | CS_HREDRAW | CS_OWNDC | CS_DBLCLKS;

winclass.lpfnWndProc = WndProc;

winclass.cbClsExtra = 0;

winclass.cbWndExtra = 0;

winclass.hInstance = hinstance;

winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);

winclass.hCursor = LoadCursor(NULL, IDC_ARROW);

winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);

winclass.lpszMenuName = NULL;

winclass.lpszClassName = "WINCLASS1";

winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

return 0;

}

这段代码目前不能运行的,因为我们还没有写lpfnWndProc的回调函数。

posted @   T0fV404  阅读(35)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示