【案例】利用Acc获取Win10通知(toast notification)

声明

该案例在参考以下帖子的基础上修改,精简并增加v2版本:

https://www.autohotkey.com/boards/viewtopic.php?f=76&t=70258&hilit=ShellExperienceHost

引用的Acc库 for AHK v1:

https://www.autohotkey.com/board/topic/77303-acc-library-ahk-l-updated-09272012/

引用的Acc库 for AHK v2:

https://www.autohotkey.com/boards/viewtopic.php?f=83&t=107857&hilit=acc+v2


功能介绍

当通知弹出(事件钩子)时,利用Acc获取Win10右下角通知的内容,包含以下3个内容,

  1. 应用名称:AutoHotkey 64-bit
  2. 标题:测试的标题
  3. 消息内容:测试的内容

【案例】利用Acc获取Win10通知(toast notification)


编写的缘由

据说微软没有提供明确的API可以获取到该消息内容,并且就算通过win spy获取ahk_class和ahk_exe

【案例】利用Acc获取Win10通知(toast notification)

也无法通过以下函数获取到窗口的id

WinExist("ahk_class Windows.UI.Core.CoreWindow ahk_exe ShellExperienceHost.exe")

还是在论坛里翻了好久,才发现可以通过事件钩子和Acc的方式获取,结果也比较理想。


代码

V1版本

;===== 启动 =====
{
    ;#NoEnv
    #SingleInstance Force
    #Persistent
    SetBatchLines,-1
	#Requires AutoHotkey v1.1.33+

    ; 注册回调函数
    try{
        HookProcAdr := RegisterCallback( "HookProc", "F" )
    }
    if (!HookProcAdr){
        MsgBox, "注册回调函数失败!"
        ExitApp
    }

    ; 指定进程可以更精准定位,也可以省略,但可能在获取后要加更多条件判断
    Process, Exist, ShellExperienceHost.exe
    pid:= ErrorLevel
    If (!pid){
        MsgBox, "进程不存在!不影响事件钩子,可以继续"
    }

    ; 设置事件钩子及回调函数
    ; 0x7546 这个事件代码是测出来的,没去查是什么事件
    hWinEventHook := SetWinEventHook( 0x7546 , 0x7546 , 0, HookProcAdr, pid, 0, 0)
	if (!hWinEventHook){
		MsgBox, "设置事件钩子失败!"
		ExitApp
	}

    ; 退出时卸掉钩子
    OnExit("UnhookWinEvent")

    ; 为了避免同时触发多次
    lastMessage :=	""
    Return
}

; 回调判断消息并触发操作
;Based on Serenity https://autohotkey.com/board/topic/32662-tool-wineventhook-messages/
HookProc( hWinEventHook, Event, hWnd, idObject, idChild, dwEventThread, dwmsEventTime ) {
    Global lastMessage

    appName :="" ;应用名称
    title :="" ;通知的标题
    message :="" ;通知的内容

    try{
        oAcc := Acc_Get("Object", "4.1.1.2", 0, "ahk_id " hWnd)
        appName:= oAcc.accName(0)
        oAcc := ""

        oAcc := Acc_Get("Object", "4.1.1.5", 0, "ahk_id " hWnd)
        title:= oAcc.accName(0)
        oAcc := ""

        oAcc := Acc_Get("Object", "4.1.1.6", 0, "ahk_id " hWnd)
        message:= oAcc.accName(0)
        oAcc := ""
    }
    Catch e{
        FileAppend, %e%`r`n, messages.txt
    }

    if (appName && message != lastMessage){

        ; 这里是记录到文件中,可以更换成你想做的代码
        FileAppend, %appName% -> %title% -> %message%`r`n, messages.txt

        ; 记录获取的消息
        lastMessage := message

        ; 1秒后重置上次获取的消息,避免在同一时间内多次触发
        SetTimer, Reset,-1000
    }
}

Reset(){
    Global lastMessage:=""
}

;==== 设置及注销事件钩子 ====
{

    SetWinEventHook(eventMin, eventMax, hmodWinEventProc, lpfnWinEventProc, idProcess, idThread, dwFlags) {
        DllCall("CoInitialize", Uint, 0)
        return DllCall("SetWinEventHook"
        , UInt, eventMin
        , UInt, eventMax
        , Ptr, hmodWinEventProc
        , Ptr, lpfnWinEventProc
        , UInt, idProcess
        , UInt, idThread
        , UInt, dwFlags
        , Ptr)

    }

    UnhookWinEvent() {
        Global
        UnhkWE:= DllCall( "UnhookWinEvent", Ptr, hWinEventHook )
        Sleep, -1
    }

}

    #Include <Acc>

v2版本

#SingleInstance Force
#Requires AutoHotkey v2.0-a
Persistent()

; 为了避免同时触发多次
global lastMessage := ""

; 指定进程可以更精准定位,也可以省略,但可能在获取后要加更多条件判断
global pid := ProcessExist("ShellExperienceHost.exe")
If (!pid){
	MsgBox "进程不存在!不影响事件钩子,可以继续"
}

; 设置事件钩子及回调函数,Acc v2自带注销函数
; 0x7546 这个事件代码是测出来的,没去查是什么事件,为了更精准定位
global hWinEventHook := Acc.RegisterWinEvent(OnGetNotification, 0x7546, , pid)
if (!hWinEventHook){
	MsgBox "设置事件钩子失败!"
	ExitApp
}

OnGetNotification(oAcc, eventInfo) {
	global lastMessage

	appName := ""
	title := ""
	message := ""

	try {
		appName := oAcc[2].Name
		title := oAcc[5].Name
		message := oAcc[6].Name
	}
	; catch as err {
	; 	FileAppend err.Message "`r`n", "messages.txt"
	; }

	if (appName && message != lastMessage) {
		lastMessage := message
		FileAppend appName " -> " title " -> " message "`r`n", "messages.txt"
		SetTimer () => lastMessage := "", -1000
	}

}

#Include <Acc-v2>

版本v1和v2的区别

关键点都一样:

  1. 确认是用哪个事件;
  2. 可以进一步定位到哪个进程,这里是固定进程,可以在开始直接注册;如果是动态进程,要考虑下重新获取,或直接不限定;
  3. 通过Acc获取到的节点路径是哪个。

 

v2的写法要更精简些,因为Acc v2版本可以说是大改,在注册事件钩子,也同时会考虑自动注销钩子;

回调函数,直接回给你oAcc对象,而不用再次去获取。


如何确认事件及Acc节点路径

分享1个技巧,先通过AccViewer(链接为河大人分享的版本,Acc v2有自带AccViewer)查看对应窗口的节点路径以及窗口id,如下

【案例】利用Acc获取Win10通知(toast notification)

在v1中,节点是与以上完全相同的;但在v2中,获取到的oAcc是4.1.1,所以在获取下一级节点的时候,就不用带4.1.1

;v1
oAcc := Acc_Get("Object", "4.1.1.6", 0, "ahk_id " hWnd)
message:= oAcc.accName(0)

;v2
message := oAcc[6].Name

 

接下来是确认事件编码,在注册事件钩子时,有一个事件范围,扩大事件范围,从0x1到0x8002,甚至更大

hWinEventHook := SetWinEventHook( 0x1 , 0x8002 , 0, HookProcAdr, pid, 0, 0)

在回调函数中,增加判断条件,如果窗口id是以上记录下来的id,则继续,否则退出;(这个方法酌情而定,这里刚好发现只要电脑不重启,它通知的窗口id都是一样的)

在后续的记录中,增加事件代码的记录

【案例】利用Acc获取Win10通知(toast notification)

那么在后续就可以看到,哪个事件是最先触发的,留下那个事件 30022 = 0x7546 即可,避免多次触发

【案例】利用Acc获取Win10通知(toast notification)

给TA捐赠
共{{data.count}}人
人已捐赠
案例

用powershell缩放并保存剪切板图片到文件

2023-9-4 16:05:03

案例

一个加载动画

2023-9-21 12:06:02

2 条回复 A文章作者 M管理员
  1. dbgba
    dbgba给作者打赏了¥2
    • gdzrh917

      感谢大佬的认可😁

个人中心
购物车
优惠劵
有新私信 私信列表
搜索