声明
该案例在参考以下帖子的基础上修改,精简并增加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个内容,
- 应用名称:AutoHotkey 64-bit
- 标题:测试的标题
- 消息内容:测试的内容
编写的缘由
据说微软没有提供明确的API可以获取到该消息内容,并且就算通过win spy获取ahk_class和ahk_exe
也无法通过以下函数获取到窗口的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的区别
关键点都一样:
- 确认是用哪个事件;
- 可以进一步定位到哪个进程,这里是固定进程,可以在开始直接注册;如果是动态进程,要考虑下重新获取,或直接不限定;
- 通过Acc获取到的节点路径是哪个。
v2的写法要更精简些,因为Acc v2版本可以说是大改,在注册事件钩子,也同时会考虑自动注销钩子;
回调函数,直接回给你oAcc对象,而不用再次去获取。
如何确认事件及Acc节点路径
分享1个技巧,先通过AccViewer(链接为河大人分享的版本,Acc v2有自带AccViewer)查看对应窗口的节点路径以及窗口id,如下
在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都是一样的)
在后续的记录中,增加事件代码的记录
那么在后续就可以看到,哪个事件是最先触发的,留下那个事件 30022 = 0x7546 即可,避免多次触发
感谢大佬的认可😁