Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Bugfix: TreeView indentation on Vista & Win7

0.00/5 (No votes)
28 Oct 2010 1  
Native TreeView is unusable with a non-default Indent setting.

Sample Image

Introduction

The native TreeView (COMCTL 6.1) deployed on all current Vista and Windows7 installations introduced a bug, which becomes apparent when using a non-default Indent style (> 19). A mismatch between the drawn and the calculated location of the expando/plusminus buttons obstructs a user to expand or collapse nodes. While the added implementation of Vista Visual Styles was probably the underlying reason for this bug, it affects non-themed operations as well. As far as I know, Windows OS and Microsoft products only use the default Indent, so it's a low priority issue and no announcements of an upcoming fix can be found. (Rant start -- MS introduced the TVITEMEX struct in COMCTL 4.71, with the iIntegral member allowing a node multiple item height. Until this day, it is unusable, as MS never cared to fix a bug in the scrolling routine. (details) -- Rant end). I actually often use a 25 Indent, as it enlarges the logical button area, thereby facilitating expand/collapse operation by the mouse. This article presents a workaround by translating Win32 mouse messages in a derived .NET TreeView. It can be easily adapted for other languages.

Background

Closer inspection reveals that the hit test values returned by the TVM_HITTEST message differ between <= WinXP (<=COMCTL 6.0) and >=Vista (COMCTL 6.1) systems. WinXP treats only the first three pixels of a node as TVHT_ONITEMINDENT; an increased indentation results in a larger TVHT_ONITEMBUTTON area. Vista enlarges the TVHT_ONITEMINDENT area and keeps the TVHT_ONITEMBUTTON area constant. With Vista drawing the button correctly at the left, but expecting it directly before the checkbox or image, mouse operations including fancy hover state indication are flawed. Without an image list assigned, Vista will shift buttons to the right, placing them logically and graphically in the middle of the indented area --- Trustworthy Computing.

Sample Image

The fix

To my surprise, trapping the TVM_HITTEST message and correcting the returned value had no effect. Instead, horizontal mouse location on WM_MOUSEMOVE, WM_LBUTTONDOWN, WM_LBUTTONUP messages is set to the logical button center, while cursor resides in the indented area of a node.

protected override void WndProc(ref Message m)
{
    switch (m.Msg)
    {
        case WM_MOUSEMOVE:
        case WM_LBUTTONDOWN:
        case WM_LBUTTONUP:
        //case WM_MOUSEHOVER:
            if (isWinVistaOrAbove && ShowPlusMinus && Indent > 19 && ImageList != null)
            {
                // lParam: cursor in client coords
                Point pt = new Point((int) m.LParam);

                if (translateMouseLocationCore(ref pt))
                {
                    m.LParam = MAKELPARAM(pt.X, pt.Y);
                }
            }
            break;
    }
    base.WndProc(ref m);
}

Calculation of the button's logical x position depends on the Indent and the current TreeNode.Level properties, plus the horizontal scrolling position. A slight variation is needed when root nodes have no button (ShowRootLines == false).

private bool translateMouseLocationCore(ref Point pt)
{
    TreeViewHitTestInfo info = HitTest(pt);
    return translateMouseLocationCore(info, ref pt);
}

private bool translateMouseLocationCore(TreeViewHitTestInfo info, ref Point pt)
{
    Debug.Assert((isWinVistaOrAbove && ShowPlusMinus && 
                  Indent > 19 && ImageList != null));
            
    if (info.Node == null || info.Node.Nodes.Count == 0)
    {
        return false;
    }

    if ((info.Location & TreeViewHitTestLocations.Indent) != 0)
    {
        SCROLLINFO si = new SCROLLINFO { cbSize = Marshal.SizeOf(typeof(SCROLLINFO)), 
                                         fMask = SIF_POS };
        SafeNativeMethods.GetScrollInfo(Handle, SB_HORZ, ref si);

        const int btnWidth2 = 8;            // half of button width
        int newX = pt.X + si.nPos;          // mouse in dc coords
        int level = info.Node.Level;

        if (ShowRootLines)
        {
            if (newX >= 3 + level * Indent)
            {
                // center of button in dc coords, that Vista expects
                newX = 3 + (level + 1) * Indent - btnWidth2;
                pt.X = newX - si.nPos;
                return true;
            }
        }
        else if (level > 0)
        {
            if (newX >= 3 + (level - 1) * Indent)
            {
                newX = 3 + level * Indent - btnWidth2;
                pt.X = newX - si.nPos;
                return true;
            }
        }
    }

    return false;
}

Using the code

Obviously, the above translation of cursor position can have side effects in the client code, although it is unusual to use the indented area, and the context menu operates as usual. However, client code can be easily provided with real mouse location, if OnMouseMove, OnMouseDown/Up (only the left button) are overridden.

protected override void OnMouseMove(MouseEventArgs e)
{
    Point pt = PointToClient(Cursor.Position);
    if (pt != e.Location)
    {
        e = new MouseEventArgs(e.Button, e.Clicks, pt.X, pt.Y, e.Delta);
    }
    base.OnMouseMove(e);
}

If Microsoft finally decides to fix the bug, we will have to alter the conditional logic, when to apply the presented workaround. Currently, it just checks the major OS version.

private static bool isWinVistaOrAbove
{
    get
    {
        OperatingSystem OS = Environment.OSVersion;
        return (OS.Platform == PlatformID.Win32NT) && (OS.Version.Major >= 6);
    }
}

History

  • November, 2010: Article posted.
  • November, 20??: MS fixes Indent and breaks ItemHeight.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here