1

我正在尝试使用 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
4

1 回答 1

1

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


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

手头的任务

当应用程序的 UI 元素的属性(在本例中为值)的状态发生变化时,需要通知您的程序。TitleBar

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

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

注意

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

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

UI 自动化提供事件处理程序模式,可用于跟踪所有描述的事件。

检测应用程序何时启动

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

AddAutomationEventHandler要求 :

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

事件类型由WindowPattern.WindowOpenedEvent字段提供。
自动化元素可以是特定元素或RootElement(前面描述的)。
Scope 由TreeScope枚举提供:它可以是 Element 本身(TreeScope.Element)或指定 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对象访问;在某些情况下需要缓存元素。
我故意跳过此功能以使其尽可能简单(和简短)
无论如何,这里讨论的元素、模式和属性值都不需要缓存。

检测属性值何时更改

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

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

自动化元素可以使用RootElement(主窗口)FindFirst()方法确定:我们需要指定搜索到的元素是后代 ( TreeScope.Descendants) 以及用于匹配该元素的条件。

文档列出了此类 的所有预定义自动化标识符。

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 自动化控件类型


示例测试代码

我使用 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}");
    }
}

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

Automation.RemoveAllEventHandlers();
于 2019-01-02T10:14:04.557 回答