作者:Descolada 翻译:河许人 2024年3月28日,第一次翻译
一、简介
检测特定窗口是否已打开或关闭是一个反复出现的问题,它有多种可能的解决方案。本教程概述了一些,但可能不是全部。我个人首选的方法是 SetWinEventHook(另请参阅 WinEvent 库)或 ShellHook(Microsoft 将其记录为已弃用,因此它可能不是最好的)。
所有示例都检测到记事本和/或计算器窗口的打开/关闭,因此,如果由于某种原因记事本或计算器的标题在您的计算机中不同,请确保相应地更改代码。
二、WinWait
此方法使用 WinWait 等待窗口打开,使用 WinWaitClose 等待窗口关闭。这是所有这些中最简单的一个,但缺点是脚本无法继续执行其他任务(热键、计时器和回调除外)。此外,如果要监视多个窗口,则代码可能会变得有点复杂。
#Requires AutoHotkey v2
Loop { ; Loop indefinitely
winId := WinWait("ahk_exe notepad.exe")
; winId := WinWaitActive("ahk_exe notepad.exe") ; Requires Notepad to exist AND be the active window
; In this case, winId can't be 0 because WinWait didn't have a timeout, so we don't need to check for it and can directly use WinGetTitle(winId).
; If a timeout was specified then check for 0 with "if winId { ... }" or "if winId := WinWait("ahk_exe notepad.exe") { ... }", otherwise WinGetTitle might throw an error.
ToolTip(WinGetTitle(winId) " window created") ; Instead of winId we could also use Last Known Window by using WinGetTitle()
SetTimer(ToolTip, -3000) ; Remove tooltip in 3 seconds
WinWaitClose("ahk_exe notepad.exe")
ToolTip("Notepad window closed")
SetTimer(ToolTip, -3000) ; Remove tooltip in 3 seconds
}
可以使用ahk_group等待多个窗口。使用这种方法,我们无法轻松确定组中的哪个窗口实际关闭,只有那个窗口关闭了。有关可能的解决方法,请参阅多个窗口的 SetTimer 示例。
#Requires AutoHotkey v2
SetTitleMatchMode(3)
GroupAdd("WindowOpenClose", "ahk_exe notepad.exe")
GroupAdd("WindowOpenClose", "Calculator")
Loop { ; Loop indefinitely
winId := WinWait("ahk_group WindowOpenClose")
; if WinWait had a timeout, then also check "if winId { ... }", otherwise WinGetTitle will throw an error
ToolTip(WinGetTitle(winId) " window created")
SetTimer(ToolTip, -3000) ; Remove tooltip in 3 seconds
WinWaitClose("ahk_group WindowOpenClose")
ToolTip("Calculator or Notepad window closed")
SetTimer(ToolTip, -3000) ; Remove tooltip in 3 seconds
}
三、SetTimer
此方法设置一个定期计时器,用于检查目标窗口是否已创建或关闭。缺点是这不是事件驱动的,这意味着大多数时候 AHK 都在浪费时间检查窗口的状态。
要仅监视一个窗口或进程,我们可以使用如下所示的内容:
#Requires AutoHotkey v2
SetTimer(WinOpenClose) ; Calls the function WinOpenClose every 250 milliseconds
; SetTimer(WinOpenClose, 0) ; This can be used to turn off the timer
Persistent() ; We have no hotkeys, so Persistent is required to keep the script going
WinOpenClose() {
static targetWindow := "ahk_exe notepad.exe", lastExist := !!WinExist(targetWindow)
if lastExist = !!WinExist(targetWindow) ; Checks whether Notepad exists and it didn't last time, or vice-versa
return
if (lastExist := !lastExist) {
ToolTip("Notepad opened")
} else {
ToolTip("Notepad closed")
}
SetTimer(ToolTip, -3000)
}
要监视多个窗口,我们需要跟踪存在哪些窗口并相应地更新它。这甚至更加耗费资源,因为每次调用计时器时,我们都需要检查所有打开的窗口的列表,并交叉检查哪些窗口已被创建/销毁:
#Requires AutoHotkey v2
SetTimer(WinOpenClose) ; Calls the function WinOpenClose every 250 milliseconds
; SetTimer(WinOpenClose, 0) ; This can be used to turn off the timer
Persistent() ; We have no hotkeys, so Persistent is required to keep the script going
WinOpenClose() {
static lastOpenWindows := ListOpenWindows()
currentOpenWindows := ListOpenWindows()
for hwnd in SortedArrayDiff([currentOpenWindows*], [lastOpenWindows*]) {
if !lastOpenWindows.Has(hwnd) {
info := currentOpenWindows[hwnd]
if (info.processName = "notepad.exe" || info.title = "Calculator") {
ToolTip(info.title " window created")
SetTimer(ToolTip, -3000)
}
} else {
info := lastOpenWindows[hwnd]
if (info.processName = "notepad.exe" || info.title = "Calculator") {
ToolTip(info.title " window closed")
SetTimer(ToolTip, -3000)
}
}
}
lastOpenWindows := currentOpenWindows
ListOpenWindows() { ; Returns Map where key=window handle and value={title, class, processName}
openWindows := Map()
for hwnd in WinGetList()
try openWindows[hwnd] := {title: WinGetTitle(hwnd), class:WinGetClass(hwnd), processName: WinGetProcessName(hwnd)}
return openWindows
}
SortedArrayDiff(arr1, arr2) { ; https://www.geeksforgeeks.org/symmetric-difference-two-sorted-array/ also accounting for array length difference
i := 1, j := 1, n := arr1.Length, m := arr2.Length, diff := []
while (i <= n && j <= m) {
if arr1[i] < arr2[j] {
diff.Push(arr1[i]), i++
} else if arr2[j] < arr1[i] {
diff.Push(arr2[j]), j++
} else {
i++, j++
}
}
while i <= n
diff.Push(arr1[i]), i++
while j <= m
diff.Push(arr2[j]), j++
return diff
}
}
四、SetWinEventHook
此方法使用我猜检测窗口打开/关闭事件的首选方法:SetWinEventHook。此钩子将导致 Microsoft 在创建或销毁窗口时通知我们,这意味着我们的程序不必通过不断检查更新来浪费资源。除了窗口创建/销毁事件外,它还可用于检测窗口移动事件 (EVENT_OBJECT_LOCATIONCHANGE)、窗口激活、最小化/最大化等等。有关可能事件的完整列表,请参阅事件常量。此外,可以使用 AccessibleObjectFromEvent 从此钩子发送的信息创建一个 Acc 对象,但这是一个完全不同的主题。
作为简化版本,可以选择使用 WinEvent 库,该库是 SetWinEventHook 的包装器。
下面的示例使用 HandleWinEvent 函数检测记事本和计算器窗口打开和关闭事件:
#Requires AutoHotkey v2
; Map out all open windows so we can keep track of their names when they're closed.
; After the window close event the windows no longer have their titles, so we can't do it afterwards.
global gOpenWindows := Map()
for hwnd in WinGetList()
try gOpenWindows[hwnd] := {title: WinGetTitle(hwnd), class:WinGetClass(hwnd), processName: WinGetProcessName(hwnd)}
global EVENT_OBJECT_CREATE := 0x8000, EVENT_OBJECT_DESTROY := 0x8001, OBJID_WINDOW := 0, INDEXID_CONTAINER := 0
; Set up our hook. Putting it in a variable is necessary to keep the hook alive, since once it gets
; rewritten (for example with hook := "") the hook is automatically destroyed.
hook := WinEventHook(HandleWinEvent, EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY)
; We have no hotkeys, so Persistent is required to keep the script going.
Persistent()
/**
* Our event handler which needs to accept 7 arguments. To ignore some of them use the * character,
* for example HandleWinEvent(hWinEventHook, event, hwnd, idObject, idChild, *)
* @param hWinEventHook Handle to an event hook function. This isn't useful for our purposes
* @param event Specifies the event that occurred. This value is one of the event constants (https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants).
* @param hwnd Handle to the window that generates the event, or NULL if no window is associated with the event.
* @param idObject Identifies the object associated with the event.
* @param idChild Identifies whether the event was triggered by an object or a child element of the object.
* @param idEventThread Id of the thread that triggered this event.
* @param dwmsEventTime Specifies the time, in milliseconds, that the event was generated.
*/
HandleWinEvent(hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime) {
Critical -1
idObject := idObject << 32 >> 32, idChild := idChild << 32 >> 32, event &= 0xFFFFFFFF, idEventThread &= 0xFFFFFFFF, dwmsEventTime &= 0xFFFFFFFF ; convert to INT/UINT
global gOpenWindows
if (idObject = OBJID_WINDOW && idChild = INDEXID_CONTAINER) { ; Filters out only windows
; GetAncestor checks that we are dealing with a top-level window, not a control. This doesn't work
; for EVENT_OBJECT_DESTROY events.
if (event = EVENT_OBJECT_CREATE && DllCall("IsTopLevelWindow", "Ptr", hwnd)) {
try {
; Update gOpenWindows accordingly
gOpenWindows[hwnd] := {title:WinGetTitle(hwnd), class:WinGetClass(hwnd), processName: WinGetProcessName(hwnd)}
if gOpenWindows[hwnd].processName = "notepad.exe"
ToolTip "Notepad window created"
else if gOpenWindows[hwnd].title = "Calculator"
ToolTip "Calculator window created"
}
} else if (event = EVENT_OBJECT_DESTROY) {
if gOpenWindows.Has(hwnd) {
if gOpenWindows[hwnd].processName = "notepad.exe"
ToolTip "Notepad window destroyed"
else if gOpenWindows[hwnd].title = "Calculator"
ToolTip "Calculator window destroyed"
; Delete info about windows that have been destroyed to avoid unnecessary memory usage
gOpenWindows.Delete(hwnd)
}
}
SetTimer(ToolTip, -3000) ; Remove created ToolTip in 3 seconds
}
}
class WinEventHook {
/**
* Sets a new WinEventHook and returns on object describing the hook.
* When the object is released, the hook is also released. Alternatively use WinEventHook.Stop()
* to stop the hook.
* @param callback The function that will be called, which needs to accept 7 arguments:
* hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime
* @param eventMin Optional: Specifies the event constant for the lowest event value in the range of events that are handled by the hook function.
* Default is the lowest possible event value.
* See more about event constants: https://learn.microsoft.com/en-us/windows/win32/winauto/event-constants
* Msaa Events List: Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd318066(V=Vs.85).Aspx
* System-Level And Object-Level Events: Https://Msdn.Microsoft.Com/En-Us/Library/Windows/Desktop/Dd373657(V=Vs.85).Aspx
* Console Accessibility: Https://Msdn.Microsoft.Com/En-Us/Library/Ms971319.Aspx
* @param eventMax Optional: Specifies the event constant for the highest event value in the range of events that are handled by the hook function.
* If eventMin is omitted then the default is the highest possible event value.
* If eventMin is specified then the default is eventMin.
* @param winTitle Optional: WinTitle of a certain window to hook to. Default is system-wide hook.
* @param PID Optional: process ID of the process for which threads to hook to. Default is system-wide hook.
* @param skipOwnProcess Optional: whether to skip windows (eg Tooltips) from the running script.
* Default is not to skip.
* @returns {WinEventHook}
*/
__New(callback, eventMin?, eventMax?, winTitle := 0, PID := 0, skipOwnProcess := false) {
if !HasMethod(callback)
throw ValueError("The callback argument must be a function", -1)
if !IsSet(eventMin)
eventMin := 0x00000001, eventMax := IsSet(eventMax) ? eventMax : 0x7fffffff
else if !IsSet(eventMax)
eventMax := eventMin
this.callback := callback, this.winTitle := winTitle, this.flags := skipOwnProcess ? 2 : 0, this.eventMin := eventMin, this.eventMax := eventMax, this.threadId := 0
if winTitle != 0 {
if !(this.winTitle := WinExist(winTitle))
throw TargetError("Window not found", -1)
this.threadId := DllCall("GetWindowThreadProcessId", "Int", this.winTitle, "UInt*", &PID)
}
this.pCallback := CallbackCreate(callback, "C", 7)
, this.hHook := DllCall("SetWinEventHook", "UInt", eventMin, "UInt", eventMax, "Ptr", 0, "Ptr", this.pCallback, "UInt", this.PID := PID, "UInt", this.threadId, "UInt", this.flags)
}
Stop() => this.__Delete()
__Delete() {
if (this.pCallback)
DllCall("UnhookWinEvent", "Ptr", this.hHook), CallbackFree(this.pCallback), this.hHook := 0, this.pCallback := 0
}
}
请注意,某些窗口的标题在创建后不会立即更新,而是略有延迟(AHK 检测到窗口的速度太快了!这可能会导致检测此类窗口时出现问题,因为我们通常使用标题作为筛选条件。解决此问题的方法是更新gOpenWindows的变量,有轻微的延迟(例如20-100ms),或者代替EVENT_OBJECT_CREATE使用EVENT_OBJECT_SHOW,一旦实际显示窗口(因此有标题),它就会被激活。请参阅此线程下方的帖子中的示例。
五、 ShellHook
这种方法也是一个事件驱动的方法,它注册我们的脚本以接收可能对 shell 应用程序有用的消息,例如创建、销毁、激活窗口。不幸的是,Microsoft 也记录了它不适合一般用途,并且可能会在后续版本的 Windows 中更改或不可用,因此 SetWinEventHook 可能是更好的选择。
#Requires AutoHotkey v2
; Map out all open windows so we can keep track of their names when they're closed.
; After the window close event the windows no longer have their titles, so we can't do it afterwards.
global gOpenWindows := Map()
for hwnd in WinGetList()
try gOpenWindows[hwnd] := {title: WinGetTitle(hwnd), class:WinGetClass(hwnd), processName: WinGetProcessName(hwnd)}
DllCall("RegisterShellHookWindow", "UInt", A_ScriptHwnd)
OnMessage(DllCall("RegisterWindowMessage", "Str", "SHELLHOOK"), ShellProc)
; The following DllCall can also be used to stop the hook at any point, otherwise this will call it on script exit
OnExit((*) => DllCall("DeregisterShellHookWindow", "UInt", A_ScriptHwnd))
Persistent()
ShellProc(wParam, lParam, *) {
global gOpenWindows
if (wParam = 1) { ; HSHELL_WINDOWCREATED
gOpenWindows[lParam] := {title: WinGetTitle(lParam), class:WinGetClass(lParam), processName: WinGetProcessName(lParam)}
if gOpenWindows[lParam].processName = "notepad.exe" || gOpenWindows[lParam].title = "Calculator" {
ToolTip(gOpenWindows[lParam].title " window opened")
SetTimer(ToolTip, -3000)
}
} else if (wParam = 2) && gOpenWindows.Has(lParam) { ; HSHELL_WINDOWDESTROYED
if gOpenWindows[lParam].processName = "notepad.exe" || gOpenWindows[lParam].title = "Calculator" {
ToolTip(gOpenWindows[lParam].title " window closed")
SetTimer(ToolTip, -3000)
}
gOpenWindows.Delete(lParam)
}
}
六、UIAutomation 事件
Microsoft 最新的辅助功能接口 UIAutomation 也可用于挂钩基于窗口的事件。以下示例使用 UIA.ahk 库来实现此操作,该库需要与示例脚本位于同一文件夹中。请注意,并非所有窗口都能可靠地触发Window_WindowClosed事件,因此可能需要以与前面方法类似的方式跟踪打开的窗口。
#Requires AutoHotkey v2
#include UIA.ahk
; Caching is necessary to ensure that we won't be requesting information about windows that don't exist any more (eg after close), or when a window was created and closed while our handler function was running
cacheRequest := UIA.CreateCacheRequest(["Name", "Type", "NativeWindowHandle"])
; I'm using an event handler group, but if only one event is needed then a regular event handler could be used as well
groupHandler := UIA.CreateEventHandlerGroup()
handler := UIA.CreateAutomationEventHandler(AutomationEventHandler)
groupHandler.AddAutomationEventHandler(handler, UIA.Event.Window_WindowOpened, UIA.TreeScope.Subtree, cacheRequest)
groupHandler.AddAutomationEventHandler(handler, UIA.Event.Window_WindowClosed, UIA.TreeScope.Subtree, cacheRequest)
; Root element = Desktop element, which means that using UIA.TreeScope.Subtree, all windows on the desktop will be monitored
UIA.AddEventHandlerGroup(groupHandler, UIA.GetRootElement())
Persistent()
AutomationEventHandler(sender, eventId) {
if eventId = UIA.Event.Window_WindowOpened {
; hwnd := DllCall("GetAncestor", "UInt", sender.CachedNativeWindowHandle, "UInt", 2) ; If the window handle (winId) is needed
if InStr(sender.CachedName, "Notepad") {
ToolTip("Notepad opened")
} else if InStr(sender.CachedName, "Calculator") {
ToolTip("Calculator opened")
}
SetTimer(ToolTip, -3000)
} else if eventId = UIA.Event.Window_WindowClosed {
if InStr(sender.CachedName, "Notepad") {
ToolTip("Notepad closed")
} else if InStr(sender.CachedName, "Calculator") {
ToolTip("Calculator closed")
}
SetTimer(ToolTip, -3000)
}
}
6. 高级:SetWindowsHookEx
此方法类似于 SetWinEventHook,但在两个重要方面有所不同:
1)它可以拦截消息,例如它可以停止或更改窗口打开/关闭/最小化以及各种其他消息。因此,使用起来也非常危险,因为如果事件处理程序写得不好,那么它可能会冻结整个系统,这可以通过注销或重新启动来修复。草率错误的一个示例可能是在全局窗口关闭事件挂钩中使用 MsgBox,而不先取消挂钩,因为这样就无法关闭 MsgBox(AHK 一次只能处理一个事件,关闭 MsgBox 将是窗口关闭事件),也无法关闭任何其他窗口。
2)它需要使用一个dll文件,该文件被注入到处理该事件的所有进程中。由于 Windows 的工作方式,64 位 AHK 可以使用 64 位 dll 注入 64 位进程,或者将 32 位 dll 和 32 位 AHK 注入 32 位进程。此外,此 dll 不能注入到以比脚本更高的级别运行的进程中:例如,如果目标窗口/进程以管理员身份打开,则脚本也需要以管理员身份运行。
请参阅此处的 SetWindowsHookEx 事件列表,事件子消息列在相应的 Proc 函数下(例如 CBTProc)。
通常注入的 dll 是用 C++ 编写的,因为 C++ 比 AHK 快得多,并且不需要 dll 和 AHK 之间的慢通信。某些钩子类型需要在短时间内处理数百条消息,因此使用 AHK 的钩子可能会明显降低系统速度。但是,以下示例使用 HookProc.dll,它是用 C++ 编写的,并且仅过滤掉选定的消息,这应该会在一定程度上缓解速度缓慢。它可以从 HookProc GitHub 存储库 (x64/Release/HookProc.dll) 下载。HookProc.dll只是一个概念证明,并不意味着在实际应用程序中使用,请自行承担使用风险。相反,我建议用 C++ 编写自己的 Proc 函数,请参阅此处的示例。如果要尝试这些示例,请HookProc.dll与脚本放在同一文件夹中。
例 1.阻止记事本关闭和打开
#Requires Autohotkey v2.0+
Persistent()
WH_CBT := 5, HCBT_CREATEWND := 3, HCBT_DESTROYWND := 4
; Register a new window message that the application will call on CBTProc event
msg := DllCall("RegisterWindowMessage", "str", "CBTProc", "uint")
OnMessage(msg, CBTProc)
hHook := WindowsHookEx(WH_CBT, msg, [HCBT_CREATEWND, HCBT_DESTROYWND], 0, 0)
; wParam is the target process' handle, and lParam is a struct containing the info:
; struct ProcInfo {
; int nCode;
; WPARAM wParam;
; LPARAM lParam;
;}; size = 24 due to struct packing
CBTProc(hProcess, lParam, msg, hWnd) {
DetectHiddenWindows(1) ; Necessary because windows that haven't been created aren't visible
; Read CBTProc arguments (nCode, wParam, lParam) from the message lParam
if !TryReadProcessMemory(hProcess, lParam, info := Buffer(24)) {
OutputDebug("Reading CBTProc arguments failed!`n")
return -1
}
nCode := NumGet(info, "int"), wParam := NumGet(info, A_PtrSize, "ptr"), lParam := NumGet(info, A_PtrSize*2, "ptr")
; Filter only for Notepad; keep in mind that controls are considered windows as well
if WinExist(wParam) && InStr(WinGetProcessName(wParam), "notepad.exe") {
if (nCode == HCBT_CREATEWND) {
; This might be a child window (eg a dialog box), which we don't want to prevent
; To determine that, get CBT_CREATEWND->lpcs->hwndParent which should be 0 for a top-level window
if !TryReadProcessMemory(hProcess, lParam, CBT_CREATEWND := Buffer(A_PtrSize*2,0)) {
OutputDebug("Reading CBT_CREATEWND failed!`n")
return -1
}
lpcs := NumGet(CBT_CREATEWND, "ptr"), hwndInsertAfter := NumGet(CBT_CREATEWND, A_PtrSize, "ptr")
if !lpcs
return -1
if !TryReadProcessMemory(hProcess, lpcs, CREATESTRUCT := Buffer(6*A_PtrSize + 6*4, 0)) {
OutputDebug("Reading CREATESTRUCT failed!`n")
return -1
}
hwndParent := NumGet(CREATESTRUCT, A_PtrSize*3, "ptr")
if hwndParent == 0 {
; Because AHK is single-threaded, unhook before the MsgBox or no window can be created/destroyed during that time
global hHook := 0
MsgBox("Creating Notepad has been blocked!")
hHook := WindowsHookEx(WH_CBT, msg, [HCBT_CREATEWND, HCBT_DESTROYWND], 0, 0)
return 1
}
} else {
; Show message only for top-level windows (not controls)
if wParam = DllCall("GetAncestor", "ptr", wParam, "uint", 2, "ptr") {
; Because AHK is single-threaded, unhook before the MsgBox or no window can be created/destroyed during that time
global hHook := 0
MsgBox("Closing Notepad has been blocked!")
hHook := WindowsHookEx(WH_CBT, msg, [HCBT_CREATEWND, HCBT_DESTROYWND], 0, 0)
}
return 1
}
}
return -1
}
/**
* Sets a new WindowsHookEx, which can be used to intercept window messages. It can only be used with 64-bit AHK, to hook 64-bit programs.
* This has the potential to completely freeze your system and force a reboot, so use it at your
* own peril!
* Syntax: WindowsHookEx(idHook, msg, nCodes, HookedWinTitle := "", timeOut := 16, ReceiverWinTitle := A_ScriptHwnd)
* @param {number} idHook The type of hook procedure to be installed.
* Common ones: WH_GETMESSAGE := 3, WH_CALLWNDPROC := 4, WH_CBT := 5
* @param {number} msg The window message number where new events are directed to.
* Can be created with `msg := DllCall("RegisterWindowMessage", "str", "YourMessageNameHere", "uint")`
* @param {array} nCodes An array of codes to be monitored (max of 9).
* For most hook types this can be one of nCode values (eg HCBT_MINMAX for WH_CBT), but in the
* case of WH_CALLWNDPROC this should be an array of monitored window messages (eg WM_PASTE).
*
* nCode 0xFFFFFFFF can be used to match all nCodes, but the use of this is not recommended
* because of the slowness of AHK and inter-process communication, which might slow down the whole system.
* @param HookedWinTitle A specific window title or hWnd to hook. Specify 0 for a global hook (all programs).
* @param {number} timeOut Timeout in milliseconds for events. Set 0 for infinite wait, but this
* isn't recommended because of the high potential of freezing the system (all other incoming
* messages would not get processed!).
* @param ReceiverWinTitle The WinTitle or hWnd of the receiver who will get the event messages.
* Default is current script.
* @returns {Object} New hook object which contains hook information, and when destroyed unhooks the hook.
*/
class WindowsHookEx {
; File name of the HookProc dll, is searched in A_WorkingDir, A_ScriptDir, A_ScriptDir\Lib\, and A_ScriptDir\Resources\
static DllName := "HookProc.dll"
; Initializes library at load-time
static __New() {
for loc in [A_WorkingDir "\" this.DllName, A_ScriptDir "\" this.DllName, A_ScriptDir "\Lib\" this.DllName, A_ScriptDir "\Resources\" this.DllName] {
if FileExist(loc) {
; WindowsHookEx.ClearSharedMemory() ; Might be useful to uncomment while debugging
this.hLib := DllCall("LoadLibrary", "str", loc, "ptr")
return
}
}
throw Error("Unable to find " this.DllName " file!", -1)
}
__New(idHook, msg, nCodes, HookedWinTitle := "", timeOut := 16, ReceiverWinTitle := A_ScriptHwnd) {
if !IsInteger(HookedWinTitle) {
if !(this.hWndTarget := WinExist(HookedWinTitle))
throw TargetError("HookedWinTitle `"" HookedWinTitle "`" was not found!", -1)
} else
this.hWndTarget := HookedWinTitle
if !(this.hWndReceiver := IsInteger(ReceiverWinTitle) ? ReceiverWinTitle : WinExist(ReceiverWinTitle))
throw TargetError("Receiver window was not found!", -1)
if !IsObject(nCodes) && IsInteger(nCodes)
nCodes := [nCodes]
this.threadId := DllCall("GetWindowThreadProcessId", "Ptr", this.hWndTarget, "Ptr", 0, "UInt")
this.idHook := idHook, this.msg := msg, this.nCodes := nCodes, this.nTimeout := timeOut
local pData := Buffer(nCodes.Length * A_PtrSize)
for i, nCode in nCodes
NumPut("ptr", nCode, pData, (i-1)*A_PtrSize)
this.hHook := DllCall(WindowsHookEx.DllName "\SetHook", "int", idHook, "ptr", msg, "int", this.threadId, "ptr", pData, "int", nCodes.Length, "ptr", this.hWndReceiver, "int", timeOut, "ptr")
}
; Unhooks the hook, which is also automatically done when the hook object is destroyed
static Unhook(hHook) => DllCall(this.DllName "\UnHook", "ptr", IsObject(hHook) ? hHook.hHook : hHook)
; Clears the shared memory space of the dll which might sometimes get corrupted during debugging
static ClearSharedMemory() => DllCall(this.DllName "\ClearSharedMemory")
__Delete() => WindowsHookEx.UnHook(this.hHook)
; Unhooks all hooks created by this script
static Close() => DllCall(this.DllName "\Close")
}
TryReadProcessMemory(hProcess, lpBaseAddress, oBuffer, &nBytesRead?) {
try return DllCall("ReadProcessMemory", "ptr", hProcess, "ptr", lpBaseAddress, "ptr", oBuffer, "int", oBuffer.Size, "int*", IsSet(nBytesRead) ? &nBytesRead:=0 : 0, "int") != 0
return 0
}
HIWORD(DWORD) => ((DWORD>>16)&0xFFFF)
LOWORD(DWORD) => (DWORD&0xFFFF)
MAKEWORD(LOWORD, HIWORD) => (HIWORD<<16)|(LOWORD&0xFFFF)
例 2.拦截记事本WM_PASTE事件
#Requires Autohotkey v2.0+
Persistent()
WH_CALLWNDPROC := 4
WM_PASTE := 0x0302
if !WinExist("ahk_exe notepad.exe") {
Run "notepad.exe"
} else
WinActivate "ahk_exe notepad.exe"
WinWaitActive "ahk_exe notepad.exe"
hWnd := WinExist()
msg := DllCall("RegisterWindowMessage", "str", "WndProc", "uint")
OnMessage(msg, WndProc)
hHook := WindowsHookEx(WH_CALLWNDPROC, msg, [WM_PASTE], hWnd, 0)
#HotIf WinActive("ahk_exe notepad.exe")
^v::MsgBox("Ctrl+V doesn't send WM_PASTE, try right-clicking and select Paste")
; WH_CALLWNDPROC points the hook to CallWndProc, but that isn't a very useful call so it isn't
; redirected to AHK. Instead, CallWndProc allows the message through, but redirects the following
; WndProc function call to AHK.
; WndProc function definition is WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
; which means the OnMessage call lParam will point to a structure containing these values:
; struct NewWndProcInfo {
; HWND hWnd;
; UINT uMsg;
; WPARAM wParam;
; LPARAM lParam;
;}; (size = 32)
WndProc(hProcess, lParam, msg, hWnd) {
; Since we are monitoring a single message we could instead use simple "return (MsgBox("Allow paste?", "AHK/Notepad", 0x4) = "Yes") ? -1 : 0"
; However, this example demonstrates how to read the WndProc arguments from the process that received the event
if TryReadProcessMemory(hProcess, lParam, info := Buffer(32)) {
hWnd := NumGet(info, "ptr"), uMsg := NumGet(info, A_PtrSize, "ptr"), wParam := NumGet(info, A_PtrSize*2, "ptr"), lParam := NumGet(info, A_PtrSize*3, "ptr")
if uMsg = WM_PASTE
return (MsgBox("Allow paste?", "AHK/Notepad", 0x4) = "Yes") ? -1 : 0
}
return -1
}
/**
* Sets a new WindowsHookEx, which can be used to intercept window messages. It can only be used with 64-bit AHK, to hook 64-bit programs.
* This has the potential to completely freeze your system and force a reboot, so use it at your
* own peril!
* Syntax: WindowsHookEx(idHook, msg, nCodes, HookedWinTitle := "", timeOut := 16, ReceiverWinTitle := A_ScriptHwnd)
* @param {number} idHook The type of hook procedure to be installed.
* Common ones: WH_GETMESSAGE := 3, WH_CALLWNDPROC := 4, WH_CBT := 5
* @param {number} msg The window message number where new events are directed to.
* Can be created with `msg := DllCall("RegisterWindowMessage", "str", "YourMessageNameHere", "uint")`
* @param {array} nCodes An array of codes to be monitored (max of 9).
* For most hook types this can be one of nCode values (eg HCBT_MINMAX for WH_CBT), but in the
* case of WH_CALLWNDPROC this should be an array of monitored window messages (eg WM_PASTE).
*
* nCode 0xFFFFFFFF can be used to match all nCodes, but the use of this is not recommended
* because of the slowness of AHK and inter-process communication, which might slow down the whole system.
* @param HookedWinTitle A specific window title or hWnd to hook. Specify 0 for a global hook (all programs).
* @param {number} timeOut Timeout in milliseconds for events. Set 0 for infinite wait, but this
* isn't recommended because of the high potential of freezing the system (all other incoming
* messages would not get processed!).
* @param ReceiverWinTitle The WinTitle or hWnd of the receiver who will get the event messages.
* Default is current script.
* @returns {Object} New hook object which contains hook information, and when destroyed unhooks the hook.
*/
class WindowsHookEx {
; File name of the HookProc dll, is searched in A_WorkingDir, A_ScriptDir, A_ScriptDir\Lib\, and A_ScriptDir\Resources\
static DllName := "HookProc.dll"
; Initializes library at load-time
static __New() {
for loc in [A_WorkingDir "\" this.DllName, A_ScriptDir "\" this.DllName, A_ScriptDir "\Lib\" this.DllName, A_ScriptDir "\Resources\" this.DllName] {
if FileExist(loc) {
; WindowsHookEx.ClearSharedMemory() ; Might be useful to uncomment while debugging
this.hLib := DllCall("LoadLibrary", "str", loc, "ptr")
return
}
}
throw Error("Unable to find " this.DllName " file!", -1)
}
__New(idHook, msg, nCodes, HookedWinTitle := "", timeOut := 16, ReceiverWinTitle := A_ScriptHwnd) {
if !IsInteger(HookedWinTitle) {
if !(this.hWndTarget := WinExist(HookedWinTitle))
throw TargetError("HookedWinTitle `"" HookedWinTitle "`" was not found!", -1)
} else
this.hWndTarget := HookedWinTitle
if !(this.hWndReceiver := IsInteger(ReceiverWinTitle) ? ReceiverWinTitle : WinExist(ReceiverWinTitle))
throw TargetError("Receiver window was not found!", -1)
if !IsObject(nCodes) && IsInteger(nCodes)
nCodes := [nCodes]
this.threadId := DllCall("GetWindowThreadProcessId", "Ptr", this.hWndTarget, "Ptr", 0, "UInt")
this.idHook := idHook, this.msg := msg, this.nCodes := nCodes, this.nTimeout := timeOut
local pData := Buffer(nCodes.Length * A_PtrSize)
for i, nCode in nCodes
NumPut("ptr", nCode, pData, (i-1)*A_PtrSize)
this.hHook := DllCall(WindowsHookEx.DllName "\SetHook", "int", idHook, "ptr", msg, "int", this.threadId, "ptr", pData, "int", nCodes.Length, "ptr", this.hWndReceiver, "int", timeOut, "ptr")
}
; Unhooks the hook, which is also automatically done when the hook object is destroyed
static Unhook(hHook) => DllCall(this.DllName "\UnHook", "ptr", IsObject(hHook) ? hHook.hHook : hHook)
; Clears the shared memory space of the dll which might sometimes get corrupted during debugging
static ClearSharedMemory() => DllCall(this.DllName "\ClearSharedMemory")
__Delete() => WindowsHookEx.UnHook(this.hHook)
; Unhooks all hooks created by this script
static Close() => DllCall(this.DllName "\Close")
}
TryReadProcessMemory(hProcess, lpBaseAddress, oBuffer, &nBytesRead?) {
try return DllCall("ReadProcessMemory", "ptr", hProcess, "ptr", lpBaseAddress, "ptr", oBuffer, "int", oBuffer.Size, "int*", IsSet(nBytesRead) ? &nBytesRead:=0 : 0, "int") != 0
return 0
}
HIWORD(DWORD) => ((DWORD>>16)&0xFFFF)
LOWORD(DWORD) => (DWORD&0xFFFF)
MAKEWORD(LOWORD, HIWORD) => (HIWORD<<16)|(LOWORD&0xFFFF)