参考链接:https://www.cnblogs.com/xm_cpppp/p/3622434.html

来吧,给你的Winform列表控件画个妆

前言

以前看别人的控件好看只有羡慕的份;以前觉得控件重绘是个很复杂的东西;以前知道MSDN很全面很专业却一直没有好好用起来;
作为初级程序猿,不能原地踏步,来吧,让我们一起把 TreeView 美化一下,每天进步一点点!

要点

1、WinForm自定义控件
2、重绘
3、MSDN使用

基础准备:利用MSDN查找我们需要的资料

1、进入MSDN技术资料库:http://msdn.microsoft.com/library
2、在右上角搜索栏里输入 TreeView
3、找到TreeView类(System.Windows.Forms)这一条【应该是搜索结果里的第二条】,里面详尽的介绍了这个类
这里直接附上地址:http://msdn.microsoft.com/zh-cn/library/system.windows.forms.treeview(v=vs.110).aspx
4、TreeView的方法、属性、事件等可以大概浏览下,知道有这个东西就行,继续往下看,我发现了我们需要的东西:
来吧,给你的Winform列表控件画个妆 - 图1
这里的意思是我们可以完全自定义TreeView的外观有2个要点:
  一个是将DrawMode属性设置为TreeViewDrawMode.Normal以外的值;
  一个是在DrawNode事件里我们可以进行对控件的皮肤修改
5、那DrawNode事件怎么用呢?截图中我们看到末尾的 DrawNode 字样是个超级链接,果断点进去一看究竟,于是发现了如下描述:
来吧,给你的Winform列表控件画个妆 - 图2
6、这段文字很详尽的介绍了DrawNode事件,我们可以总结出3个要点:
  a、TreeView提供2种自定义模式,分别是OwnerDrawText和OwnerDrawAll。
OwnerDrawText:允许我们自定义节点的字体样式
OwnerDrawAll:允许我们自定义节点的所有元素样式,包括字体、背景、图标等
  b、TreeNode有2个区域的概念:分别是【点击测试区域】和【整行区域】(整行区域这个名字自己取的,大家只要知道什么意思就行),来张图更容易理解
来吧,给你的Winform列表控件画个妆 - 图3可能区域的范围图中不是很精确,但是我们能够大概的知道 【整行区域】 是这个节点这一行所有的面积,而 【点击测试区域】 是这个节点所有表现元素(图标+字体)所占的面积,从名字上我们也大概可以猜到这个【点击测试区域】跟用户鼠标点击的位置和对应的表现行为(单击选中,双击展开等)有关。
  c、在DrawNode事件中,参数 DrawTreeNodeEventArgs.Bounds 我们可以获取到节点的【整行区域】,而 DrawTreeNodeEventArgs.Node.Bounds 我们可以获取到节点的【点击测试区域】
7、我们MSDN网页继续向下看,发现还有一段代码示例如下:
  ps:示例什么的最好了,又直接又快捷,你懂的。而且MSDN里面的代码考虑都能周全,copy下来直接放到新建的程序里稍作调整就可以直接运行看到效果,然后我们可以逐行慢慢分析理解

  1. using System;
  2. using System.Drawing;
  3. using System.Windows.Forms;
  4. public class TreeViewOwnerDraw : Form
  5. {
  6. private TreeView myTreeView;
  7. // Create a Font object for the node tags.
  8. Font tagFont = new Font("Helvetica", 8, FontStyle.Bold);
  9. public TreeViewOwnerDraw()
  10. {
  11. // Create and initialize the TreeView control.
  12. myTreeView = new TreeView();
  13. myTreeView.Dock = DockStyle.Fill;
  14. myTreeView.BackColor = Color.Tan;
  15. myTreeView.CheckBoxes = true;
  16. // Add nodes to the TreeView control.
  17. TreeNode node;
  18. for (int x = 1; x < 4; ++x)
  19. {
  20. // Add a root node to the TreeView control.
  21. node = myTreeView.Nodes.Add(String.Format("Task {0}", x));
  22. for (int y = 1; y < 4; ++y)
  23. {
  24. // Add a child node to the root node.
  25. node.Nodes.Add(String.Format("Subtask {0}", y));
  26. }
  27. }
  28. myTreeView.ExpandAll();
  29. // Add tags containing alert messages to a few nodes
  30. // and set the node background color to highlight them.
  31. myTreeView.Nodes[1].Nodes[0].Tag = "urgent!";
  32. myTreeView.Nodes[1].Nodes[0].BackColor = Color.Yellow;
  33. myTreeView.SelectedNode = myTreeView.Nodes[1].Nodes[0];
  34. myTreeView.Nodes[2].Nodes[1].Tag = "urgent!";
  35. myTreeView.Nodes[2].Nodes[1].BackColor = Color.Yellow;
  36. // Configure the TreeView control for owner-draw and add
  37. // a handler for the DrawNode event.
  38. myTreeView.DrawMode = TreeViewDrawMode.OwnerDrawText;
  39. myTreeView.DrawNode +=
  40. new DrawTreeNodeEventHandler(myTreeView_DrawNode);
  41. // Add a handler for the MouseDown event so that a node can be
  42. // selected by clicking the tag text as well as the node text.
  43. myTreeView.MouseDown += new MouseEventHandler(myTreeView_MouseDown);
  44. // Initialize the form and add the TreeView control to it.
  45. this.ClientSize = new Size(292, 273);
  46. this.Controls.Add(myTreeView);
  47. }
  48. // Clean up any resources being used.
  49. protected override void Dispose(bool disposing)
  50. {
  51. if (disposing)
  52. {
  53. tagFont.Dispose();
  54. }
  55. base.Dispose(disposing);
  56. }
  57. [STAThreadAttribute()]
  58. static void Main()
  59. {
  60. Application.Run(new TreeViewOwnerDraw());
  61. }
  62. // Draws a node.
  63. private void myTreeView_DrawNode(
  64. object sender, DrawTreeNodeEventArgs e)
  65. {
  66. // Draw the background and node text for a selected node.
  67. if ((e.State & TreeNodeStates.Selected) != 0)
  68. {
  69. // Draw the background of the selected node. The NodeBounds
  70. // method makes the highlight rectangle large enough to
  71. // include the text of a node tag, if one is present.
  72. e.Graphics.FillRectangle(Brushes.Green, NodeBounds(e.Node));
  73. // Retrieve the node font. If the node font has not been set,
  74. // use the TreeView font.
  75. Font nodeFont = e.Node.NodeFont;
  76. if (nodeFont == null) nodeFont = ((TreeView)sender).Font;
  77. // Draw the node text.
  78. e.Graphics.DrawString(e.Node.Text, nodeFont, Brushes.White,
  79. Rectangle.Inflate(e.Bounds, 2, 0));
  80. }
  81. // Use the default background and node text.
  82. else
  83. {
  84. e.DrawDefault = true;
  85. }
  86. // If a node tag is present, draw its string representation
  87. // to the right of the label text.
  88. if (e.Node.Tag != null)
  89. {
  90. e.Graphics.DrawString(e.Node.Tag.ToString(), tagFont,
  91. Brushes.Yellow, e.Bounds.Right + 2, e.Bounds.Top);
  92. }
  93. // If the node has focus, draw the focus rectangle large, making
  94. // it large enough to include the text of the node tag, if present.
  95. if ((e.State & TreeNodeStates.Focused) != 0)
  96. {
  97. using (Pen focusPen = new Pen(Color.Black))
  98. {
  99. focusPen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot;
  100. Rectangle focusBounds = NodeBounds(e.Node);
  101. focusBounds.Size = new Size(focusBounds.Width - 1,
  102. focusBounds.Height - 1);
  103. e.Graphics.DrawRectangle(focusPen, focusBounds);
  104. }
  105. }
  106. }
  107. // Selects a node that is clicked on its label or tag text.
  108. private void myTreeView_MouseDown(object sender, MouseEventArgs e)
  109. {
  110. TreeNode clickedNode = myTreeView.GetNodeAt(e.X, e.Y);
  111. if (NodeBounds(clickedNode).Contains(e.X, e.Y))
  112. {
  113. myTreeView.SelectedNode = clickedNode;
  114. }
  115. }
  116. // Returns the bounds of the specified node, including the region
  117. // occupied by the node label and any node tag displayed.
  118. private Rectangle NodeBounds(TreeNode node)
  119. {
  120. // Set the return value to the normal node bounds.
  121. Rectangle bounds = node.Bounds;
  122. if (node.Tag != null)
  123. {
  124. // Retrieve a Graphics object from the TreeView handle
  125. // and use it to calculate the display width of the tag.
  126. Graphics g = myTreeView.CreateGraphics();
  127. int tagWidth = (int)g.MeasureString
  128. (node.Tag.ToString(), tagFont).Width + 6;
  129. // Adjust the node bounds using the calculated value.
  130. bounds.Offset(tagWidth/2, 0);
  131. bounds = Rectangle.Inflate(bounds, tagWidth/2, 0);
  132. g.Dispose();
  133. }
  134. return bounds;
  135. }
  136. }

8、这段代码里我发现了几个实用的:
  1、TreeView 的 GetNodeAt 方法支持根据鼠标点击坐标获取对应节点:
    TreeNode clickedNode = myTreeView.GetNodeAt(e.X, e.Y);
  2、TreeView 的 CreateGraphics 方法可以创建当前实例的 Graphics 对象:
    Graphics g = myTreeView.CreateGraphics();
    g.Dispose();
  3、DrawNode事件中的节点状态判断写法:
    例如判断当前节点为选中状态 if ((e.State & TreeNodeStates.Selected) != 0)
  4、描绘文字的方法:
    e.Graphics.DrawString(e.Node.Text, nodeFont, Brushes.White,Rectangle.Inflate(e.Bounds, 2, 0));
  5、描绘背景的方法:
    e.Graphics.DrawRectangle(focusPen, focusBounds);
9、好的,准备工作差不多了,开始我们的皮肤美化之旅吧!

项目创建

步骤一:新建Winfrom项目,并新建一个自定义控件
步骤二:修改MyTreeView类继承TreeView,注意这里需要添加引用 using System.Windows.Forms;
步骤三:根据自己需要修改样式,下面是我的代码:

  1. public partial class BaseTreeView : TreeView
  2. {
  3. Color drawTextColor = Color.FromArgb(81, 81, 81);
  4. public BaseTreeView()
  5. {
  6. InitializeComponent();
  7. this.DrawMode = TreeViewDrawMode.OwnerDrawAll;
  8. this.FullRowSelect = true;
  9. this.ItemHeight = 23;
  10. this.HotTracking = true;
  11. this.ShowLines = true;
  12. }
  13. protected override void OnDrawNode(DrawTreeNodeEventArgs e)
  14. {
  15. base.OnDrawNode(e);
  16. //节点背景绘制
  17. if (e.Node.IsSelected)
  18. {
  19. e.Graphics.DrawImage(Resources.tree_Selected, e.Bounds);
  20. }
  21. else if ((e.State & TreeNodeStates.Hot) != 0)//|| currentMouseMoveNode == e.Node)
  22. {
  23. e.Graphics.DrawImage(Resources.tree_Hover, e.Bounds);
  24. }
  25. else
  26. {
  27. e.Graphics.FillRectangle(Brushes.White, e.Bounds);
  28. }
  29. //节点头图标绘制
  30. if (e.Node.IsExpanded)
  31. {
  32. e.Graphics.DrawImage(Resources.tree_NodeExpend, e.Node.Bounds.X - 12, e.Node.Bounds.Y + 6);
  33. }
  34. else if (e.Node.IsExpanded == false && e.Node.Nodes.Count > 0)
  35. {
  36. e.Graphics.DrawImage(Resources.tree_NodeCollaps, e.Node.Bounds.X - 12, e.Node.Bounds.Y + 6);
  37. }
  38. //文本绘制
  39. using (Font foreFont = new Font(this.Font, FontStyle.Regular))
  40. using (Brush drawTextBrush = new SolidBrush(drawTextColor))
  41. {
  42. e.Graphics.DrawString(e.Node.Text, foreFont, drawTextBrush, e.Node.Bounds.Left + 5, e.Node.Bounds.Top + 5);
  43. }
  44. }
  45. protected override void OnMouseDoubleClick(MouseEventArgs e)
  46. {
  47. base.OnMouseDoubleClick(e);
  48. TreeNode tn = this.GetNodeAt(e.Location);
  49. //调整【点击测试区域】大小,包括图标
  50. Rectangle bounds = new Rectangle(tn.Bounds.Left - 12, tn.Bounds.Y, tn.Bounds.Width - 5, tn.Bounds.Height);
  51. if (tn != null && bounds.Contains(e.Location) == false)
  52. {
  53. if (tn.IsExpanded == false)
  54. tn.Expand();
  55. else
  56. tn.Collapse();
  57. }
  58. }
  59. protected override void OnMouseClick(MouseEventArgs e)
  60. {
  61. base.OnMouseClick(e);
  62. TreeNode tn = this.GetNodeAt(e.Location);
  63. this.SelectedNode = tn;
  64. }
  65. TreeNode currentNode = null;
  66. protected override void OnMouseMove(MouseEventArgs e)
  67. {
  68. base.OnMouseMove(e);
  69. TreeNode tn = this.GetNodeAt(e.Location);
  70. Graphics g = this.CreateGraphics();
  71. if (currentNode != tn)
  72. {
  73. //绘制当前节点的hover背景
  74. if (tn != null)
  75. OnDrawNode(new DrawTreeNodeEventArgs(g, tn, new Rectangle(0, tn.Bounds.Y, this.Width, tn.Bounds.Height), TreeNodeStates.Hot));
  76. //取消之前hover的节点背景
  77. if (currentNode != null)
  78. OnDrawNode(new DrawTreeNodeEventArgs(g, currentNode, new Rectangle(0, currentNode.Bounds.Y, this.Width, currentNode.Bounds.Height), TreeNodeStates.Default));
  79. }
  80. currentNode = tn;
  81. g.Dispose();
  82. }
  83. protected override void OnMouseLeave(EventArgs e)
  84. {
  85. base.OnMouseLeave(e);
  86. //移出控件时取消Hover背景
  87. if (currentNode != null)
  88. {
  89. Graphics g = this.CreateGraphics();
  90. OnDrawNode(new DrawTreeNodeEventArgs(g, currentNode, new Rectangle(0, currentNode.Bounds.Y, this.Width, currentNode.Bounds.Height), TreeNodeStates.Default));
  91. }
  92. }
  93. }

 效果如下图:深蓝色为节点选中时的效果,淡蓝色为鼠标停留在节点上的效果
    来吧,给你的Winform列表控件画个妆 - 图4来吧,给你的Winform列表控件画个妆 - 图5来吧,给你的Winform列表控件画个妆 - 图6
说明1:因为默认的TreeView点击非【点击测试区域】时是不触发动作效果的【单击节点选中效果、双击节点展开等效果】
原本想尝试修改节点的【点击测试区域】扩大到【整行区域】,后来没有找到方法,所以只能变通的实现这个效果,因此在我的代码里在其它事件里也进行了处理,不过还好我要的效果是实现了
说明2:demo下载