Sunday, October 09, 2005

Subclassing a Window in Managed Code

There are times when the default behaviour of Windows Forms isn’t enough and you may need to hook a window procedure for another control and handle or peek at its messages.

So how do we subclass a managed control’s window?

Exactly the same way we subclass an unmanaged window, the only difference being that it requires a bit of Interop to marshal the pointers.

I have illustrated this with some snippets from a simple example that subclasses its parent Control although this can be any available control.

To start we’ll need an Interop class to encapsulate the Win32 function calls and types.



public sealed class Win32
{
public const uint WM_NOTIFY = 0x4E;

public delegate int WindowProcDelegate(
IntPtr hwnd,
uint msg,
uint wParam,
int lParam);

public sealed class MarshalToDelegate
{
[DllImport("user32.dll")]
public extern static IntPtr SetWindowLong(
IntPtr hwnd,
int nIndex,
[MarshalAs(UnmanagedType.FunctionPtr)]
WindowProcDelegate dwNewLong);
}

[DllImport("user32.dll")]
public extern static IntPtr SetWindowLong(
IntPtr hwnd,
int nIndex,
IntPtr dwNewLong);

public const int GWL_WNDPROC = -4;

[DllImport("user32.dll")]
public extern static int CallWindowProc(
IntPtr lpPrevWndFunc,
IntPtr hwnd,
uint msg,
uint wParam,
int lParam);
}


You’ll notice there are two versions of the SetWindowLong function. One in a nested class called MarshalToDelegate. This allows us to use it in different contexts and provide a descriptive syntax.

We need to add some fields to our class and an instance of the delegate we declared in the Interop class.


Win32.WindowProcDelegate windowProcDelegate;
private static IntPtr _oldWndProc;
private bool hooked;


Now for our WindowProc. It has to be a static function.



private static int WindowProc(IntPtr hwnd, uint msg,
uint wParam, int lParam)
{
if(msg == Win32.WM_NOTIFY && 0 != lParam)
{
// handle your WM_NOTIFY messages in here
}
return Win32.CallWindowProc(_oldWndProc, hwnd, msg, wParam, lParam);
}




Now we add some code that allows us to hook our WindowProc.

    private void HookWndProc()
{
IntPtr hwndParent = this.Parent.Handle;

if (IntPtr.Zero == hwndParent)
throw new InvalidOperationException("Invalid HWND");

_oldWndProc = Win32.MarshalToDelegate.SetWindowLong(hwndParent,
Win32.GWL_WNDPROC, windowProcDelegate);
hooked = true;
}

private void UnHookWndProc()
{
if(!this.Parent.IsHandleCreated)
throw new InvalidOperationException("Invalid HWND");

IntPtr hwndParent = this.Parent.Handle;

if(IntPtr.Zero != _oldWndProc)
Win32.SetWindowLong(hwndParent, Win32.GWL_WNDPROC, _oldWndProc);
}




All that remains is to add the code to hook up our WindowProc at runtime and we can sneak a peek at all of the subclassed controls' WM_NOTIFY messages.

We initialise the delegate instance in the constructor then call HookWndProc() at a convenient point. I have done it in the override of OnParentChanged() as we can be sure our parent control is initialized at this point.



windowProcDelegate += new Win32.WindowProcDelegate(WindowProc);


protected override void OnParentChanged(EventArgs e)
{
if(!hooked )
HookWndProc();

base.OnParentChanged(e);
}




Unhooking can be problematic but as I take great pains to dispose of my classes explicitly then Dispose() is as good a place as any.


protected override void Dispose( bool disposing )
{
if( disposing )
{
UnHookWndProc();
if(components != null)
{
components.Dispose();
}
}
base.Dispose( disposing );
}




You’ll have to experiment with hooking to overrides or events called by your class to suit your own implementation.

This simple example only allows one way hooking at one point in program execution but you can easily be more sophisticated and provide for multiple controls and re-hooking etc. A similar approach can be applied to all of the windows callbacks.

No comments: