利用多叉树实现Ext JS中的无限级树形菜单(一种构建多级有序树形结构JSON的方法)

一、问题研究的背景和意义

目前在Web应用程序开发领域,Ext JS框架已经逐渐被广泛使用,它是富客户端开发中出类拔萃的框架之一。在Ext的UI控件中,树形控件无疑是最为常用的控件之一,它用来实现树形结构的菜单。TreeNode用来实现静态的树形菜单,AsyncTreeNode用来实现动态的异步加载树形菜单,后者最为常用,它通过接收服务器端返回来的JSON格式的数据,动态生成树形菜单节点。动态生成树有两种思路:一种是一次性生成全部树节点,另一种是逐级加载树节点(利用AJAX,每次点击节点时查询下一级节点)。对于大数据量的菜单节点来说,逐级加载是比较合适的选择,但是对于小数据量的菜单来说,一次性生成全部节点应该是最为合理的方案。在实际应用开发中,一般不会遇到特别大数据量的场景,所以一次性生成全部菜单节点是我们重点研究的技术点,本文就是介绍基于Ext JS的应用系统中如何将数据库中的无限级层次数据一次性在界面中生成全部菜单节点(例如在界面中以树形方式一次性展示出银行所有分支机构的信息),同时对每一个层次的菜单节点按照某一属性和规则排序,展示出有序的菜单树。

解决Ext JS无限级树形菜单的问题,可以拓展出更多的应用场景,例如树形结构表格TreeGrid,一次性生成树形表格,对树形表格进行完整分页,对表格列进行全排序;或者可以利用本文的思路扩展出其他的更复杂的应用场景。

先看两个图例,有个直观上的认识:
图一,银行分支机构树形结构菜单

图二,树形结构报表

二、详细设计方案


让我们先看一段代码片段:

文件一,branchTree.html (Ext树形控件页面)

Js代码  

  1. Ext.onReady(
  2. function(){
  3. var  tree = new Ext.tree.TreePanel({
  4. height: 300,
  5. width: 400,
  6. animate:true,
  7. enableDD:true,
  8. containerScroll: true,
  9. rootVisible: false,
  10. frame: true,
  11. // getBranch.do请求服务器返回多级树形结构的JSON字符串
  12. loader: new Ext.tree.TreeLoader({dataUrl:‘getBranch.do‘}),
  13. root : new Ext.tree.AsyncTreeNode({id:‘0‘,text:‘根结点‘})
  14. });
  15. tree.expandAll();
  16. }
  17. );

文件二,branchTreeJSON.jsp (接收getBranch.do请求,返回无限级JSON字符串)

Java代码  

  1. <%
  2. // 读取银行分支机构的层次数据
  3. List result = DataAccess.getBankInfoList();
  4. // 将层次数据转换为多叉树对象(本文下面会详细介绍该数据结构的实现方法)
  5. Node root = ExtTreeHelper.createExtTree(result);
  6. %>
  7. [
  8. <%=root.toString()%> <!-- 以JSON的形式返回响应数据,Ext.tree.TreeLoader会根据此数据生成树形菜单 -->
  9. ]

以上两个程序文件是一次性生成无限级树形菜单所必须的,其中最为关键的部分就是如何生成一个无限级的JSON字符串,返回给客户端的Ext树形控件。对于银行分支机构来说,需要返回类似如下的JSON串:

Js代码  

  1. {
  2. id: ‘100000‘,
  3. text: ‘廊坊银行总行‘,
  4. children: [
  5. {
  6. id: ‘110000‘,
  7. text: ‘廊坊分行‘,
  8. children: [
  9. {
  10. id: ‘113000‘,
  11. text: ‘廊坊银行开发区支行‘,
  12. leaf: true
  13. },
  14. {
  15. id: ‘112000‘,
  16. text: ‘廊坊银行解放道支行‘,
  17. children: [
  18. {
  19. id: ‘112200‘,
  20. text: ‘廊坊银行三大街支行‘,
  21. leaf: true
  22. },
  23. {
  24. id: ‘112100‘,
  25. text: ‘廊坊银行广阳道支行‘,
  26. leaf: true
  27. }
  28. ]
  29. },
  30. {
  31. id: ‘111000‘,
  32. text: ‘廊坊银行金光道支行‘,
  33. leaf: true
  34. }
  35. ]
  36. }
  37. ]
  38. }

同时还可能需要对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序,以展示出有序的树形菜单。

现在可以把问题概括为:

1、 把数据库中的层次数据转换成JSON格式的字符串

2、 对树中每一个层次的节点按照某一属性(比如分支机构编号)进行排序

下面介绍解决问题的思路:

我们都学过数据结构,即组织大量数据的方法,无限级树形菜单就可以抽象成一种多叉树结构,即每个节点下包含多个子节点的树形结构,首先就需要把数据库中的层次数据转换成多叉树结构的对象树,也就是构造出一棵多叉树。

有了数据结构,还要实现相应的算法,我们需要实现两种算法:
1、兄弟节点横向排序算法,对隶属于同一个父节点下面的所有直接子节点按照某一节点属性和规则进行排序,保持兄弟节点横向有序;
2、先序遍历算法,递归打印出无限级JSON字符串。

概括起来分为三步:
1、 构造无序的多叉树结构
2、 实现兄弟节点横向排序方法
3、 实现先序遍历方法,打印出JSON字符串

如图所示:

三、源代码实现(Java语言版)


实现这样一颗树,需要设计三个类:树类(ExtTree.java)、节点类(Node.java)、孩子列表类(Children.java);为了方便演示,还需要构造一些假的层次数据,因此还需要建一个构造假数据的类(VirtualDataGenerator.java),以下代码拷贝出来之后可直接运行测试:

Java代码  

  1. package test;
  2. import java.util.ArrayList;
  3. import java.util.Comparator;
  4. import java.util.HashMap;
  5. import java.util.Iterator;
  6. import java.util.List;
  7. import java.util.Map;
  8. import java.util.Set;
  9. import java.util.Collections;
  10. /**
  11. * 多叉树类
  12. */
  13. public class MultipleTree {
  14. public static void main(String[] args) {
  15. // 读取层次数据结果集列表
  16. List dataList = VirtualDataGenerator.getVirtualResult();
  17. // 节点列表(散列表,用于临时存储节点对象)
  18. HashMap nodeList = new HashMap();
  19. // 根节点
  20. Node root = null;
  21. // 根据结果集构造节点列表(存入散列表)
  22. for (Iterator it = dataList.iterator(); it.hasNext();) {
  23. Map dataRecord = (Map) it.next();
  24. Node node = new Node();
  25. node.id = (String) dataRecord.get("id");
  26. node.text = (String) dataRecord.get("text");
  27. node.parentId = (String) dataRecord.get("parentId");
  28. nodeList.put(node.id, node);
  29. }
  30. // 构造无序的多叉树
  31. Set entrySet = nodeList.entrySet();
  32. for (Iterator it = entrySet.iterator(); it.hasNext();) {
  33. Node node = (Node) ((Map.Entry) it.next()).getValue();
  34. if (node.parentId == null || node.parentId.equals("")) {
  35. root = node;
  36. } else {
  37. ((Node) nodeList.get(node.parentId)).addChild(node);
  38. }
  39. }
  40. // 输出无序的树形菜单的JSON字符串
  41. System.out.println(root.toString());
  42. // 对多叉树进行横向排序
  43. root.sortChildren();
  44. // 输出有序的树形菜单的JSON字符串
  45. System.out.println(root.toString());
  46. // 程序输出结果如下(无序的树形菜单)(格式化后的结果):
  47. //  {
  48. //   id : ‘100000‘,
  49. //   text : ‘廊坊银行总行‘,
  50. //   children : [
  51. //     {
  52. //     id : ‘110000‘,
  53. //     text : ‘廊坊分行‘,
  54. //     children : [
  55. //       {
  56. //       id : ‘113000‘,
  57. //       text : ‘廊坊银行开发区支行‘,
  58. //       leaf : true
  59. //       },
  60. //       {
  61. //       id : ‘111000‘,
  62. //       text : ‘廊坊银行金光道支行‘,
  63. //       leaf : true
  64. //       },
  65. //       {
  66. //       id : ‘112000‘,
  67. //       text : ‘廊坊银行解放道支行‘,
  68. //       children : [
  69. //         {
  70. //         id : ‘112200‘,
  71. //         text : ‘廊坊银行三大街支行‘,
  72. //         leaf : true
  73. //         },
  74. //         {
  75. //         id : ‘112100‘,
  76. //         text : ‘廊坊银行广阳道支行‘,
  77. //         leaf : true
  78. //         }
  79. //       ]
  80. //       }
  81. //     ]
  82. //     }
  83. //   ]
  84. //  }
  85. // 程序输出结果如下(有序的树形菜单)(格式化后的结果):
  86. //  {
  87. //   id : ‘100000‘,
  88. //   text : ‘廊坊银行总行‘,
  89. //   children : [
  90. //     {
  91. //     id : ‘110000‘,
  92. //     text : ‘廊坊分行‘,
  93. //     children : [
  94. //       {
  95. //       id : ‘111000‘,
  96. //       text : ‘廊坊银行金光道支行‘,
  97. //       leaf : true
  98. //       },
  99. //       {
  100. //       id : ‘112000‘,
  101. //       text : ‘廊坊银行解放道支行‘,
  102. //       children : [
  103. //         {
  104. //         id : ‘112100‘,
  105. //         text : ‘廊坊银行广阳道支行‘,
  106. //         leaf : true
  107. //         },
  108. //         {
  109. //         id : ‘112200‘,
  110. //         text : ‘廊坊银行三大街支行‘,
  111. //         leaf : true
  112. //         }
  113. //       ]
  114. //       },
  115. //       {
  116. //       id : ‘113000‘,
  117. //       text : ‘廊坊银行开发区支行‘,
  118. //       leaf : true
  119. //       }
  120. //     ]
  121. //     }
  122. //   ]
  123. //  }
  124. }
  125. }
  126. /**
  127. * 节点类
  128. */
  129. class Node {
  130. /**
  131. * 节点编号
  132. */
  133. public String id;
  134. /**
  135. * 节点内容
  136. */
  137. public String text;
  138. /**
  139. * 父节点编号
  140. */
  141. public String parentId;
  142. /**
  143. * 孩子节点列表
  144. */
  145. private Children children = new Children();
  146. // 先序遍历,拼接JSON字符串
  147. public String toString() {
  148. String result = "{"
  149. + "id : ‘" + id + "‘"
  150. + ", text : ‘" + text + "‘";
  151. if (children != null && children.getSize() != 0) {
  152. result += ", children : " + children.toString();
  153. } else {
  154. result += ", leaf : true";
  155. }
  156. return result + "}";
  157. }
  158. // 兄弟节点横向排序
  159. public void sortChildren() {
  160. if (children != null && children.getSize() != 0) {
  161. children.sortChildren();
  162. }
  163. }
  164. // 添加孩子节点
  165. public void addChild(Node node) {
  166. this.children.addChild(node);
  167. }
  168. }
  169. /**
  170. * 孩子列表类
  171. */
  172. class Children {
  173. private List list = new ArrayList();
  174. public int getSize() {
  175. return list.size();
  176. }
  177. public void addChild(Node node) {
  178. list.add(node);
  179. }
  180. // 拼接孩子节点的JSON字符串
  181. public String toString() {
  182. String result = "[";
  183. for (Iterator it = list.iterator(); it.hasNext();) {
  184. result += ((Node) it.next()).toString();
  185. result += ",";
  186. }
  187. result = result.substring(0, result.length() - 1);
  188. result += "]";
  189. return result;
  190. }
  191. // 孩子节点排序
  192. public void sortChildren() {
  193. // 对本层节点进行排序
  194. // 可根据不同的排序属性,传入不同的比较器,这里传入ID比较器
  195. Collections.sort(list, new NodeIDComparator());
  196. // 对每个节点的下一层节点进行排序
  197. for (Iterator it = list.iterator(); it.hasNext();) {
  198. ((Node) it.next()).sortChildren();
  199. }
  200. }
  201. }
  202. /**
  203. * 节点比较器
  204. */
  205. class NodeIDComparator implements Comparator {
  206. // 按照节点编号比较
  207. public int compare(Object o1, Object o2) {
  208. int j1 = Integer.parseInt(((Node)o1).id);
  209. int j2 = Integer.parseInt(((Node)o2).id);
  210. return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1));
  211. }
  212. }
  213. /**
  214. * 构造虚拟的层次数据
  215. */
  216. class VirtualDataGenerator {
  217. // 构造无序的结果集列表,实际应用中,该数据应该从数据库中查询获得;
  218. public static List getVirtualResult() {
  219. List dataList = new ArrayList();
  220. HashMap dataRecord1 = new HashMap();
  221. dataRecord1.put("id", "112000");
  222. dataRecord1.put("text", "廊坊银行解放道支行");
  223. dataRecord1.put("parentId", "110000");
  224. HashMap dataRecord2 = new HashMap();
  225. dataRecord2.put("id", "112200");
  226. dataRecord2.put("text", "廊坊银行三大街支行");
  227. dataRecord2.put("parentId", "112000");
  228. HashMap dataRecord3 = new HashMap();
  229. dataRecord3.put("id", "112100");
  230. dataRecord3.put("text", "廊坊银行广阳道支行");
  231. dataRecord3.put("parentId", "112000");
  232. HashMap dataRecord4 = new HashMap();
  233. dataRecord4.put("id", "113000");
  234. dataRecord4.put("text", "廊坊银行开发区支行");
  235. dataRecord4.put("parentId", "110000");
  236. HashMap dataRecord5 = new HashMap();
  237. dataRecord5.put("id", "100000");
  238. dataRecord5.put("text", "廊坊银行总行");
  239. dataRecord5.put("parentId", "");
  240. HashMap dataRecord6 = new HashMap();
  241. dataRecord6.put("id", "110000");
  242. dataRecord6.put("text", "廊坊分行");
  243. dataRecord6.put("parentId", "100000");
  244. HashMap dataRecord7 = new HashMap();
  245. dataRecord7.put("id", "111000");
  246. dataRecord7.put("text", "廊坊银行金光道支行");
  247. dataRecord7.put("parentId", "110000");
  248. dataList.add(dataRecord1);
  249. dataList.add(dataRecord2);
  250. dataList.add(dataRecord3);
  251. dataList.add(dataRecord4);
  252. dataList.add(dataRecord5);
  253. dataList.add(dataRecord6);
  254. dataList.add(dataRecord7);
  255. return dataList;
  256. }
  257. }

好了,通过上面的代码,就可以实现多叉树的兄弟节点横向排序和先序遍历了,实现了将层次数据转换为有序无限级JSON字符串的目的。

在实际的项目中,可以把上面的有效代码融入其中,或者在此基础上进行一些扩展:
1、 实现对指定层次的排序(例如只排序第一层的节点,或者只排序某一父节点下的所有子节点)
2、 遍历输出树形结构时可以加入判断条件过滤掉某些节点
3、 实现节点的删除功能
4、 在节点类中增加一个父节点的引用,就可以计算出某一节点所处的级别
5、 在不支持层次查询的数据库应用系统中使用该算法实现相同的效果

四、思考与总结

这篇文章的重点是如何构造有序的无限级的树形结构JSON字符串,一次性生成树形菜单,而不是利用AJAX的方式,反复向服务器端发送请求,一级接一级的加载树节点。

既然可以构造无限级的JSON字符串,那么也可以根据这个思路构造无限级的XML字符串,或者构造具有层次结构的UL – LI组合(用UL - LI来展示树形结构),或者构造具有层次结构的TABLE(用TABLE来展示树形结构)。如下所示:

(1)XML层次结构

Xml代码  

  1. <menuGroup id="100000" name="廊坊银行总行">
  2. <menuGroup id="110000" name="廊坊分行">
  3. <menu id="113000" name="廊坊银行开发区支行">
  4. </menu>
  5. <menu id="111000" name="廊坊银行金光道支行">
  6. </menu>
  7. <menuGroup id="112000" name="廊坊银行解放道支行">
  8. <menu id="112200" name="廊坊银行三大街支行">
  9. </menu>
  10. <menu id="112100" name="廊坊银行广阳道支行">
  11. </menu>
  12. </menuGroup>
  13. </menuGroup>
  14. </menuGroup>

(2)UL - LI 层次结构

Html代码  

  1. <ul>
  2. <li>廊坊银行总行</li>
  3. <ul>
  4. <li>廊坊分行</li>
  5. <ul>
  6. <li>廊坊银行开发区支行</li>
  7. <li>廊坊银行解放道支行</li>
  8. <ul>
  9. <li>廊坊银行三大街支行</li>
  10. <li>廊坊银行广阳道支行</li>
  11. </ul>
  12. <li>廊坊银行金光道支行</li>
  13. </ul>
  14. </ul>
  15. </ul>

(3)TABLE层次结构

Html代码  

  1. <table>
  2. <tr><td>廊坊银行总行</td></tr>
  3. <tr><td>&nbsp;&nbsp;廊坊分行</td></tr>
  4. <tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行开发区支行</td></tr>
  5. <tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行解放道支行</td></tr>
  6. <tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行三大街支行</td></tr>
  7. <tr><td>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行广阳道支行</td></tr>
  8. <tr><td>&nbsp;&nbsp;&nbsp;&nbsp;廊坊银行金光道支行</td></tr>
  9. </table>

另外对TreeGrid树形表格也有一定的价值:

1、一次性构造树形表格,实现数据分级展示

2、通过更换比较器,实现对不同表格列的全排序(全排序指的是对所有页的数据进行排序,而不是只对当前页的数据排序)

3、实现对树形表格的完整分页(每次分页时,只取固定数目的第一层节点,之后调用toString方法,展示出完整条数的分级数据)

五、参考书籍
1、Mark Allen Weiss,数据结构与算法分析(Java语言描述)

2、Bruce Eckel,Thinking In Java Third Edition

3、David Flanagan,JavaScript: The Definitive Guide, 5th Edition

4、OCA Oracle Database 11g SQL Fundamentals I Exam Guide

时间: 2024-08-02 08:21:18

利用多叉树实现Ext JS中的无限级树形菜单(一种构建多级有序树形结构JSON的方法)的相关文章

Ext.js 中 25种类型的Ext.panel.Tool

通过Ext.panel.Panel的tools配置项来设置Ext.panel.Tool实例. 要注意的一点是,Ext框架提供的Ext.panel.Tool仅包含按钮图标而具体的点击事件处理函数需要我们自定义. ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51

图解js中常用的判断浏览器窗体、用户屏幕可视区域大小位置的方法

有时我们需要获得浏览器窗口或屏幕的大小.窗口下拉框下拉的距离等数据,对应这些需求,js中提供了不少解决方法,只是数量稍多容易混淆它们各自的意义,下面咱们用图例来解释下12个常见对象属性的作用. 其中有6个常用的浏览器窗体属性(由于offsetWidth/Height在不同浏览器下表现有出入,故不在本章讨论): document.documentElement.clientWidth document.documentElement.clientHeight document.documentEl

Ext js框架模拟Windows桌面菜单管理模板

一款超炫的后台,Ext模拟Windows桌面,Ext经典浅蓝风格,功能非常强大,包括最大化.最小化.状态栏.桌面图标等,不过需要非常懂Ext脚本的才可驾驭它.? 1.图片 ?2. [代码][HTML]代码  <html><head><meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"><title>Ext 2.0 Desktop

js中常用追加元素的几种方法:append,appendTo,after,before,insertAfter,insertBefore,appendChild

js中常用追加元素的几种方法,点击下面每个按钮,即可查看效果 我是第一个子元素 我是第二个子元素 append appendTo prepend prependTo after before appendChild insertAfter insertBefore

js中哈希表的几种用法总结

本篇文章只要是对js中哈希表的几种用法进行了总结介绍,需要的朋友可以过来参考下,希望对大家有所帮助 1. <html> <head> <script type="text/javascript"> // by Go_Rush(脚本之家) from http://www.jb51.net/ var hash={ "百度" :"http://www.baidu.com/", "Google" :

JS中date日期初始化的5种方法

原文:JS中date日期初始化的5种方法 创建一个日期对象: 代码如下: var objDate=new Date([arguments list]); 参数形式有以下5种: 1)new Date("month dd,yyyy hh:mm:ss"); 2)new Date("month dd,yyyy"); 3)new Date(yyyy,mth,dd,hh,mm,ss); 在程序中我使用的第三种初始化方法,总是显示格式化的参数不正确,仔细看了一下一定要是整型的才可

js中字符串转换为数值的两种方法的区别

在js中字符串转换为数值的方法有三种:转换函数,强制类型转换,隐式转换 1.转换函数 parseInt()   //将字符串转换为整型 parseFloat()  //将字符串转换为浮点型 转换函数在进行类型转换时是可以传入参数的,默认转换为10进制,转换成功后返回的是整数类型的数值. 例:1. parseInt('AB3', 16) //返回结果:2739,表示将字符串转换为16进制的数值 2.parseInt('13', 10) //返回结果:13,表示将字符串转换为10进制的数值 3.pa

关于js中window.location.href,location.href,parent.location.href,top.location.href的使用方法

关于js中"window.location.href"."location.href"."parent.location.href"."top.location.href"的使用方法 "window.location.href"."location.href"是本页面跳转 "parent.location.href"是上一层页面跳转 "top.locatio

js中删除数组元素的几种方法

1:js中的splice方法 splice(index,len,[item])    注释:该方法会改变原始数组. splice有3个参数,它也可以用来替换/删除/添加数组内某一个或者几个值 index:数组开始下标        len: 替换/删除的长度       item:替换的值,删除操作的话 item为空 如:arr = ['a','b','c','d'] 删除 ----  item不设置 arr.splice(1,1)   //['a','c','d']         删除起始下