代碼文件:https://github.com/Caijt/CollapsePanelnode
最近在學習作winform,想實現一個系統導航菜單,系統菜單以模塊進行分組,菜單是樹型結構。git
效果相似舊版QQ的那種摺疊面板,就是垂直並排不少個模塊按鈕,按其中一個模塊就展開哪個模塊裏面樹型菜單,以下圖所示,我先把我實現後的效果展現出來github
一開始我覺得這麼常見的控件,winform裏面確定有,結果大失所望,竟然沒有,我剛學習winform,就遇到難題,好吧,那就學下怎麼自定義控件,反正遲早要學的。數組
其實這個控件實現起來仍是滿簡單的,沒有太複雜的知識,就是把Button控件跟TreeView組合起來,主要調整它們的Dock值,配合控件的BringToFront方法跟SendToBack方法數據結構
首先先定義個人菜單數據結構,其實就是一個很簡單的樹型結構,主要有個ParentId來代表各節點的父子關係,根節點就是模塊,根節點下面的子節點,就是模塊的菜單。ide
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(); } } }