C#使用EVENT_OBJECT_NAMECHANGE从SetWinEventHook中排除工具提示弹出窗口

时间:2019-01-01 01:22:12

标签: c#

我正在尝试使用SetWinEventHook检测AIMP音乐播放器的窗口标题更改,并且它可以工作,但是问题是,当我将鼠标悬停在按钮上方时(停止,播放,最小化等),它还检测到工具提示弹出窗口。在设置SetWinEventHook或在WinEventProc事件中将其过滤掉时,我想排除这些。有什么想法吗?

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ConsoleApp2
{

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

    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

1 个答案:

答案 0 :(得分:0)

如注释中所述,

问题目标已从使用Hooks(SetWinEventHook)的解决方案更改为UI Automation。


由于您以前从未使用过UI Automation,所以这可能是一场牛仔竞技表演,因此,我将尝试解释为某些可能对这项任务有用的事件添加Automation Event处理程序的过程。 / p>

手头的任务

  

当以下属性的状态时,需要通知您的程序   应用程序的UI元素(在这种情况下为TitleBar值)   变化。

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

  • 目标应用程序主窗口需要与AutomationElement(用于标识UI对象的自动化对象,即 element in the UI Automation tree )关联。

注意

  

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

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

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

在应用程序启动时检测

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

AddAutomationEventHandler 要求:

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

事件类型由WindowPattern.WindowOpenedEvent字段提供。
自动化元素可以是特定元素,也可以是 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字段提供。

注意

  

某些元素和属性应被缓存并激活   预定义的CacheRequest:并非所有UIA值都可以访问   使用Element.Current对象;在以下位置需要缓存的元素   有些情况。
  我特意跳过此功能,以保持其为   简单(简短)
  没有元素,模式和   无论如何,这里讨论的属性值严格地需要缓存。

在属性值更改时检测

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

  • 我们要与事件处理程序关联的自动化元素。
  • 活动范围;在这种情况下,范围是元素本身( TreeScope.Element ):我们只想跟踪其属性之一,不涉及任何后代。
  • 将处理事件的AutomationPropertyChangedEventHandler委托(标准委托)
  • 我们感兴趣的一个或多个UI自动化属性。

可以使用RootElement(主窗口)FindFirst()方法来确定自动化元素:我们需要指定搜索到的元素是后代(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();