OnMessage

AutoHotKey

OnMessage()

指定一个 函数函数对象 响应脚本关注的特殊消息.

OnMessage(MsgNumber [, Function, MaxThreads])

参数

MsgNumber

需要监视或查询的消息编号, 应该介于 0 和 4294967295 (0xFFFFFFFF) 之间. 如果您不想监视 系统消息 (即编号小于 0x400 的那些), 那么最好在大于 4096 (0x1000) 的范围中选择一个. 这降低了可能对当前及将来版本的 AutoHotkey 内部所使用的消息的冲突.

Function

A 函数 的名称或 [v1.1.20+] 函数对象, 传递函数名称字符串, 必须用百分号 (%) 包围.

函数的注册和返回值取决于传递给这个参数的是函数名称还是对象. 详情参考 函数名称 vs 对象.

MaxThreads (最大线程)[v1.0.47+]

此整数通常省略, 此时表示限制监视函数在同一时刻只能运行一个 线程. 这通常是最佳的, 因为如果不这样, 则每当监视函数中断它自己时脚本将不会按照时间先后顺序来处理消息. 因此, 代替 MaxThreads 的一种方法是, 请像下面描述的那样使用 Critical :

[v1.1.20+]: 默认情况下多个函数注册到同一个 MsgNumber 时, 它们被调用的顺序按照它们注册的顺序来. 如果希望某个函数在其他比它先注册的函数之前就被调用, 可以给 MaxThreads 指定一个负数值. 例如, OnMessage(Msg, Fn, -2) 可将 Fn 函数提前到其他所有注册响应 Msg 的函数之前调用, 且允许 Fn 函数最大线程为 2. 然而, 如果函数已经注册了, 这个命令不会改变它, 除非进行解除注册之后再次注册.

函数名称 vs 对象

OnMessage 的返回值和行为取决于传递给 Function 参数的是函数名称还是对象.

函数名称 (Function Name)

为了向后兼容, 每个唯一 MsgNumber 只能用最多一个函数名称监听 -- 这被称为 "传统" 监听.

当传统监听第一个被注册, 它是先被调用还是在其他监听者之后调用, 取决于 MaxThreads 参数. 更新监听者调用不同的函数不会影响指令, 除非(原来的)监听者先解除注册.

这个注册的(函数名称)或更改当前 MsgNumber 的传统监听者 (如果传递的是变量就不要包含双引号):

Name := OnMessage(MsgNumber, "FunctionName")

返回值为以下列表之一:

  • 失败. 返回空字符串.
  • 返回前一个函数的名称, 如果存在前一个函数的话.
  • 其他情况, 返回新函数的名称.

这是取消注册当前传统监听者, 返回 MsgNumber 的监听者(函数)名称, 如果没有则为空:

Name := OnMessage(MsgNumber, "")

这是返回 MsgNumber 的当前传统监听者(函数)的名称, 如果没有则为空:

Name := OnMessage(MsgNumber)

函数对象 (Function Object)

任意数量的 函数对象 (function objects) (包括 普通函数 (normal functions)) 可用于监听某个指定的 MsgNumber.

下面两种方法意义相同, 这样注册的监听者函数对象将在任何在它之前注册的监听者函数 之后 被调用:

OnMessage(MsgNumber, FuncObj)     ; 方式 1
OnMessage(MsgNumber, FuncObj, 1)  ; 方式 2 (MaxThreads = 1)

这样注册的监听者函数对象将在之前注册的所有监听者函数 之前 被调用:

OnMessage(MsgNumber, FuncObj, -1)

要取消注册一个监听者函数对象, 把 MaxThreads 设为 0 即可:

OnMessage(MsgNumber, FuncObj, 0)

失败

Function (参数) 为以下情况时将导致失败:

  1. 不是用户定义函数对象的名称, 或是一个空字符串;
  2. 显式定义了超过 4 个参数;
  3. 在 v1.0.48.05 及更旧版本时, 使用了 ByRef可选 参数.

在 v1.1.19.03 及更旧的版本时, 当(一个函数对象)企图监听超过 500 个不同的消息时也会出错.

如果 Function 为对象时, 失败将抛出一个异常. 其他情况下, 返回空字符串.

函数的参数

这个 函数 可以接受一个或最多四个参数:

MyMessageMonitor(wParam, lParam, msg, hwnd)
{
    ... 函数体...
}

尽管您所给出的参数名无关紧要, 不过它们和下面的信息是按顺序一一对应的:

参数 #1:消息的 WPARAM 值.
参数 #2:消息的 LPARAM 值.
参数 #3:消息号, 可用在用一个函数监视多个消息时.
参数 #4:发送消息的窗口或控件的 HWND (唯一 ID). 要使用 HWND 则需要加上ahk_id.

根据运行脚本的 AutoHotkey.exe 是 32 位或 64 位,WPARAM 和 LPARAM 是无符号 32 位整数(从 0 到 232-1)或有符号 64 位整数(从 -263 到 263-1). 在 32 位脚本中, 如果传入参数应该为有符号整数, 则可以参照这个例子得到负数:

if (A_PtrSize = 4 && wParam > 0x7FFFFFFF)  ; 检查 A_PtrSize 以确保脚本为 32 位.
    wParam := -(~wParam) - 1

如果不需要相应的信息, 那么您可以在列表末尾开始省略一个或多个参数. 例如, 定义为 MyMsgMonitor(wParam, lParam) 的函数只会接收到前两个参数, 而定义为 MyMsgMonitor() 的函数将不接收任何参数.

函数中可用的附加信息

除了上面接收到的参数外, 函数中还可以使用下面的内置变量:

  • A_Gui: 当消息被发送到 GUI 窗口或控件时 A_Gui 为 Gui 窗口编号 (此窗口同时也被设置为函数的 默认 GUI 窗口); 否则为空.
  • A_GuiControl: 当消息被发送到 GUI 控件时它的值为控件的变量名或在 A_GuiControl 中说明的其他值; 否则为空. 一些控件决不会接收到某些类型的消息. 例如, 当用户点击 文本控件 时, 操作系统会发送 WM_LBUTTONDOWN 到它的父窗口而不是控件本身, 所以此时 A_GuiControl 是空的.
  • A_GuiXA_GuiY: 如果传入的消息是使用 SendMessage 发送的, 则它们的值都为 -2147483648. 如果是使用 PostMessage 发送的, 则它们的值为投递消息时鼠标光标的坐标 (相对于屏幕)
  • A_EventInfo: 如果消息是使用 SendMessage 发送的, 则它的值为 0. 如果是使用 PostMessage 发送的, 则它的值为消息投递的 tick-count 时间.

监视函数的 上次找到的窗口 初始与消息发送的目标窗口的相同 (即使消息是发送到控件的). 如果这个窗口是隐藏的但非 GUI 窗口 (例如脚本的主窗口), 那么需要在使用前打开 DetectHiddenWindows. 例如:

DetectHiddenWindows On
MsgParentWindow := WinExist()  ; 这里保存了消息发送的目标窗口的唯一 ID.

函数应该 返回 什么

如果监视函数使用不带任何参数的 Return, 或指定空值如 "" (甚至从不使用 Return), 则当此函数结束时将继续正常处理传入的消息. 同样的情况也会出现在函数使用 Exit 或者出现了运行时错误的时候 (例如 运行 不存在的文件). 与之相比, 返回一个整数时会被作为回复立即发送;即程序不会再进一步处理此消息. 例如, 监视 WM_LBUTTONDOWN (0x201) 的函数可以返回一个整数来阻止目标窗口接收到鼠标点击的通知. 在许多情况下 (例如使用 PostMessage 发送的消息), 它不关心返回了哪个整数; 不过如果不确定, 0 通常是最安全的.

有效返回值的范围与运行脚本的 AutoHotkey.exe 是 32 位还是 64 位有关. 对于 32 位脚本(A_PtrSize = 4), 非空返回值必须介于 -231 和 232-1 之间, 而 64 位脚本 A_PtrSize = 8 则必须介于 -263 和 263-1 之间.

[v1.1.20+]: 如果有多个函数监听某一个消息 (message number), 它们将会一个接一个按顺序被调用, 直到被调用的函数返回一个非空值.

一般说明

与普通的函数调用不同, 被监视消息的到达会启动新 线程 来调用函数. 因此, 函数会以设置的默认值启动, 例如 SendModeDetectHiddenWindows. 这些默认值可以在 自动执行段 改变.

发送 (而不是投递) 到控件的消息不会被监视到, 因为系统在后台直接把它们路由给控件了. 对于系统生成的消息, 这是很少见的情况, 因为它们大部分都是被投递的.

任意调用 OnMessage 的脚本会自动成为 持续运行的. 同时也会单实例运行, 不过可以使用 #SingleInstance 覆盖了默认设置.

当一个消息达到时它的监视函数仍由于处理之前同样的消息而在运行, 则不会再次调用函数 (除非 MaxThreads 大于 1); 而这个消息会被视为没有监视的那样进行处理. 如果不希望这样, 那么对于大于或等于 0x312 的消息可以通过在函数的首行中使用 Critical 来在之前运行的函数完成前把消息缓冲起来. 或者, 使用 Thread Interrupt 来实现同样的事情, 只要它持续可以让函数完成的足够长的时间. 与之相比, 无法使用 Critical 或 Thread Interrupt 对小于 0x312 的消息进行缓冲 (然而, 在 v1.0.46+, Critical 可能有所帮助, 因为它会以 更低的频率 检查消息, 这样函数可以有更多的时间来完成). 要确保这类消息不会被丢弃的唯一方法是它的监视函数在 6 毫秒之内结束 (不过这个限制的数值可以使用 Critical 30 增加). 要实现这点的一个方法是通过 投递 一个受监视的大于 0x312 的消息到它所在的脚本来把它放入将来线程的队列中. 那个消息处理函数应该在首行使用 Critical 以确保它的消息被缓冲起来.

如果在小于 0x312 的受监视消息当脚本完全不可中断时到达 (例如在 菜单, 正在进行 按键延时/鼠标延时 或正 打开 剪贴板), 则不会调用此函数而消息会被视为未监视的. 与之相比, 一个等于或大于 0x312 的受监视消息在这些不可中断期间会被缓冲起来; 即当脚本变成可中断时会调用它的处理函数.

如果小于 0x312 的受监视的消息在脚本由于 Thread InterruptCritical 的设置而不可中断时到达, 则会中断当前线程而调用消息处理函数. 与之相比, 等于或大于 0x312 的受监视消息则会被缓冲到线程结束或变成可中断之后.

OnMessage 的 优先级 总是为 0. 因此, 如果当前线程的优先级大于 0 时将不会监视或缓冲任何消息.

监视系统消息 (小于 0x400 的那些) 时应多加小心. 例如, 如果监视函数不会快速结束, 那么对消息的响应可能超过系统预期的时间, 这样可能会导致一些副作用. 如果监视函数为了阻止对消息的进一步处理而返回整数而系统期望不同的处理或响应时, 可能会发生不想要的行为.

当脚本显示系统对话框时 (例如 MsgBox), 则不会监视到任何投递到控件的消息. 例如, 如果脚本正显示 MsgBox 而用户点击一个 GUI 窗口上的按钮, 则 WM_LBUTTONDOWN 消息会被直接发送到按钮而不会调用监视函数.

尽管外部程序可以使用 PostThreadMessage() 或其他 API 调用直接投递消息给脚本的线程, 但不建议这么做, 因为如果此时脚本正显示系统窗口 (例如 MsgBox) 则消息会丢失. 相反, 通常最好投递或发送消息到脚本的主窗口或其中的某个 GUI 窗口.

相关

RegisterCallback(), OnExit, OnClipboardChange, Post/SendMessage, 函数, Windows 消息列表, 线程, Critical, DllCall()

示例

; 示例: 下面是个可运行脚本, 它监视在 GUI 窗口中的鼠标点击.
; 相关主题: GuiContextMenu

Gui, Add, Text,, Click anywhere in this window.
Gui, Add, Edit, w200 vMyEdit
Gui, Show
OnMessage(0x201, "WM_LBUTTONDOWN")
return

WM_LBUTTONDOWN(wParam, lParam)
{
    X := lParam & 0xFFFF
    Y := lParam >> 16
    if A_GuiControl
        Control := "`n(in control " . A_GuiControl . ")"
    ToolTip You left-clicked in Gui window #%A_Gui% at client coordinates %X%x%Y%.%Control%
}

GuiClose:
ExitApp
; 示例: 下面的脚本检测系统的关机/注销动作并允许您终止它
; 据说在 Windows Vista 或更高版本中不会工作).
; 相关主题:OnExit

; 下面的 DllCall 是可选的:它告诉操作系统要首先关闭此脚本(在其他所有程序之前).
DllCall("kernel32.dll\SetProcessShutdownParameters", UInt, 0x4FF, UInt, 0)
OnMessage(0x11, "WM_QUERYENDSESSION")
return

WM_QUERYENDSESSION(wParam, lParam)
{
    ENDSESSION_LOGOFF = 0x80000000
    if (lParam & ENDSESSION_LOGOFF)  ; 用户正在注销.
        EventType = Logoff
    else  ; 系统正在关机或重启.
        EventType = Shutdown
    MsgBox, 4,, %EventType% in progress.  Allow it?
    IfMsgBox Yes
        return true  ; 告诉操作系统允许关机/注销操作继续.
    else
        return false  ; 告诉操作系统终止关机/注销操作.
}
; 示例: 此脚本能接收到其他脚本或程序的自定义消息和最多两个数字
; (要发送字符串而不是数字, 请参阅下一个示例).

OnMessage(0x5555, "MsgMonitor")
OnMessage(0x5556, "MsgMonitor")

MsgMonitor(wParam, lParam, msg)
{
    ; 由于尽快返回常常很重要, 所以最好使用 ToolTip 而不是
    ; 类似 MsgBox 的进行显示, 以避免阻止函数结束:
    ToolTip Message %msg% arrived:`nWPARAM: %wParam%`nLPARAM: %lParam%
}

; 下面的代码可用于其他脚本内来激发运行上面脚本中的函数:
SetTitleMatchMode 2
DetectHiddenWindows On
if WinExist("Name of Receiving Script.ahk ahk_class AutoHotkey")
    PostMessage, 0x5555, 11, 22  ; 因为上面的有 WinExist(), 所以消息被发送到 "上次找到的窗口".
DetectHiddenWindows Off  ; 在 PostMessage 时不能是关闭的.
; 示例: 从一个脚本发送任何长度的字符串到另一个脚本.  这是个可运行的示例.
; 要使用它, 请保存并运行下面的脚本, 然后按下 Win+Space 来显示
; InputBox 来让您输入字符串.

; 保存下面的脚本为 "Receiver.ahk", 然后运行它:
#SingleInstance
OnMessage(0x4a, "Receive_WM_COPYDATA")  ; 0x4a 为 WM_COPYDATA
return

Receive_WM_COPYDATA(wParam, lParam)
{
    StringAddress := NumGet(lParam + 2*A_PtrSize)  ; 获取 CopyDataStruct 的 lpData 成员.
    CopyOfData := StrGet(StringAddress)  ; 从结构中复制字符串.
    ; 比起 MsgBox, 应该用 ToolTip 显示, 这样我们可以及时返回:
    ToolTip %A_ScriptName%`nReceived the following string:`n%CopyOfData%
    return true  ; 返回 1 (true) 是回复此消息的传统方式.
}

; 保存下面的脚本为 "Sender.ahk", 接着运行它.  然后, 按下 Win+Space 热键.
TargetScriptTitle = Receiver.ahk ahk_class AutoHotkey

#space::  ; Win+Space 热键. 按下此热键会显示 InputBox 用于输入消息字符串.
InputBox, StringToSend, Send text via WM_COPYDATA, Enter some text to Send:
if ErrorLevel  ; 用户按下了取消按钮.
    return
result := Send_WM_COPYDATA(StringToSend, TargetScriptTitle)
if result = FAIL
    MsgBox SendMessage failed. Does the following WinTitle exist?:`n%TargetScriptTitle%
else if result = 0
    MsgBox Message sent but the target window responded with 0, which may mean it ignored it.
return

Send_WM_COPYDATA(ByRef StringToSend, ByRef TargetScriptTitle)  ; 在这种情况中使用 ByRef 能节约一些内存.
; 此函数发送指定的字符串到指定的窗口然后返回收到的回复.
; 如果目标窗口处理了消息则回复为 1, 而消息被忽略了则为 0.
{
    VarSetCapacity(CopyDataStruct, 3*A_PtrSize, 0)  ; 分配结构的内存区域.
    ; 首先设置结构的 cbData 成员为字符串的大小, 包括它的零终止符:
    SizeInBytes := (StrLen(StringToSend) + 1) * (A_IsUnicode ? 2 : 1)
    NumPut(SizeInBytes, CopyDataStruct, A_PtrSize)  ; 操作系统要求这个需要完成.
    NumPut(&StringToSend, CopyDataStruct, 2*A_PtrSize)  ; 设置 lpData 为到字符串自身的指针.
    Prev_DetectHiddenWindows := A_DetectHiddenWindows
    Prev_TitleMatchMode := A_TitleMatchMode
    DetectHiddenWindows On
    SetTitleMatchMode 2
    TimeOutTime = 4000  ; Optional. Milliseconds to wait for response from receiver.ahk. Default is 5000
    ; 必须使用发送 SendMessage 而不是投递 PostMessage.
    SendMessage, 0x4a, 0, &CopyDataStruct,, %TargetScriptTitle%  ; 0x4a 为 WM_COPYDAT
    DetectHiddenWindows %Prev_DetectHiddenWindows%  ; 恢复调用者原来的设置.
    SetTitleMatchMode %Prev_TitleMatchMode%         ; 同样.
    return ErrorLevel  ; 返回 SendMessage 的回复给我们的调用者.
}
; 示例: 请参阅 WinLIRC 客户端脚本 演示来学习如何使用 OnMessage() 接收
; 在网络连接的数据到达时的通知.