樹形控制項是一種人們熟悉的使用者介面控制項,廣泛地用來顯示層次型資料。 樹形控制項具有獨特的擴充和摺疊分支的能力,能夠以較小的空間顯示出大量的資訊,一目瞭然地傳達出資料之間的層次關係。凡是熟悉圖形化使用者介面的使用者,都能夠自如地運用樹形控制項。 圖一:用JavaScript實現的樹形控制項 HTML本身不支援樹形控制項,但我們可以通過一些JavaScript指令碼代碼實現。為了提高控制項的可重用性,我們要充分運用JavaScript對物件導向編程技術的支援。本文的樹形控制項適用於IE 4+和Netscape 6.x,應當說這已經涵蓋了當前的主流瀏覽器。 一、JavaScript與物件導向 物件導向的編程有三個最基本的概念:繼承,封裝,多態性。繼承和封裝這兩個概念比較好理解,相對而言,多態性這個概念就比較難於掌握和運用。一般而言,多態性是指以多種形式表現的能力。在物件導向編程技術中,多態性表示程式設計語言擁有的一種根據對象的資料類型或類的不同而採取不同處理方式的能力。 在“純”物件導向的語言中,例如Java,多態性一般與類的繼承密不可分。也就是說,必須定義一種類的層次關係,處於頂端的是抽象類別,處於下層的是各種具體的實現。抽象類別定義了子類必須實現或覆蓋的方法,不同的子類根據自己的需要以不同的方式覆蓋抽象類別的方法。 例如,計算圓面積和矩形面積的公式完全不同,按照物件導向的設計思路,我們要先定義一個抽象類別Shape,Sharp類有一個findArea()方法,所有從Shape類派生的子類都必須實現findArea()方法。然後,我們定義一個代表矩形的Rectangle類,一個代表圓的Circle類,這兩個類都從Shape類繼承。Rectangle類和Circle類分別實現findArea()方法,兩者用不同的計算公式計算面積。最終達到這樣一個目標:不論對象屬於Shape的哪一種子類(Rectangle或Circle),都可以用相同的方式調用findArea()方法,不用去管被調用的findArea()採用什麼公式計算面積,從而有效地隱藏實現細節。 JavaScript語言不支援以類為基礎的繼承,但仍具有支援多態性的能力。JavaScript的繼承是一種基於原型(Prototype)的繼承。實際上,正如本文例子所顯示的,這種繼承方式簡化了多態性方法的編寫,而且從結構上來看,最終得到的程式也與純物件導向語言很接近。 二、準備工作 整個樹形控制項由四部分構成:圖形,CSS樣式定義,HTML架構代碼,JavaScript代碼。從圖一可以看出,樹形控制項需要三個圖形,分別表示摺疊的分支(closed.gif)、展開的分支(open.gif)和分葉節點(doc.gif)。 下面是樹形控制項要用到的CSS樣式定義: <style> body{ font: 10pt 宋體,sans-serif; color: navy; } .branch{ cursor: pointer; cursor: hand; display: block; } .leaf{ display: none; margin-left: 16px; } a{ text-decoration: none; } a:hover{ text-decoration: underline; } </style> CSS規則很簡單:body規則設定了文檔的字型和前景(文字)顏色。branch規則的用途是:當滑鼠經過擁有子節點的節點時,指標會變成手的形狀。之所以要定義兩個cursor屬性,是因為IE和Netscape使用不同的屬性名稱。在leaf規則中設定display屬性為none,這是為了實現分葉節點(不含子節點的最終節點)的摺疊效果。在指令碼代碼中,我們通過把display屬性設定成block顯示出節點,設定成none隱藏節點。 三、指令碼設計 本文實現的樹形控制項包含一個tree對象,tree對象擁有一個branches子物件集合;每一個branch(分支)對象又擁有一個子物件的集合。子物件可以是branch對象,也可以是leaf(樹葉)對象。所有這三種對象分別實現一個多態性的write()方法,不同對象的write()方法根據所屬對象的不同而執行不同的操作,也就是說:tree對象的write()方法與branch對象的write()方法不同,branch對象的write()方法又與leaf對象的write()方法不同。另外,tree和branch對象各有一個add()方法,分別用來向各自所屬的對象添加子物件。 在HTML文檔的部分加入下面這段代碼。這段代碼的作用是建立兩個Image對象,分別對應分支開啟、摺疊狀態的檔案夾圖形。另外還有幾個工具函數,用於開啟或摺疊任意分支的子項目,同時根據分支的開啟或摺疊狀態相應地變換檔案夾圖形。 <script language="JavaScript"> var openImg = new Image(); openImg.src = "open.gif"; var closedImg = new Image(); closedImg.src = "closed.gif"; function showBranch(branch){ var objBranch = document.getElementById(branch).style; if (objBranch.display=="block") objBranch.display="none"; else objBranch.display="block"; swapFolder('I' + branch); } function swapFolder(img){ objImg = document.getElementById(img); if (objImg.src.indexOf('closed.gif')>-1) objImg.src = openImg.src; else objImg.src = closedImg.src; } </script> 代碼預先裝入繪圖物件,這有利於提高以後的顯示速度。showBranch()函數首先獲得參數提供的分支的樣式,判斷並切換當前樣式的顯示內容(在block和none之間來回切換),從而形成分支的擴充和摺疊效果。swapImage()函數的原理和showBranch()函數基本相同,它首先判斷當前分支的圖形(開啟的檔案夾還是摺疊的檔案夾),然後切換圖形。 四、tree對象 下面是tree對象的建構函式: function tree(){ this.branches = new Array(); this.add = addBranch; this.write = writeTree; } tree對象代表著整個樹形結構的根。tree()建構函式建立了branches數組,這個數組用來儲存所有的子項目。add和write屬性是指向兩個多態性方法的指標,兩個多態性方法的實現如下: function addBranch(branch){ this.branches[this.branches.length] = branch; } function writeTree(){ var treeString = ''; var numBranches = this.branches.length; for (var i=0;i <numBranches;i++) treeString += this.branches[i].write(); document.write(treeString); } addBranch()方法把參數傳入的對象加入到branches數組的末尾。writeTree()方法遍曆儲存在branches數組中的每一個對象,調用每一個對象的write()方法。注意這裡利用了多態性的優點:不管branches數組的當前元素是什麼類型的對象,我們只需按照統一的方式調用write()方法,實際執行的write()方法由branches數組當前元素的類型決定——可能是branch對象的write()方法,也可能是leaf對象的write()方法。 必須說明的是,雖然JavaScript的Array對象允許儲存任何類型的資料,但這裡我們只能儲存實現了write()方法的對象。象Java這樣的純物件導向語言擁有強健的類型檢查機制,能夠自動報告類型錯誤;但JavaScript這方面的限制比較寬鬆,我們必須手工保證儲存到branches數組的對象具有正確的類型。 五、branch對象 branch對象與tree對象相似: function branch(id, text){ this.id = id; this.text = text; this.write = writeBranch; this.add = addLeaf; this.leaves = new Array(); } branch對象的建構函式有id和text兩個參數。id是一個唯一性的標識符,text是顯示在該分支的檔案夾之後的文字。leaves數組是該分支對象的子項目的集合。注意branch對象定義了必不可少的write()方法,因此可以儲存到tree對象的branches數組。tree對象和branch對象都定義了write()和add()方法,所以這兩個方法都是多態性的。下面是branch對象的add()和write()方法的具體實現: function addLeaf(leaf){ this.leaves[this.leaves.length] = leaf; } function writeBranch(){ var branchString = '<span class="branch" ' + onClick="showBranch(/'' + this.id + '/')"'; branchString += '><img src="closed.gif" id="I' + this.id + '">' + this.text; branchString += '</span>'; branchString += '<span class="leaf" id="'; branchString += this.id + '">'; var numLeaves = this.leaves.length; for (var j=0;j<numLeaves;j++) branchString += this.leaves[j].write(); branchString += '</span>'; return branchString; } addLeaf()函數和tree對象的addBranch()函數相似,它把通過參數傳入的對象加入到leaves數組的末尾。 writeBranch()方法首先構造出顯示分支所需的HTML字串,然後通過迴圈遍曆leaves數組中的每一個對象,調用數組中每一個對象的write()方法。和branches數組一樣,leaves數組也只能儲存帶有write()方法的對象。 六、leaf對象 leaf對象是三個對象之中最簡單的一個: function leaf(text, link){ this.text = text; this.link = link; this.write = writeLeaf; } leaf對象的text屬性工作表示要顯示的文字,link屬性工作表示連結。如前所述,leaf對象也要定義write()方法,它的實現如下: function writeLeaf(){ var leafString = '<a href="' + this.link + '">'; leafString += '<img src="doc.gif" border="0">'; leafString += this.text; leafString += '</a><br>'; return leafString; } writeLeaf()函數的作用就是構造出顯示當前節點的HTML字串。leaf對象不需要實現add()方法,因為它是分支的終結點,不包含子項目。 七、裝配樹形控制項 最後要做的就是在HTML頁面中裝配樹形控制項了。構造過程很簡單:建立一個tree對象,然後向tree對象添加分支節點和分葉節點。構造好整個樹形結構之後,調用tree對象的write()方法把樹形控制項顯示出來。下面是一個多層的樹形結構,只要把它加入標記內需要顯示樹形控制項的位置即可。注意下面例子中凡是應該加入連結的地方都以“#”替代: <script language="JavaScript"> var myTree = new tree(); var branch1 = new branch('branch1','JavaScript參考書'); var leaf1 = new leaf('前言','#'); var leaf2 = new leaf('緒論','#'); branch1.add(leaf1); branch1.add(leaf2); myTree.add(branch1); var branch2 = new branch('branch2','第一章'); branch2.add(new leaf('第一節','#')); branch2.add(new leaf('第二節','#')); branch2.add(new leaf('第三節','#')); branch1.add(branch2); var branch3 = new branch('branch2','第二章'); branch3.add(new leaf('第一節','#')); branch3.add(new leaf('第二節','#')); branch3.add(new leaf('第三節','#')); branch1.add(branch3); myTree.add(new leaf('聯絡我們','#')); myTree.write(); </script> 上述代碼的運行效果一所示。可以看到,裝配樹形控制項的代碼完全符合物件導向的風格,簡潔高效。 從本質上看,用物件導向技術構造的樹形控制項包含一組對象,而且這組對象實現了純物件導向的語言中稱為介面的東西,只不過由於JavaScript語言本身的限制,介面沒有明確定義而已。例如,本文的樹形控制項由tree、branch、leaf三個對象構成,而且這三個對象都實現了write介面,也就是說,這三個對象都有一個write()方法,不同對象的write()方法根據物件類型的不同提供不同的功能。又如,tree、branch對象實現了add介面,兩個對象分別根據自身的需要定義了add()方法的不同行為。可見,多態性是物件導向技術中一個重要的概念,它為構造健壯的、可伸縮的、可重用的代碼帶來了方便。
|