本文主要讲述如何通过使用TreeView控件来实现树结构的显示,以及树节点的快速查找功能。并针对通用树结构的数据结构存储进行一定的分析和设计。通过文本能够了解如何存储层次结构的数据库设计,如何快速使用TreeView控件生产树,以及如何快速查找树节点。
关键词:C# TreeView、树结构存储、树节点查找、层次结构
一、 概述:
树结构(层次结构)在项目的使用中特别常见,在不同项目中使用的控件可能不同(如:在Extjs中使用的是TreePanel控件,WinForm中可能用的是TreeView,等等),即不同的框架或类库所用的控件可能不同,但是数据结构存储基本上相同,最简单的不在乎多了一个父节点ID(ParentID)字段。在实际开发中我们主要会考虑以下问题:
1. 树结构显示的代码重用:即如何快速得到一棵树?传入什么参数就能得到树结构
2. 基本操作:树节点的增删改查
3. 所选节点的信息:如何知道该节点是否为叶子节点,该节点的深度
4. 查找子树:如何快速查找某节点及其所有的子节点
5. 关键词查询:根据某个关键词,查找出一颗子树
如果能快速解决这些问题,那么说明设计的树结构就基本上是可以了,也算是设计的一个检验标准吧!按照常例,我们还是先看一下我们要实现的功能的效果图:
图表 1 树结构显示
图表 2 树结构快速查询
注:以下例子是以中国省市区层次结构来进行说明举例。
二、 树结构通用数据库设计:
最简单的树结构只要记录三个字段即可(ID、Text、ParentID,其中ID为主键,Text为节点显示文字,ParentID为上级节点,无上级节点则为NULL)。这种设计就能满足树结构的数据的存储,非常简单,但是这种方式设计的人是简单了,但是给编程的人就苦了。比如:
1. 把所有的叶子节点的数据全部查询出来
2. 查询出一个列表,按深度降序排序?
3. 最常用的数据权限,如用户只能看到本人所属地区的下级地区结构,如用户属于浙江省,那么我看到的列表就是以浙江省为根节点的子树,用户属于南昌市,那么显示以南昌市为根节点的子树
当然这也能够根据(ID、Text、ParentID)实现上面的三个要求,但是明显开发做的工作就特别多,不敢说难,但至少我感觉没必要。(曾经我就因为别人设计好的表,写了一堆视图,目的就是为了增加Leavl、Leaf等字段,在SQL里面写递归,够害死人的,根据特郁闷)
后来在众多的项目经验中发现对于树结构如下设计主要字段将会使编程人员变的轻松多了,查询也非常简单:
通用树结构表设计方案:
1. 树结构表 Tree:
ID: (PK)主键,唯一标识符,建议为GUID
Text: 节点名称,显示的文字
ParentID: 上级节点/父节点ID
Code: 编码 01、0101、0102
Level: 深度 根节点为0
Leaf: 是否为叶子节点 1:是 0 :否
Sort: 排序
Remark: 备注
Value:对应TreeMapping.Value (可选,如果有该字段,那么可以在一个界面,维护多棵树结构,通用树结构设计方案)
……其他备用字段
2. 树结构映射表TreeMapping
ID: 对应Tree.ID
Value:值(int) 如:0代表部门树 1:代表仪器设备类别树
Text: 说明 如:部门、仪器设备类别
? 设计思路:
1、 通过Tree.Value 值可以查询某一个类别的数结构,例如要查询仪器设备类别树结构数据
SELECT * FROM TTree WHERE FValue=1 -- ORDER BY FLevel,FSort
2、 排序:排序通过深度(Level)和排序字段(Sort)综合决定(ORDER BY FLevel,FSort),Level优先级别更高。这样在新增和编辑时智能排序只需考虑同类别(Value相同)同等级(Level相同)的排序逻辑即可。
3、 查询指定节点下的自身及其所有孩子节点:可借助Code字段实现,例如查询
SELECT * FROM TTree WHERE FCode LIKE '0101%' and FValue=1。
注意:Code和排序没有任何关系,可以是Sort(0102)>Sort(0101)
? 本例中用到的数据库结构:
前面讲述的树结构设计方案是我个人按项目经验设计的,灵活度高很高,适用一切我目前碰见的层次结构。但是当然也有简化版本,像本文例子中的省市区设计就是个简化版本,(也是应公司项目局限,没能按自己的方式设计),如下图所示:
这个项目为Oracle数据库,(注:我的个人习惯是表前加T前缀,字段前加F前缀,希望不会影响大家理解,嘿嘿,个人偏爱SQL Server数据库)。
里面有(ID、Text、ParentID、Level、Leaf、AutoCode、Remark)字段,AutoCode对应通用设计里面的Code,因为有了一个Code编码字段了,这样就比我通用的少了Sort、Value字段,后面的一下字段如(FDataServerIP)都归属为我通用设计里面的备用字段,一般没这么多,这里排序交给了Code,Text组合了。
三、 通用树结构程序:
设计好表以后,应该就是树节点的增删改查了,这里我就不在讲述,毕竟我写这篇文章的标题是“如何:使用TreeView控件实现树结构显示及快速查询”,重点是展示和查询,否则就跑题了。界面很简单:
图表 3 树结构新增/编辑界面
以后有机会我再讲新增编辑删除的后台逻辑删除代码,希望到时候会有人关注。
为了实现树结构的显示和查询,我们先写一个通用类,完整代码如下:
/*
* Copy Right:(C)2011 Twilight Software Development Studio
* Creat By:xuzhihong
* Create Date: 2011-08-04
* Descriptions: 获取Department树
*/
public class GetDepartmentTree
{
public static List<TreeNode> GetTree(DataTable dt)
{
//TreeNodeCollection nodes = null;
List<TreeNode> listNodes = new List<TreeNode>();
foreach (var type in dt.Select("FParentID is null or FParentID=''","FCode,FText ASC"))
{
var node = CreatNode(type);
listNodes.Add(node);
FillChildren(type, node.Nodes, dt);
}
return listNodes;
}
public static List<TreeNode> GetTree(DataTable dt, string keyWord)
{
if (keyWord == "" || keyWord == null)
{
return GetTree(dt);
}
else
{
DataTable dtSlt = dt.Clone();
DataColumn[] primaryKeyColumn = new DataColumn[]
{
dtSlt.Columns["FID"]
};
dtSlt.PrimaryKey = primaryKeyColumn;
DataRow[] rows = dt.Select(string.Format("FText like '%{0}%'",keyWord));
foreach (var row in rows)
{
ImportParentRow(dt, dtSlt, row);
}
return GetTree(dtSlt);
}
}
/// <summary>
/// 创建节点信息
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static TreeNode CreatNode(DataRow type)
{
var entity = GetDeptEnity(type);
return new TreeNode()
{
Text = type["FCode"] + "" + type["FText"],
ToolTipText = string.Format("名称:{0} \r\n编码:{1}\r\n数据服务器:{2}\r\n媒体服务器:{3}", type["FTEXT"], type["FCode"], type["FDATASERVER"],type["FMEDIASERVER"]),
Tag = entity
};
}
/// <summary>
/// 将DataRow转化为实体
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
private static TDepartment GetDeptEnity(DataRow type)
{
return new TDepartment()
{
FID = type["FID"] + "",
FTEXT = type["FTEXT"] + "",
FPARENTID = type["FPARENTID"] + "",
FLEVEL = Convert.ToInt32(type["FLEVEL"]),
FAUTOCODE = type["FAUTOCODE"] + "",
FCODE = type["FCODE"] + "",
FDATASERVERIP = type["FDATASERVERIP"] + "",
FDATASERVERPORT = type["FDATASERVERPORT"] + "",
FDATASERVER = type["FDATASERVER"] + "",
FMEDIASERVERIP = type["FMEDIASERVERIP"] + "",
FMEDIASERVERPORT = type["FMEDIASERVERPORT"] + "",
FMEDIASERVER = type["FMEDIASERVER"] + "",
FREMARK = type["FREMARK"] + "",
FLEAF = Convert.ToInt32((type["FLEAF"]))
};
}
/// <summary>
/// 递归填充子节点
/// </summary>
/// <param name="parentType"></param>
/// <param name="parentNode"></param>
/// <param name="dt"></param>
private static void FillChildren(DataRow parentType, TreeNodeCollectionparentNode, DataTable dt)
{
foreach (var type in dt.Select(string.Format("FParentID='{0}'",parentType["FID"]), "FCode,FText ASC"))
{
var node = CreatNode(type);
parentNode.Add(node);
FillChildren(type, node.Nodes, dt);
}
}
/// <summary>
/// 导入所有父行(包括自己)
/// </summary>
/// <param name="dtSource"></param>
/// <param name="dtSlt"></param>
/// <param name="currentRow"></param>
private static void ImportParentRow(DataTable dtSource, DataTable dtSlt,DataRow currentRow)
{
if (!dtSlt.Rows.Contains(currentRow["FID"])) //不存在则导入行
{
dtSlt.ImportRow(currentRow);
}
if (!string.IsNullOrEmpty(currentRow["FParentID"] + "")) //如果还有父项
{
DataRow row = dtSource.Select(string.Format("FID='{0}'",currentRow["FParentID"]))[0];
ImportParentRow(dtSource, dtSlt, row);
}
}
}
里面都是静态方法,直接调用即可,只要看懂递归函数了,我想大部分就理解了。其中:参数DataTable dt就是select * from TTree,即所有数据。
那么我们使用TreeView控件显示和查询树就只需要调用BLL层中的下面这个方法了:
/// <summary>
/// 根据条件查询,返回查询后的DepartmentTree
/// </summary>
/// <param name="keyWord">关键词,为空表示查询整棵树</param>
/// <returns></returns>
public List<TreeNode> GetTree(string keyWord)
{
DataTable dt = GetList("");// dt就是select * from TTree,即表中所有数据。
return GetDepartmentTree.GetTree(dt,keyWord);
}
返回的是List<TreeNode>刚好适合TreeView用来绑定,如下所示:
/// <summary>
/// 查询 绑定数据源
/// </summary>
/// <param name="keyWord">关键词,为空表示显示整棵树</param>
/// <param name="tv"></param>
public void BindTreeData(TreeView tv, string keyWord)
{
tv.Nodes.Clear();
List<TreeNode> nodes = UsingBLL.department.GetTree(keyWord);
foreach (TreeNode node in nodes)
{
tv.Nodes.Add(node);
}
}
就这些通用的代码,到哪需要树,调用一下就有了,显示查询都非常方便,最后效果请见前面的概述
在TreeView查找某一节点,通常有两种方法,一种是递归的,一种不是递归,但都是深度优先算法。其中,非递归方法效率高些,而递归算法要简洁一些。
第一种,递归算法,代码如下:
private TreeNode FindNode( TreeNode tnParent, string strValue )
{
if( tnParent == null ) return null;
if( tnParent.Text == strValue ) return tnParent;
TreeNode tnRet = null;
foreach( TreeNode tn in tnParent.Nodes )
{
tnRet = FindNode( tn, strValue );
if( tnRet != null ) break;
}
return tnRet;
}
第二种,非递归算法,代码如下:
private TreeNode FindNode( TreeNode tnParent, string strValue )
{
if( tnParent == null ) return null;
if( tnParent.Text == strValue ) return tnParent;
else if( tnParent.Nodes.Count == 0 ) return null;
TreeNode tnCurrent, tnCurrentPar;
//Init node
tnCurrentPar = tnParent;
tnCurrent = tnCurrentPar.FirstNode;
while( tnCurrent != null && tnCurrent != tnParent )
{
while( tnCurrent != null )
{
if( tnCurrent.Text == strValue ) return tnCurrent;
else if( tnCurrent.Nodes.Count > 0 )
{
//Go into the deepest node in current sub-path
tnCurrentPar = tnCurrent;
tnCurrent = tnCurrent.FirstNode;
}
else if( tnCurrent != tnCurrentPar.LastNode )
{
//Goto next sible node
tnCurrent = tnCurrent.NextNode;
}
else
break;
}
//Go back to parent node till its has next sible node
while( tnCurrent != tnParent && tnCurrent == tnCurrentPar.LastNode )
{
tnCurrent = tnCurrentPar;
tnCurrentPar = tnCurrentPar.Parent;
}
//Goto next sible node
if( tnCurrent != tnParent )
tnCurrent = tnCurrent.NextNode;
}
return null;
}
程序调用,如下:
TreeNode tnRet = null;
foreach( TreeNode tn in yourTreeView.Nodes )
{
tnRet = FindNode( tn, yourValue );
if( tnRet != null ) break;
}