# OnMessage()

指定当脚本接收到指定消息时自动调用的函数或函数对象.

    OnMessage(MsgNumber , Function, MaxThreads)

## 参数

MsgNumber

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

Function

  函数的名称或 [v1.1.20+] 函数对象, 要传递原义的函数名称, 必须用引号("") 括起来.

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

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

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

  如果监控函数直接或间接导致消息再次被发送, 而该函数仍在运行, 则有必要指定一个大于 1 或小于 -1 的 MaxThreads 值, 以允许监控函数被调用为了新的消息(如果需要). 脚本自己的进程向自己 send(非 post) 的消息不能被延迟或缓冲.

  [v1.1.20+]: 指定 0 以注销以前注册的函数. 如果 Function 是字符串, 则删除 "传统" 监听器. 否则, 仅注销给定的函数对象.

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

## 函数名称 vs 对象

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

### 函数名称

为了向后兼容, 最多可以通过名称注册一个函数来监听每个唯一的 MsgNumber -- 这被称为 "传统" 监听器. (译者注: 同一个 MsgNumber 的传统监听器注册后最多只有一个生效, 更新或称为重注册, 则前一个会失效, 但函数对象可以多个同时生效.)

当首次注册传统监听器时, 它是在以前注册的监听器(函数对象) 之前还是之后调用, 取决于 MaxThreads 参数. 更新监听器调用不同的函数不会影响顺序, 除非(原来的) 监听器先取消注册.

这将为 MsgNumber 注册或更新当前传统监听器(如果传递变量, 则省略双引号):

    Name := OnMessage(MsgNumber, "FunctionName")

返回值是以下之一:

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

这将为 MsgNumber 取消注册当前的传统监听器(如果有), 并返回它的名称(如果没有则返回空白):

    Name := OnMessage(MsgNumber, "")

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

    Name := OnMessage(MsgNumber)

### 函数对象

任意数量的函数对象(包括 Func 对象) 可用于监听某个指定的 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_GuiX 和 A_GuiY: 如果传入的消息是使用 SendMessage 发送的, 则它们的值都为 -2147483648. 如果是使用 PostMessage 发送的, 则它们的值为投递消息时鼠标光标的坐标(相对于屏幕)
- A_EventInfo: 如果消息是使用 SendMessage 发送的, 则它的值为 0. 如果是使用 PostMessage 发送的, 则它的值为消息投递的 tick-count 时间.

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

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

## 函数应该 返回 什么

如果监听函数使用不带任何参数的 Return, 或指定空值如 ""(甚至从不使用 Return), 则当此函数结束时将继续正常处理传入的消息. 同样的情况也会出现在函数使用 Exit 或者出现了运行时错误的时候(例如, 运行不存在的文件). 与之相比, 返回一个整数时会被作为回复立即发送; 即程序不会再进一步处理此消息. 例如, 监听 WM_LBUTTONDOWN(0x0201) 的函数可以返回一个整数来阻止目标窗口接收到鼠标点击的通知. 在许多情况下(例如使用 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), 它们将会一个接一个按顺序被调用, 直到被调用的函数返回一个非空值.

## 一般说明

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

Send(发送)(而不是 Post) 到控件的消息不会被监听, 因为系统直接把它们发送给后台的控件了. 对于系统生成的消息来说, 这很少是一个问题, 因为它们大部分都是被 Post 的.

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

当一个消息达到时, 如果其函数由于之前到达的相同消息而仍在运行, 默认情况下, 该函数将不会被再次调用; 相反, 该消息将被视为不受监控. 如果不希望这样, 有多种方法可以避免它:

- 如果消息是 post 的, 而不是 sent 的, 并且其编号大于 0x0311, 那么可以通过指定 Critical 作为函数的第一行来缓冲, 直到其函数完成. 另外, Thread Interrupt 也可以达到同样的效果, 只要它持续的时间足够长能让函数完成.
- [v1.0.46+]: 使用 Critical 来增加消息检查间隔, 可以让函数在任何消息被派发之前有更多的时间来完成. 为了保证可靠性, 可能需要一个大于 16 的间隔时间. 由于系统定时器的粒度(通常为 15.6 毫秒), 默认的 5 毫秒(对于非-Critical 线程) 或 15 毫秒(对于 Critical 线程) 的间隔可能会在函数启动后的瞬间出现.
- 确保监控函数快速返回, 可以减少因 MaxThreads 而错过消息的风险. 一种方法是让它通过向自己的脚本 posting 大于 0x0311 的监控消息, 来排队等待未来的线程. 该消息的函数应该使用 Critical 作为其第一行, 以确保其消息被缓冲. 另外, 也可以用一个计时器来排队等待未来的线程.
- 为 MaxThreads 参数指定一个较高的值, 可以使函数被中断以处理新收到的消息.

如果在脚本不可中断的情况下, 发布了一个数值大于 0x0311 的被监视的消息, 那么这个消息就会被缓冲; 也就是说, 它的函数不会被调用直到脚本变得可中断. 然而, 发送而不是发布的消息不能被缓冲, 因为它们必须提供一个返回值. 当一个模式化的消息循环在运行时, 例如系统对话框, ListView 拖放操作或菜单, 发布的消息也可能不被缓冲.

当一个被监控的消息到达时, 如果它没有被缓冲, 并且仅仅由于 Thread Interrupt 或 Critical 的设置, 脚本是不可中断的, 那么当前线程将被中断以便可以调用该函数. 然而, 如果脚本是绝对不可中断的 -- 比如在显示菜单时, KeyDelay/MouseDelay 正在进行中, 或者正在打开剪贴板 -- 则消息的函数将不会被调用, 消息将被视为未被监控.

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

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

当脚本显示系统对话框时(例如 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(0x0201, "WM_LBUTTONDOWN")
    return

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

    GuiClose:
    ExitApp

下面的脚本检测系统的关机/注销动作并允许您中止它. 在 Windows Vista 及更高的版本中, 系统会显示一个用户界面, 显示哪个程序正在阻止关机/注销, 并允许用户强制关机/注销. 在较旧的操作系统上, 脚本显示一个确认提示. 相关主题: OnExit

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

    WM_QUERYENDSESSION(wParam, lParam)
    {
        ENDSESSION_LOGOFF := 0x80000000
        if (lParam & ENDSESSION_LOGOFF)  ; 用户正在注销.
            EventType := "Logoff"
        else  ; 系统正在关机或重启.
            EventType := "Shutdown"
        try
        {
            ; 设置显示操作系统关闭 UI 的提示. 我们不会显示自己的确认提示,
            ; 因为我们只有 5 秒钟的时间, 操作系统会显示关机 UI
            ; 同样, 没有可见窗口的程序在没有提供原因的情况下无法阻止关机.
            BlockShutdown("Example script attempting to prevent " EventType ".")
            return false
        }
        catch
        {
            ; ShutdownBlockReasonCreate 不可用,
            ; 因此这可能是 Windows XP, 2003 或 2000, 我们可以在其中实际防止关机.
            MsgBox, 4,, %EventType% in progress.  Allow it?
            IfMsgBox Yes
                return true  ; 通知操作系统允许关机/注销操作继续.
            else
                return false  ; 通知操作系统中止关机/注销操作.
        }
    }

    BlockShutdown(Reason)
    {
        ; 如果您的脚本具有可见的 GUI, 请使用它代替 A_ScriptHwnd.
        DllCall("ShutdownBlockReasonCreate", "ptr", A_ScriptHwnd, "wstr", Reason)
        OnExit("StopBlockingShutdown")
    }

    StopBlockingShutdown()
    {
        OnExit(A_ThisFunc, 0)
        DllCall("ShutdownBlockReasonDestroy", "ptr", A_ScriptHwnd)
    }

让脚本接收其他脚本或程序的自定义消息和最多两个数字(要发送字符串而不是数字, 请参阅下一个示例).

    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` 来显示输入框来让您输入字符串.

保存下面的脚本为 "Receiver.ahk", 然后运行它:

    #SingleInstance
    OnMessage(0x004A, "Receive_WM_COPYDATA")  ; 0x004A 为 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  ; 可选的. 等待 receiver.ahk 响应的毫秒数. 默认是 5000
        ; 必须使用发送 SendMessage 而不是投递 PostMessage.
        SendMessage, 0x004A, 0, &CopyDataStruct,, %TargetScriptTitle%  ; 0x004A 为 WM_COPYDAT
        DetectHiddenWindows %Prev_DetectHiddenWindows%  ; 恢复调用者原来的设置.
        SetTitleMatchMode %Prev_TitleMatchMode%         ; 同样.
        return ErrorLevel  ; 返回 SendMessage 的回复给我们的调用者.
    }

有关如何使用 OnMessage() 在数据到达网络连接时接收通知的示例, 请参阅 WinLIRC 客户端脚本.

dftg