检测窗口打开和关闭

作者: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)

给TA捐赠
共{{data.count}}人
人已捐赠
教程

AutoHotkey管理(关闭)Windows进程

2024-3-25 21:48:14

教程

屏幕缩放、DPI 和使脚本在不同的计算机中工作

2024-3-28 11:53:37

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
有新私信 私信列表
搜索