Windows Messaging

本文详细解析了Windows消息机制,包括消息的结构、分类及其前缀含义,消息如何在不同线程间传递,消息队列和线程的关系,以及消息的发送、接收与处理方式。

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


消息的是什么样子的?

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

  1. struct  MSG
  2. {
  3.      HWND  hwnd;
  4.      UINT  message;
  5.      WPARAM  wParam;
  6.      LPARAM  lParam;
  7.      DWORD  time;
  8.     POINT pt;
  9. };


消息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(
  2.      HWND  hwnd, 
  3.      UINT  uMsg, 
  4.      WPARAM  wParam, 
  5.      LPARAM  lParam);
  6. LRESULT  PostMessage(
  7.      HWND  hwnd, 
  8.      UINT  uMsg, 
  9.      WPARAM  wParam, 
  10.      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)
  2.     TranslateMessage(&msg); 
  3.     DispatchMessage(&msg);   
  4. }
这里GetMessage()从队列里取出一条消息,经过TranslateMessage(),主要是将虚拟按键消息(WM_KEYDOWN等)翻译成字符消息(WM_CHAR等)。
DispatchMessage()将调用消息处理函数。这里有一个灵活性,消息从队列拿出之后,也可以不分发,进行一些别的特殊操作。

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

  1. BOOL GetMessage(      
  2.     LPMSG lpMsg,
  3.     HWND hWnd,
  4.     UINT wMsgFilterMin,
  5.     UINT wMsgFilterMax
  6. );
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。

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

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

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

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

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

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

  1. BOOL PeekMessage(      
  2.     LPMSG lpMsg,
  3.     HWND hWnd,
  4.     UINT wMsgFilterMin,
  5.     UINT wMsgFilterMax,
  6.     UINT wRemoveMsg
  7. );
它的处理方式和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]

【无人机】基于改进粒子群算法的无人机路径规划研究[和遗传算法、粒子群算法进行比较](Matlab代码实现)内容概要:本文围绕基于改进粒子群算法的无人机路径规划展开研究,重点探讨了在复杂环境中利用改进粒子群算法(PSO)实现无人机三维路径规划的方法,并将其与遗传算法(GA)、标准粒子群算法等传统优化算法进行对比分析。研究内容涵盖路径规划的多目标优化、避障策略、航路点约束以及算法收敛性和寻优能力的评估,所有实验均通过Matlab代码实现,提供了完整的仿真验证流程。文章还提到了多种智能优化算法在无人机路径规划中的应用比较,突出了改进PSO在收敛速度和全局寻优方面的优势。; 适合人群:具备一定Matlab编程基础和优化算法知识的研究生、科研人员及从事无人机路径规划、智能优化算法研究的相关技术人员。; 使用场景及目标:①用于无人机在复杂地形或动态环境下的三维路径规划仿真研究;②比较不同智能优化算法(如PSO、GA、蚁群算法、RRT等)在路径规划中的性能差异;③为多目标优化问题提供算法选型和改进思路。; 阅读建议:建议读者结合文中提供的Matlab代码进行实践操作,重点关注算法的参数设置、适应度函数设计及路径约束处理方式,同时可参考文中提到的多种算法对比思路,拓展到其他智能优化算法的研究与改进中。
### 在 Windows 上安装和配置 Chrome Native Messaging Host 在 Windows 系统上,Chrome 插件与本地应用程序之间的通信可以通过 Native Messaging Host 实现。该机制允许扩展程序与本地进程进行双向通信,从而实现对本地资源的访问和控制。 #### 配置 Native Messaging Host 的清单文件 Chrome 要求 Native Messaging Host 必须注册为一个 JSON 格式的清单文件,该文件定义了 Host 的路径、权限等信息。该文件应放置在系统指定的注册目录中,例如: ``` C:\Users\<用户名>\AppData\Local\Google\Chrome\User Data\NativeMessagingHosts\ ``` 清单文件的内容示例如下: ```json { "name": "com.example.nativehost", "description": "Example Native Messaging Host", "path": "C:\\path\\to\\native\\host\\executable.exe", "type": "stdio", "allowed_origins": [ "chrome-extension://<your_extension_id>/" ] } ``` 其中,`path` 字段指定本地 Host 可执行文件的完整路径,`allowed_origins` 列出允许与其通信的扩展 ID。此配置确保只有授权的扩展才能与 Native Messaging Host 交互。 #### 编写本地 Host 应用程序 Native Messaging Host 必须是一个独立运行的本地程序,能够通过标准输入输出与 Chrome 扩展进行通信。其通信协议基于 JSON 格式的消息,并通过前缀的 4 字节长度字段标识每条消息的大小。 以下是一个简单的 Python 示例程序,用于接收消息并返回响应: ```python import sys import json def read_message(): raw_length = sys.stdin.buffer.read(4) if not raw_length: return None length = int.from_bytes(raw_length, byteorder='little') message = sys.stdin.buffer.read(length).decode('utf-8') return json.loads(message) def send_message(message): encoded = json.dumps(message).encode('utf-8') length = len(encoded) sys.stdout.buffer.write(length.to_bytes(4, byteorder='little')) sys.stdout.buffer.write(encoded) sys.stdout.buffer.flush() while True: message = read_message() if message is None: break print(f"Received: {message}") send_message({"echo": message}) ``` 该程序将持续监听来自扩展的消息,并将接收到的内容原样返回。 #### 在 Chrome 扩展中建立连接 Chrome 扩展可以使用 `chrome.runtime.connectNative` 或 `chrome.runtime.sendNativeMessage` 方法与 Native Messaging Host 通信。 使用 `connectNative` 建立持久连接: ```javascript const port = chrome.runtime.connectNative('com.example.nativehost'); port.onMessage.addListener((response) => { console.log("Received: " + JSON.stringify(response)); }); port.onDisconnect.addListener(() => { console.log("Disconnected from native host."); }); port.postMessage({ text: "Hello from extension" }); ``` 使用 `sendNativeMessage` 发送一次性请求: ```javascript chrome.runtime.sendNativeMessage( 'com.example.nativehost', { ping: "Ping from extension" }, function(response) { if (chrome.runtime.lastError) { console.error(chrome.runtime.lastError.message); } else { console.log("Response: " + JSON.stringify(response)); } } ); ``` #### 注意事项 - 确保 Native Messaging Host 的可执行文件具有执行权限,并能在命令行中独立运行。 - Host 程序应具备良好的错误处理机制,防止因异常退出导致通信中断。 - 若 Host 未正确注册或路径错误,Chrome 将无法建立连接,并可能抛出“pipe broken”等错误。 - 在调试过程中,可以通过 Chrome 的扩展管理页面(`chrome://extensions/`)查看日志信息,辅助排查问题[^1]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值