一、思想创意
树形控件是软件操作界面中最常见的一种数据展现控件,如下图所示:
图1
这就是我们常见的树形结构,目前有很多树控件可以供我们选择使用,如zTree、Ext中的树控件,等等。
树形控件这么多,但实现的功能就是上面这张图,树形结构图,用来展现具有层次结构的数据项。那么树形控件能否改进一下,实现更加灵活、便捷的功能呢?让我们先来看几个用例场景。
这是某某管理信息系统的操作界面,如图所示:
图2
界面中有一个输入域是选择家庭住址,采用的是下拉树的形式。默认展开第一层节点,也就是北京市下属的“区”名称,操作员可以通过点击“区”名称前面的加号,展开某个区,例如上图中,操作员展开了 “北京市→顺义区→木林镇→荣各庄村”,家庭住址细化到村一级。利用下拉树的形式,让操作员选择,可以避免操作员手工输入时发生的错误。但是这样做也有缺点,操作员选择起来很麻烦,北京市顺义区下面的村镇有很多,操作员要找到某个村,很费劲,得展开很多级节点,而且还要在数百个村镇中找到要选择的那一个。
能不能在下拉树的基础上,改进一下,使得操作员选择起来更加快捷、方便呢?看下面这个图,这是改进之后的界面:
图3
在下拉树上面增加几个功能,一个叫“排序”,一个叫“恢复默认”,还有一个叫“搜索”,下面依次介绍这几个功能。
1、搜索
先说搜索功能,这里所谓的搜索,就是操作员在输入框中输入一个关键字,然后点击“搜索”按钮,下面展示出包含关键字的树形结构,例如操作员在输入框中输入“村”字,点击搜索按钮,界面显示如下:
图4
下拉树中显示出带有“村”字的地名,包括二郎庙村、一街村、荣各庄村。如果操作员想选择“荣各庄村”的话,很容易就找到了。这个功能就是通过搜索树节点的关键字来缩小查找范围,使得操作员更快捷的找到所需选项。
这个功能有点类似于列表查询功能,如图所示:
图5
在查询条件输入框中输入查询关键字,点击查询按钮,列表中显示出满足条件的查询结果,供用户选择。
但是对树形结构的搜索是比较复杂的,需要从整棵树的根节点开始,逐个节点过滤,把包含关键字的节点全部找到,然后给用户展示出来。要实现这个功能,首先需要对整棵树进行全加载,而不是延迟加载,然后在客户端利用JavaScript编程实现节点搜索功能,最后把搜索出的节点展示出来。
搜索功能先介绍到这儿,后面会详细介绍实现方法。
2、排序
有了搜索功能,操作员就很容易通过关键字找到他所需的选项,但是有些操作员觉得输入文字比较麻烦,能不能不用输入文字,也能更快的找到所需选项呢?这就是排序功能。
这个功能是一种智能化的功能,这个排序不是简单的列表排序,一般的列表排序如下图所示:
图6
上图中的数据列表,表头列上有『上下箭头(∧∨)』图标,用户点击排序箭头后,系统按该列进行排序,每一列表头上都有排序图标,用户可以按照任何一列进行排序。
树形结构默认也有顺序,一般是按照节点编号或节点名称排序,每一层节点都按照节点编号或节点名称排序,类似于Oracle中的层次查询排序,也就是兄弟节点排序,这是通常的排序方式。
那么,我们这里所说的“排序”指的是什么意思呢?这里所说的排序方式不是按照节点编号或者节点名称进行兄弟节点排序,而是按照节点的“选择次数”排序。
“选择次数”指的就是某一节点被操作员选择的次数(也就是被操作员点击的次数)。例如,“北京市→顺义区→木林镇→荣各庄村”,荣各庄村被操作员选择过8次,荣各庄村的选择次数就是8; 而“北京市→顺义区→杨镇→李各庄村”,李各庄村被操作员选择过3次,李各庄村的选择次数就是3。按“选择次数”排序,可以使选择次数高的节点排在树形结构的前面,使操作员可以优先找到选择次数高的节点,这些节点很可能就是操作员这一次选择时仍然要选择的节点(至少选择的概率比较大)。例如,荣各庄村被选择过8次,李各庄村被选择过3次,那么,操作员这一次还有可能选择荣各庄村,这是一种合理的推论,就是说操作员以前频繁选择哪个节点,那么下一次还可能选择那个节点。把选择次数高的节点排在树形结构的前面,有利于操作员寻找他频繁选择过的节点。
看下面两张图,
图7
图8
图7是操作员点击『排序』之前的下拉树界面,图8是操作员点击『排序』之后的下拉树界面,假如荣各庄村被操作员选择过8次,其它村庄的选择次数都比荣各庄村少,操作员点击『排序』之后,系统经过计算,将“北京市→顺义区→木林镇→荣各庄村”排在了树形结构的前面,顺义区排在了海淀区前面,木林镇也排在了杨镇前面,而排序之前,顺义区是排在海淀区后面的,木林镇是排在杨镇之后的。树形结构的排序比普通的列表排序要复杂,它复杂在:移动子节点的同时,也要移动它的父节点,这样才能保持树形结构的完整性,也就是说无论怎么排序,它都是一棵完整的树形结构,并且父子关系不能发生变化,“北京市→顺义区→木林镇→荣各庄村”整条路径都要向上移动。
这就是树形结构的排序,不是按照节点编号或节点名称排序,而是按照节点的选择次数排序。排序之后不能破坏原有的父子关系,要保持树的完整性。这个『排序』也可以叫做『按选择次数排序』。
排序功能先介绍到这儿,后面会详细介绍实现方法。
3、恢复默认
当操作员点击『排序』按钮后,下拉树按照节点选择次数排序,如果要恢复成默认排序方式,也就是按节点编号排序或节点名称排序,那么,点击一下『恢复默认』按钮,就可以恢复成原来的默认排序方式。
当操作员点击『搜索』按钮后,下拉树显示出筛选后的树形结构数据,如果要恢复成全部树形结构数据,那么,点击一下『恢复默认』按钮,就可以恢复成原来的数据了。
这就是『恢复默认』功能,后面会详细介绍实现方法。
二、概要设计
要实现 『搜索』和 『按选择次数排序』,需要将JavaScript树形控件和后台Java程序结合起来,同时还需要使用一种数据格式:JSON嵌套格式(也叫做树形结构JSON),这种数据格式在Ext的树形控件 和 zTree中都有使用。
如图:
图9
这里所用的JavaScript树形控件需要支持JSON嵌套格式,便于客户端编程处理,因为JSON格式很容易转换成JavaScript对象,我猜测JavaScript树形控件也是利用JSON的这个优势,通过先序遍历JavaScript树状对象,在Html页面上打印出树形视图。『搜索』 和 『按选择次数排序』都是建立在这个JavaScript树状对象之上的。
这个树状的JavaScript对象是由JSON嵌套格式转换而来的,其实一个 eval() 方法就可以把JSON格式的数据转换成JavaScript树状对象。
JavaScript树形控件的工作原理,如下:
图10
『搜索』 和 『按选择次数排序』都需要在JavaScript树形控件上做文章。如图所示:
图11
我做了一个带有『搜索』 和 『按选择次数排序』功能的树形菜单Demo演示程序,先看一下图例:
图12
这是一个展示北京郊区村镇的树形结构视图,一共四级节点:市、区、镇、村。在这个树形视图上面增加了两个功能:『按选择次数排序』和『搜索』,还有一个按钮叫『恢复默认』。
下面对该Demo做一下演示:
1、搜索
先在输入框中输入“荣各庄”三个字,点击『搜索』按钮,
图13
图14
“荣各庄村”被搜索出来了,其它的菜单节点都被隐藏了,只筛选出被搜索的节点。用户很容易就找到“荣各庄村”这个节点了,很方便吧。
再试一次,输入“杨镇”二字,点击『搜索』按钮,
图15
杨镇和它下面的三个村子都被搜索出来,也就是说,如果搜索的节点下面有孩子节点,那么该节点和它的孩子节点都被搜索出来,方便用户选择。
2、按选择次数排序
这个功能可以把选择次数高的节点排在前面,选择次数少的节点排在后面,方便用户选择以前曾经选过的频度高的节点。
先点击三次“荣各庄村”这个节点,
图16
再点击一次“二郎庙村”这个节点,
图17
点击『按选择次数排序』按钮,结果如下:
图18
“荣各庄村”和“二郎庙村”都排在了前面,它们的父节点也都排在了前面,也就是说,节点的排序是整条路径排序,例如“北京市→顺义区→木林镇→荣各庄村”就是一条完整路径,这条路径整体都移动到了最前面,杨镇排在了第二位,“二郎庙村”在杨镇下面的三个村中排在了第一位,原来它是第三位。
『按选择次数排序』和普通的『按节点编号排序』或『按节点名称排序』是不一样的,它的排序规则是有逻辑性的。『按选择次数排序』也可以叫做“路径权值”排序,它采用“兄弟节点横向排序”方法来实现,后面会详细介绍这些概念。
3、 恢复默认
这个功能很简单,就是恢复成树形菜单的初始状态。
图19
通过『按选择次数排序』和『搜索』这两个功能,用户可以方便的对树形视图进行筛选和排序,『按选择次数排序』是一种智能化的功能,系统自动计算出选择次数高的节点,将它们由高到低排列,它建立在一种合理的推论之上:用户以前频繁选择的节点,这次可能还要选择它们,至少选择它们的概率比较大。『搜索』功能也是很实用的,在实际的应用系统中,树形控件的数据量很大,有的多达数百个节点或上千个节点,用户可能只记得某个节点大概的名字,具体名字记不住,要想找到它们,只要在搜索框中输入关键字,就可以筛选出包含该关键字的节点了,缩小了选择范围,更加方便用户。
三、详细设计
那么如何具体实现这些功能呢,下面介绍具体的实现原理。
在概要设计中,已经画了很多图了,尤其是图10和图11,画出了树形控件的工作原理,但还不够详细,现在我详细介绍『按选择次数排序』和『搜索』这两个功能的实现方法。下面是zTree的树形控件Demo演示程序,是我从http://www.ztree.me/v3/demo.php#_101 这个网站上下载的,那是zTree V3.5版本的介绍网站,网站截图如下:
图20
如上图中,点击界面右上角的『下载zTree v3.5.12』链接,把Demo演示程序下载下来。
在Demo中有一个文件,完整路径名称是:
JQuery zTree v3.5.12\demo\cn\core\standardData.html,它演示的是如何展现出树形结构。
这个文件的源代码如下:
standardData.html
- <!DOCTYPE html>
- <HTML>
- <HEAD>
- <TITLE> ZTREE DEMO - Standard Data </TITLE>
- <meta http-equiv="content-type" content="text/html; charset=UTF-8">
- <link rel="stylesheet" href="../../../css/demo.css" type="text/css">
- <link rel="stylesheet" href="../../../css/zTreeStyle/zTreeStyle.css" type="text/css">
- <script type="text/javascript" src="../../../js/jquery-1.4.4.min.js"></script>
- <script type="text/javascript" src="../../../js/jquery.ztree.core-3.5.js"></script>
- <SCRIPT type="text/javascript">
- <!--
- var setting = { };
- var zNodes =[
- { name:"父节点1 - 展开", open:true,
- children: [
- { name:"父节点11 - 折叠",
- children: [
- { name:"叶子节点111"},
- { name:"叶子节点112"},
- { name:"叶子节点113"},
- { name:"叶子节点114"}
- ]},
- { name:"父节点12 - 折叠",
- children: [
- { name:"叶子节点121"},
- { name:"叶子节点122"},
- { name:"叶子节点123"},
- { name:"叶子节点124"}
- ]},
- { name:"父节点13 - 没有子节点", isParent:true}
- ]},
- { name:"父节点2 - 折叠",
- children: [
- { name:"父节点21 - 展开", open:true,
- children: [
- { name:"叶子节点211"},
- { name:"叶子节点212"},
- { name:"叶子节点213"},
- { name:"叶子节点214"}
- ]},
- { name:"父节点22 - 折叠",
- children: [
- { name:"叶子节点221"},
- { name:"叶子节点222"},
- { name:"叶子节点223"},
- { name:"叶子节点224"}
- ]},
- { name:"父节点23 - 折叠",
- children: [
- { name:"叶子节点231"},
- { name:"叶子节点232"},
- { name:"叶子节点233"},
- { name:"叶子节点234"}
- ]}
- ]},
- { name:"父节点3 - 没有子节点", isParent:true}
- ];
- $(document).ready(function(){
- $.fn.zTree.init($("#treeDemo"), setting, zNodes);
- });
- //-->
- </SCRIPT>
- </HEAD>
- <BODY>
- <h1>最简单的树 -- 标准 JSON 数据</h1>
- <h6>[ 文件路径: core/standardData.html ]</h6>
- <div class="content_wrap">
- <div class="zTreeDemoBackground left">
- <ul id="treeDemo" class="ztree"></ul>
- </div>
- <div class="right">
- <ul class="info">
- </ul>
- </div>
- </div>
- </BODY>
- </HTML>
这就是实现一棵树最简单的方法。
要想实现我上面所提出的功能,需要增加很多代码,但这些代码与zTree无关,不会对zTree造成任何影响,而是在JSON嵌套格式数据上做文章。
修改后的完整代码如下:(这些代码可以直接拷贝下来运行测试,文件名称还叫“standardData.html”,
可以把该文件覆盖zTree V3.5 Demo演示程序中的同名文件:
JQuery zTree v3.5.12\demo\cn\core\standardData.html,覆盖之后,用IE浏览器打开运行,查看演示效果。)
standardData.html
- <!DOCTYPE html>
- <HTML>
- <HEAD>
- <TITLE> ZTREE DEMO - Standard Data </TITLE>
- <meta http-equiv="content-type" content="text/html; charset=UTF-8">
- <link rel="stylesheet" href="../../../css/demo.css" type="text/css">
- <link rel="stylesheet" href="../../../css/zTreeStyle/zTreeStyle.css" type="text/css">
- <script type="text/javascript" src="../../../js/jquery-1.4.4.min.js"></script>
- <script type="text/javascript" src="../../../js/jquery.ztree.core-3.5.js"></script>
- <SCRIPT type="text/javascript">
- <!--
- // 树形结构的数据在这里是写死的,实际应用中,是从数据库中读出的,存在request对象中,
- // 然后用request.getAttribute()方法,从一个变量中读出来,
- // 实际应用中应该是:var zNodes = <%=request.getAttribute("data")%> ,是从request中动态读出来的,
- // 关于如何拼接生成这种JSON嵌套格式的字符串,可以参考《附录一:多叉树结合JavaScript树形控件实现
- // 无限级树形结构(一种构建多级有序树形结构JSON(或XML)数据源的方法)》,
- // 《附录一》是专门介绍如何生成JSON嵌套格式的数据,
- // 注:下面的这个zNodes数据格式,我是在原有zTree标准数据格式的基础上扩展了一些属性,凡是以ex_开头
- // 的属性都是我扩展增加的,标准数据格式中没有,这些额外增加的属性,zTree是
- // 不会解析的,是我用来实现『按节点选择次数排序』和『搜索』功能时用到的,后台Java程序在构造数据时,
- // 需要构造成下面这个样子。
- //
- // (注:扩展属性的含义:
- // ex_uid:节点编号
- // ex_pid:父节点编号
- // ex_weight:节点选择次数(权值)
- // ex_visible:节点可见性
- // ex_parentNode:父节点引用)
- var zNodes = [
- {
- name: '北京市',
- ex_uid: '0',
- ex_pid: '',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '顺义区',
- ex_uid: '2',
- ex_pid: '0',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '杨镇',
- ex_uid: '3',
- ex_pid: '2',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '李各庄村',
- ex_uid: '4',
- ex_pid: '3',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- },
- {
- name: '一街村',
- ex_uid: '5',
- ex_pid: '3',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- },
- {
- name: '二郎庙村',
- ex_uid: '6',
- ex_pid: '3',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- }
- ]
- },
- {
- name: '木林镇',
- ex_uid: '7',
- ex_pid: '2',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '荣各庄村',
- ex_uid: '8',
- ex_pid: '7',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- }
- ]
- },
- {
- name: '龙湾屯镇',
- ex_uid: '9',
- ex_pid: '2',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '王泮庄村',
- ex_uid: '15',
- ex_pid: '9',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- }
- ]
- },
- {
- name: '李遂镇',
- ex_uid: '10',
- ex_pid: '2',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '柳各庄村',
- ex_uid: '14',
- ex_pid: '10',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- }
- ]
- }
- ]
- },
- {
- name: '平谷区',
- ex_uid: '11',
- ex_pid: '0',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '夏各庄镇',
- ex_uid: '12',
- ex_pid: '11',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null,
- children: [
- {
- name: '马各庄村',
- ex_uid: '13',
- ex_pid: '12',
- ex_weight: 0,
- ex_visible: true,
- ex_parentNode: null
- }
- ]
- }
- ]
- }
- ]
- }
- ];
- var setting = {
- callback: {
- onClick: clickNode
- }
- };
- // 单击节点后触发的事件,
- // 在这个方法里,做两件事:
- //
- // 1、增加节点所在路径的权值(也叫做『路径向上加权』),这里的『路径向上加权』使用的方法与
- // 《附录二:新概念『智能树形菜单』--利用加权多叉树结合JavaScript树形控件实现》中的路径加权方法不一样,这是由于在JavaScript中无法建立
- // 像Java中的那种带有双向引用的多叉树结构(即父节点引用子节点,子节点引用父节点),在JavaScript中如果做这种双向引用的话,会造成
- // 『Stack overflow』异常,所以只能分别建立两棵多叉树对象,一棵是原始树形结构对象,另一棵是利用nodeMap建立的多叉树对象,专门用于
- // 反向引用,即子节点对父节点的引用。而在Java中,直接可以根据一个节点的父节点引用,找到它所有的父节点。但是在这里,只能采用一种笨
- // 办法,先从反向引用的多叉树中找到某一节点的所有父节点,存在一个数组里,然后在原始树形结构对象中使用先序遍历方法,从顶向下依次查找,
- // 把某一节点的所有父节点的权值加1,效率较低,但与利用反向引用查找父节点的方法目的是一样的。
- //
- // 2、更新节点选择次数到数据库中,以备下次登录系统时恢复原数据
- function clickNode(event, treeId, treeNode, clickFlag) {
- var parentNodes = [];
- var currentNode = nodeMap['_' + treeNode.ex_uid];
- var parentNode = currentNode.ex_parentNode;
- while (parentNode != null) {
- parentNodes.push(parentNode);
- parentNodeparentNode = parentNode.ex_parentNode;
- }
- parentNodes.push(currentNode);
- increaseNodesWeight(zNodes[0], parentNodes);
- // 更新节点选择次数到数据库中,以备下次登录系统时恢复原数据
- modifyNodeWeigthToDB();
- }
- // 更新节点选择次数到数据库中,
- // 由于没有数据库,所以暂时不实现这个方法,实际应用时需要实现该方法
- function modifyNodeWeigthToDB() {
- // todo
- }
- // 增加节点所在路径的权值
- function increaseNodesWeight(node, parentNodes) {
- if (containNode(node, parentNodes)) {
- node.ex_weight++;
- }
- if (node.children && node.children.length != 0) {
- var i = 0;
- for (; i < node.children.length; i++) {
- if (containNode(node, parentNodes)) {
- increaseNodesWeight((node.children)[i], parentNodes);
- }
- }
- // 如果在本层节点中没有找到要增加权值的节点,说明需要增加权值的节点都已经找完了,
- // 不需要再向下一层节点中寻找了,直接退出递归函数
- if (i == node.children.length - 1) {
- return;
- }
- }
- }
- // 排序方法:按照节点选择次数排序【冒泡法排序】
- // 节点选择次数大的排在前面,如果次数相等,按照编号排,编号小的排在前面
- function bubbleSortByWeight(theArray) {
- var temp;
- for (var i = 0; i < theArray.length-1; i++) {
- for (var j = theArray.length - 1; j > i ; j--) {
- if (theArray[j].ex_weight > theArray[j - 1].ex_weight) {
- temp = theArray[j];
- theArray[j] = theArray[j - 1];
- theArray[j - 1] = temp;
- } else if (theArray[j].ex_weight == theArray[j - 1].ex_weight) {
- if (theArray[j].ex_uid < theArray[j - 1].ex_uid) {
- temp = theArray[j];
- theArray[j] = theArray[j - 1];
- theArray[j - 1] = temp;
- }
- }
- }
- }
- }
- // 排序方法:按照节点编号排序,编号小的排在前面【冒泡法排序】
- function bubbleSortByUid(theArray) {
- var temp;
- for (var i = 0; i < theArray.length-1; i++) {
- for (var j = theArray.length - 1; j > i ; j--) {
- if (theArray[j].ex_uid < theArray[j - 1].ex_uid) {
- temp = theArray[j];
- theArray[j] = theArray[j - 1];
- theArray[j - 1] = temp;
- }
- }
- }
- }
- // 按照节点选择次数对树形结构进行兄弟节点排序【递归排序】
- function orderSiblingsByWeight(node) {
- if (node.children && node.children.length != 0) {
- bubbleSortByWeight(node.children);
- for (var i = 0; i < node.children.length; i++) {
- orderSiblingsByWeight((node.children)[i]);
- }
- }
- }
- // 按照节点编号对树形结构进行兄弟节点排序【递归排序】
- function orderSiblingsByUid(node) {
- if (node.children && node.children.length != 0) {
- bubbleSortByUid(node.children);
- for (var i = 0; i < node.children.length; i++) {
- orderSiblingsByUid((node.children)[i]);
- }
- }
- }
- // 设置树节点为“不可见”状态【先序遍历法】
- function setTreeNotVisible(root) {
- root.ex_visible = false;
- if (root.children && root.children.length != 0) {
- for (var i = 0; i < root.children.length; i++) {
- setTreeNotVisible((root.children)[i]);
- }
- }
- }
- // 设置树节点为“可见”状态【先序遍历法】
- function setTreeVisible(root) {
- root.ex_visible = true;
- if (root.children && root.children.length != 0) {
- for (var i = 0; i < root.children.length; i++) {
- setTreeVisible((root.children)[i]);
- }
- }
- }
- // 设置当前节点及其所有上级节点为“可见”状态
- function setRouteVisible(root, node, nodeMap) {
- node.ex_visible = true;
- var parentNodes = [];
- var currentNode = nodeMap['_' + node.ex_uid];
- var parentNode = currentNode.ex_parentNode;
- while (parentNode != null) {
- parentNodes.push(parentNode);
- parentNodeparentNode = parentNode.ex_parentNode;
- }
- // 如果没有上级节点,说明当前节点就是根节点,直接返回即可
- if (parentNodes.length == 0) {
- return;
- }
- setParentNodesVisible(root, parentNodes);
- }
- // 设置所有上级节点为“可见”,
- // 这里的『设置上级节点为“可见”』使用的方法与《附录二:新概念『智能树形菜单』--利用加权多叉树结合JavaScript树形控件实现》
- // 中的『设置功能路径可见』方法不一样,这是由于在JavaScript中无法建立像Java中的那种带有双向引用的多叉树结构(即父节点
- // 引用子节点,子节点引用父节点),在JavaScript中如果做这种双向引用的话,会造成『Stack overflow』异常,所以只能分别建立
- // 两棵多叉树对象,一棵是原始树形结构对象,另一棵是利用nodeMap建立的多叉树对象,专门用于反向引用,即子节点对父节点的引用。
- // 而在Java中,直接可以根据一个节点的父节点引用,找到它所有的父节点。但是在这里,只能采用一种笨办法,先从反向引用的多叉树
- // 中找到某一节点的所有父节点,存在一个数组里,然后在原始树形结构对象中使用先序遍历方法,从顶向下依次查找,把某一节点的所有
- // 父节点设置为可见,效率较低,但与利用反向引用查找父节点的方法目的是一样的。
- function setParentNodesVisible(node, parentNodes) {
- if (containNode(node, parentNodes)) {
- node.ex_visible = true;
- }
- if (node.children && node.children.length != 0) {
- var i = 0;
- for (; i < node.children.length; i++) {
- if (containNode(node, parentNodes)) {
- setParentNodesVisible((node.children)[i], parentNodes);
- }
- }
- // 如果在本层节点中没有找到要设置“可见性”的节点,说明需要设置“可见性”的节点都已经找完了,不需要再向下一层节点中寻找了,
- // 直接退出递归函数
- if (i == node.children.length - 1) {
- return;
- }
- }
- }
- // 检查数组中是否包含与指定节点编号相同的节点
- function containNode(node, parentNodes) {
- for (var i = 0; i < parentNodes.length; i++) {
- if (parentNodes[i].ex_uid == node.ex_uid) {
- return true;
- }
- }
- return false;
- }
- // 搜索包含关键字的树节点,将包含关键字的节点所在路径设置为“可见”,例如:如果某一节点包含搜索关键字,
- // 那么它的所有上级节点和所有下级节点都设置为“可见”【先序遍历法】
- function searchTreeNode(root1, root2, nodeMap, keyWord) {
- if (root2.name.indexOf(keyWord) > -1) {
- setTreeVisible(root2);
- setRouteVisible(root1, root2, nodeMap);
- } else {
- if (root2.children && root2.children.length != 0) {
- for (var i = 0; i < root2.children.length; i++) {
- searchTreeNode(root1, (root2.children)[i], nodeMap, keyWord);
- }
- }
- }
- }
- // 将原树形结构数据复制出一个副本,以备对副本进行搜索过滤,而不破坏原始数据(原始数据用来恢复原状用)【先序遍历法】
- function cloneTreeNodes(root) {
- var treeJSON = '{' + 'name : \'' + root.name + '\', ex_uid : \'' + root.ex_uid + '\',' + 'ex_pid : \'' + root.ex_pid + '\',' + ' ex_weight : ' + root.ex_weight + ', ex_visible : true, ex_parentNode : null';
- if (root.children && root.children.length != 0) {
- treeJSON += ', children : [';
- for (var i = 0; i < root.children.length; i++) {
- treeJSON += cloneTreeNodes((root.children)[i]) + ',';
- }
- treeJSONtreeJSON = treeJSON.substring(0, treeJSON.length - 1);
- treeJSON += "]";
- }
- return treeJSON + '}';
- }
- // 构造节点映射表【先序遍历法】
- // 这里特殊说明一下:
- // 构造节点映射表的目的,是为了下面建立子节点对父节点的引用,这是一个中间步骤,但是有个小问题:
- // 在javascript中,如果是在原树状对象上建立子节点对父节点的引用,会发生『Stack overflow』错误,
- // 我估计是由于循环引用造成的,因为原树状对象已经存在父节点对子节点的引用,此时再建立子节点对
- // 父节点的引用,造成循环引用,这在Java中是没有问题的,但是在JavaScript中却有问题,所以为了避免
- // 这个问题,我创建了一批新的节点,这些节点的内容和原树状结构节点内容一致,但是没有children属性,
- // 也就是没有父节点对子节点的引用,然后对这批新节点建立子节点对父节点的引用关系,这个方法会被buildParentRef()方法调用,来完成这个目的。
- function buildNodeMap(node, nodeMap) {
- var newnewObj = new Object();
- newObj.name = node.name;
- newObj.ex_uid = node.ex_uid;
- newObj.ex_pid = node.ex_pid;
- newObj.ex_weight = node.ex_weight;
- newObj.ex_visible = node.ex_visible;
- nodeMap['_' + node.ex_uid] = newObj;
- if (node.children && node.children.length != 0) {
- for (var i = 0; i < node.children.length; i++) {
- buildNodeMap((node.children)[i], nodeMap);
- }
- }
- return nodeMap; // 这里需要将nodeMap返回去,然后传给buildParentRef()函数使用,这和Java中的引用传递不一样,怪异!!
- }
- // 建立子节点对父节点的引用
- function buildParentRef(node, nodeMap) {
- for (ex_uid in nodeMap) {
- if ((nodeMap[ex_uid]).ex_pid == '') {
- (nodeMap[ex_uid]).ex_parentNode = null;
- } else {
- (nodeMap[ex_uid]).ex_parentNode = nodeMap['_' + (nodeMap[ex_uid]).ex_pid];
- }
- }
- return nodeMap;
- }
- // 对树形结构数据进行搜索过滤后,根据JavaScript树状对象,重新生成JSON字符串【先序遍历法】
- function reBuildTreeJSON(node) {
- if (node.ex_visible) {
- var treeJSON = '{' + 'name : \'' + node.name + '\', ex_uid : \'' + node.ex_uid + '\',' + 'ex_pid : \'' + node.ex_pid + '\',' + ' ex_weight : ' + node.ex_weight + ', ex_visible : ' + node.ex_visible + ', ex_parentNode : null';
- if (node.children && node.children.length != 0) {
- treeJSON += ', children : [';
- for (var i = 0; i < node.children.length; i++) {
- if ((node.children)[i].ex_visible) {
- treeJSON += reBuildTreeJSON((node.children)[i]) + ',';
- } else {
- treeJSON += reBuildTreeJSON((node.children)[i]);
- }
- }
- treeJSONtreeJSON = treeJSON.substring(0, treeJSON.length - 1);
- treeJSON += "]";
- }
- return treeJSON + '}';
- } else {
- return '';
- }
- }
- // 树形结构搜索
- function searchTreeNodesByKeyWord() {
- // 声明一个新的树对象
- var newZNodes = null;
- // 将原树形结构恢复默认状态
- orderSiblingsByUid(zNodes[0]);
- // 将原树对象复制出一个副本,并将这个副本JSON字符串转换成新的树对象
- var treeJSON = cloneTreeNodes(zNodes[0]);
- newZNodes = eval('(' + '[' + treeJSON + ']' + ')');
- var root = newZNodes[0];
- // 对新树对象建立反向引用关系(在子节点中增加父节点的引用)
- var nodeMap = {};
- // 构造节点映射表(下面借助该映射表建立反向引用关系)
- nodeMap = buildNodeMap(root, nodeMap);
- // 建立子节点对父节点的引用
- nodeMap = buildParentRef(root, nodeMap);
- // 设置树节点为“不可见”状态
- setTreeNotVisible(root);
- // 搜索包含关键字的树节点,将包含关键字的节点所在路径设置为“可见”,例如:如果某一节点包含搜索关键字,
- // 那么它的所有上级节点和所有下级节点都设置为“可见”
- searchTreeNode(root, root, nodeMap, document.getElementById('search').value);
- // 对树形结构数据进行搜索过滤后,根据JavaScript树状对象,重新生成JSON字符串
- treeJSON = reBuildTreeJSON(root);
- newZNodes = eval('(' + '[' + treeJSON + ']' + ')');
- $.fn.zTree.init($("#treeDemo"), setting, newZNodes);
- $.fn.zTree.getZTreeObj("treeDemo").expandAll(true);
- }
- // 按照节点选择次数排序(按选择次数排序)
- function orderByWeight() {
- orderSiblingsByWeight(zNodes[0]);
- $.fn.zTree.init($("#treeDemo"), setting, zNodes);
- $.fn.zTree.getZTreeObj("treeDemo").expandAll(true);
- document.getElementById('search').value = '';
- }
- // 按照节点编号排序(恢复默认状态)
- function orderByUid() {
- orderSiblingsByUid(zNodes[0]);
- $.fn.zTree.init($("#treeDemo"), setting, zNodes);
- $.fn.zTree.getZTreeObj("treeDemo").expandAll(true);
- document.getElementById('search').value = '';
- }
- //============建立原始树状对象节点的反向引用关系,即子节点对父节点的引用=============//
- //============单击树节点,进行路径向上加权时会用到===================================//
- // 这里的nodeMap是全局变量,和searchTreeNodesByKeyWord()方法中的nodeMap变量作用域不一样
- var nodeMap = {};
- // 构造节点映射表(下面借助该映射表建立反向引用关系)
- nodeMap = buildNodeMap(zNodes[0], nodeMap);
- // 建立子节点对父节点的引用
- buildParentRef(zNodes[0], nodeMap);
- //===============================================================================//
- // 初始化该树形结构,默认展开所有节点
- $(document).ready(function(){
- orderSiblingsByUid(zNodes[0]);
- $.fn.zTree.init($("#treeDemo"), setting, zNodes);
- $.fn.zTree.getZTreeObj("treeDemo").expandAll(true);
- });
- //-->
- </SCRIPT>
- </HEAD>
- <BODY>
- <h1></h1>
- <h6></h6>
- <div class="content_wrap">
- <div class="zTreeDemoBackground left">
- <ul class="info">
- <li style="padding-bottom:8px"> <input value="按选择次数排序" type="button" onclick="orderByWeight()" /> <input value="恢复默认" type="button" onclick="orderByUid()" /> </li>
- <li> <input type="text" value="" size="16" id="search"> <input value="搜索" type="button" onclick="searchTreeNodesByKeyWord()" /> </li>
- </ul>
- <ul id="treeDemo" class="ztree"></ul>
- </div>
- </div>
- </BODY>
- </HTML>
这些增加的代码都是围绕着JSON嵌套格式数据对象做文章,也就是变量名叫zNodes的JavaScript树状对象。它是一个天然的树形结构,与Java中的『多叉树』数据结构类似。数据结构可以应用在C语言、Java语言、JavaScript语言以及其它语言上面,并不局限于C语言和Java语言。如果把『多叉树』思想借鉴到JavaScript中来,就可以实现我上面所提出的功能了。
这种多叉树结构在《附录一:多叉树结合JavaScript树形控件实现无限级树形结构(一种构建多级有序树形结构JSON(或XML)数据源的方法)》和《附录二:新概念『智能树形菜单』 — 利用加权多叉树结合JavaScript树形控件实现》中有详细介绍,在继续阅读下文之前,请务必先读懂《附录一》和《附录二》,否则无法看懂我上面的代码,这需要花费您一定的时间,但这是理解本文的基础。
实现『按选择次数排序』和『搜索』这两个功能需要使用一种叫做『加权多叉树』的数据结构,加权多叉树的结构图如下:
图21
加权多叉树区别于普通多叉树的地方在于,每个节点上增加一个权值属性,一条路径(从根节点到叶子节点)上的所有节点的权值拼起来就是『路径权值』。例如上图中,路径“根→D→K→S”,它的路径权值为1→1→1。路径权值的意义在于『比较』。各条路径的路径权值之间可以进行比较,通过比较路径权值,可以按路径权值排序,对路径进行排序,路径权值大的排在前面,路径权值小的排在后面。如果想把一条路径移动到多叉树的前面,则需要增加它的路径权值,然后排序,就可以把它排在前面了。那么如何排序路径,如何增加路径权值呢?排序路径采用的是『兄弟节点横向排序方法』,增加路径权值采用的是『向上加权方法』。『兄弟节点横向排序』规则就是Oracle数据库中的层次查询语句中使用的兄弟排序规则,也就是逐层递归排序,对兄弟节点由上到下逐层排序。『兄弟节点横向排序方法』可以按照任何一个属性排序,可以按照节点编号、节点名称、节点权值排序。按照节点编号、节点名称排序之后,整棵树看起来更加顺眼,只是看起来顺眼,不凌乱。例如上图中的加权多叉树,按照节点编号排序前和排序后的对比图如下:
图22
如果是按照路径权值排序的话,排序之后的结果如下:
图23
路径“根→D→K→S”排在了最前面,因为它的路径权值最大。按照路径权值排序是有逻辑意义的,它不同于按『节点编号』和『节点名称』排序,权值的英文单词叫“weight”,是重量的意思,也可以叫做权重。权重可以表示成多种意思,例如,节点点击次数就可以称为权重。为了规范起见,我管节点的点击次数叫做『选择次数』,『选择次数』就可以称为权重。也为了规范起见,我管权重叫做权值,『权值』是科学的叫法。某一节点的选择次数越大,该节点的权值越大。当某一节点的权值增大以后,该节点所在的路径权值也要相应增大,增加路径权值的方法就是『向上加权』。所谓『向上加权』,就是把该节点所在路径上的所有上级节点的权值加1,例如节点S的权值加1之后,变成2,那么路径“根→D→K→S”整体都要增加权值,也就是说节点K和节点D的权值也要加1,都变成2,路径“根→D→K→S”的权值加1之后变成“2→2→2”,如下图所示:
图24
如果把树形菜单比作一棵加权多叉树的话,假如这个树形菜单就是“图24”中的树形结构图,初始权值都是0。经过用户多次点击之后,节点R被使用3次,节点F被使用5次,节点P被使用1次,那么树形结构图变为:
图25
『按选择次数排序』之后,树形菜单变为如下:
图26
如上图所示,路径“B→F”、“B→G→P”、“D→K→R”的路径权值大小依次是:“6→5” > “6→1→1” >
“3→3→3”,由于路径排序采用的是兄弟节点横向排序,所以『路径权值比较』采用的是逐层比较,如下:
这就是『按选择次数排序』的工作原理,比较复杂,需要充分理解『加权多叉树』。
说完『按选择次数排序』,该说搜索了,搜索也是利用这棵树,需要再增加一个节点属性:visible,中文翻译是“可见性”。和路径权值一样,这里所说的“可见性”,指的是路径可见性。搜索时从根节点自顶向下依次匹配,如果某个节点被匹配到了,那么将该节点的所在路径的可见性全部设置为“可见”,其它路径设置为“不可见”。这里所说的『该节点所在路径』指的是包含该节点的所有路径,也就是该节点的所有上级节点和该节点的所有下级节点都设置为“可见”,如下图所示,搜索关键字为K的节点,搜索到之后,树形菜单变成如下:
图27
(红色线条代表“可见路径”,其它为“不可见路径”)
通过关键字搜索之后,系统向用户展现的结果如下:
图28
这就是『搜索』功能的工作原理,需要充分理解『多叉树』和『路径可见性』。
以上就是带有排序和搜索功能的树形控件的实现原理,我的Demo代码中有清晰的注释,请结合《附录一》和《附录二》,很快就能看明白。
四、思考与总结
思想创意往往来源于实际的需求,以及对其它事物的借鉴和参照。树形控件的排序和搜索就是来源于实际的需求,让用户使用树形控件更加方便、快捷;“多叉树思想”是借鉴Java中的数据结构思想,把数据结构思想从其它语言借鉴到JavaScript中来,这也是一种创新。
-------------------------------------------------------------------------------------------------------------------------------------
合笔记本电脑的时候,如果键盘上无意间放了什么东西,
肯定会把电脑显示屏硌碎,有什么好办法防止这种情况发生??
这是一个很实际的需求!!