本文主要讲述如何通过使用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);

}

}

就这些通用的代码,到哪需要树,调用一下就有了,显示查询都非常方便,最后效果请见前面的概述

2  如何C#中实现在TreeView查找某一节点

在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;

}

如何:使用TreeView控件实现树结构显示及快速查询的更多相关文章

  1. 在TreeView控件节点中显示图片

    实现效果: 知识运用: TreeView控件中Nodes集合的Add方法 //创建节点并将节点放入集合中 public virtual TreeNode Add (string key,string ...

  2. asp.net TreeView控件绑定数据库显示信息

    using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.We ...

  3. Delphi下Treeview控件基于节点编号的访问

    有时我们需要保存和重建treeview控件,本文提供一种方法,通过以树结构节点的编号访问树结构,该控件主要提供的方法如下:      function GetGlobeNumCode(inNode:T ...

  4. Delphi下Treeview控件基于节点编号的访问1

    有时我们需要保存和重建treeview控件,本文提供一种方法,通过以树结构节点的编号访问树结构,该控件主要提供的方法如下:      function GetGlobeNumCode(inNode:T ...

  5. 部门树形结构,使用Treeview控件显示部门

    部门树形结构.设计张部门表用于存储部门编码.名称.上级部门id,使用Treeview控件显示部门树,并实现部门增删改.移动.折叠等功能.特别提示,部门有层级关系,可用donetbar的adtree控件 ...

  6. [转] C#2010 在TreeView控件下显示路径下所有文件和文件夹

    原文 张丹-小桥流水,C#2010 在TreeView控件下显示路径下所有文件和文件夹 C#2010学习过程中有所收获,便总结下来,希望能给和我一样在学习遇到困难的同学提供参考. 本文主要介绍两个自定 ...

  7. WPF之Treeview控件简单用法

    TreeView:表示显示在树结构中分层数据具有项目可展开和折叠的控件 TreeView 的内容是可以包含丰富内容的 TreeViewItem 控件,如 Button 和 Image 控件.TreeV ...

  8. 【ASP.NET 进阶】TreeView控件学习

    这几天上班没事做,也不好打酱油,学点没接触过的新东西吧,基本了解了下TreeView控件. TreeView 控件用于在树结构中显示分层数据,例如目录或文件目录等. 下面看代码吧: 1.效果图 2.静 ...

  9. Visual Studio 2010下ASPX页面的TreeView控件循环遍历

    如果维护一个老系统就总会遇到各种问题,而这次是TreeView的循环遍历.对于Visual Studio2010上aspx页面的TreeView控件,我感受到了什么叫集微软之大智慧.与二叉树型不一样. ...

随机推荐

  1. QT学习笔记5:QMouseEvent鼠标事件简介

    一.QMouseEvent的详细描述 首先请注意,Qt中的QMouseEvent一般只涉及鼠标左键或右键的单击.释放等操作,而对鼠标滚轮的响应则通过QWheeEvent来处理. QMouseEvent ...

  2. BZOJ 4826: [Hnoi2017]影魔 单调栈 主席树

    https://www.lydsy.com/JudgeOnline/problem.php?id=4826 年少不知空间贵,相顾mle空流泪. 和上一道主席树求的东西差不多,求两种对 1. max(a ...

  3. 让 Git 全局性的忽略 .DS_Store

    让 Git 全局性的忽略 .DS_Store Mac 中每个目录都会有个文件叫.DS_Store, 用于存储当前文件夹的一些 Meta 信息.每次提交代码时,我都要在代码仓库的 .gitignore ...

  4. PIVOT函数与UNPIVOT函数的运用

    PIVOT用于将行转为列,完整语法如下: TABLE_SOURCE PIVOT( 聚合函数(value_column) FOR pivot_column IN(<column_list>) ...

  5. 解决IE11下载文件 文件名乱码问题

    1.Win + R输入gpedit.msc打开组策略编辑器:(不会请看下图) 2.定位到计算机配置→管理模板→windows组件→Internet Explorer→自定义用户代理字符串(有些系统用的 ...

  6. Tasker to stop Poweramp control for the headset while there is an incoming SMS - frozen

    If you usually like to use Poweramp or any other media player to enjoy the music with headset plugge ...

  7. 淘宝--印风 专注于MySQL内核代码

    http://blog.csdn.net/zhaiwx1987/article/details/6113472?utm_source=jiancool

  8. QT 安装 4.8.7 on solaris 10

    1.  下载 QT 4.8.7: http://download.qt.io/official_releases/qt/4.8/4.8.7/qt-everywhere-opensource-src-4 ...

  9. C/C++ 函数指针 总结

    什么是函数指针 就像某一变量的地址可以存储在相应的指针变量中一样,指向函数的指针中保存着函数代码起始处的地址 函数指针的声明 当声明一个函数指针时,必须声明它指向的函数类型.要指定函数类型,就要指出函 ...

  10. Li的前期工作Level_Set_Evolution_Without_Re-initialization_A_New_Variational_Formulation

    注意:因为页面显示原因.里头的公式没能做到完美显示,有须要的朋友请到我的资源中下载 无需进行又一次初始化的水平集演化:一个新的变分公式 Chunming Li , Chenyang Xu , Chan ...