OnMessage()


指定一个函数,当脚本接收到指定消息时会自动调用它。

OnMessage(MsgNumber [, "FunctionName", MaxThreads])

参数

MsgNumber 需要监视或查询的消息编号,应该在 0 与 4294967295 (0xFFFFFFFF) 之间。如果您不希望监视系统消息 (编号小于 0x400),最好在大于 4096 (0x1000) 的数中选择一个。这可以尽量避免干扰到当前及以后版本的 AutoHotkey 内部所使用的消息。
FunctionName 函数名,如果是一个原意的字符串,必须用引号引起来。当脚本收到 MsgNumber 时该函数会被自动调用。省略这个和下一个参数可以获取到函数当前正在监视的 MsgNumber (若无则为空)。指定一个空字符串 ("") 或一个空变量可以关闭对 MsgNumber 的监视。
MaxThreads
[v1.0.47+]
该整数通常省略,表示限制监视函数在同一时刻只能有一个 线程。这通常是最佳的,因为若不如此,只要监视函数中断它自己,脚本将不会按照时间先后顺序处理消息。因此,可以考虑使用 Critical 来代替 MaxThreads 。说明见下

返回值

如果省略 FunctionNameMaxThreads,将返回正在监视 MsgNumber 的函数名(若无则为空)。无论如何,这并不会改变什么。

如果明确地将 FunctionName 置空 (如 ""),将返回正在监视 MsgNumber 的函数名(若无则为空),然后关闭对 MsgNumber 的监视。

如果 FunctionName 非空:如果 MsgNumber 已经处于被监视状态,则返回先前的函数名并让新函数生效。否则,指派 FunctionName 监视 MsgNumber 然后返回相同的 FunctionName。无论哪种情况,当失败时会返回一个空值。发生失败的原因是当 FunctionName:1) 不存在(可能是由于 FunctionName 前后缺少引号);2) 接受多于四个参数;或者 3) 有任何的 ByRef可选 参数。发生以下情况也会失败:当已经有500个消息受到监视的情况下脚本试图监视新消息。

函数的参数

一个被指派用于监视一个或多个消息的 函数 最多可以接受四个参数:

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

您所给出的参数名是什么没有关系,但是以下信息是与这些参数的顺序一一对应的:

参数 #1:消息的 WPARAM 值,一个 0 与 4294967295 之间的整数。
参数 #2: 消息的 LPARAM 值,一个 0 与 4294967295 之间的整数。
参数 #3: 消息码,在函数监视多于一个消息时很有用。
参数 #4: 接收消息的窗口或控件的 HWND(唯一标识号)。要使用 HWND 可以加上 ahk_id

当您不需要从右边开始的一个或多个参数所对应的信息时,可以省略它们。例如,一个定义成 MyMsgMonitor(wParam, lParam) 的函数将只接收头两个参数,而定义成 MyMsgMonitor() 的函数将不接收任何参数。

如果一个新来的 WPARAM 或 LPARAM 应该是一个有符号数,任何负数可以用以下例子展现出来:

if wParam > 0x7FFFFFFF
    wParam := -(~wParam) - 1

函数中有效的附加信息

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

一个监视函数的 上一次找到的窗口 开始于消息的目的父窗口(即使消息被发送到一个控件)。如果窗口是隐藏的但不是一个 GUI 窗口(如脚本的主窗口),则在使用它前开启 DetectHiddenWindows。举个例子:

DetectHiddenWindows On
MsgParentWindow := WinExist()  ; 保存接收消息的窗口的唯一标识号(unique ID)。

函数应该 返回 什么

如果一个监视函数使用不带任何参数的 Return,或者指定一个空值如 "" (或者根本不使用 Return ),当函数结束时新来的消息将继续被正常处理。同样的事也会发生在当函数使用 Exit 退出或产生了一个运行时错误,比如 运行 了一个不存在的文件。相比之下,返回一个 -2147483648 与 4294967295 之间的整数会使得那个数直接作为一个回覆被发送;也就是说,程序不会再进一步处理该消息。举个例子,一个监视 WM_LBUTTONDOWN (0x201) 的函数可以返回一个整数以阻止目标窗口收到鼠标点击产生的通知。在多数情况下(例如消息经由 PostMessage 而来),返回哪个值并没有关系;不过若您吃不准,0 通常是最安全的。

综合注意事项

不像普通的函数调用,被监视消息的到来会开一个新 线程 调用监视函数。因此,函数会全新地以设置(如 SendModeDetectHiddenWindows)的默认值开始运行。这些默认值可以在自动执行段中被修改。

被发送(send)(不是投递(post))给一个控件的消息将不会被监视到,因为系统会在后台直接将它们路由给控件。在系统生成的消息里是一种罕见的现象,因为它们多数是被投递的。

任何的在任何地方调用 OnMessage 的脚本都会自动地常驻(persistent)。同时还是单一进程(single-instance)的,除非使用 #SingleInstance 覆盖。

当一个消息到来时如果它的函数由于没有处理完先前到来的同样的消息而仍在运行,该函数将不会被再次调用(除非 MaxThreads 大于 1);或者说,该消息将会被视为不受监视的。如果您不希望如此,通过在函数的第一行指定 Critical 可以让一个大于等于 0x312 的消息在函数完成前被缓冲。或者,Thread Interrupt 可以完成同样的事,只要它持续到函数完成。相比之下,小于 0x312 的消息是不能通过 Critical 或 Thread Interrupt 被缓冲的(然而在 v1.0.46 及以后版本中,Critical 可能会有所帮助,因为它 以更低的频率 检查消息,这可以让函数有更多的时间去完成)。只有一种方法可以保证没有遗漏类似的消息,就是要确保函数在 6 毫秒内(不过这个限制可以通过 Critical 30 来提升)执行完成。要做到这一点,一个方法是通过将一个受监视的大于 0x312 的消息投递到脚本主窗口来让新来的消息等候一个未来线程。这个大于 0x312 消息的监视函数应该在第一行使用 Critical 以确保它的消息被缓冲。

如果一个数值小于 0x312 的被监视消息在脚本处于绝对不可中断状态——例如 菜单 正在被显示,一个 KeyDelay/MouseDelay 正在进行中,或者剪贴板正要被 打开——时到来,则消息监视函数将不会被调用,并且消息会被视为不受监视的。相比之下,大于等于 0x312 的被监视消息将在这个不可中断时期内被缓冲;就是说,它的函数将在脚本变为可中断时被调用。

如果一个数值小于 0x312 的被监视消息在脚本不可中断时到来,而且不可中断的原因仅仅是因为 Thread InterruptCritical 的设置,那么当前线程将被中断,好让监视函数得以被调用。相比之下,大于等于 0x312 的被监视消息将在线程结束或变为可中断前被缓冲。

OnMessage 线程的 优先级 总是 0 。因此,若当前线程的优先级高于 0 将不会有消息被监视或缓冲。

监视系统消息(低于 0x400)时应多加小心。举个例子,如果一个监视函数不是很快地就结束,那么对消息的响应时间可能超出系统预期,由此可能引发副作用。如果一个监视函数为了阻止消息被进一步处理而返回一个整数,但是系统期望的却是不同的处理或不同的响应,那么不想要的行为亦有可能发生。

如果脚本正在显示一个系统对话框比如 MsgBox ,任何投递给控件的消息将不会被监视到。举个例子,如果脚本正在显示一个 MsgBox 且用户点击了一个 GUI 窗口中的某个按钮, WM_LBUTTONDOWN 消息将被直接发送给按钮而不调用监视函数。

尽管一个外部程序可以通过 PostThreadMessage() 或其他 API 调用直接将消息投递给脚本的线程,但并不推荐这样做,因为如果脚本正在显示一个系统窗口比如 MsgBox 的话会导致消息丢失。或者说,通常最好投递或发送消息给脚本的主窗口或其中的一个 GUI 窗口。

相关命令

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

示例

; 示例:下面是一个可用脚本,监视 GUI 窗口中的鼠标点击。
; 相关主题: GuiContextMenu

Gui, Add, Text,, 点击此窗口中的任何地方
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(位于控件 " . A_GuiControl . ")"
    ToolTip 您在 Gui 窗口 #%A_Gui% 中的客户区坐标 %X%x%Y%.%Control% 处单击了。
}

GuiClose:
ExitApp

 

; 示例:以下脚本检测系统的关机/注销事件并允许你终止它(据说
; 在 Windows Vista 下不会工作)。
; 相关主题:OnExit

; 以下 DllCall 是可选的:它告诉操作系统要最先(优先于其他所有的应用程序)关闭该脚本。
; 在 Windows 9x 下该调用无效。
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 = 注销
    else  ; 系统正在关闭或重启。
        EventType = 关机
    MsgBox, 4,, 正在%EventType%,是否允许?
    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("接收脚本名.ahk ahk_class AutoHotkey")
    PostMessage, 0x5555, 11, 22  ; 因为上面有 WinExist(),消息将被发送到 "上一次找到的窗口"。
DetectHiddenWindows Off  ; 在 PostMessage 之前不可设为关闭(off)。

 

; 示例:从一个脚本发送任意长度的字符串到另一个脚本。这是一个可用的例子。
; 使用方法:保存并运行以下两个脚本后按 Win+空格,会显示一个
; InputBox 提示您输入一个字符串。

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

Receive_WM_COPYDATA(wParam, lParam)
{
    StringAddress := NumGet(lParam + 8)  ; lParam+8 是 CopyDataStruct 的 lpData 成员的地址。
    StringLength := DllCall("lstrlen", UInt, StringAddress)
    if StringLength <= 0
        ToolTip %A_ScriptName%`n接收到了一个空串或遇到一个错误。
    else
    {
        VarSetCapacity(CopyOfData, StringLength)
        DllCall("lstrcpy", "str", CopyOfData, "uint", StringAddress)  ; 复制字符串到结构体外。
        ; 用 ToolTip 而不是 MsgBox 来显示,这样我们就可以及时返回了:
        ToolTip %A_ScriptName%`n接收到了以下字串:`n%CopyOfData%
    }
    return true  ; 返回 1 (true) 是回应此消息的传统方法。
}

; 以 "Sender.ahk" 为名保存下列脚本并运行它。之后,按 Win+空格。
TargetScriptTitle = Receiver.ahk ahk_class AutoHotkey

#space::  ; Win+空格 热键。按下该热键会显示一个用于输入消息字符串的输入框(Inputbox)。
InputBox, StringToSend, 通过 WM_COPYDATA 发送文本, 输入要发送的文本:
if ErrorLevel  ; 用户按下了 Cancel(取消) 按钮。
    return
result := Send_WM_COPYDATA(StringToSend, TargetScriptTitle)
if result = FAIL
    MsgBox SendMessage 失败。具有下列标题的窗口是否存在?:`n%TargetScriptTitle%
else if result = 0
    MsgBox 消息被发送了,但是目标窗口用 0 回应该消息,这可能意味着该窗口忽略了它。
return

Send_WM_COPYDATA(ByRef StringToSend, ByRef TargetScriptTitle)  ; 此种情况下 ByRef 会节约一点内存。
; 该函数发送指定的字符串到指定的窗口然后返回。
; 若目标窗口处理了该消息则返回 1 ,否则返回 0 表示忽略了它。
{
    VarSetCapacity(CopyDataStruct, 12, 0)  ; 分配结构体内存区域。
    ; 首先设置结构体的 cbData 成员为字符串长度,包括零结束符:
    NumPut(StrLen(StringToSend) + 1, CopyDataStruct, 4)  ; 操作系统要求这已完成。
    NumPut(&StringToSend, CopyDataStruct, 8)  ; 设置 lpData 为指向字符串自身的指针。
    Prev_DetectHiddenWindows := A_DetectHiddenWindows
    Prev_TitleMatchMode := A_TitleMatchMode
    DetectHiddenWindows On
    SetTitleMatchMode 2
    SendMessage, 0x4a, 0, &CopyDataStruct,, %TargetScriptTitle%  ; 0x4a 是 WM_COPYDATA。必须使用 Send 而不是 Post。
    DetectHiddenWindows %Prev_DetectHiddenWindows%  ; 为调用者恢复原来的设置。
    SetTitleMatchMode %Prev_TitleMatchMode%         ; 同上。
    return ErrorLevel  ; 将 SendMessage 的返回值返回给我们的调用者。
}

 

; 示例:参见 WinLIRC 客户端脚本 ,该脚本演示了如何使用 OnMessage() 接收
; 当一个网络连接上有数据到来时的通知。