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.
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:
if (isWinVistaOrAbove && ShowPlusMinus && Indent > 19 && ImageList != null)
{
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; int newX = pt.X + si.nPos; int level = info.Node.Level;
if (ShowRootLines)
{
if (newX >= 3 + level * Indent)
{
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
.