Detecting whether a particular window has opened or closed is a recurring problem which has multiple possible solutions. This tutorial outlines some, but probably not all of them. My personal preferred method is either SetWinEventHook (see also the WinEvent library) or ShellHook (which is documented by Microsoft as deprecated, so it might not be the best).
All the examples detect the Notepad and/or Calculator window open/close, so if for some reason the title of Notepad or Calculator differs in your computer, make sure to change the code accordingly.
Table of contents
1. WinWait
2. SetTimer
3. SetWinEventHook
4. ShellHook
5. UIAutomation event
6. Advanced: SetWindowsHookEx
1. WinWait
This method uses WinWait to wait for a window to open and WinWaitClose for a window to close. It is the simplest one of all of these, but the downside is that the script can't continue on with other tasks (except hotkeys, timers and callbacks). In addition, if there are multiple windows that are to be monitored, then the code might get a bit complex.
Code: Select all
#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
}
Code: Select all
#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
}
This method sets up a recurring timer that checks whether the target window has been created or closed. The downside is that this is not event-driven, which means that most of the time AHK is wasting time checking the status of the window.
To monitor just one window or process, we can use something like the following:
Code: Select all
#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)
}
Code: Select all
#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
}
}
This method uses I guess the preferred method of detecting window open/close events: SetWinEventHook. This hook will cause Microsoft to notify us whenever a window is created or destroyed, which means our program doesn't have to waste resources by constantly checking for updates. In addition to window created/destroyed events it can also be used to detect window move events (EVENT_OBJECT_LOCATIONCHANGE), window activation, minimize/maximize and much more. See the Event Constants for a full list of possible events. In addition, an Acc object can be created with AccessibleObjectFromEvent from information sent by this hook, but this is a whole other topic.
As a simplified version there is the option of using the WinEvent library, which is a wrapper for SetWinEventHook.
The following example detects Notepad and Calculator window open and close events with HandleWinEvent function:
Code: Select all
#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
}
}
4. ShellHook
This method is also an event-driven one which registers our script to receive messages that could be useful for shell applications such as window created, destroyed, activated. Unfortunately it's also documented by Microsoft that this is not intended for general use and may be altered or unavailable in subsequent versions of Windows, so SetWinEventHook might be a better option.
Code: Select all
#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)
}
}
The latest of Microsofts' accessibility interfaces - UIAutomation - can also be used to hook window-based events. The following example uses the UIA.ahk library to implement this, which will need to be in the same folder as the example script. Note that Window_WindowClosed event isn't reliably triggered by all windows, so it might be necessary to keep track of open windows in a similar manner as in the previous methods.
Code: Select all
#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)
}
}
This method is similar to SetWinEventHook, but differs in two important ways:
1) It can intercept messages, for example it can stop or change window open/close/minimize and all sorts of other messages. Because of this it is also quite dangerous to use, because if the event handler is written badly then it might freeze up the whole system, which can be fixed by logging out or a reboot. An example of a sloppy mistake might be using a MsgBox inside a global window close event hook without unhooking first, because then the MsgBox can't be closed (AHK can process only one event at a time, and closing a MsgBox would be a window-close event), nor can any other window be closed.
2) It requires a dll file to be used, which is injected into all processes that process the event. Due to how Windows works, a 64-bit dll can be used by 64-bit AHK to inject into 64-bit processes, or 32-bit dll by 32-bit AHK into 32-bit processes. Also, this dll can't be injected into a process running at a more elevated level than the script: for example if the target window/process was opened as administrator, then the script needs to be ran as administrator as well.
See list of SetWindowsHookEx events here, event submessages are listed under the corresponding Proc functions (eg CBTProc).
Usually the injected dll is written in C++, because C++ is much faster than AHK and wouldn't require slow communication between the dll and AHK. Some hook types require processing hundreds of messages in a short period, so a hook using AHK might slow the system down noticeably. However, the following examples use HookProc.dll, which is written in C++ and filters out only select messages, which should mitigate the slowness somewhat. It's downloadable from HookProc GitHub repo (x64/Release/HookProc.dll). HookProc.dll is only a proof of concept and isn't meant to be used in actual applications, use it at your own risk. Instead I recommend writing your own Proc functions in C++, see example here. If you want to try these examples, put HookProc.dll in the same folder as the script.
Example 1. Blocking Notepad close and open
Code: Select all
#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)
Code: Select all
#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)
Code: Select all
23.08.23. Added explanations to WinWait examples about possibly checking for winId=0 in case a timeout was specified.
04.10.23. Added clarification to SetWinEventHook about sometimes needing a slight delay.
16.10.23. Added SetWindowsHookEx.
03.02.24. Updated SetWinEventHook code.
05.03.24. Added reference to the WinEvent library.
10.03.24. Modified WinEventHook class to contain WinEventHook.Stop(), and changed the default flag to not skip own process.
26.03.24. Removed "Fast" option from CallbackCreate.