来源: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 可以拦截消息,甚至阻止或修改窗口打开、关闭、最小化、粘贴等行为。

原帖特别强调两个重要差异:

  1. SetWindowsHookEx 可以拦截消息。如果回调写得不好,可能冻结整个系统。例如在全局窗口关闭钩子里直接弹 MsgBox,而不先解除钩子,就可能导致 MsgBox 自己也无法关闭。
  2. 它通常需要 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 实战建议

如果只是写普通自动化脚本,我建议按这个顺序考虑:

  1. 能用 WinWait 解决,就先用 WinWait。
  2. 脚本还要继续干别的事,就改用 SetTimer。
  3. 需要长期监听、低资源占用,就用 SetWinEventHook。
  4. 目标窗口是现代 UI,或者本来就在用 UIA,就考虑 UIAutomation event。
  5. 除非你明确要拦截系统消息,否则不要轻易使用 SetWindowsHookEx。

另外,窗口识别条件尽量不要只靠标题。更推荐先用 AHKInfo、Window Spy 或你站内 AHKEditor 附带的工具确认 ahk_class、ahk_exe、窗口标题前缀、PID、句柄等信息。对长期脚本来说,稳定的窗口定位条件比监听方式本身还重要。

声明:站内资源为整理优化好的代码上传分享与学习研究,如果是开源代码基本都会标明出处,方便大家扩展学习路径。请不要恶意搬运,破坏站长辛苦整理维护的劳动成果。本站为爱好者分享站点,所有内容不作为商业行为。如若本站上传内容侵犯了原著者的合法权益,请联系我们进行删除下架。