将FolderBrowserDialog搬进“下拉列表框”中!

 

      前几天见有人问如何在ComboBox中加入复选框的问题,从那时起我就想在里面加点复杂点但有使用价值的东西,思来想去我决定将FolderBrowserDialog塞进去,因为我对系统提供的那个一直觉得有点麻烦!标题中之所以在下拉列表框加引号,是说它不是在ComboBox控件的基础上扩展,不是从Control扩展,只不过是外观类似ComboBox而已,所以该控件只能从下拉部分选择目录而不支持手动输入,当然了,因为那个标准的FBD(FolderBrowserDialog,下同)支持新建目录功能,所以这个自定义控件也支持新建,另外还可以将选定的目录在资源管理器中打开,本来我还打算加上对文件夹重命名的,但放弃了!咱这虽然有点山寨,但该有的还是不能缺!

      效果就是下面两幅图片所示:

 

以下是步骤代码节选:

  1. 首先需要三个控件,一个是UserControl或Control,它负责会制ComboBox外观和绘制数据,另一个是TreeView,它负责显示本机的文件系统目录,但不能直接用,需要修理修理。最后一个就是负责承载TreeView的ToolStripDropDown,它就是平时不可见的下拉的那部分!、
  2. 添加引用(COM):Shell32.dll;
  3. 对TreeView的修改:

        //定义一个对象,负责加载目录.

        Shell32.ShellClass shell = new Shell32.ShellClass();
        // 获取或设置控件选择的路径

        public string SelectedPath{get;set;}

        //获取或设置控件使用的根目录。

        public Environment.SpecialFolder Root{get;set;}

       

        //隐藏Nodes属性,这步并非可有可无,是必须!

        [Browsable(false)]
        [EditorBrowsable( EditorBrowsableState.Never)]
        [Localizable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public new TreeNodeCollection Nodes
        {
            get { return base.Nodes; }
        }
   

        //然后设置以下属性     

       ShowNodeToolTips = true;
        ShowRootLines = false;            
        BeforeExpand += new TreeViewCancelEventHandler(EtFolderTreeView_BeforeExpand);
//使控件自绘制,
        DrawMode = TreeViewDrawMode.OwnerDrawAll;
        ImageList = new ImageList();//设置该属性但不向里面添加任何图片,目换是让TreeView在每个节点处留出绘制图标的位置。
        DrawNode += new DrawTreeNodeEventHandler(FolderTreeView_DrawNode);
        AfterSelect += new TreeViewEventHandler(EtFolderTreeView_AfterSelect);
        FillNodesFor(null);//null参数表示当前添加的为要节点,但不限于Desktop,取决于Root属性的设置。
        }
。。。。。。

       //在当前选定节点下新建目录

       public bool NewFolderInSelectedFolder(string name){}

       //刷新指定节点,即重新加载其下子目录,然后使其展开

        public void RefreshItem(TreeNode node)
        {
            FillNodesFor(node);
            if (!node.IsExpanded) node.Expand();
        }

        // 在每次选中节点后检查其对应的是快捷方式还是目录,验证目录或快捷方式的目标位置的可用性并设置SelectedPath属性。
        void EtFolderTreeView_AfterSelect(object sender, TreeViewEventArgs e)
        {
            PathEntry pe = (PathEntry)e.Node.Tag;
            string path = pe.IsLink ? pe.LinkTarget : pe.Path;
            if (Directory.Exists(path))
                SelectedPath = path;
            else
                SelectedPath = "";
        }

        //为提高性能,设定在某节点展开前加载其对应目录的子目录

        void EtFolderTreeView_BeforeExpand(object sender, TreeViewCancelEventArgs e)
        {
            FillNodesFor(e.Node);
        }

        void FolderTreeView_DrawNode(object sender, DrawTreeNodeEventArgs e)
        {           
            if (!e.Node.IsVisible) return;
            Rectangle rect = new Rectangle(e.Bounds.Location, new Size(16, 16));
            int indent = this.Indent * e.Node.Level;
            if (CheckBoxes) indent += this.Indent;
            rect.Offset(indent + 1, 1);
            Icon icon = null;
            try
            {
                PathEntry pe = (PathEntry)e.Node.Tag;
                if (pe.vDir.ToString() == "0") icon = PlatFormInformation.ThemesIcons.GetDesktopIcon(true);
                if (icon == null)
                {
                    //比用Path好,自动区分需要特殊处理的图标,与桌面上的我的文档和与其对应的物理目录
                    // 不应用同一图标。而当pe为指向某文件夹的快捷方式时,使用Path为的是显示箭头
                    string path =pe.IsLink?pe.Path: pe.vDir.ToString();
                    if (path != null)
                    {
                        Win32.Shell32Api.SHFILEINFO info = new Etonesoft.Win32.Shell32Api.SHFILEINFO();
                        Win32.Shell32Api.SHFILEINFOITEM item = Win32.Shell32Api.SHFILEINFOITEM.SHGFI_ICON | Etonesoft.Win32.Shell32Api.SHFILEINFOITEM.SHGFI_SMALLICON | Etonesoft.Win32.Shell32Api.SHFILEINFOITEM.SHGFI_ATTRIBUTES;
                        if (e.Node.IsExpanded) item |= Etonesoft.Win32.Shell32Api.SHFILEINFOITEM.SHGFI_OPENICON;
                        Win32.Shell32Api.SHGetFileInfo(path, Etonesoft.Win32.Shell32Api.SHFILEATTRIBUTE.FILE_ATTRIBUTE_DIRECTORY, ref info, item);
                        // 之所以进行如下代码的替换,是因为个别节点,如网上邻居下的整个网络,无法从其自身的Path属性获取图标,
                        //需要与其父节点的Path属性相结合,尽管如此,依然无法获取整个网络节点的各子节点图标,以后需改进!!
                        //if (info.hIcon != IntPtr.Zero) icon = Icon.FromHandle(info.hIcon);
                        TreeNode tn = e.Node;
                        while (info.hIcon==IntPtr.Zero && tn.Parent!=null)
                        {
                            path = ((PathEntry)e.Node.Parent.Tag).vDir + "//"+path;
                            Win32.Shell32Api.SHGetFileInfo(path, Etonesoft.Win32.Shell32Api.SHFILEATTRIBUTE.FILE_ATTRIBUTE_DIRECTORY, ref info, item);
                            tn = tn.Parent;
                        }
                        if (info.hIcon != IntPtr.Zero) icon = Icon.FromHandle(info.hIcon);
                    }
                }
                if (icon != null)
                {
                    e.Graphics.DrawIcon(icon, rect);
                    icon.Dispose();
                }
               
            }
            catch (InvalidCastException)
            { return; }
            catch (NullReferenceException)
            {
                if (e.Node.Tag == null) return;
                throw;
            }
            catch { throw; }
            finally { e.DrawDefault = true; }
        }
        protected override void OnNodeMouseClick(TreeNodeMouseClickEventArgs e)
        {           
            base.OnNodeMouseClick(e);
            //注释:下句不可缺少,否则,如果之前该节点的某一子节点已处于
            // 选中状态,则该节点不会因此次鼠标点击事件而引发AfterSelected事件。           
            this.SelectedNode = e.Node;
            if(e.Node.Bounds.Contains(e.Location))e.Node.Toggle();           
        }
        private void FillNodesFor(TreeNode treeNode)
        {
            try
            {
                PathEntry peRoot;
                IEnumerable<TreeNode> nodes;
                this.Cursor = Cursors.WaitCursor;
                if (treeNode == null)
                {//加载根目录
                    this.Nodes.Clear();
                    object vDir = Environment.GetFolderPath(Root);
                    switch (Root)
                    {
                        case Environment.SpecialFolder.MyComputer:
                            vDir = "::" + PlatFormInformation.CLSIDs.MyComputer;
                            break;
                        case Environment.SpecialFolder.MyDocuments:
                            vDir = "::" + PlatFormInformation.CLSIDs.MyDocuments;
                            break;
                        case Environment.SpecialFolder.Desktop://此处比较特殊,要想加载桌面,参数必须为0,而不是其物理目录。只有这样才会出现我的文档,我的电脑回收站及网上邻居等
                            vDir = 0;
                            break;
                        default:
                            break;
                    }
                    peRoot = GetPathEntry(null, (shell.NameSpace(vDir) as Shell32.Folder3).Self);
                    this.Nodes.Add(treeNode = GetTreeNodeFromFileItem(peRoot));
                }
                else
                {
                    treeNode.Nodes.Clear();
                    peRoot = (PathEntry)treeNode.Tag;
                    // 拒绝回收站里被逻辑删除的文件,通过检查对目录里是否以回收站的CLSID确定。
                    if (peRoot.vDir.ToString().EndsWith(PlatFormInformation.CLSIDs.RecycleBin)) return;
                }               
                Shell32.Folder folder = shell.NameSpace(peRoot.vDir);
                if (folder == null) return;
                nodes = from Shell32.FolderItem dir in folder.Items()
                        let pe = GetPathEntry(treeNode, dir)
                        where pe.Valid
                        select GetTreeNodeFromFileItem(pe);
                treeNode.Nodes.AddRange(nodes.Where(x => (x != null)).ToArray());
            }
            catch (FileNotFoundException)
            {
                treeNode.Remove();
                return;
            }
            catch (UnauthorizedAccessException){ return; }
                catch(NotImplementedException){ return; }
            catch (Exception ex)
            {

                MessageBox.Show(ex.ToString() + "/n" + treeNode.Name);
                return;
            }
            finally { this.Cursor = Cursors.Default; }
        }

        //经过该判断,过滤出来的文件夹项只能是文件夹或快捷方式
        private PathEntry GetPathEntry(TreeNode treeNode, Shell32.FolderItem x)
        {
            PathEntry entry = new PathEntry();
            entry.vDir = entry.Path = x.Path;
            entry.Title = x.Name;
            Environment.SpecialFolder sf;
            if (GetSpecialFolder(x.Path,out sf))
            {
                entry.IsSpecialFolder = true;
                entry.SpecialFolder = sf;
                if (treeNode==null ||  treeNode.Level ==0)
                {                   
                    switch (sf)
                    {
                        case Environment.SpecialFolder.Desktop:
                            entry.vDir =0;
                            break;
                        case Environment.SpecialFolder.MyComputer:
                            entry.vDir = "::" + PlatFormInformation.CLSIDs.MyComputer;
                            break;
                        case Environment.SpecialFolder.MyDocuments:
                            entry.vDir = "::" + PlatFormInformation.CLSIDs.MyDocuments;
                            break;
                        default:
                            break;
                    }
                }
            }
           
            try
            {
                entry.Local = true;
                if (x.IsFolder)
                {
                    entry.Valid= treeNode==null || !treeNode.Nodes.ContainsKey(x.Path);             

                    //如果不作判断,则桌面上的“我的文档”的图标将被绘制为变通的文件夹!       
                    if (x.Name == "我的文档")//该处判断不够合理,需要改进!!
                    {
                        entry.IsSpecialFolder = true;
                        entry.SpecialFolder= Environment.SpecialFolder.MyDocuments;
                    }
                }
                if (x.IsLink)
                {
                  
                    Shell32.ShellLinkObject link = x.GetLink as Shell32.ShellLinkObject;
                    if (link != null && File.Exists(x.Path) && Directory.Exists(link.Path))// &&
                        //上述条件最后两项之所以分别用Directory和File进行测试是针对快捷方式的特点,用File是测试快捷方式
                        //文件本身测试,这可以快捷方式文件本身的位置;用Directory是测试快捷方式所指向的目标文件夹
                        //是否对本机可见及目标位置是否是一个文件夹而不是一个文件,可避免目标位置无效的快捷方式出现在列表中。
                    {
                        entry.Valid= treeNode==null || !treeNode.Nodes.ContainsKey(link.Path);
                        entry.IsLink = true;
                        entry.vDir = entry.LinkTarget = link.Path;
                    }
                }       
                return entry;
            }
            catch (FileNotFoundException) { return entry; }
            catch { throw; }
        }

        //根据指定的路径确定当前是否是一个特殊文件夹,即Environment。SpecialFolder

        //但这个地方我认为判定手段有点问题,但又想不到更好的

        private bool GetSpecialFolder(string p, out Environment.SpecialFolder sf)
        {
            switch (p)
            {
                case "::" + PlatFormInformation.CLSIDs.MyComputer:
                    sf = Environment.SpecialFolder.MyComputer;
                    return true;
                case "::" + PlatFormInformation.CLSIDs.MyDocuments:
                    sf = Environment.SpecialFolder.MyDocuments;
                    return true;
                default:
                    Array array = Enum.GetValues(typeof(Environment.SpecialFolder));
                    foreach (Environment.SpecialFolder item in array)
                    {
                        if (p==Environment.GetFolderPath(item))
                        {
                            sf = item;
                            return true;
                        }
                    }
                    sf = Environment.SpecialFolder.Desktop;
                    return false;
            }
        }

        //根据取得的PathEntry构造TreeNode,但每个新建的节点都要加一个空节点,目录嘛,就是让其首次出现时也会在左侧出现加号,只有这样才会有机会触发AfterExpend事件进而加载其下的子目录,虽然有     //的目录根本没有子目录
        private TreeNode GetTreeNodeFromFileItem(PathEntry pe)
        {
            TreeNode node= new TreeNode { Text = pe.Title, Tag = pe };
            node.Nodes.Add("");
            return node;
        }

        //一个结构,包含构造树节点的必需信息
        struct PathEntry
        {
            internal bool Valid;
            internal bool IsSpecialFolder;
            internal bool Local;
            internal bool IsLink;
            internal string LinkTarget;
            internal object vDir;
            internal string Title;
            internal string Path;
            internal Environment.SpecialFolder SpecialFolder;
        }

        //实现将选中的目录在系统Explorer中打开

        internal void OpenFolder()
        {       }

。。。。。。

4. 对UserControl的扩展

//EtComboBoxHost是从UserControl继承而来的,它里面定义了一个ToolStripDropDown的东西(本例中名称为controlDropDown)。只在放在里面的东西才会出现在下拉部分中,
//并且,还实现在弹出和隐藏的方法。

public partial class EtFolderSelector : EtComboBoxHost
    {

       //左侧的图像栏的宽度,因为有个TreeView在上面,想会制OFFICE2007中的效果,否则的话,如果里面只ToolStripMenuItem什么的,完全可以将controlDropDown改成ToolStripDropDownMenu
//,它可以实现左侧的边栏,现在我不想让TreeView也有边栏,只能扩展ToolStripDropDown以实现边栏了。
        private const int IMAGEMARGIN = 26;
        private EtFolderTreeView treeView = new EtFolderTreeView();
        private ToolStripTextBox tstbxNewFolder = new ToolStripTextBox();
        private ToolStripMenuItem tsmiSelectPath = new ToolStripMenuItem("选择文件夹");
        ToolStripMenuItem tsmiOpenInExplorer = new ToolStripMenuItem("打开文件夹"); 
               #region RootDirectory 属性定义
        private event EventHandler e_RootDirectoryChanged;
        /// <summary>
        /// 当RootDirectory属性更改时发生。
        /// </summary>
        public event EventHandler RootDirectoryChanged
        {
            add { e_RootDirectoryChanged += value; }
            remove { e_RootDirectoryChanged -= value; }
        }
        /// <summary>
        /// 引发按控件的RootDirectory属性。
        /// </summary>
        /// <param name="e">包含事件所需要数据的事件参数对象。</param>
        protected virtual void OnRootDirectoryChanged(EventArgs e)
        {
            if(this.ControlDropDown.Visible)this.HideDropDown();
            treeView.Root = m_RootDirectory;
            SelectedPath = Environment.GetFolderPath(m_RootDirectory);
            //如果控件的RootDirectoryChanged事件不为空且委托链长度大于0,则引发该事件。
            if (e_RootDirectoryChanged != null && e_RootDirectoryChanged.GetInvocationList().Length > 0)
            {
                e_RootDirectoryChanged(this, e);
            }
        }
        private Environment.SpecialFolder m_RootDirectory =  Environment.SpecialFolder.Desktop;
        /// <summary>
        /// 获取或设置控件的初始目录。
        /// </summary>
        [Category("扩展")]
        [Browsable(true)]
        [DefaultValue(typeof(Environment.SpecialFolder), "Desktop")]
        [Description("获取或设置控件的初始目录。")]
        public Environment.SpecialFolder RootDirectory
        {
            get
            {
                return m_RootDirectory;
            }
            set
            {
                if (m_RootDirectory == value) return;
                m_RootDirectory = value;
                OnRootDirectoryChanged(new EventArgs());
            }
        }
        #endregion

        #region SelectedPath 属性定义
                public string SelectedPath{get;set;}

        #endregion

        public EtFolderSelector()
            : base()
        {
            InitializeComponent();
            base.RenderMode = ToolStripRenderMode.ManagerRenderMode;

            treeView.BorderStyle = BorderStyle.None;
            treeView.AfterSelect += new TreeViewEventHandler(treeView_AfterSelect);
            treeView.HideSelection = false;
            treeView.MinimumSize = new Size(200, 0);
            tsmiSelectPath.Click += new EventHandler(tsmiSelectPath_Click);
            tsmiOpenInExplorer.Click += new EventHandler(tsmiOpenInExpore_Click);
            tsmiOpenInExplorer.Tag = "OPEN";
            tstbxNewFolder.Tag = "NEW";
            tsmiSelectPath.Tag = "SELECT";
            tsmiSelectPath.TextAlign = ContentAlignment.MiddleLeft;
            tsmiOpenInExplorer.TextAlign= ContentAlignment.MiddleLeft;
            tstbxNewFolder.ToolTipText = "[新建文件夹],要新建文件夹,请输入新文件夹名称后按回车";
            tstbxNewFolder.KeyDown += new KeyEventHandler(tstbxNewFolder_KeyDown);
            InitControlDropDown();
           
        }

        ......

        protected override void OnDropDownOpening(CancelEventArgs e)
        {
            base.OnDropDownOpening(e);
            //设置下拉部分的最小尺寸
            foreach (ToolStripItem item in ControlDropDown.Items)
            {
                ToolStripControlHost host = (item as ToolStripControlHost);
                if (host != null)
                    host.Control.MinimumSize = new Size(this.Width - 2 * (IMAGEMARGIN + 2),host.Control==treeView?200:0);
            }
                    }
        protected override void OnDropDownClosed(EventArgs e)
        {
            base.OnDropDownClosed(e);
            tstbxNewFolder.Text = "";           
           
        }

        private void InitControlDropDown()
        {

//PlatFormInformation.ShellStockIcons,在其他地方定义的类型,负责访问Shell32.dll里的图标,参数TRUE表示获取小图标,否则为大图标
            ControlDropDown.ImageList = new ImageList { ColorDepth= ColorDepth.Depth32Bit};
            ControlDropDown.ImageList.Images.Add("OPEN", PlatFormInformation.ShellStockIcons.GetFolderOpenned(true));
            ControlDropDown.ImageList.Images.Add("NEW", PlatFormInformation.ShellStockIcons.GetNewFolder(true));
            ControlDropDown.ImageList.Images.Add("SELECT", PlatFormInformation.ShellStockIcons.GetIconFor(110,true));
            ControlDropDown.LayoutStyle = ToolStripLayoutStyle.VerticalStackWithOverflow;
            ControlDropDown.Items.Add(new ToolStripControlHost(treeView));
            ControlDropDown.Items.Add(new ToolStripSeparator());
            ControlDropDown.Items.Add(tstbxNewFolder);
            ControlDropDown.Items.Add(tsmiOpenInExplorer);
            ControlDropDown.Items.Add(tsmiSelectPath);           
            ControlDropDown.Paint += new PaintEventHandler(ControlDropDown_Paint);

//为配合边栏,设置各项(除TreeView外)的Margin,让出左侧一定的宽度。
            foreach (ToolStripItem item in ControlDropDown.Items)
            {
                if (item is ToolStripControlHost)
                {
                    if ((item as ToolStripControlHost).Control == treeView)
                        continue;
                    else if (item is ToolStripTextBox)
                    {
                        item.Margin = new Padding(IMAGEMARGIN + 4,1, IMAGEMARGIN , 1);
                        continue;
                    }
                }
                item.Margin = new Padding(IMAGEMARGIN +2,1,2,1);
                item.Padding = new Padding(1, 3, 1, 3);
            }
        }

        void ControlDropDown_Paint(object sender, PaintEventArgs e)
        {

//绘制左侧边栏
            ControlDropDown.Renderer.DrawImageMargin(
                new ToolStripRenderEventArgs(
                    e.Graphics,
                    ControlDropDown,
                    ControlDropDown.DisplayRectangle.LeftPart(IMAGEMARGIN),
                    SystemColors.Menu)
                    );

//绘制各项的图标,如果有。
            foreach (ToolStripItem item in ControlDropDown.Items)
            {
                Rectangle rect = item.Bounds.LeftPart(IMAGEMARGIN);
                rect.X = 0;
                rect.Inflate((16-rect.Width) / 2, (16-rect.Height) / 2);
                Image img = null;
                if(item.Tag!=null)img=ControlDropDown.ImageList.Images[item.Tag.ToString()];
                ControlDropDown.Renderer.DrawItemImage(new ToolStripItemImageRenderEventArgs(e.Graphics,item,img,rect));
               
            }
        }

//将用户选中的目录绘制到用户的客户区(ContentRect,在父类中定义!)

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);
            e.Graphics.DrawString(this.SelectedPath, this.Font, this.Focused ? SystemBrushes.HighlightText : SystemBrushes.ControlText, this.ContentRect, new StringFormat { Alignment = StringAlignment.Near, LineAlignment = StringAlignment.Near, Trimming = StringTrimming.EllipsisPath, FormatFlags = StringFormatFlags.NoClip | StringFormatFlags.NoWrap });
        }

 

 

 

 

 

 

 

需要说明的是里面有几个东西是我在另的地方定义的,如PlatformImformation等。

posted @ 2010-04-17 22:26  蒋启磊  阅读(754)  评论(0编辑  收藏  举报