TreeGrid是一種DHTML控件,其完全使用JavaScript語(yǔ)言編寫,用以在HTML頁(yè)面上展示具有層次結(jié)構(gòu)的數(shù)據(jù)項(xiàng),其核心技術(shù)為多叉樹。

外文名

TreeGrid

定義

是一個(gè)DHTML控件

軟件功能

本文主要介紹TreeGrid的概念和實(shí)現(xiàn)原理,下面這張圖就是一個(gè)完整的TreeGrid,

點(diǎn)擊圖片,查看大圖。

假設(shè)列1為A-Z這26個(gè)字母及其子節(jié)點(diǎn)組成的層次結(jié)構(gòu)樹:

(顧名思義,TreeGrid就是樹形表格)

TreeGrid

選擇”按列2降序排序”,排序后的結(jié)果如下:

(實(shí)際上應(yīng)該對(duì)所有頁(yè)的數(shù)據(jù)進(jìn)行排序,而不是只對(duì)當(dāng)前頁(yè)的數(shù)據(jù)排序,這里為了說(shuō)明排序規(guī)則,只展示了A,B,C三個(gè)節(jié)點(diǎn)及其子節(jié)點(diǎn))

TreeGrid

點(diǎn)擊”下一頁(yè)”,

(只對(duì)第一層節(jié)點(diǎn)進(jìn)行分頁(yè),假設(shè)每頁(yè)顯示10個(gè)節(jié)點(diǎn))

TreeGrid

關(guān)于TreeGrid的實(shí)現(xiàn)原理,請(qǐng)閱讀下面這篇文章:

多叉樹及其應(yīng)用:多叉樹結(jié)合JavaScript樹形控件實(shí)現(xiàn)無(wú)限級(jí)

樹形結(jié)構(gòu)

(一種構(gòu)建多級(jí)有序樹形結(jié)構(gòu)JSON(或XML)數(shù)據(jù)源的方法)

研究原因

一、問(wèn)題研究的背景和意義

在Web應(yīng)用程序開發(fā)領(lǐng)域,基于Ajax技術(shù)的JavaScript樹形控件已經(jīng)被廣泛使用,它用來(lái)在Html頁(yè)面上展現(xiàn)具有層次結(jié)構(gòu)的數(shù)據(jù)項(xiàng)。目前市場(chǎng)上常見的JavaScript框架及組件庫(kù)中均包含自己的樹形控件,例如jQuery、Ext JS等,還有一些獨(dú)立的樹形控件,例如dhtmlxTree等,這些樹形控件完美的解決了層次數(shù)據(jù)的展示問(wèn)題。展示離不開數(shù)據(jù),樹形控件主要利用Ajax技術(shù)從服務(wù)器端獲取數(shù)據(jù)源,數(shù)據(jù)源的格式主要包括JSON、XML等,而這些層次數(shù)據(jù)一般都存儲(chǔ)在數(shù)據(jù)庫(kù)中?!盁o(wú)限級(jí)樹形結(jié)構(gòu)”,顧名思義,沒(méi)有級(jí)別的限制,它的數(shù)據(jù)通常來(lái)自數(shù)據(jù)庫(kù)中的無(wú)限級(jí)層次數(shù)據(jù),這種數(shù)據(jù)的存儲(chǔ)表通常包括id和parentId這兩個(gè)字段,以此來(lái)表示數(shù)據(jù)之間的層次關(guān)系?,F(xiàn)在問(wèn)題來(lái)了,既然樹形控件的數(shù)據(jù)源采用JSON或XML等格式的字符串來(lái)組織層次數(shù)據(jù),而層次數(shù)據(jù)又存儲(chǔ)在數(shù)據(jù)庫(kù)的表中,那么如何建立起樹形控件與層次數(shù)據(jù)之間的關(guān)系,換句話說(shuō),

如何將數(shù)據(jù)庫(kù)中的層次數(shù)據(jù)轉(zhuǎn)換成對(duì)應(yīng)的層次結(jié)構(gòu)的JSON或XML格式的字符串,返回給客戶端的JavaScript樹形控件?這就是我們要解決的關(guān)鍵技術(shù)問(wèn)題。

本文將以目前市場(chǎng)上比較知名的Ext JS框架為例,講述實(shí)現(xiàn)無(wú)限級(jí)樹形結(jié)構(gòu)的方法,該方法同樣適用于其它類似的JavaScript樹形控件。

Ext JS框架是富客戶端開發(fā)中出類拔萃的框架之一。在Ext的UI控件中,樹形控件無(wú)疑是最為常用的控件之一,它用來(lái)實(shí)現(xiàn)樹形結(jié)構(gòu)的視圖。TreeNode用來(lái)實(shí)現(xiàn)靜態(tài)的樹形結(jié)構(gòu),AsyncTreeNode用來(lái)實(shí)現(xiàn)動(dòng)態(tài)的異步加載樹形結(jié)構(gòu),后者最為常用,它通過(guò)接收服務(wù)器端返回來(lái)的JSON格式的數(shù)據(jù),動(dòng)態(tài)生成樹形結(jié)構(gòu)節(jié)點(diǎn)。動(dòng)態(tài)生成樹有兩種思路:一種是一次性生成全部樹節(jié)點(diǎn),另一種是逐級(jí)加載樹節(jié)點(diǎn)(利用Ajax,每次點(diǎn)擊節(jié)點(diǎn)時(shí)查詢下一級(jí)節(jié)點(diǎn))。對(duì)于大數(shù)據(jù)量的樹節(jié)點(diǎn)來(lái)說(shuō),逐級(jí)加載是比較合適的選擇,但是對(duì)于小數(shù)據(jù)量的樹節(jié)點(diǎn)來(lái)說(shuō),一次性生成全部節(jié)點(diǎn)應(yīng)該是最為合理的方案。在實(shí)際應(yīng)用開發(fā)中,一般不會(huì)遇到特別大數(shù)據(jù)量的場(chǎng)景,所以一次性生成全部樹節(jié)點(diǎn)是我們重點(diǎn)研究的技術(shù)點(diǎn),也就是本文要解決的關(guān)鍵技術(shù)問(wèn)題。本文以基于Ext JS的應(yīng)用系統(tǒng)為例,講述如何將數(shù)據(jù)庫(kù)中的無(wú)限級(jí)層次數(shù)據(jù)一次性在界面中生成全部樹節(jié)點(diǎn)(例如在界面中以樹形方式一次性展示出銀行所有分支機(jī)構(gòu)的信息),同時(shí)對(duì)每一個(gè)層次的節(jié)點(diǎn)按照某一屬性和規(guī)則排序,展示出有序的樹形結(jié)構(gòu)。

解決一次性構(gòu)造無(wú)限級(jí)樹形結(jié)構(gòu)的問(wèn)題,可以拓展出更多的應(yīng)用場(chǎng)景,例如樹形結(jié)構(gòu)表格TreeGrid,一次性生成樹形表格,對(duì)樹形表格進(jìn)行完整分頁(yè),對(duì)表格列進(jìn)行全排序;或者可以利用本文的思路擴(kuò)展出其他的更復(fù)雜的應(yīng)用場(chǎng)景。

先看兩個(gè)圖例,有個(gè)直觀上的認(rèn)識(shí):

圖一,銀行分支機(jī)構(gòu)樹形結(jié)構(gòu)

TreeGrid

圖二,樹形結(jié)構(gòu)表格

TreeGrid

二、詳細(xì)設(shè)計(jì)方案

讓我們先看兩段代碼片段:

文件一,branchTree.html (Ext樹形控件頁(yè)面)Ext.onReady(

function(){

var tree = new Ext.tree.TreePanel({

height: 300,

width: 400,

animate:true,

enableDD:true,

containerScroll: true,

rootVisible: false,

frame: true,

// getBranch.do請(qǐng)求服務(wù)器返回多級(jí)樹形結(jié)構(gòu)的JSON字符串

loader: new Ext.tree.TreeLoader({dataUrl:'getBranch.do'}),

root : new Ext.tree.AsyncTreeNode({id:'0',text:'根結(jié)點(diǎn)'})

});

tree.expandAll();

}

);

文件二,branchTreeJSON.jsp (接收getBranch.do請(qǐng)求,返回多級(jí)樹形結(jié)構(gòu)的JSON字符串)

<%

// 讀取銀行分支機(jī)構(gòu)的層次數(shù)據(jù)

List result = DataAccess.getBankInfoList();

// 將層次數(shù)據(jù)轉(zhuǎn)換為多叉樹對(duì)象(本文下面會(huì)詳細(xì)介紹該數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)方法)

Node root = ExtTreeHelper.createExtTree(result);

%>

[

<%=root.toString()%>

]

以上兩個(gè)程序文件是一次性生成無(wú)限級(jí)樹形結(jié)構(gòu)所必須的,其中最為關(guān)鍵的部分就是如何生成一個(gè)無(wú)限級(jí)的樹形結(jié)構(gòu)JSON字符串,返回給客戶端的Ext樹形控件。對(duì)于銀行分支機(jī)構(gòu)來(lái)說(shuō),需要返回類似如下的JSON串:

{

id: '100000',

text: '廊坊銀行總行',

children: [

{

id: '110000',

text: '廊坊分行',

children: [

{

id: '113000',

text: '廊坊銀行開發(fā)區(qū)支行',

leaf: true

},

{

id: '112000',

text: '廊坊銀行解放道支行',

children: [

{

id: '112200',

text: '廊坊銀行三大街支行',

leaf: true

},

{

id: '112100',

text: '廊坊銀行廣陽(yáng)道支行',

leaf: true

}

]

},

{

id: '111000',

text: '廊坊銀行金光道支行',

leaf: true

}

]

}

]

}

同時(shí)還需要對(duì)樹中每一個(gè)層次的節(jié)點(diǎn)按照某一屬性(比如分支機(jī)構(gòu)編號(hào))進(jìn)行排序,以展示出有序的樹形結(jié)構(gòu)。

現(xiàn)在可以把問(wèn)題概括為:

1、把數(shù)據(jù)庫(kù)中的層次數(shù)據(jù)轉(zhuǎn)換成多級(jí)樹形結(jié)構(gòu)的JSON格式的字符串

2、對(duì)樹中每一個(gè)層次的節(jié)點(diǎn)按照某一屬性(比如分支機(jī)構(gòu)編號(hào))進(jìn)行排序

下面介紹解決問(wèn)題的思路:

在數(shù)據(jù)結(jié)構(gòu)這門課中,我們都學(xué)過(guò)樹,無(wú)限級(jí)樹形結(jié)構(gòu)就可以抽象成一種多叉樹結(jié)構(gòu),即每個(gè)節(jié)點(diǎn)下包含多個(gè)子節(jié)點(diǎn)的樹形結(jié)構(gòu),首先就需要把數(shù)據(jù)庫(kù)中的層次數(shù)據(jù)轉(zhuǎn)換成多叉樹結(jié)構(gòu)的對(duì)象樹,也就是構(gòu)造出一棵多叉樹。

有了數(shù)據(jù)結(jié)構(gòu),還要實(shí)現(xiàn)相應(yīng)的算法,我們需要實(shí)現(xiàn)兩種算法:

1、兄弟節(jié)點(diǎn)橫向排序算法,對(duì)隸屬于同一個(gè)父節(jié)點(diǎn)下面的所有直接子節(jié)點(diǎn)按照某一節(jié)點(diǎn)屬性和規(guī)則進(jìn)行排序,保持兄弟節(jié)點(diǎn)橫向有序;

2、先序遍歷算法,遞歸打印出無(wú)限級(jí)JSON字符串。

概括起來(lái)分為三步:

1、構(gòu)造無(wú)序的多叉樹結(jié)構(gòu)

2、實(shí)現(xiàn)兄弟節(jié)點(diǎn)橫向排序方法

3、實(shí)現(xiàn)先序遍歷方法,打印出JSON字符串

如圖所示:

TreeGrid

三、源代碼實(shí)現(xiàn)(Java版)

實(shí)現(xiàn)這樣一顆樹,需要設(shè)計(jì)兩個(gè)類:樹類(Tree)、節(jié)點(diǎn)類(Node);排序時(shí)還需要一個(gè)比較器類(NodeIDComparator);為了方便演示,還需要構(gòu)造一些假的層次數(shù)據(jù),因此還需要建一個(gè)構(gòu)造假數(shù)據(jù)的類(VirtualDataGenerator),以下代碼拷貝出來(lái)之后可直接運(yùn)行測(cè)試:

package test;

import java.util.ArrayList;

import java.util.Comparator;

import java.util.HashMap;

import java.util.Iterator;

import java.util.List;

import java.util.Map;

import java.util.Set;

import java.util.Collections;

/**

* 多叉樹類

*/

public class Tree {

public static void main(String[] args) {

// 讀取層次數(shù)據(jù)結(jié)果集列表

List dataList = VirtualDataGenerator.getVirtualResult();

// 節(jié)點(diǎn)列表(映射表,用于臨時(shí)存儲(chǔ)節(jié)點(diǎn)對(duì)象)

HashMap nodeList = new HashMap();

// 根節(jié)點(diǎn)

Node root = null;

// 將結(jié)果集存入映射表(后面將借助映射表構(gòu)造多叉樹)

for (Iterator it = dataList.iterator(); it.hasNext();) {

Map dataRecord = (Map) it.next();

Node node = new Node();

node.id = (String) dataRecord.get("id");

node.text = (String) dataRecord.get("text");

node.parentId = (String) dataRecord.get("parentId");

nodeList.put(node.id, node);

}

// 構(gòu)造無(wú)序的多叉樹

Set entrySet = nodeList.entrySet();

for (Iterator it = entrySet.iterator(); it.hasNext();) {

Node node = (Node) ((Map.Entry) it.next()).getValue();

if (node.parentId == null || node.parentId.equals("")) {

root = node;

} else {

((Node) nodeList.get(node.parentId)).addChild(node);

}

}

// 輸出無(wú)序的樹形結(jié)構(gòu)的JSON字符串

System.out.println(root);

// 對(duì)多叉樹進(jìn)行橫向排序

root.sortChildren();

// 輸出有序的樹形結(jié)構(gòu)的JSON字符串

System.out.println(root);

// 程序輸出結(jié)果如下:

//

// 無(wú)序的樹形結(jié)構(gòu)(格式化后的結(jié)果):

// {

// id : '100000',

// text : '廊坊銀行總行',

// children : [

// {

// id : '110000',

// text : '廊坊分行',

// children : [

// {

// id : '113000',

// text : '廊坊銀行開發(fā)區(qū)支行',

// leaf : true

// },

// {

// id : '111000',

// text : '廊坊銀行金光道支行',

// leaf : true

// },

// {

// id : '112000',

// text : '廊坊銀行解放道支行',

// children : [

// {

// id : '112200',

// text : '廊坊銀行三大街支行',

// leaf : true

// },

// {

// id : '112100',

// text : '廊坊銀行廣陽(yáng)道支行',

// leaf : true

// }

// ]

// }

// ]

// }

// ]

// }

// 有序的樹形結(jié)構(gòu)(格式化后的結(jié)果):

// {

// id : '100000',

// text : '廊坊銀行總行',

// children : [

// {

// id : '110000',

// text : '廊坊分行',

// children : [

// {

// id : '111000',

// text : '廊坊銀行金光道支行',

// leaf : true

// },

// {

// id : '112000',

// text : '廊坊銀行解放道支行',

// children : [

// {

// id : '112100',

// text : '廊坊銀行廣陽(yáng)道支行',

// leaf : true

// },

// {

// id : '112200',

// text : '廊坊銀行三大街支行',

// leaf : true

// }

// ]

// },

// {

// id : '113000',

// text : '廊坊銀行開發(fā)區(qū)支行',

// leaf : true

// }

// ]

// }

// ]

// }

}

}

/**

* 節(jié)點(diǎn)類

*/

class Node {

/**

* 節(jié)點(diǎn)編號(hào)

*/

public String id;

/**

* 節(jié)點(diǎn)內(nèi)容

*/

public String text;

/**

* 父節(jié)點(diǎn)編號(hào)

*/

public String parentId;

/**

* 孩子節(jié)點(diǎn)列表

*/

private List children = new ArrayList();

// 添加孩子節(jié)點(diǎn)

public void addChild(Node node) {

children.add(node);

}

//先序遍歷,拼接JSON字符串

public String toString() {

String result = "{" + "id : '" + id + "'" + ", text : '" + text + "'";

if (children.size() != 0) {

result += ", children : [";

for (int i = 0; i < children.size(); i++) {

result += ((Node) children.get(i)).toString() + ",";

}

result = result.substring(0, result.length() - 1);

result += "]";

} else {

result += ", leaf : true";

}

return result + "}";

}

// 兄弟節(jié)點(diǎn)橫向排序

public void sortChildren() {

if (children.size() != 0) {

// 對(duì)本層節(jié)點(diǎn)進(jìn)行排序(可根據(jù)不同的排序?qū)傩?,傳入不同的比較器,這里 傳入ID比較器)

Collections.sort(children, new NodeIDComparator());

// 對(duì)每個(gè)節(jié)點(diǎn)的下一層節(jié)點(diǎn)進(jìn)行排序

for (int i = 0; i < children.size(); i++) {

((Node) children.get(i)).sortChildren();

}

}

}

}

/**

* 節(jié)點(diǎn)比較器

*/

class NodeIDComparator implements Comparator {

// 按照節(jié)點(diǎn)編號(hào)比較

public int compare(Object o1, Object o2) {

int j1 = Integer.parseInt(((Node) o1).id);

int j2 = Integer.parseInt(((Node) o2).id);

return (j1 < j2 ? -1 : (j1 == j2 ? 0 : 1));

}

}

/**

* 構(gòu)造虛擬的層次數(shù)據(jù)

*/

class VirtualDataGenerator {

// 構(gòu)造無(wú)序的結(jié)果集列表,實(shí)際應(yīng)用中,該數(shù)據(jù)應(yīng)該從數(shù)據(jù)庫(kù)中查詢獲得;

public static List getVirtualResult() {

List dataList = new ArrayList();

HashMap dataRecord1 = new HashMap();

dataRecord1.put("id", "112000");

dataRecord1.put("text", "廊坊銀行解放道支行");

dataRecord1.put("parentId", "110000");

HashMap dataRecord2 = new HashMap();

dataRecord2.put("id", "112200");

dataRecord2.put("text", "廊坊銀行三大街支行");

dataRecord2.put("parentId", "112000");

HashMap dataRecord3 = new HashMap();

dataRecord3.put("id", "112100");

dataRecord3.put("text", "廊坊銀行廣陽(yáng)道支行");

dataRecord3.put("parentId", "112000");

HashMap dataRecord4 = new HashMap();

dataRecord4.put("id", "113000");

dataRecord4.put("text", "廊坊銀行開發(fā)區(qū)支行");

dataRecord4.put("parentId", "110000");

HashMap dataRecord5 = new HashMap();

dataRecord5.put("id", "100000");

dataRecord5.put("text", "廊坊銀行總行");

dataRecord5.put("parentId", "");

HashMap dataRecord6 = new HashMap();

dataRecord6.put("id", "110000");

dataRecord6.put("text", "廊坊分行");

dataRecord6.put("parentId", "100000");

HashMap dataRecord7 = new HashMap();

dataRecord7.put("id", "111000");

dataRecord7.put("text", "廊坊銀行金光道支行");

dataRecord7.put("parentId", "110000");

dataList.add(dataRecord1);

dataList.add(dataRecord2);

dataList.add(dataRecord3);

dataList.add(dataRecord4);

dataList.add(dataRecord5);

dataList.add(dataRecord6);

dataList.add(dataRecord7);

return dataList;

}

}

好了,通過(guò)上面的代碼,就可以實(shí)現(xiàn)多叉樹的兄弟節(jié)點(diǎn)橫向排序和先序遍歷了,實(shí)現(xiàn)了將層次數(shù)據(jù)轉(zhuǎn)換為有序無(wú)限級(jí)樹形結(jié)構(gòu)JSON字符串的目的。

在實(shí)際的項(xiàng)目中,可以把上面的有效代碼融入其中,或者在此基礎(chǔ)上進(jìn)行一些擴(kuò)展:

1、實(shí)現(xiàn)對(duì)指定層次的排序(例如只排序第一層的節(jié)點(diǎn),或者只排序某一父節(jié)點(diǎn)下的所有子節(jié)點(diǎn))

2、遍歷輸出樹形結(jié)構(gòu)時(shí)可以加入判斷條件過(guò)濾掉某些節(jié)點(diǎn)

3、實(shí)現(xiàn)節(jié)點(diǎn)的刪除功能

4、在節(jié)點(diǎn)類中增加一個(gè)父節(jié)點(diǎn)的引用,就可以計(jì)算出某一節(jié)點(diǎn)所處的級(jí)別

5、在不支持層次查詢的數(shù)據(jù)庫(kù)應(yīng)用系統(tǒng)中使用該算法實(shí)現(xiàn)相同的效果

四、思考與總結(jié)

這篇文章的重點(diǎn)是如何構(gòu)造有序的無(wú)限級(jí)的樹形結(jié)構(gòu)JSON字符串,一次性生成樹形結(jié)構(gòu),而不是利用Ajax的方式,反復(fù)向服務(wù)器端發(fā)送請(qǐng)求,一級(jí)接一級(jí)的加載樹節(jié)點(diǎn)。

既然可以構(gòu)造無(wú)限級(jí)的JSON字符串,那么也可以根據(jù)這個(gè)思路構(gòu)造無(wú)限級(jí)的XML字符串,或者構(gòu)造具有層次結(jié)構(gòu)的UL – LI組合(用UL - LI來(lái)展示樹形結(jié)構(gòu)),或者構(gòu)造具有層次結(jié)構(gòu)的TABLE(用TABLE來(lái)展示樹形結(jié)構(gòu))。如下所示:

(1)XML層次結(jié)構(gòu)

(2)UL - LI 層次結(jié)構(gòu)

  • 廊坊銀行總行
    • 廊坊分行
      • 廊坊銀行開發(fā)區(qū)支行
      • 廊坊銀行解放道支行
        • 廊坊銀行三大街支行
        • 廊坊銀行廣陽(yáng)道支行

      • 廊坊銀行金光道支行

(3)TABLE層次結(jié)構(gòu)

廊坊銀行總行
??廊坊分行
????廊坊銀行開發(fā)區(qū)支行
????廊坊銀行解放道支行
??????廊坊銀行三大街支行
??????廊坊銀行廣陽(yáng)道支行
????廊坊銀行金光道支行

另外對(duì)TreeGrid樹形表格也有一定的價(jià)值:

1、一次性構(gòu)造樹形表格,實(shí)現(xiàn)數(shù)據(jù)分級(jí)展示

2、通過(guò)更換比較器,實(shí)現(xiàn)對(duì)不同表格列的全排序(全排序指的是對(duì)所有頁(yè)的數(shù)據(jù)進(jìn)行排序,而不是只對(duì)當(dāng)前頁(yè)的數(shù)據(jù)排序;排序規(guī)則與Oracle數(shù)據(jù)庫(kù)中的層次查詢類似,即兄弟節(jié)點(diǎn)橫向排序)

3、實(shí)現(xiàn)對(duì)樹形表格的完整分頁(yè)(每次分頁(yè)時(shí),只取固定數(shù)目的第一層節(jié)點(diǎn),之后調(diào)用toString方法,展示出完整條數(shù)的分級(jí)數(shù)據(jù),即每頁(yè)的記錄條數(shù)是不固定的,但必須是完整的樹形結(jié)構(gòu))