C# WinForm 自绘TreeView
1. 问题提出
TreeView 控件很常用,常见的功能及UI方面的需求总结起来有如下几点:
(1). 自定义节点高度:这是继承TreeView不能实现的功能
(2). Tree失去焦点后当前节点仍然需要突出显示:C# WinForm中无此功能
(3). 自定义展开闭合图标、节点图标,自定义节点背景前景
(4). 多列显示
(5). 其他
(6). 原文地址:http://blog.csdn.net/ljfblog
2. 基本思路
如果直接继承TreeView,1.(1)无法实现或很难实现,1.(4)很难实现。
所以这里使用DataGridView来曲线救国,主要要解决的技术问题显而易见,就是节点从属、展开闭合的处理。
本文中的源码不能直接运行,需要您认真查看代码理解来龙去脉后替换通用函数和资源文件。而且本文中的代码也只是给个思路,细节和功能带完善,同时也未免会有疏漏Bug。
3. 关键问题
3.1 DataGridView共享行的处理
为了提高运行效率,微软使用共享行来节省内存开销。比如我们插入了一个节点,运行时你会发现它的Index是-1,也没有列信息。这也是LDataGridViewTreeViewNode中会有ListedRow和NodeRow属性的原因。
3.2 节点从属
这里仿照TreeView,使用LDataGridViewTreeViewNode和LDataGridViewTreeViewNodeCollection两个主要的类来实现。参考.Net4.5.1 TreeView源码第148行,我们也使用了一个辅助的根节点来减少处理过程中的例外。当然,要使用递归调用来解决所有父子节点的遍历问题。相关代码见4节。
3.3 展开闭合的处理
每个节点有三个状态:展开显示的、展开但隐藏的、闭合的。在展开闭合的函数中处理,使展开闭合逻辑大体和TreeView相似。本文中的代码在初始状态是全部显示的,需要读者进一步完善。在LDataGridViewTreeViewNode中实现,相关代码见4节。
3.4 绘制
使用了DataGridView绘制不是什么问题,有了3.2,3.3的基础,对缩进、图标、文字的绘制都很容易实现。在LDataGridViewTreeView中实现,相关代码见4节。
3.5 大量数据显示
大量数据显示,本控件不会有太高的效率,如果非使用不可,请异步处理(比如默认所有根节点是闭合的,实际闭合在里面的只有一个“正在读取”的节点),注意释放内存。
3.6 事件处理
本文源码没有实现诸如NodeExpanded,TextChanged之类的事件处理。请自行修改。
3.7 高分屏
如果不考虑高分屏处理,请替换相关函数成像素数字,如LCom.PointToPixelH(20)
4.关键源码
public class LDataGridViewTreeViewNode:DataGridViewRow
{
#region Class
public enum VisibleState
{
ExpandVisible,
ExpandHidden,
Collapse
}
#endregion
#region Fileds
private LDataGridViewTreeView treeView;
private LDataGridViewTreeViewNode parent;
private LDataGridViewTreeViewNodeCollection nodes;
private string name = "";
private int nodeIndex=-1;
private string text="";
private VisibleState nodeVisibleState;
private Font font;
private int nodeHeight;
private Image image;
private Color backColor = Color.Empty;
private Color selectionBackColor = Color.Empty;
private Color selectionForeColor = Color.Empty;
private Color foreColor = Color.Empty;
#endregion Fileds
#region Constructor
public LDataGridViewTreeViewNode()
{
this.LDataGridViewTreeViewNode_Init("", "");
}
public LDataGridViewTreeViewNode(string name,string text)
{
this.LDataGridViewTreeViewNode_Init(name, text);
}
internal LDataGridViewTreeViewNode(LDataGridViewTreeView treeView)
{
this.treeView = treeView;
this.LDataGridViewTreeViewNode_Init("","");
}
private void LDataGridViewTreeViewNode_Init(string name, string text)
{
this.name = name;
this.text = text;
}
#endregion
#region Public Properties
public int Level
{
get
{
if (this.parent == null) return -1;
return this.parent.Level+1;
}
}
public string Name
{
get
{
return name;
}
set
{
this.name = value;
}
}
public string Text
{
get
{
return this.text;
}
set
{
this.text = value;
if(this.TreeView != null && this.ListedRow != null)
this.ListedRow.Cells[this.TreeView.NodeTextColumnName].Value = this.text;
}
}
public LDataGridViewTreeViewNodeCollection Nodes
{
get
{
if(this.nodes== null)
{
this.nodes = new LDataGridViewTreeViewNodeCollection(this);
}
return this.nodes;
}
}
public LDataGridViewTreeViewNode Parent
{
get { return this.parent; }
internal set
{
this.parent = value;
}
}
public int NodeIndex
{
get
{
return this.nodeIndex;
}
internal set
{
this.nodeIndex = value;
}
}
public int ListedIndex
{
get { return this.ListedRow==null?-1: this.ListedRow.Index; }
}
public LDataGridViewTreeView TreeView
{
get
{
if(this.treeView == null)
{
LDataGridViewTreeViewNode node = this;
while (node.parent != null)
node = node.parent;
this.treeView = node.treeView;
}
return treeView;
}
}
public bool IsExpanded
{
get
{
if (this.Nodes.Count == 0) return true;
return this.Nodes[0].nodeVisibleState != VisibleState.Collapse;
}
}
public LDataGridViewTreeViewNode ListedRow
{
get;
internal set;
}
public LDataGridViewTreeViewNode NodeRow
{
get;
private set;
}
public int NodeHeight
{
get { return this.nodeHeight; }
set
{
this.nodeHeight = value;
if (this.TreeView != null)
{
if (this.nodeHeight <= 0)
{
this.ListedRow.Height = this.TreeView.RowTemplate.Height;
}
else
{
this.ListedRow.Height = this.nodeHeight;
}
}
}
}
internal Rectangle LevelSquare(Rectangle rLevel)
{
Rectangle rLevelSquare = rLevel;
rLevelSquare.X = rLevel.Right - this.TreeView.LevelIndentWidth;
rLevelSquare.Width = this.TreeView.LevelIndentWidth;
return rLevelSquare;
}
internal Rectangle LevelSquare()
{
return this.LevelSquare(this.LevelRectangle);
}
public VisibleState NodeVisibleState
{
get
{
return this.nodeVisibleState;
}
private set
{
this.nodeVisibleState = value;
if (this.nodeVisibleState == VisibleState.ExpandVisible)
{
if(this.ListedRow.Visible==false)
this.ListedRow.Visible = true;
}
else if(this.ListedRow.Visible)
{
this.ListedRow.Visible = false;
}
#if DEBUG
if(this.TreeView.dgvDebug != null)
this.TreeView.dgvDebug.Rows[this.ListedIndex].Cells[0].Value = this.NodeVisibleState.ToString();
#endif
}
}
public Image Image
{
get
{
return this.image;
}
set
{
this.image = value;
this.Invalidate();
}
}
public Color BackColor
{
get
{
return this.backColor;
}
set
{
this.backColor = value;
this.Invalidate();
}
}
public Color SelectionBackColor
{
get
{
return this.selectionBackColor;
}
set
{
this.selectionBackColor = value;
this.Invalidate();
}
}
public Color ForeColor
{
get
{
return this.foreColor;
}
set
{
this.foreColor = value;
this.Invalidate();
}
}
public Color SelectionForeColor
{
get
{
return this.selectionForeColor;
}
set
{
this.selectionForeColor = value;
this.Invalidate();
}
}
private void Invalidate()
{
if (this.TreeView != null)
this.TreeView.Invalidate();
}
public Rectangle LevelRectangle
{
get
{
if (this.ListedRow == null) return Rectangle.Empty;
Rectangle r = new Rectangle(0, 0, this.TreeView.LevelIndentWidth*(1+this.Level), this.ListedRow.Height);
return r;
}
}
public bool HasNodes { get; internal set; }
public Font Font
{
get { return this.font; }
set
{
this.font = value;
if (this.TreeView != null)
this.TreeView.Invalidate();
}
}
#endregion Public Properties
#region Public Methods
public override string ToString()
{
return
"[Name:"+(string.IsNullOrEmpty(this.name) ? "NULL" : this.name) + "]" +
"[ListedIndex:" + this.ListedIndex + "]" +
"[NodeIndex:" + this.NodeIndex + "]" +
"[Level:" + this.Level + "]";
}
public void Expand()
{
Expand(this);
}
public void Collapse()
{
Collapse(this);
}
#endregion Public Methods
#region Internal Methods
internal void AddToGridRows()
{
if(this.TreeView != null)
{
this.AddToGridRows(this);
}
}
#endregion Internal Methods
#region Private Methods
private void Expand(LDataGridViewTreeViewNode node)
{
foreach (LDataGridViewTreeViewNode n in node.Nodes)
{
if (node.Equals(this))
{
n.NodeVisibleState = VisibleState.ExpandVisible;
}
else
{
if(n.Parent.NodeVisibleState == VisibleState.ExpandVisible &&
n.NodeVisibleState == VisibleState.ExpandHidden)
{
n.NodeVisibleState = VisibleState.ExpandVisible;
}
}
Expand(n);
}
}
private void Collapse(LDataGridViewTreeViewNode node)
{
foreach (LDataGridViewTreeViewNode n in node.Nodes)
{
if (node.Equals(this))
{
n.NodeVisibleState = VisibleState.Collapse;
}
else
{
if (n.NodeVisibleState == VisibleState.ExpandVisible)
n.NodeVisibleState = VisibleState.ExpandHidden;
}
Collapse(n);
}
}
private void AddToGridRows(LDataGridViewTreeViewNode node)
{
int index = GetGridIndex(node);
if (node.Font == null)
node.Font = this.TreeView.DefaultCellStyle.Font;
node.TreeView.Rows.Insert(index, node);
node.ListedRow = node.TreeView.Rows[index] as LDataGridViewTreeViewNode;
node.ListedRow.NodeRow = node;
node.ListedRow.Name = node.name+"_L";
node.NodeVisibleState = VisibleState.ExpandVisible;
node.NodeHeight = node.nodeHeight;
for (int i = 0; i < node.Nodes.Count; i++)
{
LDataGridViewTreeViewNode sn = node.Nodes[i];
sn.treeView = this.TreeView;
AddToGridRows(sn);
}
}
private bool GetNodeVisible(LDataGridViewTreeViewNode node)
{
bool v = true;
LDataGridViewTreeViewNode n = node;
while (n.Parent != null)
{
if (n.Parent.IsExpanded == false)
{
v = false;
break;
}
n = n.parent;
}
return v;
}
private int GetGridIndex(LDataGridViewTreeViewNode node)
{
LDataGridViewTreeViewNode parentNode = node.Parent;
int c = parentNode.ListedIndex+1;
for(int i = 0; i < node.NodeIndex; i++)
{
int nc = GetAllFollowNodesCount(parentNode.Nodes[i]);
c += nc+1;
}
return c;
}
private int GetAllFollowNodesCount(LDataGridViewTreeViewNode node)
{
int c = 0;
GetAllFollowNodesCount(node, ref c);
return c;
}
private void GetAllFollowNodesCount(LDataGridViewTreeViewNode node, ref int count)
{
count += node.Nodes.Count;
foreach (LDataGridViewTreeViewNode n in node.Nodes)
{
GetAllFollowNodesCount(n, ref count);
}
}
#endregion Private Methods
}
public class LDataGridViewTreeViewNodeCollection:List
{
private LDataGridViewTreeViewNode owner;
public LDataGridViewTreeViewNodeCollection(LDataGridViewTreeViewNode owner)
{
this.owner = owner;
}
public new void Add(LDataGridViewTreeViewNode node)
{
base.Add(node);
node.Parent=this.owner;
node.NodeIndex=this.Count - 1;
node.AddToGridRows();
owner.HasNodes = true;
}
public new void AddRange(IEnumerable< LDataGridViewTreeViewNode> nodes)
{
for(int i = 0; i < nodes.Count(); i++)
{
this.Add(nodes.ElementAt(i));
}
}
}
public class LDataGridViewTreeView:LDataGridView
{
private LDataGridViewTreeViewNode rootNode;
private static StringFormat sfmt = null;
public LDataGridView dgvDebug;
private int levelIndentWidth=40;
public LDataGridViewTreeView()
{
if(sfmt == null)
{
sfmt = new StringFormat();
sfmt.Alignment = StringAlignment.Near;
sfmt.LineAlignment = StringAlignment.Center;
}
this.rootNode = new LDataGridViewTreeViewNode(this);
this.RootNode.Name = "__RootNode__";
}
internal LDataGridViewTreeViewNode RootNode
{
get { return this.rootNode; }
}
public LDataGridViewTreeViewNodeCollection Nodes
{
get
{
return this.rootNode.Nodes;
}
}
public string NodeTextColumnName { get; set; }
public int LevelIndentWidth
{
get { return this.levelIndentWidth; }
set { this.levelIndentWidth = value; this.Invalidate(); }
}
public Image ImageExpandedArrow { get; set; }
public Image ImageCollapseArrow { get; set; }
public Image ImageNoChildArrow { get; set; }
protected override void OnColumnAdded(DataGridViewColumnEventArgs e)
{
base.OnColumnAdded(e);
e.Column.SortMode = DataGridViewColumnSortMode.NotSortable;
}
protected override void OnCellMouseDoubleClick(DataGridViewCellMouseEventArgs e)
{
base.OnCellMouseDoubleClick(e);
if(e.RowIndex >= 0 && e.ColumnIndex >= 0)
{
LDataGridViewTreeViewNode n = this.Rows[e.RowIndex] as LDataGridViewTreeViewNode;
n = n.NodeRow;
if (n.IsExpanded)
n.Collapse();
else
n.Expand();
}
}
protected override void OnCellMouseClick(DataGridViewCellMouseEventArgs e)
{
base.OnCellMouseClick(e);
if(e.RowIndex>=0 && e.ColumnIndex >=0 && e.Button == MouseButtons.Left)
{
DataGridViewColumn col = this.Columns[e.ColumnIndex];
if (col.Name == this.NodeTextColumnName)
{
LDataGridViewTreeViewNode node = this.Rows[e.RowIndex] as LDataGridViewTreeViewNode;
node = node.NodeRow;
if (node.LevelSquare().Contains(e.Location))
{
if (node.IsExpanded)
node.Collapse();
else
node.Expand();
}
}
}
}
protected override void OnCellPainting(DataGridViewCellPaintingEventArgs e)
{
base.OnCellPainting(e);
if(e.RowIndex>=0 && e.ColumnIndex >= 0)
{
if(this.Columns[e.ColumnIndex].Name == this.NodeTextColumnName)
{
this.OnCellPainting(e,e.RowIndex,e.ColumnIndex);
}
}
}
private void OnCellPainting(DataGridViewCellPaintingEventArgs e,int row,int col)
{
LDataGridViewTreeViewNode node = this.Rows[e.RowIndex] as LDataGridViewTreeViewNode;
node = node.NodeRow;
Rectangle r = e.CellBounds;
Rectangle rText = r;
Graphics g = e.Graphics;
g.PageUnit = GraphicsUnit.Pixel;
bool selected = (e.State & DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected;
Color foreColor = selected ?
(node.SelectionForeColor == Color.Empty ? this.DefaultCellStyle.SelectionForeColor : node.SelectionForeColor)
: (node.ForeColor == Color.Empty ? this.DefaultCellStyle.ForeColor : node.ForeColor);
Color backColor = selected ?
(node.SelectionBackColor == Color.Empty ? this.DefaultCellStyle.SelectionBackColor : node.SelectionBackColor)
: (node.BackColor == Color.Empty ? this.DefaultCellStyle.BackColor : node.BackColor);
e.PaintBackground(e.ClipBounds, selected);
g.FillRectangle(new SolidBrush(backColor), r);
Rectangle rLevel = node.LevelRectangle;
rLevel.Location = r.Location;
Rectangle rLevelSquare = node.LevelSquare(rLevel);
Image levelImage = node.HasNodes?(node.IsExpanded?this.ImageExpandedArrow:this.ImageCollapseArrow):this.ImageNoChildArrow;
if(levelImage != null)
{
g.DrawImage(levelImage, new PointF(rLevelSquare.X + (rLevelSquare.Width- levelImage.Width) / 2, rLevelSquare.Y + (rLevelSquare.Height-levelImage.Height) / 2));
}
rText.X = rLevel.Right;
rText.Width = r.Width - rLevel.Width;
if (node.Image != null)
{
Rectangle rImage = rLevelSquare;
rImage.X += rImage.Width;
g.DrawImage(node.Image, new PointF(rImage.X + (rImage.Width - node.Image.Width) / 2, rImage.Y + (rImage.Height - node.Image.Height) / 2));
rText.X = rImage.Right;
rText.Width = r.Width - rLevel.Width - rImage.Width;
}
g.DrawString(node.Text, node.Font, new SolidBrush(foreColor), rText, sfmt);
e.Handled = true;
}
}
public partial class FormLDataGridViewTreeView : Form
{
public FormLDataGridViewTreeView()
{
InitializeComponent();
this.tv.NodeTextColumnName = "Title";
this.tv.RowTemplate.Height = LCom.PointToPixelH(20);
int imageHeight = this.tv.RowTemplate.Height - this.tv.RowTemplate.Height / 3;
this.tv.LevelIndentWidth = imageHeight;
this.tv.ImageCollapseArrow = LRes.GetImage(imageHeight, LImages.ArrowToRight);
this.tv.ImageExpandedArrow = LRes.GetImage(imageHeight, LImages.ArrowToDown);
this.tv.Columns.Add("Title", "Title");
this.tv.Columns[0].Width = this.tv.Width / 2;
LDataGridViewTreeViewNode node1 = new LDataGridViewTreeViewNode("Node1","01");
node1.Image = LRes.GetImage(imageHeight, LImages.CreateFile);
node1.BackColor = Color.LightGray;
node1.SelectionBackColor = Color.Gray;
node1.ForeColor = Color.Black;
node1.SelectionForeColor = Color.White;
node1.Font = Skin.FormFontBold;
node1.NodeHeight = LCom.PointToPixelH(35);
LDataGridViewTreeViewNode node1_1 = new LDataGridViewTreeViewNode("Node1-1", "01-01");
LDataGridViewTreeViewNode node1_2 = new LDataGridViewTreeViewNode("Node1-2", "01-02");
LDataGridViewTreeViewNode node1_3 = new LDataGridViewTreeViewNode("Node1-3", "01-03");
node1.Nodes.Add(node1_1);
node1.Nodes.Add(node1_2);
node1.Nodes.Add(node1_3);
this.tv.Nodes.Add(node1);
LDataGridViewTreeViewNode node2 = new LDataGridViewTreeViewNode("Node2", "02");
node2.Image = LRes.GetImage(imageHeight, LImages.Form);
this.tv.Nodes.Add(node2);
LDataGridViewTreeViewNode node3 = new LDataGridViewTreeViewNode("Node3", "03");
node3.Image = LRes.GetImage(imageHeight, LImages.Home);
node3.BackColor = Color.LightPink;
node3.SelectionBackColor = Color.Pink;
node3.Collapse();
LDataGridViewTreeViewNode node3_1 = new LDataGridViewTreeViewNode("Node3-1", "03-01");
LDataGridViewTreeViewNode node3_2 = new LDataGridViewTreeViewNode("Node3-2", "03-02");
LDataGridViewTreeViewNode node3_3 = new LDataGridViewTreeViewNode("Node3-3", "03-03");
node3_3.Collapse();
LDataGridViewTreeViewNode node3_3_1 = new LDataGridViewTreeViewNode("Node3-3-1", "03-03_01");
LDataGridViewTreeViewNode node3_3_2 = new LDataGridViewTreeViewNode("Node3-3-2", "03-03_02");
LDataGridViewTreeViewNode node3_3_3 = new LDataGridViewTreeViewNode("Node3-3-3", "03-03_03");
LDataGridViewTreeViewNode node3_3_3_1 = new LDataGridViewTreeViewNode("Node3-3-3-1", "03-03_03-1");
LDataGridViewTreeViewNode node3_3_3_2 = new LDataGridViewTreeViewNode("Node3-3-3-2", "03-03_03-2");
LDataGridViewTreeViewNode node3_3_3_3 = new LDataGridViewTreeViewNode("Node3-3-3-3", "03-03_03-3");
node3_3_3.Nodes.AddRange(new LDataGridViewTreeViewNode[] { node3_3_3_1, node3_3_3_2, node3_3_3_3 });
LDataGridViewTreeViewNode node3_3_4 = new LDataGridViewTreeViewNode("Node3-3-4", "03-03_04");
node3_3.Nodes.AddRange(new LDataGridViewTreeViewNode[] { node3_3_1, node3_3_2, node3_3_3, node3_3_4 });
LDataGridViewTreeViewNode node3_4 = new LDataGridViewTreeViewNode("Node3-4", "03-04");
LDataGridViewTreeViewNode node3_5 = new LDataGridViewTreeViewNode("Node3-5", "03-05");
LDataGridViewTreeViewNode node3_6 = new LDataGridViewTreeViewNode("Node3-6", "03-06");
node3.Nodes.AddRange(new LDataGridViewTreeViewNode[] { node3_1 , node3_2, node3_3, node3_4, node3_5, node3_6 });
this.tv.Nodes.Add(node3);
LDataGridViewTreeViewNode node4 = new LDataGridViewTreeViewNode("Node4", "04");
node4.Image = LRes.GetImage(imageHeight, LImages.SendReceive);
this.tv.Nodes.Add(node4);
this.dgvDebug.Columns.Add("NodeVisibleState", "NodeVisibleState");
for(int i = 0; i < this.tv.Rows.Count; i++)
{
this.dgvDebug.Rows.Add();
}
this.tv.dgvDebug = this.dgvDebug;
}
}