最近看到不少程序、网页都有类似C#工具箱的效果,恰好新写一个进销存系统,也想使用这种效果,于是花了点时间仔细研究了一下。
C#中并没有现存的控件可用,仔细观察C#工具箱的效果,开始设想用Graphics对象自绘,利用容器控件(GroupBox,Panel等等)做隐藏显示等功能,都觉得太麻烦。再看工具箱,除了外观以外,分明就是一个TreeView的基本功能。何不看看C#中TreeView控件新增了哪些东西。
C#的TreeView新增了一个DrawNodo属性,看了一下文档,发现这个属性还比较熟悉,应该是和Win32 API中有关Comm32控件组DLL中的某些回调函数类似。以前曾经在VB中使用DLL自绘TreeView、ListView等控件,在C#中应该更容易。
折腾了一番,终于完成了全部功能,记录下过程。
先看DrawNode属性,C#文档中说明为:
Normal
TreeView 由操作系统绘制。
OwnerDrawAll
TreeView 节点的所有元素均为手动绘制,包括图标、复选框、加号和减号以及连接节点的线。
OwnerDrawText
TreeView 节点的标签部分均为手动绘制。其他节点元素由操作系统绘制,包括图标、复选框、加号和减号以及连接节点的线。
只要把属性设置为OwnerDrawAll,即可完全自绘节点外观。
相关属性——DrawNode事件参数DrawTreeNodeEventArgs,事件参数中包含绘制节点的Graphics对象,节点边界Bounds属性,可以根据此属性获得要绘制的节点在TreeView控件中的坐标及大小。State属性,返回要绘制的节点状态,把它和枚举TreeNodeStates中的成员按位运算,即可获得要绘制的节点的当前状态。
新建C# Windows应用程序,添加TreeView控件,命名为treeViewMenu,把DrawNode属性设为OwnerDrawAll,由于需要点击节点所在行即获得NodeClick行为,因此把FullRowSelect设为true,ShowLine设为false(当ShowLine属性为true时,FullRowSelect属性被忽略)。
//增加节点,递归调用
//table 来自数据库中设定好的菜单模块,结构为ID——节点代码,Name——节点名称,Parent——父节点代码
//调用入口为:AddNote(treeViewMenu.Nodes,"0",table)
private void AddNode(TreeNodeCollection nodes,string parent,DataTable table)
{
DataRow[] rows = table.Select("MainMenu='"+parent+"'");
if (rows.Length == 0) return;
for (int i = 0; i < rows.Length; i++)
{
TreeNode node = nodes.Add(rows[i]["Name"].ToString());
node.Name = rows[i]["ID"].ToString();
this.AddNode(node.Nodes, node.Name, table);
}
}
节点的DrawNode事件代码
private void treeViewMenu_DrawNode(object sender, DrawTreeNodeEventArgs e)
{
if (e.Node.Level == 0) //如果是根节点,给节点画一个背景,并使用稍大的字体
{
Rectangle imgBounds = new Rectangle(new Point(0, e.Bounds.Top), new Size(treeViewMenu.Width, 18));
Point textPoint = new Point(imgBounds.Left + 16, imgBounds.Top + 1); //节点文本左上角坐标,预留了节点前加减号的位置。
e.Graphics.DrawImage(nodeBg, imgBounds); //画根节点背景。nodeBg是一个Bitmap对象,存放节点的背景图片,图片的高度应与节点的ItemHeight属性对应,我使用的图片为200*20,因此ItemHeight属性设为20。
Pen pen = new Pen(Brushes.Blue); //根节点字体颜色
e.Graphics.DrawRectangle(pen, imgBounds.X + 4, imgBounds.Y + 2, 10, 10);
e.Graphics.DrawLine(pen, imgBounds.X + 6, imgBounds.Top + 7, imgBounds.Left + 12, imgBounds.Top + 7); //这两个语句画节点展开后的减号,
if (!e.Node.IsExpanded)
e.Graphics.DrawLine(pen, imgBounds.X + 9, imgBounds.Top + 4, imgBounds.Left + 9, imgBounds.Top + 10); //如果节点未展开,则在减号中添加一条线,变成加号
e.Graphics.DrawString(e.Node.Text, new Font("宋体", 10), Brushes.Blue, textPoint); //字体大小为11磅。
}
else
{
//画子节点,当AddNode方法执行完成后,DrawNode事件第一次触发,此时子节点并未显示,因此DrawNode不会绘制子节点。当首次展开一个根节点时,触发事件并执行下列代码,此时e.Bounds为空,无需绘制子节点。
if (!e.Bounds.IsEmpty) //如果子节点的Bounds属性不为空(Empty),绘制该节点。
{
Point textPoint = new Point(e.Bounds.Left + 16, e.Bounds.Top + 4); //子节点文本坐标
Pen pen;
Brush brush;
Rectangle box = new Rectangle(new Point(e.Bounds.Left, e.Bounds.Top), new Size(e.Bounds.Width – 1, e.Bounds.Height – 1)); //当鼠标在子节点上移动时,显示一个带颜色的方框。要达到此效果,需将节点的HotTranking属性设为true。
Rectangle fill = new Rectangle(e.Bounds.Left+1,e.Bounds.Top+1,e.Bounds.Width-2,e.Bounds.Height-2); //填充区域,比方框小一个象素。
if ((e.State & TreeNodeStates.Hot) != 0) //判断鼠标指针是否在该节点上。
{
//定义方框的边框颜色和填充颜色
pen = new Pen(new SolidBrush(Color.FromArgb(49, 106, 197)));
brush = new SolidBrush(Color.FromArgb(193, 210, 238));
}
else
{
//使用背景色擦除之前所画的方框。
brush = new SolidBrush(treeViewMenu.BackColor);
pen = new Pen(new SolidBrush(treeViewMenu.BackColor));
}
e.Graphics.DrawRectangle(pen, box);
e.Graphics.FillRectangle(brush, fill);
//如果节点处于选中状态,绘制一个不同颜色的方框。
if ((e.State & TreeNodeStates.Selected) != 0)
{
brush = new SolidBrush(Color.FromArgb(49, 106, 197));
e.Graphics.FillRectangle(brush, fill);
}
//绘制子节点文本。
e.Graphics.DrawString(e.Node.Text, new Font("宋体", 9), Brushes.Black, textPoint);
}
}
}
节点绘制完成,我们还需要做一些善后,使它更加类似C#工具箱的效果。
首先是单击展开节点,由于FullRowSelect设置为true,因此在节点行的任意位置单击,都将触发NodeMouseClick事件,而不仅仅是在标签上单击才触发该事件,这样我们可以在该事件达到我们想要的效果。
private void treeViewMenu_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
{
//单击展开或收起节点
if (e.Node.IsExpanded)
e.Node.Collapse();
else
e.Node.Expand();
}
最后别忘了,把TreeView的属性ShowPlusMinus属性设置为false,即不显示控件本身在节点前的加减号。虽然节点是自绘的,看不见系统加上去的加减号,但鼠标点击该位置,仍然会使节点展开或收起,这和我们自己处理的单击展开收起节点会有冲突。