欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2023-11-03 17:44:22
代码文件:https://github.com/Caijt/CollapsePanel 最近在学习做winform,想实现一个系统导航菜单,系统菜单以模块进行分组,菜单是树型结构。 效果类似旧版QQ的那种折叠面板,就是垂直并排很多个模块按钮,按其中一个模块就展开哪一个模块里面树型菜单,如下图所示,我 ......

代码文件:https://github.com/caijt/collapsepanel

 

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

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

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

一开始我以为这么常见的控件,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();
        }
    }
}