Setting Up Hook on Windows Messages

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

Hook window messages

If you want to hook window messages for a window that you create, you can set the WindowProc property of the control that wraps the window. If that is possible then it is the right way to go.

Your call to SetWindowHookEx fails because you are not passing a hook procedure. You are passing an instance method. Your hook procedure must be a non-member function declared like this:

function HookProc(nCode: Integer; wParam: WPARAM;
lParam: LPARAM): LRESULT; stdcall;

You must also adhere to the documented instructions. Remember the HHOOK instance that you are given by SetWindowHookEx and make sure you call CallNextHookEx to respect the hook chain.

How to hook for particular windows message without subclassing?

AFAIK there's no better solution than what you mentioned. And, of course, subclassing the window is better than hooking all the messages of the thread.

Let's think which path the message passes up until it's handled by the window:

  1. The message is either posted or sent to the window, either by explicit call to PostMessage/SendMessage or implicitly by the OS.
  2. Posted messages only: eventually the thread pops this message from the message queue (by calling GetMessage or similar), and then calls DispatchMessage.
  3. The OS invokes the window's procedure by calling CallWindowProc (or similar).
  4. The CallWindowProc identifies the window procedore associated with the window (via GetClassLong/GetWindowLong)
  5. The above procedure is called.

Subclassing - means replacing the window procedure for the target window. This seems to be the best variant.
Installing hook with WH_GETMESSAGE flag will monitor all the messages posted to the message queue. This is bad because of the following:

  1. Performance reasons.
  2. You'll get notified only for windows created in the specific thread
  3. You'll get notified only for posted messages (sent messages will not be seen)
  4. A "posted" message doesn't necessarily means "delivered". That is, it may be filtered by the message loop (thrown away without calling DispatchMessage).
  5. You can't see what the actual window does and returns for that message.

So that subclassing seems much better.

One more solution - in case your specific message is posted (rather than sent) you may override the message loop, and for every retrieved message you may do some pre/post-processing

How do I intercept messages being sent to a window?

You need to inject your own code into the process that owns the windows you wish to intercept messages from. Fortunately, SetWindowsHookEx() makes this fairly easy, although you may have a bit of trouble at first if you've only used it for in-process hooking up to now.

I can recommend two excellent articles on the subject:

  1. Joseph Newcomber's Hooks and DLLs
  2. Robert Kuster's Three Ways to Inject Your Code into Another Process

Is there a windows message that I can hook for when an application starts?

If polling is not a problem you could use one of the approaches described in the answers to a related question:

C# Process Monitor

The suggested solutions use WMI or Windows audit process tracking mechanism.

Capture all Windows Messages

You can use SetWindowsHookEx to set low level hooks to catch (specific) windows messages.
Specifically these hook-ids might be interesting for monitoring:

WH_CALLWNDPROC (4) Installs a hook procedure that monitors messages
before the system sends them to the destination window procedure. For
more information, see the CallWndProc hook procedure.

WH_CALLWNDPROCRET(12) Installs a hook procedure that monitors
messages after they have been processed by the destination window
procedure. For more information, see the CallWndRetProc hook
procedure.

It's been a while since I've implemented it, but as an example I've posted the base class I use to hook specific messages. (For example, I've used it in a global mousewheel trapper, that makes sure my winforms apps behave the same as internet explorer: scroll the control underneath the cursor, instead of the active control).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using Subro.Win32;

namespace Subro
{
/// <summary>
/// Base class to relatively safely register global windows hooks
/// </summary>
public abstract class GlobalHookTrapper : FinalizerBase
{
[DllImport("user32", EntryPoint = "SetWindowsHookExA")]
static extern IntPtr SetWindowsHookEx(int idHook, Delegate lpfn, IntPtr hmod, IntPtr dwThreadId);

[DllImport("user32", EntryPoint = "UnhookWindowsHookEx")]
private static extern int UnhookWindowsHookEx(IntPtr hHook);

[DllImport("user32", EntryPoint = "CallNextHookEx")]
static extern int CallNextHook(IntPtr hHook, int ncode, IntPtr wParam, IntPtr lParam);

[DllImport("kernel32.dll")]
static extern IntPtr GetCurrentThreadId();

IntPtr hook;
public readonly int HookId;
public readonly GlobalHookTypes HookType;

public GlobalHookTrapper(GlobalHookTypes Type):this(Type,false)
{
}

public GlobalHookTrapper(GlobalHookTypes Type, bool OnThread)
{
this.HookType = Type;
this.HookId = (int)Type;
del = ProcessMessage;
if (OnThread)
hook = SetWindowsHookEx(HookId, del, IntPtr.Zero, GetCurrentThreadId());
else
{
var hmod = IntPtr.Zero; // Marshal.GetHINSTANCE(GetType().Module);
hook = SetWindowsHookEx(HookId, del, hmod, IntPtr.Zero);
}

if (hook == IntPtr.Zero)
{
int err = Marshal.GetLastWin32Error();
if (err != 0)
OnHookFailed(err);
}
}

protected virtual void OnHookFailed(int Error)
{
throw Win32Functions.TranslateError(Error);
}

private const int HC_ACTION = 0;

[MarshalAs(UnmanagedType.FunctionPtr)]
private MessageDelegate del;

private delegate int MessageDelegate(int code, IntPtr wparam, IntPtr lparam);

private int ProcessMessage(int hookcode, IntPtr wparam, IntPtr lparam)
{
if (HC_ACTION == hookcode)
{
try
{
if (Handle(wparam, lparam)) return 1;
}
catch { }
}
return CallNextHook(hook, hookcode, wparam, lparam);
}

protected abstract bool Handle(IntPtr wparam, IntPtr lparam);

protected override sealed void OnDispose()
{
UnhookWindowsHookEx(hook);
AfterDispose();
}

protected virtual void AfterDispose()
{
}

}

public enum GlobalHookTypes
{
BeforeWindow = 4, //WH_CALLWNDPROC
AfterWindow = 12, //WH_CALLWNDPROCRET
KeyBoard = 2, //WH_KEYBOARD
KeyBoard_Global = 13, //WH_KEYBOARD_LL
Mouse = 7, //WH_MOUSE
Mouse_Global = 14, //WH_MOUSE_LL
JournalRecord = 0, //WH_JOURNALRECORD
JournalPlayback = 1, //WH_JOURNALPLAYBACK
ForeGroundIdle = 11, //WH_FOREGROUNDIDLE
SystemMessages = 6, //WH_SYSMSGFILTER
MessageQueue = 3, //WH_GETMESSAGE
ComputerBasedTraining = 5, //WH_CBT
Hardware = 8, //WH_HARDWARE
Debug = 9, //WH_DEBUG
Shell = 10, //WH_SHELL
}

public abstract class FinalizerBase : IDisposable
{
protected readonly AppDomain domain;
public FinalizerBase()
{
System.Windows.Forms.Application.ApplicationExit += new EventHandler(Application_ApplicationExit);
domain = AppDomain.CurrentDomain;
domain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
domain.DomainUnload += new EventHandler(domain_DomainUnload);
}

private bool disposed;
public bool IsDisposed{get{return disposed;}}
public void Dispose()
{
if (!disposed)
{
GC.SuppressFinalize(this);
if (domain != null)
{
domain.ProcessExit -= new EventHandler(CurrentDomain_ProcessExit);
domain.DomainUnload -= new EventHandler(domain_DomainUnload);
System.Windows.Forms.Application.ApplicationExit -= new EventHandler(Application_ApplicationExit);
}
disposed = true;
OnDispose();
}
}

void Application_ApplicationExit(object sender, EventArgs e)
{
Dispose();
}

void domain_DomainUnload(object sender, EventArgs e)
{
Dispose();
}

void CurrentDomain_ProcessExit(object sender, EventArgs e)
{
Dispose();
}

protected abstract void OnDispose();
/// Destructor
~FinalizerBase()
{
Dispose();
}
}

}

Why must SetWindowsHookEx be used with a windows message queue

The low-level hooks, WH_KEYBOARD_LL and WH_MOUSE_LL are different from all the other hooks. They don't require a DLL to be injected into the target process. Instead, Windows calls your hook callback directly, inside your own process. To make that work, a message loop is required. There is no other mechanism for Windows to make callbacks on your main thread, the callback can only occur when you've called Get/PeekMessage() so that Windows is in control.

A global hook like WH_KEYBOARD is very different. It requires a DLL and the callback occurs within the process that processes the keyboard message. You need some kind of inter-process communication to let your own program be aware of this. Named pipes are the usual choice. Which otherwise of course requires that this injected process pumps a message loop. It wouldn't get keyboard messages otherwise.

Favor a low-level hook, they are much easier to get going. But do pump or it won't work. And beware of timeouts, if you're not responsive enough then Windows will kill your hook without notice.

Understanding the low-level mouse and keyboard hook (win32)



Related Topics



Leave a reply



Submit