/// /// Copyright © 2003-2008 JetBrains s.r.o. /// You may distribute under the terms of the GNU General Public License, as published by the Free Software Foundation, version 2 (see License.txt in the repository root folder). /// using System; using System.Collections; using System.Drawing; using System.Runtime.InteropServices; using System.Windows.Forms; using JetBrains.Interop.WinApi; using JetBrains.UI.Interop; namespace JetBrains.UI.Components.CustomTreeView { public enum NodeCheckState { None = 0, Unchecked = 1, Checked = 2, Grayed = 3 } /// /// A custom drawn TreeView control which allows using different node presentations /// public class CustomTreeView : System.Windows.Forms.TreeView { /// /// Node painter to use /// private INodePainter myNodePainter; /// /// Mouse coordinates /// private Point myMouseCoords; /// /// ImageList with images for three-state checkboxes. /// private ImageList myCheckImageList; /// /// Whether three-state checkboxes are enabled. /// private bool myThreeStateCheckboxes = false; /// /// Whether double-buffering is used for drawing the tree. /// private bool myDoubleBuffer = false; /// /// The bitmap used for double-buffered drawing. /// private Bitmap myDoubleBufferBitmap; /// /// Enable or disable multi-selection /// private bool myMultiSelect = false; private bool myDraggingOver = false; private bool mySelectionChanged = false; private Point myLastMouseUpPoint = new Point( -1, -1 ); private DateTime myLastMouseUpTime; public event EventHandler MultiSelectChanged; protected override void Dispose( bool disposing ) { if ( disposing ) { if ( myDoubleBufferBitmap != null ) { myDoubleBufferBitmap.Dispose(); myDoubleBufferBitmap = null; } if ( myCheckImageList != null ) { myCheckImageList.Dispose(); myCheckImageList = null; } } base.Dispose( disposing ); } /// /// Gets or sets node painter /// public INodePainter NodePainter { get { return myNodePainter; } set { myNodePainter = value; } } /// /// Enables or disables the 3-state checkboxes in the tree. /// public bool ThreeStateCheckboxes { get { return myThreeStateCheckboxes; } set { myThreeStateCheckboxes = value; if ( myThreeStateCheckboxes ) { if ( myCheckImageList == null ) { CreateCheckImageList(); Win32Declarations.SendMessage( Handle, (int) TreeViewMessage.TVM_SETIMAGELIST, new IntPtr( (int) TreeViewImageList.TVSIL_STATE ), myCheckImageList.Handle ); } } } } /// /// Enables or disables double-buffering when drawing the tree view. /// public bool DoubleBuffer { get { return myDoubleBuffer; } set { if ( myDoubleBuffer != value ) { myDoubleBuffer = value; if ( myDoubleBuffer ) { CreateDoubleBufferBitmap(); } else if ( myDoubleBufferBitmap != null ) { myDoubleBufferBitmap.Dispose(); myDoubleBufferBitmap = null; } } } } public event ThreeStateCheckEventHandler AfterThreeStateCheck; public void SetNodeCheckState( TreeNode node, NodeCheckState checkState ) { if ( node.TreeView != this ) { return; } TVITEM item = new TVITEM(); item.mask = TreeViewItemFlags.STATE | TreeViewItemFlags.HANDLE; item.stateMask = 0xF000; item.state = (int) checkState << 12; item.hItem = node.Handle; Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_SETITEMA, 0, ref item ); } public NodeCheckState GetNodeCheckState( TreeNode node ) { TVITEM item = new TVITEM(); item.mask = TreeViewItemFlags.STATE | TreeViewItemFlags.HANDLE; item.stateMask = 0xFFFF; item.hItem = node.Handle; Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_GETITEMA, 0, ref item ); return (NodeCheckState) (item.state >> 12); } protected override void WndProc(ref Message m) { switch (m.Msg) { case Win32Declarations.OCM_NOTIFY: OnWmNotify(ref m); break; case Win32Declarations.WM_ERASEBKGND: if( !myDoubleBuffer ) base.WndProc( ref m ); break; case Win32Declarations.WM_PAINT: if ( !myDoubleBuffer ) base.WndProc( ref m ); else PaintWithDoubleBuffer( ref m ); break; default: base.WndProc(ref m); break; } } /// /// Handles the WM_NOTIFY message of the parent control /// /// The message to handle /// Whether the message was handled private void OnWmNotify( ref Message m ) { // Marshal lParam into NMHDR: NMHDR hdr = new NMHDR(); hdr = (NMHDR)Marshal.PtrToStructure(m.LParam, typeof(NMHDR)); switch (hdr.code) { case Win32Declarations.NM_CUSTOMDRAW: NMTVCUSTOMDRAW customDraw = new NMTVCUSTOMDRAW(); customDraw = (NMTVCUSTOMDRAW)Marshal.PtrToStructure(m.LParam, typeof(NMTVCUSTOMDRAW)); OnCustomDraw(ref customDraw, ref m); break; default: base.WndProc( ref m ); break; } } protected override void OnSizeChanged(EventArgs e) { base.OnSizeChanged (e); if ( myDoubleBuffer ) { CreateDoubleBufferBitmap(); } } private void CreateDoubleBufferBitmap() { if ( myDoubleBufferBitmap != null ) { myDoubleBufferBitmap.Dispose(); } if ( ClientSize.Width <= 0 || ClientSize.Height <= 0 ) { myDoubleBufferBitmap = new Bitmap( 1, 1 ); } else { myDoubleBufferBitmap = new Bitmap( ClientSize.Width, ClientSize.Height ); } } private void PaintWithDoubleBuffer( ref Message m ) { using( Graphics g = Graphics.FromImage( myDoubleBufferBitmap ) ) { // TODO: use correct background brush for non-standard background g.FillRectangle( SystemBrushes.Window, 0, 0, ClientSize.Width, ClientSize.Height ); IntPtr hdc = g.GetHdc(); // the treeview ignores the drawing options anyway Win32Declarations.SendMessage( Handle, Win32Declarations.WM_PRINTCLIENT, hdc, IntPtr.Zero ); g.ReleaseHdc( hdc ); } PAINTSTRUCT ps = new PAINTSTRUCT(); Win32Declarations.BeginPaint( m.HWnd, ref ps ); using( Graphics g = Graphics.FromHdc( ps.hdc ) ) { g.DrawImage( myDoubleBufferBitmap, 0, 0 ); } Win32Declarations.EndPaint( m.HWnd, ref ps ); } #region Drawing logic /// /// Erases node /// private void EraseNode( ref NMTVCUSTOMDRAW customDraw ) { try { //TreeNode node = TreeNode.FromHandle(this, (IntPtr)customDraw.nmcd.dwItemSpec); using (Graphics g = Graphics.FromHdc(customDraw.nmcd.hdc)) { //int offset = CalculateOffset(node); Rectangle rect = new Rectangle(customDraw.nmcd.rc.left/* + offset*/, customDraw.nmcd.rc.top, customDraw.nmcd.rc.right - customDraw.nmcd.rc.left/* - offset*/, customDraw.nmcd.rc.bottom - customDraw.nmcd.rc.top); g.FillRectangle(new SolidBrush(BackColor), rect); } } catch (Exception ex) { System.Diagnostics.Trace.WriteLine("CustomTreeView.EraseNode failed : " + ex, "UI"); } } /// /// Draws node /// private void DrawNode( ref NMTVCUSTOMDRAW customDraw ) { try { TreeNode node = TreeNode.FromHandle(this, (IntPtr)customDraw.nmcd.dwItemSpec); if (myNodePainter != null && myNodePainter.IsHandled(node)) { Rectangle rect = new Rectangle(customDraw.nmcd.rc.left, customDraw.nmcd.rc.top, customDraw.nmcd.rc.right - customDraw.nmcd.rc.left, customDraw.nmcd.rc.bottom - customDraw.nmcd.rc.top); if (MultiSelect) { SetNodeSelectedState(node, (node == SelectedNode || mySelectedNodes.Contains(node) )); } myNodePainter.Draw(node, customDraw.nmcd.hdc, rect); } } catch (Exception ex) { System.Diagnostics.Trace.WriteLine("CustomTreeView.DrawNode failed : " + ex, "UI"); } } #endregion internal bool NeedFocusRect() { return ShowFocusCues; } /// /// Handles the NM_CUSTOMDRAW notification /// private void OnCustomDraw( ref NMTVCUSTOMDRAW customDraw, ref Message m ) { switch (customDraw.nmcd.dwDrawStage) { case Win32Declarations.CDDS_PREPAINT: m.Result = (IntPtr)(Win32Declarations.CDRF_NOTIFYITEMDRAW | Win32Declarations.CDRF_NOTIFYPOSTPAINT); break; case Win32Declarations.CDDS_ITEMPREPAINT: TreeNode node = TreeNode.FromHandle(this, (IntPtr)customDraw.nmcd.dwItemSpec); if (myNodePainter != null && node != null && myNodePainter.IsHandled(node)) { //DrawNode( ref customDraw ); m.Result = (IntPtr)(Win32Declarations.CDRF_NOTIFYITEMDRAW | Win32Declarations.CDRF_NOTIFYPOSTPAINT /*| Win32Declarations.CDRF_SKIPDEFAULT */); } else m.Result = (IntPtr)Win32Declarations.CDRF_NOTIFYITEMDRAW; break; case Win32Declarations.CDDS_ITEMPOSTPAINT: DrawNode(ref customDraw); break; case Win32Declarations.CDDS_POSTERASE: EraseNode(ref customDraw); break; default: m.Result = (IntPtr)Win32Declarations.CDRF_DODEFAULT; break; } } protected override void OnItemDrag( ItemDragEventArgs e ) { base.OnItemDrag( e ); if ( MultiSelect ) { Invalidate(); } } protected override void OnDragEnter( DragEventArgs drgevent ) { myDraggingOver = true; base.OnDragEnter( drgevent ); if ( MultiSelect ) { Refresh(); } } protected override void OnDragLeave( EventArgs e ) { base.OnDragLeave( e ); myDraggingOver = false; if ( MultiSelect ) { Invalidate(); } } protected override void OnDragDrop( DragEventArgs drgevent ) { base.OnDragDrop( drgevent ); myDraggingOver = false; if ( MultiSelect ) { Invalidate(); } } protected override void OnEnter( EventArgs e ) { base.OnEnter( e ); if ( MultiSelect && SelectedNodes != null && SelectedNodes.Length > 1 ) { Invalidate(); } } protected override void OnLeave( EventArgs e ) { base.OnLeave( e ); if ( MultiSelect && SelectedNodes != null && SelectedNodes.Length > 1 ) { Invalidate(); } } internal bool DraggingOver { get { return myDraggingOver; } set { myDraggingOver = value; } } #region Click tracking protected override void OnMouseDown(MouseEventArgs e) { myMouseCoords = new Point(e.X, e.Y); myLastMouseButton = e.Button; mySelectionChanged = false; base.OnMouseDown (e); } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp( e ); if ( myThreeStateCheckboxes ) { bool doubleClick = false; if ( myLastMouseUpPoint.X != -1 ) { if ( Math.Abs( e.X - myLastMouseUpPoint.X ) <= SystemInformation.DoubleClickSize.Width && Math.Abs( e.Y - myLastMouseUpPoint.Y ) <= SystemInformation.DoubleClickSize.Height ) { TimeSpan ts = DateTime.Now - myLastMouseUpTime; if ( ts.TotalMilliseconds <= SystemInformation.DoubleClickTime ) { doubleClick = true; } } } myLastMouseUpPoint = new Point( e.X, e.Y ); myLastMouseUpTime = DateTime.Now; if ( doubleClick ) { return; } TVHITTESTINFO hti = new TVHITTESTINFO(); hti.pt = new POINT( e.X, e.Y ); Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_HITTEST, 0, ref hti ); if ( ( hti.flags & TreeViewHitTestFlags.ONITEMSTATEICON ) != 0 ) { TreeNode node = TreeNode.FromHandle( this, hti.hItem ); NodeCheckState state = GetNodeCheckState( node ); if ( state == NodeCheckState.Checked || state == NodeCheckState.Grayed ) { state = NodeCheckState.Unchecked; } else { state = NodeCheckState.Checked; } ChangeNodeCheckState( node, state ); } } } /** * Changes the checked state of a node and fires the AfterThreeStateCheck event. */ private void ChangeNodeCheckState( TreeNode node, NodeCheckState state ) { SetNodeCheckState( node, state ); if ( AfterThreeStateCheck != null ) { AfterThreeStateCheck( this, new ThreeStateCheckEventArgs( node, state ) ); } } /** * Handles selection changes when the user clicks a part of the node which is only * included in the area drawn by the custom painter. */ protected override void OnClick(EventArgs e) { try { if ( myNodePainter != null && !mySelectionChanged ) { TreeNode pntNode = myNodePainter.GetNodeAt(this, myMouseCoords); if (pntNode != null ) { if (MultiSelect) SelectedNode = null; SelectedNode = pntNode; } } } catch (Exception ex) { System.Diagnostics.Trace.WriteLine("CustomTreeView.OnClick failed : " + ex, "UI"); } base.OnClick (e); } #endregion #region 3-state checkboxes support private void CreateCheckImageList() { myCheckImageList = new ImageList(); Bitmap bmp = new Bitmap( 16, 16 ); using( Graphics g = Graphics.FromImage( bmp ) ) { using( Brush br = new SolidBrush( SystemColors.Window ) ) { g.FillRectangle( br, 0, 0, 16, 16 ); } } myCheckImageList.Images.Add( bmp ); AddCheckIcon( ButtonState.Normal ); AddCheckIcon( ButtonState.Checked ); AddCheckIcon( ButtonState.Checked | ButtonState.Inactive ); } private void AddCheckIcon( ButtonState bs ) { Bitmap bmp = new Bitmap( 16, 16 ); using( Graphics g = Graphics.FromImage( bmp ) ) { ControlPaint.DrawCheckBox( g, 0, 0, 16, 16, bs ); } myCheckImageList.Images.Add( bmp ); } protected override void OnKeyDown( KeyEventArgs e ) { base.OnKeyDown( e ); if ( myThreeStateCheckboxes && e.KeyCode == Keys.Space ) { TreeNode node = SelectedNode; if ( node != null && GetNodeCheckState( node ) != NodeCheckState.None) { if ( GetNodeCheckState( node ) == NodeCheckState.Checked ) { ChangeNodeCheckState( node, NodeCheckState.Unchecked ); } else { ChangeNodeCheckState( node, NodeCheckState.Checked ); } } } } #endregion #region Multi-Selection support #region Selected Nodes Collection private class SelectedNodesCollection { private CustomTreeView myTreeView; private ArrayList myNodes = new ArrayList(); public SelectedNodesCollection(CustomTreeView treeView) { myTreeView = treeView; } public void Add (TreeNode node) { myNodes.Add(node); myTreeView.SetNodeSelectedState(node, true); myTreeView.InvalidateNode (node); } public void AddRange (ICollection range) { foreach (TreeNode node in range) { if ( node != null ) { myNodes.Add( node ); myTreeView.SetNodeSelectedState(node, true); myTreeView.InvalidateNode (node); } } } public void Remove (TreeNode node) { myNodes.Remove (node); myTreeView.SetNodeSelectedState(node, false); myTreeView.InvalidateNode (node); } public void Clear() { foreach (TreeNode node in myNodes) { myTreeView.SetNodeSelectedState(node, false); myTreeView.InvalidateNode (node); } myNodes.Clear(); } public int Count { get {return myNodes.Count;} } public bool Contains (TreeNode node) { return myNodes.Contains(node); } public TreeNode[] Nodes { get { return (TreeNode[])myNodes.ToArray(typeof(TreeNode)); } } } #endregion /// /// Array of currently selected nodes /// private SelectedNodesCollection mySelectedNodes = null; private TreeNode myFirstMultiSelectNode, myLastMultiSelectNode; private MouseButtons myLastMouseButton; /// /// Enables or disable multiselection /// public bool MultiSelect { get { return myMultiSelect; } set { if ( myMultiSelect != value ) { myMultiSelect = value; if (myMultiSelect) mySelectedNodes = new SelectedNodesCollection(this); else mySelectedNodes = null; OnMultiSelectChanged(); } } } protected virtual void OnMultiSelectChanged() { if ( MultiSelectChanged != null ) { MultiSelectChanged( this, EventArgs.Empty ); } } public TreeNode[] SelectedNodes { get { if ( !MultiSelect ) { if ( SelectedNode == null ) return new TreeNode[] {}; return new TreeNode[] { SelectedNode }; } return mySelectedNodes.Nodes; } set { if ( value == null || value.Length == 0 ) { SelectedNode = null; } else { SelectedNode = value [0]; } if ( MultiSelect ) { mySelectedNodes.Clear(); for( int i=0; i 0) mySelectedNodes.Clear(); mySelectedNodes.Add(node); } } #endregion protected override void OnGotFocus( EventArgs e ) { try { base.OnGotFocus( e ); if ( SelectedNode != null && myNodePainter != null ) { myNodePainter.InvalidateNode( SelectedNode ); } } catch (Exception ex) { System.Diagnostics.Trace.WriteLine("CustomTreeView.EraseNode failed : " + ex, "UI"); } } public new event EventHandler LostFocus; protected override void OnLostFocus(EventArgs e) { if (LostFocus != null) LostFocus(this, e); base.OnLostFocus (e); } /** * Marks the node as drop-highlighted. */ public void SetDropHighlightNode( TreeNode node ) { Win32Declarations.SendMessage( Handle, (int) TreeViewMessage.TVM_SELECTITEM, (IntPtr) Win32Declarations.TVGN_DROPHILITE, (node != null) ? node.Handle : IntPtr.Zero ); } /** * Checks if a node is marked as drop-highlighted. */ public bool IsNodeDropHighlighted( TreeNode node ) { TVITEM item = new TVITEM(); item.mask = TreeViewItemFlags.HANDLE | TreeViewItemFlags.STATE; item.hItem = node.Handle; item.stateMask = 0xFFFF; Win32Declarations.SendMessage( Handle, TreeViewMessage.TVM_GETITEMA, 0, ref item ); return (item.state & (int) TreeViewItemState.DROPHILITED) != 0; } public TreeNode DropHighlightedNode { get { int result = Win32Declarations.SendMessage( Handle, (int) TreeViewMessage.TVM_GETNEXTITEM, (IntPtr) Win32Declarations.TVGN_DROPHILITE, IntPtr.Zero ); if ( result == 0 ) return null; return TreeNode.FromHandle( this, new IntPtr( result ) ); } } public bool IsParent(TreeNode parentNode, TreeNode childNode) { if (parentNode == childNode) return true; TreeNode n = childNode; bool bFound = false; while (!bFound && n != null) { n = n.Parent; bFound = (n == parentNode); } return bFound; } internal void InvalidateNode( TreeNode node ) { if ( node.TreeView == this && myNodePainter != null ) { myNodePainter.InvalidateNode( node ); } } } public class ThreeStateCheckEventArgs: EventArgs { private readonly TreeNode myNode; private readonly NodeCheckState myCheckState; public ThreeStateCheckEventArgs( TreeNode node, NodeCheckState checkState ) { myNode = node; myCheckState = checkState; } public TreeNode Node { get { return myNode; } } public NodeCheckState CheckState { get { return myCheckState; } } } public delegate void ThreeStateCheckEventHandler( object sender, ThreeStateCheckEventArgs e ); }