Setting up Hook on Windows messages

Here’s a different approach: skip the SetWindowsHook API, and instead use WinEvents, which use SetWinEventHook instead. These are somewhat similar to the windows hooks, in that both involve a callback function that is called at specific events, but WinEvents are far easier to use from C#: you can specific that WinEvents are delivered “out context”, meaning the events are posted back to your own process, so you don’t need a separate DLL. (Your code does need to run a message loop on the same thread that called SetWinEventHook, however.)

It turns out that one of the type of events that WinEvent supports is a ‘name change’ event, which is automatically fired by USER32 whenever the title text of a HWND changes, which seems is what you are looking for. (WinEvents can also be used to track focus changes and various types of state changes; see MSDN for more information.) It’s also fired by other controls when their internal UI changes – eg by a listbox when the text of a list item changes, so we have to do some filtering.

Here’s some sample code that prints out title changes on any HWND on the desktop – you’ll see it print out a notification as the text in the clock on the taskbar changes, for example. You’ll want to modify this code to filter for just the HWND you’re tracking in Spotify. Also, this code listens to name changes on all processes/threads; you should get the threadID from the target HWND using GetWindowThreadProcessId and only listen to events from that thread.

Note also that this is something of a fragile approach; if Spotify changes how it displays the text, or changes the format of it, you’ll need to modify your code to keep up with its changes.

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

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, 0, 0, WINEVENT_OUTOFCONTEXT);

        // MessageBox provides the necessary mesage loop that SetWinEventHook requires.
        // In real-world code, use a regular message loop (GetMessage/TranslateMessage/
        // DispatchMessage etc or equivalent.)
        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;
        }
        Console.WriteLine("Text of hwnd changed {0:x8}", hwnd.ToInt32()); 
    }
}

Leave a Comment