TreeView自定义DrawNode .NET 3.5 Windows窗体

时间:2012-02-03 23:33:48

标签: c# windows forms treeview

我想在TreeView中以OwnerDrawText模式自定义DrawNode。我发现即使使用这个处理程序它也很慢:

    void RegistryTreeDrawNode(object sender, DrawTreeNodeEventArgs e)
    {
        e.DrawDefault = true;
    }

我做错了吗?

感谢。

1 个答案:

答案 0 :(得分:11)

我认为您可能需要为您尝试执行的操作显示更多代码。不应该有任何明显不同的绘画,而不是所有者绘画;你基本上覆盖了默认的绘制,然后在你发布的内容中撤消它。这是丑陋的,没有任何收获......但不应该是一个性能。

因此,摆脱缺乏代码并追求自定义绘制树的核心愿望,让我告诉你,现在没有很多好的信息。

过去几天我一直在做我自己的自定义树视图工作,最终可能会编写一本关于我所学到的所有内容的教程。在此期间,请随时查看我的代码,看看它是否会帮助您。

我只是一个自定义绘制的资源管理器树视图。填充树视图的代码与TreeView绘图代码分开。如果你想运行我的代码,你可能需要添加自己的+/-图像。

实用程序\ IconReader.cs

using System;
using System.Runtime.InteropServices;

namespace TreeViewTestProject.Utilities
{
    public class IconReader
    {
        public enum IconSize
        {
            Large = 0,
            Small = 1
        };

        public enum FolderType
        {
            Open = 0,
            Closed = 1
        };

        /// <summary>
        /// Returns an icon for a given file - indicated by the name parameter.
        /// </summary>
        /// <param name="name">Pathname for file.</param>
        /// <param name="size">Large or small</param>
        /// <param name="linkOverlay">Whether to include the link icon</param>
        /// <returns>System.Drawing.Icon</returns>
        public static System.Drawing.Icon GetFileIcon(string name, IconSize size, bool linkOverlay)
        {
            Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
            uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;

            if(true == linkOverlay) flags |= Shell32.SHGFI_LINKOVERLAY;

            /* Check the size specified for return. */
            if(IconSize.Small == size)
            {
                flags |= Shell32.SHGFI_SMALLICON;
            }
            else
            {
                flags |= Shell32.SHGFI_LARGEICON;
            }

            Shell32.SHGetFileInfo(name,
                Shell32.FILE_ATTRIBUTE_NORMAL,
                ref shfi,
                (uint)System.Runtime.InteropServices.Marshal.SizeOf(shfi),
                flags);

            // Copy (clone) the returned icon to a new object, thus allowing us to clean-up properly
            System.Drawing.Icon icon = (System.Drawing.Icon)System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();
            User32.DestroyIcon(shfi.hIcon);     // Cleanup
            return icon;
        }

        /// <summary>
        /// Used to access system folder icons.
        /// </summary>
        /// <param name="size">Specify large or small icons.</param>
        /// <param name="folderType">Specify open or closed FolderType.</param>
        /// <returns>System.Drawing.Icon</returns>
        public static System.Drawing.Icon GetFolderIcon(string Foldername, IconSize size, FolderType folderType)
        {
            // Need to add size check, although errors generated at present!
            uint flags = Shell32.SHGFI_ICON | Shell32.SHGFI_USEFILEATTRIBUTES;

            if(FolderType.Open == folderType)
            {
                flags |= Shell32.SHGFI_OPENICON;
            }

            if(IconSize.Small == size)
            {
                flags |= Shell32.SHGFI_SMALLICON;
            }
            else
            {
                flags |= Shell32.SHGFI_LARGEICON;
            }

            // Get the folder icon
            Shell32.SHFILEINFO shfi = new Shell32.SHFILEINFO();
            Shell32.SHGetFileInfo(Foldername,
                Shell32.FILE_ATTRIBUTE_DIRECTORY,
                ref shfi,
                (uint)System.Runtime.InteropServices.Marshal.SizeOf(shfi),
                flags);

            System.Drawing.Icon.FromHandle(shfi.hIcon); // Load the icon from an HICON handle

            // Now clone the icon, so that it can be successfully stored in an ImageList
            System.Drawing.Icon icon = (System.Drawing.Icon)System.Drawing.Icon.FromHandle(shfi.hIcon).Clone();

            User32.DestroyIcon(shfi.hIcon);     // Cleanup
            return icon;
        }
    }

    public class Shell32
    {
        public const int    MAX_PATH = 256;
        [StructLayout(LayoutKind.Sequential)]
        public struct SHITEMID
        {
            public ushort cb;
            [MarshalAs(UnmanagedType.LPArray)]
            public byte[] abID;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct ITEMIDLIST
        {
            public SHITEMID mkid;
        }

        [StructLayout(LayoutKind.Sequential)]
        public struct BROWSEINFO
        {
            public IntPtr       hwndOwner;
            public IntPtr       pidlRoot;
            public IntPtr       pszDisplayName;
            [MarshalAs(UnmanagedType.LPTStr)]
            public string       lpszTitle;
            public uint         ulFlags;
            public IntPtr       lpfn;
            public int          lParam;
            public IntPtr       iImage;
        }

        // Browsing for directory.
        public const uint BIF_RETURNONLYFSDIRS   =  0x0001;
        public const uint BIF_DONTGOBELOWDOMAIN  =  0x0002;
        public const uint BIF_STATUSTEXT         =  0x0004;
        public const uint BIF_RETURNFSANCESTORS  =  0x0008;
        public const uint BIF_EDITBOX            =  0x0010;
        public const uint BIF_VALIDATE           =  0x0020;
        public const uint BIF_NEWDIALOGSTYLE     =  0x0040;
        public const uint BIF_USENEWUI           =  (BIF_NEWDIALOGSTYLE | BIF_EDITBOX);
        public const uint BIF_BROWSEINCLUDEURLS  =  0x0080;
        public const uint BIF_BROWSEFORCOMPUTER  =  0x1000;
        public const uint BIF_BROWSEFORPRINTER   =  0x2000;
        public const uint BIF_BROWSEINCLUDEFILES =  0x4000;
        public const uint BIF_SHAREABLE          =  0x8000;

        [StructLayout(LayoutKind.Sequential)]
        public struct SHFILEINFO
        {
            public const int NAMESIZE = 80;
            public IntPtr   hIcon;
            public int      iIcon;
            public uint dwAttributes;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = MAX_PATH)]
            public string szDisplayName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = NAMESIZE)]
            public string szTypeName;
        };

        public const uint SHGFI_ICON                = 0x000000100;     // get icon
        public const uint SHGFI_DISPLAYNAME         = 0x000000200;     // get display name
        public const uint SHGFI_TYPENAME            = 0x000000400;     // get type name
        public const uint SHGFI_ATTRIBUTES          = 0x000000800;     // get attributes
        public const uint SHGFI_ICONLOCATION        = 0x000001000;     // get icon location
        public const uint SHGFI_EXETYPE             = 0x000002000;     // return exe type
        public const uint SHGFI_SYSICONINDEX        = 0x000004000;     // get system icon index
        public const uint SHGFI_LINKOVERLAY         = 0x000008000;     // put a link overlay on icon
        public const uint SHGFI_SELECTED            = 0x000010000;     // show icon in selected state
        public const uint SHGFI_ATTR_SPECIFIED      = 0x000020000;     // get only specified attributes
        public const uint SHGFI_LARGEICON           = 0x000000000;     // get large icon
        public const uint SHGFI_SMALLICON           = 0x000000001;     // get small icon
        public const uint SHGFI_OPENICON            = 0x000000002;     // get open icon
        public const uint SHGFI_SHELLICONSIZE       = 0x000000004;     // get shell size icon
        public const uint SHGFI_PIDL                = 0x000000008;     // pszPath is a pidl
        public const uint SHGFI_USEFILEATTRIBUTES   = 0x000000010;     // use passed dwFileAttribute
        public const uint SHGFI_ADDOVERLAYS         = 0x000000020;     // apply the appropriate overlays
        public const uint SHGFI_OVERLAYINDEX        = 0x000000040;     // Get the index of the overlay

        public const uint FILE_ATTRIBUTE_DIRECTORY  = 0x00000010;
        public const uint FILE_ATTRIBUTE_NORMAL     = 0x00000080;

        [DllImport("Shell32.dll")]
        public static extern IntPtr SHGetFileInfo(
            string pszPath,
            uint dwFileAttributes,
            ref SHFILEINFO psfi,
            uint cbFileInfo,
            uint uFlags
            );
    }

    public class User32
    {
        /// <summary>
        /// Provides access to function required to delete handle. This method is used internally
        /// and is not required to be called separately.
        /// </summary>
        /// <param name="hIcon">Pointer to icon handle.</param>
        /// <returns>N/A</returns>
        [DllImport("User32.dll")]
        public static extern int DestroyIcon(IntPtr hIcon);
    }
}

ExplorerTreeView.cs:

using System;
using System.IO;
using System.Windows.Forms;
using TreeViewTestProject.Utilities;
using System.Collections;

namespace TreeViewTestProject
{
    public partial class ExplorerTreeView : TreeViewEx
    {
                                                                                                    #region ExplorerNodeSorter Class
    private class ExplorerNodeSorter : IComparer
    {
        public int Compare(object x, object y)
        {
            TreeNode nx = x as TreeNode;
            TreeNode ny = y as TreeNode;
            bool nxDir = (nx.ImageKey == kDirectoryImageKey);
            bool nyDir = (ny.ImageKey == kDirectoryImageKey);

            if(nxDir && !nyDir)
            {
                return -1;
            }
            else if(nyDir && !nxDir)
            {
                return 1;
            }
            else
            {
                return string.Compare(nx.Text, ny.Text);
            }
        }
    }
    #endregion

        private const string kDirectoryImageKey = "directory";
        private const string kReplacementText   = "C43C65D1-D40F-46F0-BC5E-57265322DDFC";

        public ExplorerTreeView()
        {
            InitializeComponent();

            this.BeforeExpand       += new TreeViewCancelEventHandler(ExplorerTreeView_BeforeExpand);
            this.ImageList          = m_FileIcons;
            this.TreeViewNodeSorter = new ExplorerNodeSorter();
            this.LabelEdit          = true;

            // Create the root of the tree and populate it
            PopulateTreeView(@"C:\");
        }

        private void PopulateTreeView(string DirectoryName)
        {
            this.BeginUpdate();

            string rootDir      = DirectoryName;
            TreeNode rootNode   = CreateTreeNode(rootDir);
            rootNode.Text       = rootDir;

            this.Nodes.Add(rootNode);
            PopulateDirectory(rootNode);

            this.EndUpdate();
        }

        private bool PathIsDirectory(string FullPath)
        {
            FileAttributes attr = File.GetAttributes(FullPath);
            return ((attr & FileAttributes.Directory) == FileAttributes.Directory);
        }

        private TreeNode CreateTreeNode(string FullPath)
        {
            string key  = FullPath.ToLower();
            string name = "";
            object tag  = null;

            if(PathIsDirectory(key))
            {
                DirectoryInfo info = new DirectoryInfo(FullPath);
                key     = kDirectoryImageKey;
                name    = info.Name;
                tag     = info;
            }
            else
            {
                FileInfo info = new FileInfo(FullPath);
                name    = info.Name;
                tag     = info;
            }

            if(!m_FileIcons.Images.ContainsKey(key))
            {
                if(key == "directory")
                {
                    m_FileIcons.Images.Add(key, IconReader.GetFolderIcon(Environment.CurrentDirectory, IconReader.IconSize.Small, IconReader.FolderType.Open).ToBitmap());
                }
                else
                {
                    m_FileIcons.Images.Add(key, IconReader.GetFileIcon(FullPath, IconReader.IconSize.Small, false));
                }
            }

            TreeNode node           = new TreeNode(name);            
            node.Tag                = tag;
            node.ImageKey           = key;
            node.SelectedImageKey   = key;

            return node;
        }

        private void PopulateDirectory(TreeNode ParentNode)
        {
            DirectoryInfo parentInfo = ParentNode.Tag as DirectoryInfo;
            foreach(DirectoryInfo subDir in parentInfo.GetDirectories())
            {
                TreeNode child = CreateTreeNode(subDir.FullName);
                PopulateForExpansion(child);
                ParentNode.Nodes.Add(child);
            }

            foreach(FileInfo file in parentInfo.GetFiles())
            {
                ParentNode.Nodes.Add(CreateTreeNode(file.FullName));
            }
        }

        private void PopulateForExpansion(TreeNode ParentNode)
        {
            // We need the +/- to show up if this directory isn't empty... but only want to populate the node on demand
            DirectoryInfo parentInfo = ParentNode.Tag as DirectoryInfo;
            try
            {
                if((parentInfo.GetDirectories().Length > 0) || (parentInfo.GetFiles().Length > 0))
                {
                    ParentNode.Nodes.Add(kReplacementText);
                }
            }
            catch { }
        }

        private void ReplacePlaceholderDirectoryNode(TreeNode ParentNode)
        {
            if((ParentNode.Nodes.Count == 1) && (ParentNode.Nodes[0].Text == kReplacementText))
            {
                ParentNode.Nodes.Clear();
                PopulateDirectory(ParentNode);
            }
        }

        private void ExplorerTreeView_BeforeExpand(object sender, TreeViewCancelEventArgs e)
        {
            this.BeginUpdate();
            ReplacePlaceholderDirectoryNode(e.Node);
            this.EndUpdate();
        }
    }
}

TreeViewEx.cs:

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

namespace TreeViewTestProject
{
    public partial class TreeViewEx : TreeView
    {
        // Notes: TextRenderer uses GDI to render the text, whereas Graphics uses GDI+.  "TreeView" has existed for a long long time
        // and thus uses GDI under the covers.  For User Drawing TreeNode's, we need to make sure we use the TextRenderer version
        // of text rendering functions.

        #region Properties

        private DashStyle m_SelectionDashStyle = DashStyle.Dot;
        public DashStyle SelectionDashStyle
        {
            get { return m_SelectionDashStyle; }
            set { m_SelectionDashStyle = value; }
        }

        private DashStyle m_LineStyle = DashStyle.Solid;
        public DashStyle LineStyle
        {
            get { return m_LineStyle; }
            set { m_LineStyle = value; }
        }

        private bool m_ShowLines = true;
        public new bool ShowLines           // marked as 'new' to replace base functionality fixing ShowLines/FullRowSelect issues in base.
        {
            get { return m_ShowLines; }
            set { m_ShowLines = value; }
        }

        protected override CreateParams CreateParams
        {
            get
            {
                // Removes all the flickering of repainting node's
                // This is the only thing I found that works properly for doublebuffering a treeview.
                CreateParams cp = base.CreateParams;
                cp.ExStyle |= 0x02000000; // WS_CLIPCHILDREN
                return cp;
            }
        }

        #endregion

        [DllImport("user32.dll", ExactSpelling = false, CharSet = CharSet.Auto)]
        private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
        private static extern IntPtr SendMessage(HandleRef hWnd, uint Msg, IntPtr wParam, HandleRef lParam);

        private const int GWL_STYLE         = -16;
        private const int WS_VSCROLL        = 0x00200000;
        private const uint TV_FIRST         = 0x1100;
        private const uint TVM_EDITLABELA   = (TV_FIRST + 14);
        private const uint TVM_EDITLABELW   = (TV_FIRST + 65);

        private bool m_SelectionChanged = false;
        private bool m_DoubleClicked    = false;
        private bool m_HierarchyChanged = false;

        public TreeViewEx()
        {
            InitializeComponent();

            // ShowLines must be "false" for FullRowSelect to work - so we're overriding the variable to correct for that.
            base.ShowLines = false;
            this.FullRowSelect = true;

            this.ItemHeight = 21;
            this.DrawMode = TreeViewDrawMode.OwnerDrawAll;
            this.DrawNode += OnDrawNode;
        }

        private void OnDrawNode(object sender, DrawTreeNodeEventArgs e)
        {
            e.DrawDefault = false;

            if(e.Node.Bounds.IsEmpty) return;

            // Clear out the previous contents for the node.  If we don't do this, when you mousehover the font will get slightly more bold
            Rectangle bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - 1, e.Node.Bounds.Height - 1);
            e.Graphics.FillRectangle(SystemBrushes.Window, bounds);

            // Draw everything
            DrawNodeFocusedHighlight(e);
            DrawNodeLines(e);
            DrawPlusMinus(e);
            DrawNodeIcon(e);
            DrawNodeText(e);
        }

        private void DrawNodeFocusedHighlight(DrawTreeNodeEventArgs e)
        {
            if(SelectedNode != e.Node) return;

            int scrollWidth = 0;

            if(VScrollVisible())
            {
                scrollWidth = SystemInformation.VerticalScrollBarWidth;
            }

            Rectangle bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - scrollWidth, e.Node.Bounds.Height - 1);

            if(!e.Node.IsEditing)
            {
                e.Graphics.FillRectangle(SystemBrushes.Highlight, bounds);
            }

            using(Pen focusPen = new Pen(Color.Black))
            {
                focusPen.DashStyle = SelectionDashStyle;

                bounds = new Rectangle(0, e.Node.Bounds.Y, this.Width - scrollWidth - 5, e.Node.Bounds.Height - 2);
                e.Graphics.DrawRectangle(focusPen, bounds);
            }
        }

        private void DrawNodeText(DrawTreeNodeEventArgs e)
        {
            if(e.Node.Bounds.IsEmpty) return;
            if(e.Node.IsEditing) return;


            Rectangle bounds = e.Node.Bounds;
            using(Font font = e.Node.NodeFont)
            {
                bounds.Width = TextRenderer.MeasureText(e.Node.Text, font).Width;
                bounds.Y -= 1;
                bounds.X += 1;
                if(IsRootNode(e.Node))
                {
                    bounds = new Rectangle(this.Margin.Size.Width + Properties.Resources.minus.Width + 9, 0, bounds.Width, bounds.Height);
                }

                Color fontColor = SystemColors.InactiveCaptionText;
                if(this.Focused)
                {
                    fontColor = e.Node.IsSelected?SystemColors.HighlightText:this.ForeColor;
                }

                TextRenderer.DrawText(e.Graphics, e.Node.Text, font, bounds, fontColor);
            }
        }

        private bool IsRootNode(TreeNode Node)
        {
            return (Node.Level == 0 && Node.PrevNode == null);
        }

        private void DrawNodeLines(DrawTreeNodeEventArgs e)
        {
            DrawNodeLineVertical(e);
            DrawNodeLineHorizontal(e);
        }

        private void DrawNodeLineVertical(DrawTreeNodeEventArgs e)
        {
            if(IsRootNode(e.Node)) return;
            if(!ShowLines) return;

            Pen linePen       = new Pen(Color.Black);
            linePen.DashStyle = LineStyle;

            for(int x = 0; x < e.Node.Level; x++)
            {
                int xLoc = this.Indent + (x * this.Indent) + (Properties.Resources.minus.Width / 2);
                int height = e.Bounds.Height;

                if(ShouldDrawVerticalLineForLevel(e.Node, x))
                {
                    e.Graphics.DrawLine(linePen, xLoc, e.Bounds.Top, xLoc, e.Bounds.Top + height);
                }
            }

            // Draw the half line for the last node
            if(e.Node.Parent.LastNode == e.Node)
            {
                int halfLoc = (e.Node.Level * this.Indent) + (Properties.Resources.minus.Width / 2);
                e.Graphics.DrawLine(linePen, halfLoc, e.Bounds.Top, halfLoc, (e.Bounds.Top + e.Bounds.Height / 2) - 1);
            }
        }

        private bool ShouldDrawVerticalLineForLevel(TreeNode Current, int Level)
        {
            TreeNode node = Current;
            TreeNode c = Current;
            while(node.Level != Level)
            {
                c = node;
                node = node.Parent;
            }
            return !(node.LastNode == c);
        }

        private void DrawNodeLineHorizontal(DrawTreeNodeEventArgs e)
        {
            if(IsRootNode(e.Node)) return;
            if(!ShowLines) return;

            Pen linePen         = new Pen(Color.Black);
            int xLoc            = (e.Node.Level * this.Indent) + (Properties.Resources.minus.Width / 2);
            int midY            = (e.Bounds.Top + e.Bounds.Bottom) / 2 - 1;
            e.Graphics.DrawLine(linePen, xLoc, midY, xLoc + 7, midY);
        }

        private void DrawNodeIcon(DrawTreeNodeEventArgs e)
        {
            if(this.ImageList == null) return;

            int indent = (e.Node.Level * this.Indent) + this.Margin.Size.Width;
            int iconLeft = indent + this.Indent;

            int imgIndex = this.ImageList.Images.IndexOfKey(e.Node.ImageKey);

            if(!IsRootNode(e.Node))
            {
                if(imgIndex >= 0)
                {
                    Image img = this.ImageList.Images[imgIndex];

                    int y = (e.Bounds.Y + e.Bounds.Height / 2) - (img.Height / 2) - 1;
                    e.Graphics.DrawImage(img, new Rectangle(iconLeft, y, img.Width, img.Height), new Rectangle(0, 0, img.Width, img.Height), GraphicsUnit.Pixel);
                }
            }
        }

        private void DrawPlusMinus(DrawTreeNodeEventArgs e)
        {
            if(e.Node.Nodes.Count == 0) return;

            int indent = (e.Node.Level * this.Indent) + this.Margin.Size.Width;
            int iconLeft = indent + this.Indent;

            Image img = Properties.Resources.plus;
            if(e.Node.IsExpanded) img = Properties.Resources.minus;

            e.Graphics.DrawImage(img, iconLeft - img.Width - 2, (e.Bounds.Y + e.Bounds.Height / 2) - (img.Height / 2) - 1);
        }

        private bool VScrollVisible()
        {
            int style = GetWindowLong(this.Handle, GWL_STYLE);
            return ((style & WS_VSCROLL) != 0);
        }

        private void BeginEditNode()
        {
            if(this.SelectedNode == null) return;
            if(!this.LabelEdit) throw new Exception("This TreeView is not configured with LabelEdit=true");

            IntPtr result = SendMessage(new HandleRef(this, this.Handle), TVM_EDITLABELA, IntPtr.Zero, new HandleRef(this.SelectedNode, this.SelectedNode.Handle));
            if(result == IntPtr.Zero)
            {
                throw new Exception("Failed to send EDITLABEL message to TreeView control.");
            }
        }

        private void TreeViewEx_BeforeLabelEdit(object sender, NodeLabelEditEventArgs e)
        {
            if(m_DoubleClicked)
            {
                m_DoubleClicked = false;
                return;
            }

            if(m_SelectionChanged)
            {
                e.CancelEdit = true;
                m_SelectionChanged = false;
            }
        }

        private void TreeViewEx_MouseDoubleClick(object sender, MouseEventArgs e)
        {
            if(m_HierarchyChanged)
            {
                m_HierarchyChanged = false;
                return;
            }

            if((e.Button & MouseButtons.Left) > 0)
            {
                if(this.LabelEdit && (this.SelectedNode != null))
                {
                    m_DoubleClicked = true;
                    BeginInvoke(new MethodInvoker(delegate() { this.SelectedNode.BeginEdit(); }));
                }
            }
        }

        private void TreeViewEx_AfterCollapse(object sender, TreeViewEventArgs e)
        {
            m_HierarchyChanged = true;
        }

        private void TreeViewEx_AfterExpand(object sender, TreeViewEventArgs e)
        {
            m_HierarchyChanged = true;
        }
    }
}