1

How can I handle any of the tree view notifications listed here in a C# class that is derived from the .NET TreeView control?

I tried to handle the click notification, for example, like this:

class ExtendedTreeView : TreeView
{
    private const Int32 NM_FIRST = (Int32)(0U - 0U);
    private const Int32 NM_CLICK = unchecked((Int32)((UInt32)NM_FIRST - 2U));

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == NM_CLICK)
        {
            MessageBox.Show("NM_CLICK");
        }
        base.WndProc(ref m);
    }
}

But the message box is never shown. This is the first time I try to use Win32 API to modify the behaviour of a .NET control, so I have no idea what goes wrong.

Is this the correct approach to handle these notifications?

FYI: I know that the .NET TreeView control has a click event. This is just a first test. Later I want to enable the TVS_EX_MULTISELECT style. Since the .NET TreeView control does not fire any AfterSelect events when TVS_EX_MULTISELECT is enabled, I want to investigate the behaviour of the TVN_SELCHANGED and TVN_ITEMCHANGED notifications later.

Robert Hegner
  • 9,014
  • 7
  • 62
  • 98

3 Answers3

6

It is not that simple. Check the MSDN article, the NM_CLICK notification is delivered as a WM_NOTIFY message. And it is sent to the parent of the treeview. Winforms has plumbing in place to echo it back to the original control to allow the message to be handled by a class derived from TreeView and customize the event handling. That's done by adding 0x2000 to the message, the value of WM_REFLECT in the Winforms source code.

So the code should look like this:

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

class ExtendedTreeView : TreeView {
    protected override void WndProc(ref Message m) {
        if (m.Msg == WM_REFLECT + WM_NOFITY) {
            var notify = (NMHDR)Marshal.PtrToStructure(m.LParam, typeof(NMHDR));
            if (notify.code == NM_CLICK) {
                MessageBox.Show("yada");
                m.Result = (IntPtr)1;
                return;
            }

        }
        base.WndProc(ref m);
    }
    private const int NM_FIRST = 0;
    private const int NM_CLICK = NM_FIRST - 2;
    private const int WM_REFLECT = 0x2000;
    private const int WM_NOFITY = 0x004e;

    [StructLayout(LayoutKind.Sequential)]
    private struct NMHDR {
        public IntPtr hwndFrom;
        public IntPtr idFrom;
        public int code;
    }
}

Beware that TreeView already does all this, that's how the NodeMouseClick, Click and MouseClick events get generated. The code that does this also works around some quirks in the native control so be sure you really need this before committing to use it. Review the Reference Source if you want to know what's going on.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • @peenut - yes, and the parent sends it back. Check the use of WM_REFLECT in the snippet. It is a pure Winforms implementation detail but otherwise common in class libraries that wrap the api. – Hans Passant May 16 '12 at 12:42
  • FYI the trace of `0x2000` in another question http://stackoverflow.com/questions/10637133/wm-reflect-notify-vs-wm-notify/10637914#10637914 – Roman R. May 17 '12 at 14:53
2

Notifications are sent to the control's parent:

Notifies the parent window of a tree-view control that the user has clicked the left mouse button within the control.

This is done with the WM_NOITIFY message. Luckily, the authors also included a mechanism called reflection to allow sub classes of the treeview to receive the notifications too. The message is &H2000 | WM_NOTIFY which you can treat exactly as WM_NOTIFY.

Also note that NM_CLICK is not a message, but a notification wrapped inside an NMHDR structure

This notification code is sent in the form of a WM_NOTIFY message.

Deanna
  • 23,876
  • 7
  • 71
  • 156
  • faster, but without nice working code/example. anyway, it's win32, we can't be sure unless we run it, right? ;-) – peenut May 16 '12 at 12:36
0

There are 2 important things, which are mentioned in MSDN: 1) msg.lparam is pointer to NMHDR structure 2) notifications are send to parent control

So the working code is (compile as console app - it will print messages there):

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

class MyTreeView : TreeView
{
    public TreeView RealTreeView;
    public MyTreeView()
    {
        RealTreeView = new TreeView();
        RealTreeView.Dock = DockStyle.Fill;
        Controls.Add(RealTreeView);
    }
    enum WM
    {
        NOTIFY = 78
    }
    enum NM : uint
    {
        FIRST = 0,
        NM_CLICK = unchecked(FIRST - 2),
        NM_CUSTOMDRAW = unchecked(FIRST - 12),
        NM_DBLCLK = unchecked(FIRST - 3),
        NM_KILLFOCUS = unchecked(FIRST - 8),
        NM_RCLICK = unchecked(FIRST - 5),
        NM_RDBLCLK = unchecked(FIRST - 6),
        NM_RETURN = unchecked(FIRST - 4),
        NM_SETCURSOR = unchecked(FIRST - 17),
        NM_SETFOCUS = unchecked(FIRST - 7)
    }

    [StructLayout(LayoutKind.Sequential)]
    struct NMHDR {
        public IntPtr hwndFrom;
        public UIntPtr idFrom;
        public uint code;
    }

    protected override void WndProc(ref Message m)
    {
        base.WndProc(ref m);
        if (m.Msg == (int)WM.NOTIFY)
        {
            uint code;
            unsafe
            {
                var nmhdr = (NMHDR*)m.LParam.ToPointer();
                code = nmhdr->code;
            }
            NM nmCode = (NM)code;
            Console.WriteLine("WM_NOTIFY " + nmCode);
        }
    }
}

public class MyGuiClass
{
    public static void Main()
    {
        Form f = new Form();
        var tv = new MyTreeView();
        tv.RealTreeView.Nodes.Add("zero").Nodes.Add("sub-zero");
        tv.RealTreeView.Nodes.Add("one");
        tv.RealTreeView.Nodes.Add("two");
        tv.RealTreeView.Nodes.Add("three");
        tv.Dock = DockStyle.Fill;
        f.Controls.Add(tv);
        Application.Run(f);
    }
}

Edit: and don't forget to compile with /unsafe of course.

peenut
  • 3,366
  • 23
  • 24