Subclassing controls in .NETCF 2.0, part 2

Last time, we introduced the WndProcHooker class to facilitate hooking windows messages for managed controls at the native WndProc level. This time, we will use that WndProcHooker class to extend the .NETCF supplied TreeView control and add the NodeMouseClicked event.

caveat: You will need to have a post-beta2 build of the .NET Compact Framework for this sample to compile and work properly. The TreeNode class in the beta2 build did not have the Handle property used by this sample. Any .NETCF build after 2.00.5133.00 will suffice.

First off, here is the extended TreeView Class:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace SubclassSample
{
/// <summary>
/// Extends the standard TreeView control to add an implementation
/// of the NodeMouseClick event.
/// </summary>
public partial class TreeViewBonus : TreeView
{
/// <summary>
/// The original parent of this control.
/// </summary>
Control prevParent = null;

/// <summary>
/// Creates a new instance of the derived TreeView control
/// </summary>
public TreeViewBonus()
{
InitializeComponent();
}

/// <summary>
/// Called when the control's parent is changed. Here we hook into that
/// parent's WndProc and spy on the WM_NOTIFY message. When the parent
/// changes, we unhook the old parent's WndProc and hook into the new one.
/// </summary>
/// <param name="e">The arguments for this event</param>
protected override void OnParentChanged(EventArgs e)
{
// unhook the old parent
if (this.prevParent != null)
{
WndProcHooker.UnhookWndProc(prevParent, Win32.WM_NOTIFY);
}
// update the previous parent
prevParent = this.Parent;

// hook up the new parent
if (this.Parent != null)
{
WndProcHooker.HookWndProc(this.Parent,
new WndProcHooker.WndProcCallback(this.WM_Notify_Handler),
Win32.WM_NOTIFY);
}

base.OnParentChanged(e);
}

/// <summary>
/// Occurs when the user clicks a TreeNode with the mouse.
/// </summary>
public event TreeNodeMouseClickEventHandler NodeMouseClick;

/// <summary>
/// Occurs when the mouse pointer is over the control and a mouse button is clicked.
/// </summary>
/// <param name="e">Provides data for the NodeMouseClick event.</param>
protected void OnNodeMouseClick(TreeNodeMouseClickEventArgs e)
{
if (NodeMouseClick != null)
NodeMouseClick(this, e);
}

/// <summary>
/// The method that gets called when a WM_NOTIFY message is received by the
/// TreeView's parent.
/// </summary>
/// <param name="hwnd">The handle of the window that received the message</param>
/// <param name="msg">The message received</param>
/// <param name="wParam">The wParam arguments for the message</param>
/// <param name="lParam">The lParam arguments for the message</param>
/// <param name="handled">Set to true to indicate that this message was handled</param>
/// <returns>An appropriate returen code for the message handled (see MSDN)</returns>
int WM_Notify_Handler(
IntPtr hwnd, uint msg, uint wParam, int lParam,
ref bool handled)
{
Win32.NMHDR nmHdr = new Win32.NMHDR();
System.Runtime.InteropServices.Marshal.PtrToStructure((IntPtr)lParam, nmHdr);
switch (nmHdr.code)
{
case Win32.NM_RCLICK:
case Win32.NM_CLICK:
// get the cursor coordinates on the client
Point msgPos = Win32.LParamToPoint((int)Win32.GetMessagePos());
msgPos = this.PointToClient(msgPos);

// check to see if the click was on an item
Win32.TVHITTESTINFO hti = new Win32.TVHITTESTINFO();
hti.pt.X = msgPos.X;
hti.pt.Y = msgPos.Y;
int hitem = Win32.SendMessage(this.Handle, Win32.TVM_HITTEST, 0, ref hti);
uint htMask = (
Win32.TVHT_ONITEMICON |
Win32.TVHT_ONITEMLABEL |
Win32.TVHT_ONITEMINDENT |
Win32.TVHT_ONITEMBUTTON |
Win32.TVHT_ONITEMRIGHT |
Win32.TVHT_ONITEMSTATEICON);
if ((hti.flags & htMask) != 0)
{
bool leftButton = (nmHdr.code == Win32.NM_CLICK);
RaiseNodeMouseClickEvent(hti.hItem,
leftButton ? MouseButtons.Left : MouseButtons.Right,
msgPos);
return 0;
}
break;

default:
break;
}
return 0;
}

/// <summary>
/// Raises the TreeNodeMouseClick event for the TreeNode with the specified handle.
/// </summary>
/// <param name="hNode">The handle of the node for which the event is raised</param>
/// <param name="button">The [mouse] buttons that were pressed to raise the event</param>
/// <param name="coords">The [client] cursor coordinates at the time of the event</param>
void RaiseNodeMouseClickEvent(IntPtr hNode, MouseButtons button, Point coords)
{
TreeNode tn = FindTreeNodeFromHandle(this.Nodes, hNode);

TreeNodeMouseClickEventArgs e = new TreeNodeMouseClickEventArgs(
tn,
button,
1, coords.X, coords.Y);

OnNodeMouseClick(e);
}

/// <summary>
/// Finds a TreeNode in the provided TreeNodeCollection that has the handle specified.
/// Warning: recursion!
/// </summary>
/// <param name="tnc">The TreeNodeCollection to search</param>
/// <param name="handle">The handle of the TreeNode to find in the collection</param>
/// <returns>The TreeNode if found; null otherwise</returns>
TreeNode FindTreeNodeFromHandle(TreeNodeCollection tnc, IntPtr handle)
{
foreach (TreeNode tn in tnc)
{
if (tn.Handle == handle) return tn;
// we couldn't have clicked on a child of this node if this node
// is not expanded!
if (tn.IsExpanded)
{
TreeNode tn2 = FindTreeNodeFromHandle(tn.Nodes, handle);
if (tn2 != null) return tn2;
}
}
return null;
}
}
}

Note, we need to add the TreeNodeMouseClickEventHandler and TreeNodeMouseClickEventArgs classes under System.Windows.Forms.

namespace System.Windows.Forms
{
/// <summary>
/// Represents the method that will handle the NodeMouseClick event of a TreeView
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">A TreeNodeMouseClickEventArgs that contains the event data.</param>
public delegate void TreeNodeMouseClickEventHandler(object sender, TreeNodeMouseClickEventArgs e);

/// <summary>
/// Provides data for the System.Windows.Forms.TreeView.NodeMouseClick event
/// </summary>
public class TreeNodeMouseClickEventArgs : MouseEventArgs
{
/// <summary>
/// Initializes a new instance of the TreeNodeMouseClickEventArgs class.
/// </summary>
/// <param name="node">The node that was clicked</param>
/// <param name="button">One of the System.Windows.Forms.MouseButtons members</param>
/// <param name="clicks">The number of clicks that occurred</param>
/// <param name="x">The x-coordinate where the click occurred</param>
/// <param name="y">The y-coordinate where the click occurred</param>
public TreeNodeMouseClickEventArgs(TreeNode node, MouseButtons button, int clicks, int x, int y) :
base(button, clicks, x, y, 0)
{
nodeValue = node;
}

/// <summary>
/// Gets the node that was clicked.
/// </summary>
public TreeNode Node
{
get { return nodeValue; }
set { nodeValue = value; }
}
TreeNode nodeValue;

public override string ToString()
{
return string.Format(
"TreeNodeMouseClickEventArgs\r\n\tNode: {0}\r\n\tButton: {1}\r\n\tX: {2}\r\n\tY: {3}",
nodeValue.Text, Button.ToString(), X, Y);
}
}
}

Lastly, here are the Win32 constants, structures and such that need to be defined. Add these to the Win32 class defined yesterday.

    public const uint WM_NOTIFY = 0x4E;

    public const uint NM_CLICK = 0xFFFFFFFE;
public const uint NM_RCLICK = 0xFFFFFFFB;

public const uint TV_FIRST = 0x1100;
public const uint TVM_HITTEST = TV_FIRST + 17;

    public const uint TVHT_NOWHERE = 0x0001;
public const uint TVHT_ONITEMICON = 0x0002;
public const uint TVHT_ONITEMLABEL = 0x0004;
public const uint TVHT_ONITEM = (TVHT_ONITEMICON | TVHT_ONITEMLABEL | TVHT_ONITEMSTATEICON);
public const uint TVHT_ONITEMINDENT = 0x0008;
public const uint TVHT_ONITEMBUTTON = 0x0010;
public const uint TVHT_ONITEMRIGHT = 0x0020;
public const uint TVHT_ONITEMSTATEICON = 0x0040;
public const uint TVHT_ABOVE = 0x0100;
public const uint TVHT_BELOW = 0x0200;
public const uint TVHT_TORIGHT = 0x0400;
public const uint TVHT_TOLEFT = 0x0800;

    [System.Runtime.InteropServices.StructLayout(LayoutKind.Sequential)]
public class NMHDR
{
public IntPtr hwndFrom;
public uint idFrom;
public uint code;
}

    public struct TVHITTESTINFO
{
public POINT pt;
public uint flags;
public IntPtr hItem;
}

    /// <summary>
/// Helper function to convert a Windows lParam into a Point
/// </summary>
/// <param name="lParam">The parameter to convert</param>
/// <returns>A Point where X is the low 16 bits and Y is the
/// high 16 bits of the value passed in</returns>
public static Point LParamToPoint(int lParam)
{
uint ulParam = (uint)lParam;
return new Point(
(int)(ulParam & 0x0000ffff),
(int)((ulParam & 0xffff0000) >> 16));
}

#if DESKTOP
[DllImport("user32.dll")]
#else
[DllImport("coredll.dll")]
#endif
public extern static uint GetMessagePos();

#if DESKTOP
[DllImport("user32.dll")]
#else
[DllImport("coredll.dll")]
#endif
public extern static int SendMessage(
IntPtr hwnd, uint msg, uint wParam, ref TVHITTESTINFO lParam);
 

The original incarnation of this sample was to extend the Button class and give the button a GradientFill for its background. As it turned out, that sample was far too complex for what I was trying to demonstrate. Since the native button class appears to render its state in a double-buffered fashion, I couldn't just hook the WM_ERASEBKGND message, draw the background and let the button do the rest. No, I needed to hook WM_PAINT, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MOUSEMOVE, WM_KEYDOWN and WM_KEYUP and render the button as appropriate for the message and the button's state. As it turns out, it is a lot easier to just extend System.Windows.Forms.Control and make your fancy buttons from there.

The other interesting thing that came out of this sample is this note on performance. In the original implementation of WndProcHooker.WindowProc from yesterday, I wasn't making use of the Contains() method on the hwndDict object. Instead, I was just calling the indexer directly and catching and eating the KeyNotFoundException. The performance of the sample was so bad I was considering scraping it altogether until my friend Chris pointed out just how many messages were going through that WindowProc method. Transitioning between native and managed code for every message is expensive, but throwing and catching (and ignoring!) an exception for 99% of them is outrageous.

That's it for now.

Tim

This posting is provided "AS IS" with no warranties, and confers no rights.