标签:窗口 鼠标 Windows 再谈 应用程序 循环 消息 函数
一、什么是Windows消息循环
- 概念介绍
- 在 Windows 操作系统中,消息循环是应用程序处理消息的核心机制。消息是 Windows 应用程序与操作系统以及应用程序内部不同组件之间通信的基本单元。这些消息可以是由用户操作产生的,如鼠标点击、键盘按键;也可以是系统内部产生的,如窗口大小改变、定时器事件等。
- 消息队列
- 消息首先被放入消息队列。消息队列是一个先进先出(FIFO)的数据结构。当用户进行操作(如按下鼠标左键)时,操作系统会把相应的消息(例如 WM_LBUTTONDOWN 消息,表示鼠标左键按下)放入应用程序的消息队列中。
- 应用程序有两种类型的消息队列:系统消息队列和应用程序消息队列。系统消息队列接收来自硬件设备(如键盘、鼠标)的消息,然后将这些消息分发给对应的应用程序消息队列。
- 消息循环的工作流程
- 应用程序通常会在一个主函数(如 WinMain)中启动消息循环。消息循环的基本结构是一个循环体,不断地从消息队列中获取消息并进行处理。
- 典型的消息循环代码如下(以 C++ 和 Win32 API 为例):
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
首先是GetMessage
函数,它从应用程序消息队列中获取一条消息,并将消息的信息填充到MSG
结构(msg
变量)中。如果获取到的消息是WM_QUIT
消息(表示应用程序要退出),GetMessage
函数返回0
,消息循环结束。
接着TranslateMessage
函数用于处理键盘消息,将按键消息转换为字符消息。例如,当用户按下并释放一个按键时,它会生成相应的字符消息(如果按键对应的是可打印字符),这样应用程序可以直接处理字符输入,而不是复杂的按键扫描码。 最后DispatchMessage
函数将消息分发给窗口过程(WndProc
)函数进行处理。窗口过程函数是一个自定义的函数,用于处理特定窗口接收到的消息。不同的窗口可以有不同的窗口过程函数,根据消息的类型(如WM_PAINT
用于重绘窗口、WM_COMMAND
用于处理菜单和按钮等控件的命令消息),窗口过程函数会执行相应的操作来响应消息。
- 窗口过程函数(WndProc)
- 窗口过程函数是消息循环机制的关键部分。它接收消息、消息的参数(如鼠标位置、按键的虚拟键码等),并根据消息类型执行相应的操作。
- 例如,对于
WM_PAINT
消息,窗口过程函数可能会调用绘图函数来绘制窗口的内容;对于WM_DESTROY
消息,它可能会进行一些清理工作,并发送WM_QUIT
消息来结束应用程序的消息循环。
- 一个简单的窗口过程函数示例如下:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_PAINT:
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// 在这里进行绘图操作
EndPaint(hWnd, &ps);
break;
}
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
- 重要性和应用场景
- 消息循环使得 Windows 应用程序能够以事件驱动的方式运行。它允许应用程序在等待用户输入或其他事件的同时,不会占用过多的系统资源。因为应用程序在没有消息需要处理时,会处于一种等待状态,直到有新的消息进入消息队列。
- 这种机制广泛应用于各种 Windows 桌面应用程序的开发中,无论是简单的工具软件还是复杂的图形界面应用程序,都依赖消息循环来处理用户交互和系统事件。
二、windows下点击一个按钮,经历了哪些?
- 硬件层面消息产生
- 当用户点击鼠标按钮时,鼠标硬件会检测到这个物理动作。鼠标会将这个动作转换为电信号,通过接口(如 USB 接口)发送给计算机的硬件中断系统。这个信号包含了鼠标按键按下的基本信息,如按下的是左键、右键还是中键等。
- 系统消息队列接收消息
- 计算机的硬件中断系统会将鼠标点击产生的信号传递给操作系统内核。操作系统内核会将这个鼠标点击事件包装成一个消息,放入系统消息队列。这个消息的类型可能是
WM_LBUTTONDOWN
(鼠标左键按下)、WM_RBUTTONDOWN
(鼠标右键按下)等,具体取决于按下的是哪个鼠标按键。系统消息队列会按照消息产生的先后顺序来排列这些消息。
- 消息分发到应用程序消息队列
- 操作系统会从系统消息队列中取出鼠标点击消息,并根据当前鼠标指针所在的位置等因素,确定该消息应该发送到哪个应用程序的消息队列。如果鼠标指针位于某个应用程序的窗口(包括窗口内的按钮)之上,操作系统就会将鼠标点击消息发送到该应用程序的消息队列。这个过程涉及到窗口管理和消息路由机制,操作系统会根据窗口的层次结构、可见性以及焦点状态等因素来准确地分发消息。
- 应用程序消息循环获取消息
- 应用程序在其主函数(如 WinMain)中的消息循环开始工作。消息循环会不断地调用
GetMessage
函数从应用程序消息队列中获取消息。当获取到鼠标点击相关的消息(如WM_LBUTTONDOWN
)后,消息循环会继续执行后续步骤来处理这个消息。
- 消息预处理(TranslateMessage)
- 在获取消息后,应用程序会调用
TranslateMessage
函数。对于鼠标点击消息来说,这个函数可能不会对其进行太多实质性的改变,但如果涉及到组合键(例如 Ctrl + 鼠标点击等)或者其他需要转换的情况,它会进行相应的处理。在简单的鼠标点击按钮场景下,这个函数主要是按照标准的消息处理流程来传递消息。
- 消息分发到窗口过程函数(DispatchMessage)
- 接着,
DispatchMessage
函数会将鼠标点击消息发送到对应的窗口过程函数(WndProc
)。窗口过程函数是应用程序中用于处理特定窗口消息的函数。每个窗口通常都有自己的窗口过程函数来处理发送到该窗口的消息。
- 窗口过程函数处理消息
- 在窗口过程函数中,会有一个
switch
语句(或类似的消息处理结构)来根据消息的类型进行处理。当收到鼠标点击按钮相关的消息(如WM_LBUTTONDOWN
)时,会检查点击位置是否在按钮的区域内。如果在按钮区域内,会触发按钮对应的操作,可能包括发送自定义的命令消息(如WM_COMMAND
)来通知应用程序的其他部分按钮被点击了,然后执行按钮被按下后的逻辑,如改变按钮的外观(按下状态的视觉效果),并调用相关的函数来完成按钮点击后应该执行的功能,如打开一个新窗口、保存文件等操作。最后,当鼠标按键被释放时,还会产生WM_LBUTTONUP
消息,经过类似的消息传递和处理过程,按钮会恢复到未按下的外观状态。
三、是否每个程序都有自己的消息循环
- 硬件层面消息产生
- 当用户点击鼠标按钮时,鼠标硬件会检测到这个物理动作。鼠标会将这个动作转换为电信号,通过接口(如 USB 接口)发送给计算机的硬件中断系统。这个信号包含了鼠标按键按下的基本信息,如按下的是左键、右键还是中键等。
- 系统消息队列接收消息
- 计算机的硬件中断系统会将鼠标点击产生的信号传递给操作系统内核。操作系统内核会将这个鼠标点击事件包装成一个消息,放入系统消息队列。这个消息的类型可能是
WM_LBUTTONDOWN
(鼠标左键按下)、WM_RBUTTONDOWN
(鼠标右键按下)等,具体取决于按下的是哪个鼠标按键。系统消息队列会按照消息产生的先后顺序来排列这些消息。
- 消息分发到应用程序消息队列
- 操作系统会从系统消息队列中取出鼠标点击消息,并根据当前鼠标指针所在的位置等因素,确定该消息应该发送到哪个应用程序的消息队列。如果鼠标指针位于某个应用程序的窗口(包括窗口内的按钮)之上,操作系统就会将鼠标点击消息发送到该应用程序的消息队列。这个过程涉及到窗口管理和消息路由机制,操作系统会根据窗口的层次结构、可见性以及焦点状态等因素来准确地分发消息。
- 应用程序消息循环获取消息
- 应用程序在其主函数(如 WinMain)中的消息循环开始工作。消息循环会不断地调用
GetMessage
函数从应用程序消息队列中获取消息。当获取到鼠标点击相关的消息(如WM_LBUTTONDOWN
)后,消息循环会继续执行后续步骤来处理这个消息。
- 消息预处理(TranslateMessage)
- 在获取消息后,应用程序会调用
TranslateMessage
函数。对于鼠标点击消息来说,这个函数可能不会对其进行太多实质性的改变,但如果涉及到组合键(例如 Ctrl + 鼠标点击等)或者其他需要转换的情况,它会进行相应的处理。在简单的鼠标点击按钮场景下,这个函数主要是按照标准的消息处理流程来传递消息。
- 消息分发到窗口过程函数(DispatchMessage)
- 接着,
DispatchMessage
函数会将鼠标点击消息发送到对应的窗口过程函数(WndProc
)。窗口过程函数是应用程序中用于处理特定窗口消息的函数。每个窗口通常都有自己的窗口过程函数来处理发送到该窗口的消息。
- 窗口过程函数处理消息
- 在窗口过程函数中,会有一个
switch
语句(或类似的消息处理结构)来根据消息的类型进行处理。当收到鼠标点击按钮相关的消息(如WM_LBUTTONDOWN
)时,会检查点击位置是否在按钮的区域内。如果在按钮区域内,会触发按钮对应的操作,可能包括发送自定义的命令消息(如WM_COMMAND
)来通知应用程序的其他部分按钮被点击了,然后执行按钮被按下后的逻辑,如改变按钮的外观(按下状态的视觉效果),并调用相关的函数来完成按钮点击后应该执行的功能,如打开一个新窗口、保存文件等操作。最后,当鼠标按键被释放时,还会产生WM_LBUTTONUP
消息,经过类似的消息传递和处理过程,按钮会恢复到未按下的外观状态。
三、命令行程序的消息循环
- 多数简单命令行应用程序没有消息循环
- 大部分简单的命令行应用程序确实没有消息循环。这类应用程序的主要目的是执行特定的命令任务,例如一个简单的文件复制命令行工具。它接收用户在命令行输入的参数(如源文件路径和目标文件路径),执行文件复制操作,然后退出。
- 这些应用程序通常是按照线性的流程运行:从命令行读取参数,执行相应的操作(如计算、文件处理等),最后输出结果。它们不涉及处理图形界面的消息,如鼠标点击、窗口重绘等,所以不需要消息循环来处理这类消息。
- 命令行应用程序中可能存在消息循环的情况
- 需要长时间运行并监控外部事件的命令行应用程序
- 有些命令行应用程序需要长时间运行并等待特定事件。例如,一个命令行的网络监控工具,它需要持续监测网络接口的数据流量。在这种情况下,虽然没有图形界面消息需要处理,但它可能会利用类似于消息循环的机制来等待和处理网络事件。
- 它可能会使用操作系统提供的异步事件通知机制(如在 Linux 系统中使用
select
、poll
或epoll
等函数,在 Windows 系统中使用WSAAsyncSelect
或WSAEventSelect
等函数)。这些机制可以看作是一种特殊的 “消息循环”,用于等待特定的事件(如网络数据到达、网络连接断开等),当事件发生时,执行相应的处理操作。
- 具有简单图形界面组件的命令行应用程序
- 当命令行应用程序包含一些简单的图形界面元素时,例如一个带有进度条显示的命令行下载工具。为了更新进度条的状态(这涉及到图形界面的更新),它可能需要处理窗口消息,这时就需要某种形式的消息循环。
- 这种情况下,应用程序可能会在后台运行一个简单的消息循环来处理图形界面相关的消息,同时在前台继续执行主要的命令行任务(如文件下载),并根据下载进度更新图形界面元素。
四、消息循环的管理
- GetMessage 函数的基本原理
GetMessage
函数主要用于从调用线程的消息队列中获取消息。在 Windows 操作系统中,每个线程都可以有自己的消息队列。当一个应用程序有多个窗口或者线程时,消息队列的管理就变得很重要。
- 对于一个普通的单线程、单窗口应用程序,
GetMessage
函数获取的是与该应用程序窗口相关的消息。这些消息包括用户操作产生的消息(如鼠标点击、键盘按键),以及系统产生的消息(如窗口大小改变、定时器消息等)。
- 获取自身消息的情况
- 在大多数简单应用程序场景下,
GetMessage
函数获取的消息主要是针对应用程序自身窗口的消息。例如,一个只有一个主窗口的应用程序,GetMessage
函数会获取这个主窗口以及其内部子窗口(如果有)所产生的消息。
- 假设一个应用程序中有一个按钮,当用户点击这个按钮时,产生的
WM_LBUTTONDOWN
(鼠标左键按下)和WM_COMMAND
(命令消息)等消息会被GetMessage
函数获取,因为这些消息是与应用程序自身的窗口操作相关的。
- 获取系统消息和其他窗口消息的特殊情况
- 系统消息:应用程序也会获取一些系统级别的消息。例如,当系统的显示设置发生变化,如屏幕分辨率改变时,应用程序会收到
WM_DISPLAYCHANGE
消息,GetMessage
函数同样会获取这类消息,以便应用程序能够做出相应的调整,如重新布局窗口内的控件。
- 其他窗口消息:如果一个应用程序通过某种方式(如进程间通信或者窗口父子关系)与其他窗口相关联,它也可能获取到其他窗口的消息。例如,一个父窗口可以接收子窗口发送的消息,或者通过消息钩子(
SetWindowsHookEx
函数)拦截其他窗口的消息,在这种情况下,GetMessage
函数获取的消息范围就可能超出了应用程序自身初始创建的窗口消息。
- 比如,应用程序设置了一个全局的鼠标钩子,用于记录所有鼠标操作。当其他应用程序的窗口发生鼠标点击时,通过钩子机制,本应用程序的
GetMessage
函数也有可能获取到相关的鼠标消息,从而实现对其他窗口鼠标操作的监控。
五、消息的产生
- 鼠标硬件和操作系统协作产生点击消息
- 当用户点击鼠标按钮时,首先是鼠标硬件产生一个电信号。鼠标内部的电路会检测到按钮按下的动作,并将这个物理事件转换为数字信号。这个信号通过鼠标的连接线(如 USB 线)或者无线连接方式(如蓝牙)传输到计算机的接口。
- 计算机的操作系统在接收到这个来自鼠标硬件的信号后,会对其进行处理和包装。操作系统会根据信号的来源(鼠标设备)、鼠标的当前状态(如位置、按钮状态)等信息,生成一个对应的消息。例如,在 Windows 操作系统中,会生成
WM_LBUTTONDOWN
(鼠标左键按下)或WM_RBUTTONDOWN
(鼠标右键按下)等消息。
- 操作系统在消息产生过程中的关键作用
- 操作系统负责将硬件信号转换为应用程序能够理解和处理的消息格式。这包括确定消息的类型(如鼠标按键类型、键盘按键的虚拟键码等)、消息的参数(如鼠标点击的位置坐标)。
- 操作系统还会根据窗口系统的状态来进一步处理消息。例如,它会判断鼠标指针当前所在的窗口是哪个应用程序的窗口,从而确定应该将生成的点击消息发送到哪个应用程序的消息队列。如果鼠标指针位于多个窗口重叠的区域,操作系统会根据窗口的层次结构、可见性和焦点等因素来准确地分配消息。
- 应用程序的被动接收角色
- 应用程序在这个过程中主要是被动接收由操作系统产生并分发的消息。应用程序本身通常不会直接产生鼠标点击消息,而是等待操作系统将这些消息发送到自己的消息队列中。
- 不过,在某些特殊情况下,应用程序可能会模拟鼠标点击消息。例如,在自动化测试工具或者一些脚本程序中,通过调用操作系统提供的相关 API(如
SendInput
函数),应用程序可以模拟用户的鼠标点击操作,从而产生相应的消息并发送到系统消息队列,就好像真正的用户进行了鼠标点击一样。但这种情况属于人为地通过应用程序间接产生鼠标点击消息,正常情况下鼠标点击消息的源头还是来自于鼠标硬件和操作系统的处理。
六、常见框架的消息循环
- 基于传统 Windows API 开发的情况
- 如果是使用传统的 Windows API(如 Win32 API)开发包含 UI 窗体的应用程序,一般会有消息循环。因为在这种情况下,为了处理用户与 UI 窗体(如窗口、按钮、菜单等)之间的交互,需要通过消息循环来获取和处理各种消息。
- 例如,在一个简单的 Win32 窗口应用程序中,
WinMain
函数通常会包含一个消息循环,用于从消息队列中获取消息(如用户的鼠标点击、键盘按键消息),并将这些消息分发到对应的窗口过程函数进行处理。这个消息循环是保证 UI 窗体能够正确响应用户操作的关键机制。
- 使用高级框架开发的情况
- MFC(Microsoft Foundation Classes)框架
- 在 MFC 框架下开发的应用程序,当创建了 UI 窗体(如对话框、文档视图结构的窗口等)时,消息循环是由框架自动管理的。开发者在创建 MFC 应用程序时,不需要像在 Win32 API 中那样显式地编写消息循环代码。
- MFC 内部会在应用程序启动时建立消息循环,并且会根据应用程序的结构(如对话框的消息映射、文档视图的消息处理机制等)将消息正确地分发到对应的处理函数中。这个过程对于开发者来说是相对透明的,但消息循环机制依然存在于幕后,用于处理与 UI 窗体相关的消息。
- .NET Framework(Windows Forms)和.NET Core(Windows Presentation Foundation - WPF)
- 在 Windows Forms 应用程序中,当创建了一个包含 UI 窗体的应用程序时,
Application.Run
方法会启动消息循环。这个方法隐藏了消息循环的复杂细节,使得开发者可以更专注于 UI 设计和事件处理。
- 类似地,在 WPF 应用程序中,也有一个类似的消息处理机制。WPF 应用程序以事件驱动的方式运行,通过处理各种事件(如按钮点击事件、窗口加载事件等)来响应用户操作,其底层也是通过消息循环来处理这些事件,只是在编程模型上更侧重于事件处理,而不是直接处理消息循环的代码。
- 特殊情况:进程中 UI 窗体的被动显示或简单嵌入
- 有时候,一个进程可能会有一个 UI 窗体,但这个 UI 窗体只是简单地被显示或者嵌入,而没有真正的交互需求。例如,一个进程主要用于后台数据处理,但会弹出一个简单的进度报告窗口,这个窗口只需要显示进度信息,不需要响应用户操作。
- 在这种情况下,可能不会有完整的消息循环来处理复杂的用户交互消息。不过,即使是这样的简单 UI 窗体,通常也会有一些基本的消息处理机制,用于确保窗口能够正确地显示和更新(如处理
WM_PAINT
消息来进行重绘),但这与完整的、用于处理各种用户交互的消息循环有所不同。
七、模态对话框和非模态对话框在消息循环上有什么不同
- 模态对话框的消息循环特点
- 消息循环的阻塞性质
- 模态对话框创建并显示后,会启动自己的消息循环,并且这个消息循环会在一定程度上阻塞其父窗口(或调用它的窗口)的消息循环。当模态对话框显示时,用户通常只能与该对话框进行交互,而不能操作它后面的父窗口或其他窗口。
- 例如,在一个文本编辑应用程序中,当用户选择 “打开文件” 菜单项并弹出模态的 “打开文件” 对话框时,父窗口(文本编辑窗口)的消息循环好像被 “冻结” 了。用户无法在这个模态对话框存在时,对文本编辑窗口进行如编辑文本、调整窗口大小等操作。
- 消息处理范围相对狭窄
- 模态对话框的消息循环主要关注自身相关的消息。它会集中处理与对话框内的控件(如按钮、文本框等)相关的消息,如
WM_COMMAND
消息(用于处理按钮点击等操作)、WM_INITDIALOG
消息(用于对话框的初始化)。
- 例如,当用户在模态对话框中点击 “确定” 或 “取消” 按钮,产生的
WM_COMMAND
消息会在模态对话框的消息循环中被优先处理,以决定是否关闭对话框并返回相应的值给父窗口。
- 与父窗口消息循环的交互有限
- 模态对话框在关闭之前,会向父窗口返回一个值(通常是通过
EndDialog
函数),这个值可以被父窗口用于后续的决策。但在对话框打开期间,除了这个返回值的传递机制外,父窗口和模态对话框之间的消息循环相对独立,父窗口基本处于等待状态,直到模态对话框关闭。
- 非模态对话框的消息循环特点
- 消息循环的非阻塞性质
- 非模态对话框显示后,不会阻塞父窗口的消息循环。父窗口和非模态对话框的消息循环可以同时运行,用户可以在操作非模态对话框的同时,继续与父窗口进行交互。
- 例如,在图形编辑软件中,一个属性设置的非模态对话框显示后,用户可以一边在主图形编辑窗口中进行图形绘制操作,一边在非模态对话框中修改图形的属性(如颜色、线条粗细等)。
- 消息处理的独立性和关联性并存
- 非模态对话框有自己独立的消息循环来处理自身的消息,如
WM_PAINT
消息用于重绘自身、WM_COMMAND
消息用于处理内部控件的操作。同时,它也需要和父窗口以及其他窗口进行适当的消息交互。
- 例如,当非模态对话框中的某些设置发生变化(如颜色选择改变),可能需要发送消息给父窗口,通知父窗口更新图形的显示颜色。这种消息交互可能通过自定义消息或者直接调用父窗口的函数来实现。
- 消息循环的复杂性增加
- 由于非模态对话框和父窗口的消息循环同时运行,并且需要相互配合,这使得消息循环的情况更加复杂。需要考虑如何避免消息冲突,以及如何确保消息在不同窗口之间的正确传递。
- 例如,当父窗口和非模态对话框都有自己的定时器消息(
WM_TIMER
)时,需要合理安排这些定时器的处理顺序和时间间隔,避免相互干扰,以保证每个窗口都能按照预期处理自己的消息,同时又不影响其他窗口的正常运行。
标签:窗口,
鼠标,
Windows,
再谈,
应用程序,
循环,
消息,
函数
From: https://www.cnblogs.com/xietianjiao/p/18584038