標準 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; }
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();
}
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;
}
// 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 模式,按需建立節點。TreeView + HierarchicalDataTemplate 原生支援資料繫結分組。RebuildTree 可移至背景執行緒配合 BeginInvoke 更新 UI。ItemDrag、DragEnter、DragDrop 支援節點重新排序並回寫資料來源。這個 DbTreeControl 範例展示了 Windows Forms 控制項開發的核心技巧:繼承現有控制項、實作複雜資料繫結介面 (IList/CurrencyManager)、自訂節點類型區分語意、事件模型擴充、多控制項同步協調。掌握這些模式,即可打造符合領域需求的高階可重用元件。