首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

Windows Forms 控制項開發實戰:擴充 TreeView 實現資料繫結與分組

     編輯:關於C#

標準 TreeView 控制項功能強大,但缺乏原生資料繫結與階層式分組能力。本文示範繼承 TreeView 建立 DbTreeControl,支援扁平資料來源自動依指定欄位分組、顯示/值欄位分離、葉節點與群組節點區分事件、多控制項同步選取等企業級功能。

設計目標與使用者介面

期望開發者能以聲明式程式碼完成複雜分組:

dbTreeControl.ValueMember = "au_id";        // 葉節點值欄位
dbTreeControl.DisplayMember = "au_lname";   // 葉節點顯示欄位
dbTreeControl.DataSource = myDataTable.DefaultView;
dbTreeControl.AddGroup("Publisher", "pub_id", "pub_name", "pub_id", 0); // 群組名、群組鍵、顯示、值、圖示索引
dbTreeControl.AddGroup("Title", "title_id", "title", "title_id", 1);

扁平資料表 (含 pub_id, pub_name, title_id, title, au_id, au_lname) 自動轉為三層樹狀結構:Publisher → Title → Author(葉節點)。

核心架構設計

自訂節點類別


// 群組節點:僅作分組容器,不對應資料列
public class TreeGroupNode : TreeNode
{
    public int Level { get; set; } // 分組層級
}

// 葉節點:對應資料來源單一列
public class TreeLeafNode : TreeNode
{
    public int Position { get; set; } // 資料來源中的列索引
    public object DataItem { get; set; } // 原始資料物件
    public object Value { get; set; } // ValueMember 值
}

資料繫結核心屬性實作


private object m_DataSource;
private CurrencyManager cm;

public object DataSource
{
    get => m_DataSource;
    set
    {
        if (value == null) { cm = null; RebuildTree(); return; }
        if (!(value is IList || value is IListSource))
            throw new ArgumentException("資料來源必須實作 IList 或 IListSource");
        
        if (value is IListSource src && src.ContainsListCollection)
            throw new ArgumentException("不支援多清單集合 (如 DataSet),請指定單一 DataTable/DataView");
        
        m_DataSource = value;
        cm = (CurrencyManager)BindingContext[value];
        RebuildTree();
    }
}

public string ValueMember { get; set; }
public string DisplayMember { get; set; }

分組定義集合與 AddGroup 方法


public class GroupDefinition
{
    public string Name { get; set; }
    public string GroupField { get; set; } // 分組鍵欄位
    public string DisplayField { get; set; } // 顯示文字欄位
    public string ValueField { get; set; }   // 值欄位
    public int ImageIndex { get; set; }      // ImageList 圖示索引
}

private List groups = new List();

public void AddGroup(string name, string groupField, string displayField, string valueField, int imageIndex)
{
    groups.Add(new GroupDefinition { Name = name, GroupField = groupField, DisplayField = displayField, ValueField = valueField, ImageIndex = imageIndex });
    if (DataSource != null) RebuildTree();
}

樹狀結構重建邏輯 (RebuildTree)


private void RebuildTree()
{
    Nodes.Clear();
    if (cm == null || cm.Count == 0) return;

    // 取得屬性描述子
    PropertyDescriptorCollection props = cm.GetItemProperties();
    PropertyDescriptor[] groupKeys = groups.Select(g => props[g.GroupField]).ToArray();
    PropertyDescriptor[] groupDisplays = groups.Select(g => props[g.DisplayField]).ToArray();
    PropertyDescriptor[] groupValues = groups.Select(g => props[g.ValueField]).ToArray();
    PropertyDescriptor leafDisplay = props[DisplayMember];
    PropertyDescriptor leafValue = props[ValueMember];

    object[] prevGroupKeys = new object[groups.Count];
    bool[] isFirstRow = new bool[groups.Count];
    Array.Fill(isFirstRow, true);

    TreeNode[] currentGroupNodes = new TreeNode[groups.Count];

    for (int row = 0; row < cm.Count; row++)
    {
        cm.Position = row;
        object currItem = cm.Current;

        // 檢查各層群組鍵是否變更
        for (int level = 0; level < groups.Count; level++)
        {
            object key = groupKeys[level].GetValue(currItem);
            bool keyChanged = !Equals(key, prevGroupKeys[level]);
            
            if (keyChanged || isFirstRow[level])
            {
                // 建立新群組節點
                string display = groupDisplays[level].GetValue(currItem)?.ToString() ?? "";
                object value = groupValues[level].GetValue(currItem);
                var gNode = new TreeGroupNode { Text = display, Tag = value, Level = level, ImageIndex = groups[level].ImageIndex, SelectedImageIndex = groups[level].ImageIndex };
                
                if (level == 0) Nodes.Add(gNode);
                else currentGroupNodes[level - 1].Nodes.Add(gNode);
                
                currentGroupNodes[level] = gNode;
                prevGroupKeys[level] = key;
                
                // 重設較深層級的首列旗標
                for (int d = level + 1; d < groups.Count; d++) isFirstRow[d] = true;
                isFirstRow[level] = false;
            }
        }

        // 建立葉節點
        string leafText = leafDisplay.GetValue(currItem)?.ToString() ?? "";
        object leafVal = leafValue.GetValue(currItem);
        var leafNode = new TreeLeafNode { Text = leafText, Tag = leafVal, Position = row, DataItem = currItem };
        currentGroupNodes[groups.Count - 1].Nodes.Add(leafNode);
    }
    ExpandAll();
}

事件模型與同步機制

區分群組/葉節點選取事件


public event EventHandler AfterGroupSelect;
public event EventHandler AfterLeafSelect;

protected override void OnAfterSelect(TreeViewEventArgs e)
{
    TreeLeafNode leaf = null;
    if (e.Node is TreeGroupNode gNode)
    {
        leaf = FindFirstLeafNode(gNode);
        AfterGroupSelect?.Invoke(this, new GroupTreeViewEventArgs(e, gNode));
    }
    else if (e.Node is TreeLeafNode lNode)
    {
        leaf = lNode;
        AfterLeafSelect?.Invoke(this, new LeafTreeViewEventArgs(e, lNode));
    }

    // 同步 CurrencyManager Position
    if (leaf != null && cm != null && cm.Position != leaf.Position)
        cm.Position = leaf.Position;

    base.OnAfterSelect(e);
}

private TreeLeafNode FindFirstLeafNode(TreeNode node)
{
    if (node is TreeLeafNode leaf) return leaf;
    if (node.Nodes.Count > 0) return FindFirstLeafNode(node.Nodes[0]);
    return null;
}

外部控制項變更時同步樹狀選取


private void cm_PositionChanged(object sender, EventArgs e)
{
    if (cm.Position >= 0 && cm.Position < cm.Count)
    {
        TreeNode target = FindNodeByPosition(cm.Position);
        if (target != null && SelectedNode != target)
            SelectedNode = target;
    }
}

private TreeNode FindNodeByPosition(int pos, TreeNodeCollection nodes = null)
{
    nodes ??= Nodes;
    foreach (TreeNode n in nodes)
    {
        if (n is TreeLeafNode leaf && leaf.Position == pos) return n;
        var found = FindNodeByPosition(pos, n.Nodes);
        if (found != null) return found;
    }
    return null;
}

完整事件引數類別


public class GroupTreeViewEventArgs : TreeViewEventArgs
{
    public TreeGroupNode GroupNode { get; }
    public GroupTreeViewEventArgs(TreeViewEventArgs e, TreeGroupNode node) : base(e.Node, e.Action) => GroupNode = node;
}

public class LeafTreeViewEventArgs : TreeViewEventArgs
{
    public TreeLeafNode LeafNode { get; }
    public LeafTreeViewEventArgs(TreeViewEventArgs e, TreeLeafNode node) : base(e.Node, e.Action) => LeafNode = node;
}

使用範例:搭配 DataGridView 雙控制項同步


// Form_Load
DataTable dt = GetFlatData(); // 含 Publisher, Title, Author 欄位
dbTreeControl.DataSource = dt.DefaultView;
dbTreeControl.ValueMember = "au_id";
dbTreeControl.DisplayMember = "au_lname";
dbTreeControl.AddGroup("Publisher", "pub_id", "pub_name", "pub_id", 0);
dbTreeControl.AddGroup("Title", "title_id", "title", "title_id", 1);

dataGridView1.DataSource = dt.DefaultView; // 同一 DataView

// 使用者在 TreeView 點選作者 → DataGridView 自動定位至該列
// 使用者在 DataGridView 移動列 → TreeView 自動展開並選取對應葉節點

擴充建議與現代化考量

  • 虛擬模式:大量資料 (萬筆以上) 建議實作 IVirtualTreeView 模式,按需建立節點。
  • WPF/UWP:新專案建議使用 TreeView + HierarchicalDataTemplate 原生支援資料繫結分組。
  • 非同步載入:RebuildTree 可移至背景執行緒配合 BeginInvoke 更新 UI。
  • 拖放排序:實作 ItemDrag、DragEnter、DragDrop 支援節點重新排序並回寫資料來源。

這個 DbTreeControl 範例展示了 Windows Forms 控制項開發的核心技巧:繼承現有控制項、實作複雜資料繫結介面 (IList/CurrencyManager)、自訂節點類型區分語意、事件模型擴充、多控制項同步協調。掌握這些模式,即可打造符合領域需求的高階可重用元件。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved