1

I'm trying to detect window title changes of AIMP music player using the SetWinEventHook and it works, the problem is that it also detects Tooltip popups when I hover over buttons with mouse (stop, play, minimize, etc).

I would like to exclude these when setting the SetWinEventHook or filter it out in the WinEventProc event.
Any ideas?

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);
        }
    }
}

outputs:

Current song: Michael Jackson - Speed Demon
Current song: Minimize
11
  • Try filtering the WinEventProc idObject using, for example, OBJID_TITLEBAR = 0xFFFFFFFE. I don't really know where these changes occur, the TitleBar is just a guess. At this time, you're only excluding SWEH_CHILDID_SELF = 0. For other possible values see here: Move window when external application's window moves, you can find all the other object IDs listed in an enumerator. Commented Jan 1, 2019 at 1:56
  • Actually, I'm excluding everything BUT the SWEH_CHILDID_SELF. The idObject is 0 for both the window title change and the tooltip popup. Commented Jan 1, 2019 at 2:58
  • Yes, sure. That excluding was meant to be filtering. You have to filter SWEH_CHILDID_SELF, because you can receive more than one idObject in sequence. I was suggesting to exclude all that is not what you're expecting. I have no means to test this now. If you (or others) don't come up with something, I'll give it a look. Commented Jan 1, 2019 at 3:09
  • Another (unsollicited?) suggestion is to use UI Automation instead of Hooks. Those tools would provide much more precise results in this case. Commented Jan 1, 2019 at 5:38
  • I'll look into the UIA later tonight. Thanks Commented Jan 1, 2019 at 16:48

1 Answer 1

1

Question objective changed, as discussed in the comments, from a solution using Hooks (SetWinEventHook) to a UI Automation one.


Since you've never used UI Automation before, this could be a rodeo, so I'll try to explain the process of adding Automation Event handlers for some type of events that can be useful for this task.

The task at hand:

Your program needs to be notified when the status of a property of an UI Element (in this case, a TitleBar value) of an Application changes.

First of all, you probably want to know whether the target application is already running when your program starts.
We can use Process.GetProcessesByName() to determine if an application process is active.

  • The target application Main Window needs to be associated with a AutomationElement (the Automation object used to identify an UI object - in other words, an element in the UI Automation tree).

Note:

We cannot associate the target Main Window with a specific Automation Element when setting up an event handler that detects an Application main Window creation.
We could, with the AutomationElement.FromHandle([Handle]) method, using the Handle returned by Process.MainWindowHandle. But this Automation Element will be strictly tied to a specific Process instance, thus a specific Process.Id. If the target Application is closed and reopened, its Process.Id would be different and the Event Handler will not recognize it.

  • We need to associate the Event Handler that detects a Window creation with the AutomationElement.RootElement, representing the root element of the current Desktop (any UI element, or Window, in practice), then determine if it's the Main Windows of the target Application, inspecting some relevant property of the Automation Element provided by the Event as the source object (as any standard event). In the sample code, I'm using the Element.Current.ClassName.
  • Since the target application can be closed at some point, we need to be notified when this happen, too.
    Our program might need to make some decisions based on the status of the target application.
    Or simply notify the User and/or update its own UI.
  • The target application can be opened and closed over and over during the life-time of the program. We will need to track these changes over time.
  • When a property value is changed, we can receive a notification using a AutomationPropertyChangedEventHandler. This event is raised when a specific property of a defined Automation Element or Element Type changes (see the event type descriptions that follow).

UI Automation provides Event Handlers and Patterns that can be used to track all the described events.

Detect when application starts:

We need to set an AutomationEventHandler delegate, using Automation.AddAutomationEventHandler, that raises an event when a Window is created.

The AddAutomationEventHandler requires:

  • The type of Automation Event that will be handled
  • The Automation Element that is associated with the event
  • The scope of Event. The scope can be limited to the Automation Element specified or extended to all its ancestors and descendants elements.
  • The method delegate that will be called when the event is raised

The Event type is provided by the WindowPattern.WindowOpenedEvent field.
The Automation Element can be a specific Element or the RootElement (previously described).
The Scope is provided by the TreeScope enumeration: it can be the Element itself (TreeScope.Element) or all the subtree of the specified Element (TreeScope.Subtree). We're using the latter in this case, it's required when referencing the RootElement in this context.
The method delegate is a standard event handler delegate:

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;
}

Detect when application closes:

Same as above, except the eventId is provided by a WindowPattern.WindowClosedEvent field instead.

Note:

Some Elements and Properties should be cached and accessed activating a pre-defined CacheRequest: not all UIA values can be accessed using the Element.Current object; a cached Element is required in some cases.
I'm deliberately skipping this feature to keep this as simple (and short) as possible.
None of the Elements, Patterns and Property values discussed here strictly need caching, anyway.

Detect when a property value changes:

A property change is notified using a AutomationPropertyChangedEventHandler, which requires:

  • The Automation Element with which we want to associate the event handler.
  • A Scope for the event; in this case, the scope is the Element itself (TreeScope.Element): we only want to track one of its properties, no descendants are involved.
  • An AutomationPropertyChangedEventHandler delegate that will handle the event (standard delegate)
  • One or more UI Automation properties we're interested in.

The Automation Element can be determined using the RootElement (Main Window) FindFirst() method: we need to specify that the searched Element is a descendant (TreeScope.Descendants) and the criteria used to match the Element.

The Docs list all the pre-defined Automation Identifiers for this class.

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) { }
}

See also: UI Automation Control Types.


Sample Test Code:

I'm using Windows Notepad as the target Application to track. It can be any other application.
Also, I'm using the Application Class Name to identify it. It could be any other know detail that can single it out.

This code requires a Project reference to:

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

When the application (or the Form or Window) closes, remove the Automation Event Handlers still active:

Automation.RemoveAllEventHandlers();
Sign up to request clarification or add additional context in comments.

16 Comments

Amazing material. It would take me lifetimes to figure this out. Thanks an infinity! So far I got it working with Notepad++ and Firefox, but with AIMP I get an error at Automation.AddAutomationPropertyChangedEventHandler(TitleBarElement,... saying Value cannot be null. Parameter name: element. I don't really understand why. If I make a breakpoint, the NotepadElement at TitleBarElement = NotepadElement... contains all the info, just like with Notepad++.
If I set ControlType.Window and NotepadElement.FindFirst(TreeScope.Element then there's no error and it at least detects AIMP starting and closing but not the title change (surprisingly, still works with Notepad++ though, both start/close and title change). I bet I'm missing something simple. I'll do more testing tomorrow.
Well, I don't know that app. There is a chance that it doesn't have a real TitleBar. Some apps, to improve their look, use some tricks. A typical one is to use DwmExtendFrameIntoClientArea, to draw customized Windows parts. Use the Inspect utility previously mentioned to see what control type that is. Possibly, not a ControlType.TitleBar. If you can't figure out that control type (if it's a control), maybe tell me what it is and I'll see to patch the code to support that thing.
Inspect.exe says it's UIA_WindowControlTypeId (0xC370) which is ControlType.Window but that's same with Notepad++. Am I missing something?
But the NotepadElement does contain the title value when I make a breakpoint so it should work?
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.