C#使用Event_Object_NameChange从SetWinEventHook中排除工具提示弹出窗口 [英] C# Exclude tooltip popups from SetWinEventHook with EVENT_OBJECT_NAMECHANGE

查看:57
本文介绍了C#使用Event_Object_NameChange从SetWinEventHook中排除工具提示弹出窗口的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试使用SetWinEventHook检测AIMP音乐播放器的窗口标题更改,它可以工作,但问题是当我将鼠标悬停在按钮上(停止、播放、最小化等)时,它也会检测到工具提示弹出窗口。

我希望在设置SetWinEventHook或在WinEventProc事件中将其筛选出来时排除它们。
有什么想法吗?

using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Forms;

class NameChangeTracker
{
    delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    [DllImport("user32.dll")]
    static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr
       hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess,
       uint idThread, uint dwFlags);

    [DllImport("user32.dll")]
    static extern bool UnhookWinEvent(IntPtr hWinEventHook);

    const uint EVENT_OBJECT_NAMECHANGE = 0x800C;
    const uint WINEVENT_OUTOFCONTEXT = 0;

    // Need to ensure delegate is not collected while we're using it,
    // storing it in a class field is simplest way to do this.
    static WinEventDelegate procDelegate = new WinEventDelegate(WinEventProc);

    public static void Main()
    {
        // Listen for name change changes across all processes/threads on current desktop...
        IntPtr hhook = SetWinEventHook(EVENT_OBJECT_NAMECHANGE, EVENT_OBJECT_NAMECHANGE, IntPtr.Zero,
                procDelegate, (uint)Process.GetProcessesByName("AIMP").FirstOrDefault().Id, 0, WINEVENT_OUTOFCONTEXT);

        MessageBox.Show("Tracking name changes on HWNDs, close message box to exit.");
        UnhookWinEvent(hhook);
    }

    static void WinEventProc(IntPtr hWinEventHook, uint eventType,
        IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        // filter out non-HWND namechanges... (eg. items within a listbox)
        if (idObject != 0 || idChild != 0) return; 
        
        if (Process.GetProcessesByName("AIMP").FirstOrDefault().MainWindowHandle.ToInt32() == hwnd.ToInt32())
        {
            Console.WriteLine("Current song: " + Process.GetProcessesByName("AIMP").FirstOrDefault().MainWindowTitle);
        }
    }
}

输出:

Current song: Michael Jackson - Speed Demon
Current song: Minimize

推荐答案

问题目标已更改,如评论中所述,从使用挂钩的解决方案(SetWinEventHook)更改为UI自动化解决方案。


由于您以前从未使用过UI Automation,因此这可能是一次牛仔竞技表演,因此我将尝试解释为某些类型的事件添加自动化事件处理程序的过程,这些事件对此任务可能很有用。

手头的任务

属性的状态 应用程序的UI元素(在本例中为TitleBar值) 更改。

首先,您可能想知道程序启动时目标应用程序是否已经在运行。
我们可以使用Process.GetProcessesByName()来确定应用程序进程是否处于活动状态。

  • 目标应用程序主窗口需要与AutomationElement(用于标识UI对象的Automation对象-换句话说,element in the UI Automation tree)相关联。

注意:

我们无法将目标主窗口与特定的自动化关联 元素设置检测应用程序的事件处理程序 创建主窗口。
我们可以,有了 AutomationElement.FromHandle([Handle])方法,使用 由Process.MainWindowHandle返回。但是这个自动化 元素将严格绑定到特定的流程实例,因此 具体Process.Id。如果目标应用程序已关闭,并且 重新打开时,其Process.Id将不同,并且事件处理程序 将无法识别它。

  • 我们需要将检测窗口创建的事件处理程序与 AutomationElement.RootElement,表示当前桌面的根元素(实际上是任何UI元素或窗口),然后确定它是否是目标应用程序的主窗口,检查作为源对象(作为任何标准事件)的事件提供的Automation元素的某些相关属性。在示例代码中,我使用Element.Current.ClassName
  • 由于目标应用程序可以在某个时刻关闭,因此也需要在发生这种情况时通知我们。
    我们的程序可能需要根据目标应用程序的状态做出一些决定。
    或者只需通知用户和/或更新其自己的用户界面。
  • 目标应用程序可以在程序的生命周期内反复打开和关闭。我们将需要随着时间的推移跟踪这些变化。
  • 当属性值更改时,我们可以使用AutomationPropertyChangedEventHandler收到通知。当定义的自动化元素或元素类型的特定属性更改时引发此事件(请参阅下面的事件类型说明)。

UI Automation提供了Event HandlersPatterns,可用于跟踪所有描述的事件。

检测应用程序启动时间

我们需要使用Automation.AddAutomationEventHandler设置一个在创建窗口时引发事件的AutomationEventHandler委托。

AddAutomationEventHandler要求:

  • 要处理的Automation Event的类型
  • 与该事件关联的Automation Element
  • 事件的范围。作用域可以限制为Automation Element指定或扩展到其所有祖先和后代元素。
  • 引发事件时将调用的方法委托

事件类型由WindowPattern.WindowOpenedEvent字段提供。
Automation元素可以是特定元素或RootElement(如前所述)。
作用域由TreeScope枚举提供:它可以是元素本身(TreeScope.Element),也可以是指定元素的所有子树(TreeScope.Subtree)。我们在本例中使用后者,在此上下文中引用RootElement时需要使用后者。
该方法委托是标准事件处理程序委托:

AutomationElement TargetElement = AutomationElement.RootElement;
AutomationEventHandler WindowOpenedHandler = null;

Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, TargetElement,
    TreeScope.Subtree, WindowOpenedHandler = new AutomationEventHandler(OnTargetOpened));

public void OnTargetOpened(object source, AutomationEventArgs e)
{
    AutomationElement element = source as AutomationElement;
}

检测应用程序何时关闭

同上,只是eventIdWindowPattern.WindowClosedEvent字段提供。

注意:

激活时应缓存和访问某些元素和属性 A预定义CacheRequest:并非所有UIA值都可以访问 使用Element.Current对象;中需要缓存元素 某些情况下。
我特意跳过此功能以将其保留为 尽可能简单(简短)。
没有任何元素、图案和 无论如何,此处讨论的属性值都严格需要缓存。

检测属性值何时更改

属性更改使用AutomationPropertyChangedEventHandler通知,这需要:

  • 要与事件处理程序关联的Automation元素。
  • 事件的作用域;在本例中,作用域是元素本身(TreeScope.Element):我们只想跟踪它的一个属性,不涉及后代。
  • 将处理事件的AutomationPropertyChangedEventHandler委托(标准委托)
  • 我们感兴趣的一个或多个UI自动化属性。
可以使用RootElement(主窗口)FindFirst()方法确定Automation元素:我们需要指定搜索到的元素是子代元素(TreeScope.Descendants)以及用于匹配该元素的条件。

单据列出了本类所有预定义的Automation Identifiers

AutomationPropertyChangedEventHandler TargetTitleBarHandler = null;

Condition titleBarCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TitleBar);

TitleBarElement = RootElement.FindFirst(TreeScope.Descendants, titleBarCondition);

Automation.AddAutomationPropertyChangedEventHandler(TitleBarElement, TreeScope.Element,
    TargetTitleBarHandler = new AutomationPropertyChangedEventHandler(OnTargetTitleBarChange),
    AutomationElement.NameProperty);

public void OnTargetTitleBarChange(object source, AutomationPropertyChangedEventArgs e)
{
    if (e.Property == AutomationElement.NameProperty) { }
}

另见:UI Automation Control Types


测试代码示例

我正在使用Windows记事本作为要跟踪的目标应用程序。它可以是任何其他应用程序。
另外,我使用应用程序类名称来标识它。它可能是任何其他已知的细节,可以挑出它。

此代码需要项目引用:

UIAutomationClient
UIAutomationTypes

using System.Windows.Automation;

AutomationEventHandler NotepadHandlerOpen = null;
AutomationEventHandler NotepadHandlerClose = null;
AutomationPropertyChangedEventHandler NotepadTitleBarHandler = null;
AutomationElement NotepadElement = AutomationElement.RootElement;
AutomationElement TitleBarElement = null;

//-----------------------------------------------------------------------------------
// This section of code can be inserted in the app start, Form/Window constructor
// or the event handler of a controls (a Button.Cick maybe)
//-----------------------------------------------------------------------------------

using (Process NotepadProc = Process.GetProcessesByName("notepad").FirstOrDefault())
{
    try
    {
        Automation.AddAutomationEventHandler(WindowPattern.WindowOpenedEvent, NotepadElement,
            TreeScope.Subtree, NotepadHandlerOpen = new AutomationEventHandler(OnNotepadStart));
    }
    finally
    {
        if (NotepadProc != null)
            this.BeginInvoke(NotepadHandlerOpen, 
                AutomationElement.FromHandle(NotepadProc.MainWindowHandle), 
                new AutomationEventArgs(WindowPattern.WindowOpenedEvent));
    }
}

//-----------------------------------------------------------------------------------

public void OnNotepadStart(object source, AutomationEventArgs e)
{
    AutomationElement element = source as AutomationElement;
    if (e.EventId == WindowPattern.WindowOpenedEvent && element.Current.ClassName.Contains("Notepad"))
    {
        NotepadElement = element;
        Console.WriteLine("Notepad is now opened");
        Condition titleBarCondition = new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.TitleBar);
        TitleBarElement = NotepadElement.FindFirst(TreeScope.Descendants, titleBarCondition);

        Automation.AddAutomationEventHandler(WindowPattern.WindowClosedEvent, NotepadElement,
            TreeScope.Element, NotepadHandlerClose = new AutomationEventHandler(OnNotepadClose));

        Automation.AddAutomationPropertyChangedEventHandler(TitleBarElement, TreeScope.Element,
            NotepadTitleBarHandler = new AutomationPropertyChangedEventHandler(OnNotepadTitleBarChange),
            AutomationElement.NameProperty);
    }
}

public void OnNotepadClose(object source, AutomationEventArgs e)
{
    if (e.EventId == WindowPattern.WindowClosedEvent)
    {
        Console.WriteLine("Notepad is now closed");
        Automation.RemoveAutomationEventHandler(WindowPattern.WindowClosedEvent, NotepadElement, NotepadHandlerClose);
        Automation.RemoveAutomationPropertyChangedEventHandler(TitleBarElement, NotepadTitleBarHandler);
    }
}

public void OnNotepadTitleBarChange(object source, AutomationPropertyChangedEventArgs e)
{
    if (e.Property == AutomationElement.NameProperty)
    {
        Console.WriteLine($"New TitleBar value: {e.NewValue}");
    }
}

当应用程序(或FormWindow)关闭时,删除仍处于活动状态的自动化事件处理程序:

Automation.RemoveAllEventHandlers();

这篇关于C#使用Event_Object_NameChange从SetWinEventHook中排除工具提示弹出窗口的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆