RegisterCallback() [v1.0.47+]

创建机器码地址, 当它被调用时会重定向到脚本中的函数.

Address := RegisterCallback("FunctionName" , Options := "", ParamCount := FormalCount, EventInfo := Address)

参数

Address

成功时, RegisterCallback() 返回一个数值地址, 它可以被 DllCall() 或其他能调用机器码函数的东西调用. 失败时, 它返回空字符串. 当 FunctionName 出现下列情况时会引起失败: 1) 不存在; 2) 接收到比 ParamCount 过多或过少的参数; 或 3) 接收到任何一个 ByRef 参数.

FunctionName

函数名, 如果是原义的字符串, 则必须包围在双引号中. 每当调用 Address 时会自动调用此函数. 此函数同时会接收到传递给 Address 的参数.

[v1.1.06+]: 可以传递函数引用来代替函数名.

Options

指定零个或多个下列单词. 在选项间使用空格分隔(例如 C Fast ).

FastF: 避免每次调用 FunctionName 时都启动新线程. 尽管这样执行的更好, 但必须避免调用 Address 的线程发生变化(例如当回调函数被传入的消息触发时). 这是因为 FunctionName 能改变在它被调用时刚好在运行的线程的全局设置(例如 ErrorLevel, A_LastError上次找到的窗口). 想了解更多细节, 请参阅备注.

CDeclC: 让 Address 遵循"C"调用约定. 此选项通常省略, 因为在回调函数中标准调用约定更常用.

ParamCount

Address 的调用者会传递给它的参数数目. 如果完全省略, 则它默认为 FunctionName定义中强制参数的数目. 在这两种情况中, 必须确保调用者准确传递此数目的参数.

EventInfo

每当通过此 Address 调用 FunctionName 时, 它将在 A_EventInfo 中看到这个整数. 这可用于一个 FunctionName 同时被多个 Address 调用时. 如果省略, 则它默认为 Address. 注: 与其他全局设置不同, 当前线程的 A_EventInfo 不会受到快速模式的干扰.

如果运行当前脚本的主程序为 32 位, 那么此参数必须介于 0 和 4294967295 之间. 如果主程序为 64 位, 那么此参数可以为 64 位整数. 尽管 A_EventInfo 通常返回无符号整数, 但由于 AutoHotkey 不完全支持 64 位无符号整数, 因此对这个值进行某些操作可能让它溢出从而进入有符号整数的范围.

回调函数的参数

分配了回调地址的函数可以接受多达 31 个参数. 允许有可选参数, 这可用于函数被多个调用者调用时.

正确的解析此参数需要您对 x86 的调用转换机制有所了解. 在 AutoHotKey 中没有形参的概念, 回调的参数列表由整数和一些必要的重解析组成.

32 位: 所有传入参数都是 32 位无符号整数. 如果某个传入参数应该为有符号整数, 则可以参照下面的其中一个例子得到负数:

如果传入参数被当作有符号整数, 所有的负数将被显示成下列两种形式中的一种:

; 方法 #1
if (wParam > 0x7FFFFFFF)
    wParam := -(~wParam) - 1

; 方法 #2: 依赖于 AutoHotkey 有 64 位有符号整数的原生支持.
wParam := wParam << 32 >> 32

64 位: 所有传入参数都是 64 位有符号整数. AutoHotkey 没有对 64 位无符号整数的原生支持.

AutoHotkey 32 位/64 位: 如果传入参数被当作 8 位或 16 位(x64 上是 32 位) 整数, 此数值的高位可能会包含杂位, 可通过以下方法加以过滤:

Callback(UCharParam, UShortParam, UIntParam) {
    UCharParam &= 0xFF
    UShortParam &= 0xFFFF
    UIntParam &= 0xFFFFFFFF
    ;...
}

如果某个传入参数应该为字符串, 那么它实际接收的是这个字符串的地址. 要获取这样的字符串, 请使用 StrGet:

MyString := StrGet(MyParameter)  ; 需要 [AHK_L 46+]

如果某个传入参数为结构的地址, 则可以参照 DllCall() 结构中描述的步骤提取其中的各个成员.

接收参数的地址[AHK_L 60+]: 如果函数声明为可变参数的, 那么它的最后一个参数被赋值为首个没有赋值给脚本参数的回调参数 地址. 例如:

callback := RegisterCallback("TheFunc", "F", 3)  ; 必须指定参数列表的大小.
TheFunc("TheFunc was called directly.")          ; 直接调用 TheFunc.
DllCall(callback, "float", 10.5, "int64", 42)        ; 通过回调调用 TheFunc.
TheFunc(params*) {
    if IsObject(params)
        MsgBox % params[1]
    else
        MsgBox % NumGet(params+0, "float") ", " NumGet(params+A_PtrSize, "int64")
}

大部分回调使用 stdcall 调用约定, 这种方式需要固定的参数数目. 在这种情况下, ParamCount 必须设置为参数列表的大小, Int64 需要两倍于 32 位的参数.

对于 Cdecl 或 64 位调用约定, ParamCount 仅被已经赋值的参数所影响. 如果省略, 所有可选参数被设为他们的默认值, 且不参与 params 保存地址大小的计算.

函数应该 返回 什么

如果函数使用不带任何参数的 Return, 或指定空值如 ""(甚至从不使用 Return), 则返回 0 给 Address 的调用者. 否则, 函数应该返回一个介于 -2147483648 和 4294967295 之间的整数, 它会被返回给 Address 的调用者.

快速与慢速

默认/慢速的模式会让函数以设置的默认值启动, 例如 SendModeDetectHiddenWindows. 这些默认值可以在自动执行段改变.

与之相比, 快速模式会继承在调用函数时刚好在运行的线程的全局设置. 而且, 函数对全局设置的任何修改(包括 ErrorLevel上次找到的窗口) 都会在当前线程生效. 因此, 快速模式应该只在确切知道函数会被哪个线程调用时才使用.

要避免被自己(或其他任何线程) 中断, 回调函数可以在它的首行使用 Critical. 然而, 通过小于 0x312 消息达到的间接调用函数时, 这并不是完全有效的(增加 Critical 的间隔也许有帮助). 而且, Critical 不会阻止执行一些可能导致间接调用它自己的动作, 例如调用 SendMessageDllCall().

内存

每次使用 RegisterCallback() 都会分配少量的内存(32 字节加上系统开销). 由于操作系统在脚本退出时会自动释放这些内存, 所以任何分配少量, 固定 数量的回调的脚本不需要明确地(显式) 释放内存. 与之相比, 不确定/无限制次数调用 RegisterCallback() 的脚本应明确对任何不使用的回调执行下列操作:

DllCall("GlobalFree", "Ptr", Address, "Ptr")

相关

DllCall(), OnMessage(), OnExit, OnClipboardChange, Sort 的回调, Critical, Post/SendMessage, 函数, Windows 消息列表, 线程

示例

#1: 下面是个可运行脚本, 它显示了所有顶层窗口的摘要.

; 考虑到性能和内存的保持, 只为指定的回调调用一次 RegisterCallback():
if not EnumAddress  ; 由于只能从这个线程调用, 所以可以使用快速模式:
    EnumAddress := RegisterCallback("EnumWindowsProc", "Fast")

DetectHiddenWindows On  ; 由于是快速模式, 所以此设置也会在回调中生效.

; 把控制传给 EnumWindows(), 它会重复调用回调:
DllCall("EnumWindows", "Ptr", EnumAddress, "Ptr", 0)
MsgBox %Output%  ; 显示由回调收集的信息.
    
EnumWindowsProc(hwnd, lParam)
{
    global Output
    WinGetTitle, title, ahk_id %hwnd%
    WinGetClass, class, ahk_id %hwnd%
    if title
        Output .= "HWND: " . hwnd . "`tTitle: " . title . "`tClass: " . class . "`n"
    return true  ; 告知 EnumWindows() 继续执行, 一直到枚举完所有的窗口.
}

#2: 下面是个可运行脚本, 它演示了如何在脚本中通过把 GUI 窗口的 WindowProc 重定向到新的 WindowProc 来子类化窗口. 此时, 文本控件的背景颜色被改变为自定义颜色.

TextBackgroundColor := 0xFFBBBB  ; BGR 格式的自定义颜色.
TextBackgroundBrush := DllCall("CreateSolidBrush", "UInt", TextBackgroundColor)

Gui, Add, Text, HwndMyTextHwnd, Here is some text that is given`na custom background color.
Gui +LastFound
GuiHwnd := WinExist()

; 64 位脚本必须调用 SetWindowLongPtr 代替 SetWindowLong:
SetWindowLong := A_PtrSize=8 ? "SetWindowLongPtr" : "SetWindowLong"

WindowProcNew := RegisterCallback("WindowProc", ""  ; 指定 "" 来避免子类化中使用快速模式.
    , , MyTextHwnd)  ; 在 [v1.1.12+] 中, 可以像这样省略 ParamCount.
WindowProcOld := DllCall(SetWindowLong,  "Ptr", GuiHwnd, "Int", -4  ; -4 是 GWL_WNDPROC
    ,  "Ptr", WindowProcNew,  "Ptr") ; 返回值必须设置为 Ptr 或 UPtr 而不是 Int.

Gui Show
return

WindowProc(hwnd, uMsg, wParam, lParam)
{
    Critical
    global TextBackgroundColor, TextBackgroundBrush, WindowProcOld
    if (uMsg = 0x138 && lParam = A_EventInfo)  ; 0x138 为 WM_CTLCOLORSTATIC.
    {
        DllCall("SetBkColor", "Ptr", wParam, "UInt", TextBackgroundColor)
        return TextBackgroundBrush  ; 返回 HBRUSH 来通知操作系统我们改变了 HDC.
    }
    ; 否则 (如果上面没有返回), 传递所有的未处理事件到原来的 WindowProc.
    return DllCall("CallWindowProc", "Ptr", WindowProcOld, "Ptr", hwnd, "UInt", uMsg, "Ptr", wParam, "Ptr", lParam)
}

GuiClose:
ExitApp