OnMessage()

优质
小牛编辑
131浏览
2023-12-01

指定当脚本接收到特定消息时自动调用的函数

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

参数

MsgNumber

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

FunctionName

函数 名, 如果是原义的字符串, 则必须包围在双引号中. 当脚本接收到 MsgNumber 时会自动调用此函数. 省略该参数可以获取当前正在监视 MsgNumber 的函数名(如果没有则为空)。指定空字符串 ("") 或空变量来关闭对 MsgNumber 的监视.

MaxThreads
[v1.0.47+]

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

返回值

如果省略 FunctionName,则返回当前正在监视 MsgNumber 的函数名(如果没有则为空)。

如果明确指定 FunctionName 为空 (例如 ""), 则它返回当前正在监视 MsgNumber 的函数名 (如果没有则为空), 然后关闭对 MsgNumber 的监视.

FunctionName 不为空时: 如果 MsgNumber 已经处于被监视状态, 则它返回原来监视的函数名并让新函数开始监视. 否则,它指定 FunctionName 来监视 MsgNumber 然后返回相同的 FunctionName。在两种情况中,失败时都返回空值。当 FunctionName 出现下列情况时会导致失败:1) 不存在(可能由于 FunctionName 两边没有加上双引号);2) 接受多于四个参数;或 3) 在 v1.0.48.05 或更早的版本中含有 ByRef可选参数。如果脚本当前已经在监视 500 个消息时还试图监视新消息, 那么也会失败.

函数的参数

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

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

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

参数 #1:消息的 WPARAM 值。
参数 #2:消息的 LPARAM 值。
参数 #3:消息号,可用在用一个函数监视多个消息时。
参数 "https://www.xnip.cn/doc/AutoHotkey-WinTitle">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 time.

监视函数的 上次找到的窗口 初始与消息发送的目标窗口的相同 (即使消息是发送到控件的). 如果这个窗口是隐藏的但非 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 之间。

一般说明

与普通的函数调用不同, 被监视消息的到达会启动新 线程 来调用函数. 因此, 函数会以设置的默认值启动, 例如 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
    SendMessage, 0x4a, 0, &CopyDataStruct,, %TargetScriptTitle%  ; 0x4a 为 WM_COPYDATA. 必须使用发送而不是投递.
    DetectHiddenWindows %Prev_DetectHiddenWindows%  ; 恢复调用者原来的设置.
    SetTitleMatchMode %Prev_TitleMatchMode%         ; 同样.
    return ErrorLevel  ; 返回 SendMessage 的回复给我们的调用者.
}
; 示例: 请参阅 WinLIRC 客户端脚本 演示来学习如何使用 OnMessage() 接收
; 在网络连接的数据到达时的通知.