winform实现自定义折叠面板控件

代码文件:https://github.com/Caijt/CollapsePanel

 

最近在学习做winform,想实现一个系统导航菜单,系统菜单以模块进行分组,菜单是树型结构。

效果类似旧版QQ的那种折叠面板,就是垂直并排很多个模块按钮,按其中一个模块就展开哪一个模块里面树型菜单,如下图所示,我先把我实现后的效果展示出来

一开始我以为这么常见的控件,winform里面肯定有,结果大失所望,居然没有,我刚学习winform,就遇到难题,好吧,那就学下怎么自定义控件,反正早晚要学的。

其实这个控件实现起来还是满简单的,没有太复杂的知识,就是把Button控件跟TreeView组合起来,主要调整它们的Dock值,配合控件的BringToFront方法跟SendToBack方法

 

首先先定义我的菜单数据结构,其实就是一个很简单的树型结构,主要有个ParentId来表明各节点的父子关系,根节点就是模块,根节点下面的子节点,就是模块的菜单。

namespace CollapsePanelForm
{
    public class MenuData
    {
        public int Id { get; set; }
        //可为空,当为空时,说明当前节点是根节点
        public int? ParentId { get; set; }
        //模块或菜单的名称
        public string Name { get; set; }
        //这个是用于构建菜单对应Form控件的路径的,可以利用反射实现打开匹配路径的Form控件
        public string Path { get; set; }
    }
}

 

下面是控件的完整代码,已进行注释,我相信你们看得明白的。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Windows.Forms;

namespace CollapsePanelForm
{
    public partial class CollapsePanel : UserControl
    {
        /// <summary>
        /// 这是菜单列表数据,控件公开的属性必须定义以下这些特性,不然会出错,提示未标记为可序列化
        /// </summary>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        [Localizable(true)]
        [MergableProperty(false)]
        public List<MenuData> Menus { get; set; }
        /// <summary>
        /// 菜单双击事件
        /// </summary>
        public event EventHandler MenuDoubleClick;
        /// <summary>
        /// 模块的按钮列表
        /// </summary>
        private List<Button> headerButtons;
        /// <summary>
        /// 模块下的菜单,每一个模块下面的菜单对应一个TreeView控件
        /// </summary>
        private List<TreeView> treeViews;
        /// <summary>
        /// 当前控件打开的模块索引值
        /// </summary>
        private int? openMenuIndex = null;
        /// <summary>
        /// 当模块处理打开状态时,模块名称后带的符号
        /// </summary>
        private string openArrow = " <<";
        /// <summary>
        /// 当模块处理关闭状态时,模块名称后带的符号
        /// </summary>
        private string hideArrow = " >>";
        public CollapsePanel()
        {
            InitializeComponent();
            headerButtons = new List<Button>();
            treeViews = new List<TreeView>();
            Menus = new List<MenuData>();
            this.InitMenus();
        }
        /// <summary>
        /// 根据Menus的数据初始化控件,就是动态增加Button跟TreeView控件
        /// </summary>
        public void InitMenus()
        {
            this.Controls.Clear();
            //过滤出所有ParentId为null的根节点,就是模块列表
            foreach (var menu in Menus.Where(a => a.ParentId == null))
            {
                Button headerButton = new Button();
                headerButton.Dock = DockStyle.Top;
                headerButton.Tag = menu.Name;
                headerButton.Text = menu.Name + hideArrow;
                headerButton.TabStop = false;
                headerButton.Click += headerButton_Click;
                headerButtons.Add(headerButton);

                this.Controls.Add(headerButton);
                //这个BringToFront置于顶层方法对于布局很重要
                headerButton.BringToFront();

                TreeView tree = new TreeView();
                //用一个递归方法构建出nodes节点
                tree.Nodes.AddRange(buildTreeNode(menu.Id, menu.Path.Substring(0, 1).ToUpper() + menu.Path.Substring(1)));
                tree.Visible = false;
                tree.Dock = DockStyle.Fill;
                tree.NodeMouseDoubleClick += Tree_DoubleClick;
                treeViews.Add(tree);
                this.Controls.Add(tree);
            }
        }

        private void Tree_DoubleClick(object sender, EventArgs e)
        {
            if (MenuDoubleClick != null)
            {
                MenuDoubleClick(sender, e);
            }
        }
        /// <summary>
        /// 模块按钮单击事件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void headerButton_Click(object sender, EventArgs e)
        {
            var clickButton = sender as Button;
            //得出当前单击的模块按钮索引值
            var clickMenuIndex = headerButtons.IndexOf(clickButton);
            //如果当前单击的模块按钮索引值等于已经打开的模块索引值的话,那么当前模块要关闭,否则则打开
            if (openMenuIndex == clickMenuIndex)
            {
                clickButton.Text = clickButton.Tag.ToString() + hideArrow;
                this.treeViews[clickMenuIndex].Hide();
                openMenuIndex = null;
            }
            else
            {
                //关闭之前打开的模块按钮
                if (openMenuIndex.HasValue)
                {
                    this.treeViews[openMenuIndex.Value].Hide();
                    headerButtons[openMenuIndex.Value].Text = headerButtons[openMenuIndex.Value].Tag.ToString() + hideArrow;
                }
                clickButton.Text = clickButton.Tag.ToString() + openArrow;
                openMenuIndex = clickMenuIndex;
                this.treeViews[clickMenuIndex].Show();
            }
            //以下的操作也很重要,根据当前单击的模块按钮索引值,小于这个值的模块按钮移到上面,大于的移到下面
            int i = 0;
            foreach (var b in headerButtons)
            {
                if (i <= clickMenuIndex || openMenuIndex == null)
                {
                    b.Dock = DockStyle.Top;
                    b.BringToFront();

                }
                else
                {
                    b.Dock = DockStyle.Bottom;
                    b.SendToBack();
                }
                i++;
            }
            //最后对应的TreeView控件得置于顶层,这样布局就完美了
            this.treeViews[clickMenuIndex].BringToFront();
        }
        /// <summary>
        /// 递归根据节点的Id,构建出TreeNode数组,这个prefixPath是用来构建完美的Path路径的
        /// </summary>
        /// <param name="parentId"></param>
        /// <param name="prefixPath"></param>
        /// <returns></returns>
        private TreeNode[] buildTreeNode(int parentId, string prefixPath)
        {
            List<TreeNode> nodeList = new List<TreeNode>();
            Menus.ForEach(m =>
            {
                if (m.ParentId == parentId)
                {
                    //拼接当前节点完整路径,然后再传给递归方法
                    string path = prefixPath + "." + m.Path.Substring(0, 1).ToUpper() + m.Path.Substring(1);
                    TreeNode node = new TreeNode();
                    node.Text = m.Name;
                    node.Tag = path;
                    node.Nodes.AddRange(buildTreeNode(m.Id, path));
                    nodeList.Add(node);

                }
            });
            return nodeList.ToArray();
        }
    }
}

 

posted on 2019-06-27 15:54  菜工  阅读(...)  评论(... 编辑 收藏

导航