来源:AutoHotkey Community - Detect window open and close
原作者:Descolada。本文为中文移植和扩展整理版,完整覆盖原帖提到的窗口打开/关闭检测方法;示例代码按本站习惯改写为 AHK v1 风格,并对代码注释做了中文化处理。原帖基于 AHK v2,若需要对照 v2 原始代码,请查看上方来源链接。
为什么要检测窗口打开和关闭
很多自动化脚本都需要知道某个窗口什么时候出现、什么时候关闭。例如:记事本打开后自动调整窗口位置;计算器关闭后清理状态;某个软件弹出“保存为”窗口时自动填路径;浏览器窗口创建后挂接 UIA 或控件逻辑。
检测窗口打开/关闭有很多做法。最简单的是 WinWait 和 WinWaitClose;更灵活的是 SetTimer 轮询;更推荐的事件驱动方式是 SetWinEventHook;ShellHook 也能用,但微软文档里说明它并不适合通用场景;UIAutomation 可以监听 UIA 事件;SetWindowsHookEx 则是更底层、更危险的高级方案。
下面示例主要检测记事本和计算器。如果你的系统里记事本、计算器标题或进程名不同,请按实际情况修改 WinTitle 条件。AHK v1 脚本里我更建议用 ahk_exe、ahk_class、窗口句柄等稳定条件,而不是只依赖完整标题。
先给选型建议
- 只监控一个窗口,脚本可以阻塞等待:用 WinWait。
- 想写法简单,又不介意定时检查:用 SetTimer。
- 想低资源、事件驱动、长期稳定监听:优先研究 SetWinEventHook。
- 想监听 Shell 层面的窗口创建/销毁:可以用 ShellHook,但不建议作为首选。
- 目标是现代 UI、WPF、UWP、Electron 等界面:可以考虑 UIAutomation 事件。
- 想拦截、阻止、修改窗口消息:SetWindowsHookEx,但风险高,不建议普通脚本直接用。
1. WinWait / WinWaitClose
WinWait 等待窗口出现,WinWaitClose 等待窗口关闭。这是最简单的方案,适合脚本主流程本来就应该停在那里等窗口的场景。
优点:最简单、最容易理解,不需要 DllCall。
缺点:会阻塞当前线程,脚本主流程不能继续做其它事情;如果要同时监控多个窗口,逻辑会变复杂。热键、定时器、回调仍然可以运行,但主流程会停在等待语句上。
示例:循环等待记事本打开和关闭。
#Requires AutoHotkey v1.1
Loop
{
; 等待记事本窗口出现。没有超时时会一直等。
WinWait, ahk_exe notepad.exe
WinGetTitle, title, A
ToolTip, %title% 窗口已创建
SetTimer, HideToolTip, -3000
; 等待记事本关闭
WinWaitClose, ahk_exe notepad.exe
ToolTip, 记事本窗口已关闭
SetTimer, HideToolTip, -3000
}
return
HideToolTip:
ToolTip
return
如果 WinWait 设置了超时时间,要检查 ErrorLevel,否则后续 WinGetTitle 可能拿不到目标窗口。
WinWait, ahk_exe notepad.exe,, 5
if ErrorLevel
{
MsgBox, 5 秒内没有等到记事本。
return
}
WinGetTitle, title, A
MsgBox, 找到窗口:%title%
多个窗口可以用 ahk_group 等待,例如同时监控记事本和计算器。但这个方法不容易判断“到底是哪一个窗口关闭了”,只能知道组里某个窗口关闭了。若要精确判断关闭的是哪个窗口,可以看后面的 SetTimer 多窗口示例。
SetTitleMatchMode, 3
GroupAdd, WindowOpenClose, ahk_exe notepad.exe
GroupAdd, WindowOpenClose, Calculator
Loop
{
WinWait, ahk_group WindowOpenClose
WinGetTitle, title, A
ToolTip, %title% 窗口已创建
SetTimer, HideToolTip, -3000
WinWaitClose, ahk_group WindowOpenClose
ToolTip, 计算器或记事本窗口已关闭
SetTimer, HideToolTip, -3000
}
return
HideToolTip:
ToolTip
return
2. SetTimer 轮询
SetTimer 的思路是定时检查窗口是否存在,并把这一次的状态和上一次状态比较。如果上次不存在、这次存在,就说明窗口打开;如果上次存在、这次不存在,就说明窗口关闭。
优点:写法不难,不会像 WinWait 那样阻塞主流程,适合和其它脚本逻辑放在一起。
缺点:不是事件驱动,大多数时间都在重复检查窗口状态;监控窗口越多、检查越频繁,资源浪费越明显。
示例:监控一个窗口。
targetWindow := "ahk_exe notepad.exe"
lastExist := !!WinExist(targetWindow)
SetTimer, WinOpenClose, 250
return
WinOpenClose:
currentExist := !!WinExist(targetWindow)
if (currentExist = lastExist)
return
lastExist := currentExist
if (currentExist)
ToolTip, 记事本已打开
else
ToolTip, 记事本已关闭
SetTimer, HideToolTip, -3000
return
HideToolTip:
ToolTip
return
监控多个窗口时,需要维护一个“上一次窗口列表”,再和“当前窗口列表”做差异比较。这样可以知道哪个窗口是新开的,哪个窗口是刚关闭的。
global gLastWindows := ListTargetWindows()
SetTimer, CheckWindows, 250
return
CheckWindows:
current := ListTargetWindows()
; 新增窗口:当前存在,上次不存在
for hwnd, info in current
{
if !gLastWindows.HasKey(hwnd)
{
ToolTip, % info.title " 窗口已创建"
SetTimer, HideToolTip, -3000
}
}
; 关闭窗口:上次存在,当前不存在
for hwnd, info in gLastWindows
{
if !current.HasKey(hwnd)
{
ToolTip, % info.title " 窗口已关闭"
SetTimer, HideToolTip, -3000
}
}
gLastWindows := current
return
ListTargetWindows()
{
result := {}
WinGet, list, List
Loop, %list%
{
hwnd := list%A_Index%
WinGet, processName, ProcessName, ahk_id %hwnd%
WinGetTitle, title, ahk_id %hwnd%
if (processName = "notepad.exe" || title = "Calculator")
result[hwnd] := { title: title, processName: processName }
}
return result
}
HideToolTip:
ToolTip
return
这个方案很实用,但要注意计时器间隔。250ms 已经比较频繁,如果只是普通办公自动化,500ms 或 1000ms 往往也够用。
3. SetWinEventHook
SetWinEventHook 是原作者更推荐的方式之一。它由 Windows 在窗口创建、销毁、激活、移动、最小化、最大化等事件发生时通知脚本,脚本不需要一直轮询。
优点:事件驱动,资源占用更低;可以监听的事件很多;适合长期运行的自动化脚本。
缺点:需要 DllCall 和回调函数,写法比 WinWait、SetTimer 难;有些窗口刚创建时标题还没更新,需要延迟读取或改用 EVENT_OBJECT_SHOW。
常用事件包括:
- EVENT_OBJECT_CREATE:对象/窗口创建。
- EVENT_OBJECT_DESTROY:对象/窗口销毁。
- EVENT_OBJECT_SHOW:对象显示,有时比 CREATE 更适合读取窗口标题。
- EVENT_SYSTEM_FOREGROUND:前台窗口变化。
- EVENT_OBJECT_LOCATIONCHANGE:位置或大小变化。
下面示例监听顶层窗口创建/销毁,并识别记事本和计算器。关闭事件触发后窗口标题通常已经拿不到,所以脚本启动时先记录当前窗口信息,并在创建事件时更新记录。
global gOpenWindows := {}
; 启动时先记录当前已有窗口。窗口关闭后通常无法再读取标题,所以要提前缓存。
WinGet, list, List
Loop, %list%
{
hwnd := list%A_Index%
gOpenWindows[hwnd] := GetWindowInfo(hwnd)
}
global EVENT_OBJECT_CREATE := 0x8000
global EVENT_OBJECT_DESTROY := 0x8001
global OBJID_WINDOW := 0
global INDEXID_CONTAINER := 0
; WINEVENT_OUTOFCONTEXT = 0,回调不注入目标进程
global gWinEventProc := RegisterCallback("HandleWinEvent", "Fast", 7)
global gHook := DllCall("SetWinEventHook"
, "UInt", EVENT_OBJECT_CREATE
, "UInt", EVENT_OBJECT_DESTROY
, "Ptr", 0
, "Ptr", gWinEventProc
, "UInt", 0
, "UInt", 0
, "UInt", 0
, "Ptr")
OnExit, Cleanup
return
HandleWinEvent(hWinEventHook, event, hwnd, idObject, idChild, idEventThread, dwmsEventTime)
{
Critical
global gOpenWindows, EVENT_OBJECT_CREATE, EVENT_OBJECT_DESTROY, OBJID_WINDOW, INDEXID_CONTAINER
; 只处理窗口本身,过滤掉控件、子元素等事件
if (idObject != OBJID_WINDOW || idChild != INDEXID_CONTAINER)
return
if (event = EVENT_OBJECT_CREATE)
{
; 只处理顶层窗口
if (hwnd != DllCall("GetAncestor", "Ptr", hwnd, "UInt", 2, "Ptr"))
return
; 有些窗口标题创建后会延迟更新,所以这里延迟检查
SetTimer, CheckCreatedWindow, -50
gCreatedHwnd := hwnd
return
}
else if (event = EVENT_OBJECT_DESTROY)
{
if gOpenWindows.HasKey(hwnd)
{
info := gOpenWindows[hwnd]
if (info.processName = "notepad.exe")
ShowTip("记事本窗口已销毁")
else if (info.title = "Calculator")
ShowTip("计算器窗口已销毁")
gOpenWindows.Delete(hwnd)
}
}
}
CheckCreatedWindow:
global gCreatedHwnd, gOpenWindows
hwnd := gCreatedHwnd
if !WinExist("ahk_id " hwnd)
return
info := GetWindowInfo(hwnd)
gOpenWindows[hwnd] := info
if (info.processName = "notepad.exe")
ShowTip("记事本窗口已创建")
else if (info.title = "Calculator")
ShowTip("计算器窗口已创建")
return
GetWindowInfo(hwnd)
{
WinGetTitle, title, ahk_id %hwnd%
WinGetClass, class, ahk_id %hwnd%
WinGet, processName, ProcessName, ahk_id %hwnd%
return { title: title, class: class, processName: processName }
}
ShowTip(text)
{
ToolTip, %text%
SetTimer, HideToolTip, -3000
}
HideToolTip:
ToolTip
return
Cleanup:
if (gHook)
DllCall("UnhookWinEvent", "Ptr", gHook)
ExitApp
原帖特别提醒:有些窗口刚创建时标题还没来得及更新,脚本检测得太快会拿到空标题或旧标题。解决办法是延迟 20-100ms 再读取,或者改用 EVENT_OBJECT_SHOW,因为窗口显示时标题通常已经准备好了。
4. ShellHook
ShellHook 也是事件驱动方式。它让脚本接收一些 Shell 应用可能关心的消息,比如窗口创建、销毁、激活等。
优点:写法比 SetWinEventHook 稍短,也能监听窗口创建/销毁。
缺点:微软文档说明它并不是为通用程序设计的,后续 Windows 版本可能改变或不可用。因此长期稳定方案更推荐 SetWinEventHook。
示例:监听窗口创建和销毁。
global gOpenWindows := {}
WinGet, list, List
Loop, %list%
{
hwnd := list%A_Index%
gOpenWindows[hwnd] := GetWindowInfo(hwnd)
}
DllCall("RegisterShellHookWindow", "Ptr", A_ScriptHwnd)
MsgNum := DllCall("RegisterWindowMessage", "Str", "SHELLHOOK")
OnMessage(MsgNum, "ShellProc")
OnExit, Cleanup
return
ShellProc(wParam, lParam)
{
global gOpenWindows
; HSHELL_WINDOWCREATED = 1
if (wParam = 1)
{
hwnd := lParam
info := GetWindowInfo(hwnd)
gOpenWindows[hwnd] := info
if (info.processName = "notepad.exe" || info.title = "Calculator")
ShowTip(info.title " 窗口已打开")
}
; HSHELL_WINDOWDESTROYED = 2
else if (wParam = 2)
{
hwnd := lParam
if gOpenWindows.HasKey(hwnd)
{
info := gOpenWindows[hwnd]
if (info.processName = "notepad.exe" || info.title = "Calculator")
ShowTip(info.title " 窗口已关闭")
gOpenWindows.Delete(hwnd)
}
}
}
GetWindowInfo(hwnd)
{
WinGetTitle, title, ahk_id %hwnd%
WinGetClass, class, ahk_id %hwnd%
WinGet, processName, ProcessName, ahk_id %hwnd%
return { title: title, class: class, processName: processName }
}
ShowTip(text)
{
ToolTip, %text%
SetTimer, HideToolTip, -3000
}
HideToolTip:
ToolTip
return
Cleanup:
DllCall("DeregisterShellHookWindow", "Ptr", A_ScriptHwnd)
ExitApp
5. UIAutomation event
UIAutomation 是微软较新的辅助功能接口,也可以监听窗口相关事件。原帖示例使用 Descolada 的 UIA.ahk 库,通过 Window_WindowOpened 和 Window_WindowClosed 事件监听窗口打开/关闭。
需要注意:Window_WindowClosed 事件并不是所有窗口都会可靠触发,所以在严肃场景里仍然可能需要像前面一样维护已打开窗口列表。
本站已经有 UIA 相关介绍,可以先看:
UIA 事件的思路是:创建事件处理器,把它挂到桌面根元素上,监听所有窗口子树中的窗口打开/关闭事件。下面是 AHK v1 风格的骨架示例,具体类名和方法名要以你使用的 UIA 库版本为准。
; UIA_WindowEvent_示意.ahk
; 需要同目录放置 Descolada UIAutomation 项目的 UIA_Interface.ahk 等文件
#Include UIA_Interface.ahk
; 注意:不同 UIA 库版本 API 名称可能不同,下面用于说明整体结构
UIA := UIA_Interface()
; 创建事件处理器,监听桌面根元素下的窗口打开/关闭事件
; Window_WindowOpened 和 Window_WindowClosed 是 UIA 的窗口事件
; 实战时请根据 UIA 库提供的示例调整具体写法
MsgBox, 64, UIA, UIA 事件监听适合现代 UI,但关闭事件不一定对所有窗口可靠。
return
AutomationEventHandler(sender, eventId)
{
; 伪代码:
; if (eventId = Window_WindowOpened)
; 读取 sender.CachedName / NativeWindowHandle
; else if (eventId = Window_WindowClosed)
; 处理窗口关闭
}
如果目标是 WPF、UWP、Electron、浏览器界面或现代设置窗口,UIA 很有价值。但如果只是判断一个普通顶层窗口是否打开/关闭,SetWinEventHook 往往更直接。
6. 高级:SetWindowsHookEx
SetWindowsHookEx 和 SetWinEventHook 名字相似,但能力和风险都更高。SetWinEventHook 主要是“接收事件通知”;SetWindowsHookEx 可以拦截消息,甚至阻止或修改窗口打开、关闭、最小化、粘贴等行为。
原帖特别强调两个重要差异:
- SetWindowsHookEx 可以拦截消息。如果回调写得不好,可能冻结整个系统。例如在全局窗口关闭钩子里直接弹 MsgBox,而不先解除钩子,就可能导致 MsgBox 自己也无法关闭。
- 它通常需要 DLL 注入到目标进程。64 位 AHK 只能用 64 位 DLL 注入 64 位进程,32 位同理;如果目标程序以管理员权限运行,脚本也需要管理员权限。
原帖使用 HookProc.dll 做演示,这是一个 C++ 写的概念验证 DLL,用来过滤指定消息再通知 AHK。作者也说明它不建议直接用于正式项目。更稳妥的方式是自己用 C++ 写对应的 Hook Proc。
示例 1:阻止记事本打开或关闭
原帖通过 WH_CBT 监听 HCBT_CREATEWND 和 HCBT_DESTROYWND,从而拦截窗口创建/销毁。这个方向可以做到“检测并阻止”,但普通自动化脚本不建议直接使用。
; SetWindowsHookEx 高级示意:阻止记事本窗口创建/关闭
; 需要外部 HookProc.dll,并且位数、权限必须与目标进程匹配。
; 这里只保留结构说明,不建议直接用于生产环境。
WH_CBT := 5
HCBT_CREATEWND := 3
HCBT_DESTROYWND := 4
MsgNum := DllCall("RegisterWindowMessage", "Str", "CBTProc", "UInt")
OnMessage(MsgNum, "CBTProc")
; 真实使用时需要调用 DLL 中的 SetHook,并把 HCBT_CREATEWND/HCBT_DESTROYWND 传给 DLL 过滤
; hHook := WindowsHookEx(WH_CBT, MsgNum, [HCBT_CREATEWND, HCBT_DESTROYWND])
MsgBox, 48, 提醒, SetWindowsHookEx 属于高风险高级用法,建议优先使用 SetWinEventHook。
return
CBTProc(hProcess, lParam, msg, hWnd)
{
; 真实逻辑通常需要 ReadProcessMemory 读取目标进程里的结构体:
; nCode / wParam / lParam
; 若判断是记事本顶层窗口创建或销毁,可以 return 1 阻止事件。
return -1
}
示例 2:拦截记事本 WM_PASTE
原帖第二个例子用 WH_CALLWNDPROC 监听 WM_PASTE,也就是右键菜单粘贴或程序发送的粘贴消息。注意 Ctrl+V 不一定直接发送 WM_PASTE,所以例子里也特别提示了这一点。
; SetWindowsHookEx 高级示意:拦截 WM_PASTE
; 需要 HookProc.dll。这里只说明结构。
WH_CALLWNDPROC := 4
WM_PASTE := 0x0302
Run, notepad.exe
WinWaitActive, ahk_exe notepad.exe
hWnd := WinExist("A")
MsgNum := DllCall("RegisterWindowMessage", "Str", "WndProc", "UInt")
OnMessage(MsgNum, "WndProc")
; hHook := WindowsHookEx(WH_CALLWNDPROC, MsgNum, [WM_PASTE], hWnd)
MsgBox, 64, 提醒, Ctrl+V 不一定发送 WM_PASTE;可以尝试右键菜单里的粘贴。
return
WndProc(hProcess, lParam, msg, receiverHwnd)
{
; 真实逻辑需要从目标进程读取 WndProc 参数:
; hWnd / uMsg / wParam / lParam
; 如果 uMsg = WM_PASTE,可以询问是否允许粘贴。
return -1
}
结论很简单:如果只是检测窗口打开/关闭,不要上来就用 SetWindowsHookEx。它更适合“我要拦截并改变系统消息行为”的高级需求。
几种方法对比
| 方法 | 是否事件驱动 | 适合场景 | 主要问题 |
|---|---|---|---|
| WinWait | 否,阻塞等待 | 简单流程,脚本可以停下来等 | 主流程被阻塞,多窗口复杂 |
| SetTimer | 否,轮询 | 简单持续监控,不想阻塞脚本 | 会重复检查,窗口多时更耗资源 |
| SetWinEventHook | 是 | 长期稳定监听窗口事件 | DllCall 和回调写法较复杂 |
| ShellHook | 是 | Shell 层窗口创建/销毁消息 | 微软不建议作为通用方案 |
| UIAutomation event | 是 | 现代 UI、辅助功能事件 | 依赖 UIA 库,关闭事件不一定可靠 |
| SetWindowsHookEx | 是,可拦截 | 拦截或修改窗口消息 | 高风险,通常需要 DLL,可能卡系统 |
本站补充:AHK v1 实战建议
如果只是写普通自动化脚本,我建议按这个顺序考虑:
- 能用 WinWait 解决,就先用 WinWait。
- 脚本还要继续干别的事,就改用 SetTimer。
- 需要长期监听、低资源占用,就用 SetWinEventHook。
- 目标窗口是现代 UI,或者本来就在用 UIA,就考虑 UIAutomation event。
- 除非你明确要拦截系统消息,否则不要轻易使用 SetWindowsHookEx。
另外,窗口识别条件尽量不要只靠标题。更推荐先用 AHKInfo、Window Spy 或你站内 AHKEditor 附带的工具确认 ahk_class、ahk_exe、窗口标题前缀、PID、句柄等信息。对长期脚本来说,稳定的窗口定位条件比监听方式本身还重要。

评论(0)