2008年12月29日

第 2 章 震撼吧!讓你知道ext表格控件的厲害 - PART I

2.1. 功能豐富,無人能出其右





無論是界面之美,還是功能之強,ext的表格控件都高居榜首。



單選行,多選行,高亮顯示選中的行,推拽改變列寬度,按列排序,這些基本功能咱們就不提了。



自動生成行號,支援checkbox全選,動態選擇顯示哪些列,支援本地以及遠程分頁,可以對單元格按照自己的想法進行渲染,這些也算可以想到的功能。



再加上可編輯grid,添加新行,刪除一或多行,提示臟數據,推拽改變grid大小,grid之間推拽一或多行,甚至可以在tree和grid之間進行拖拽,啊,這些功能實在太神奇了。更令人驚嘆的是,這些功能竟然都在ext表格控件裡實現了。



呵呵~不過ext也不是萬能的,與fins的ecside比較,ext不能鎖定列(土豆說1.x裡支援鎖定列,但是2.0裡沒有了,因為影響效率。),也 沒有預設的統計功能,也不支援excel,pdf等匯出數據。另外fins說,透過測試ecside的效率明顯優於ext呢。:)

2.2. 讓我們搞一個grid出來耍耍吧。





光說不練不是我們的道統,讓我們基於examples裡的例子,來自己搞一個grid看看效果,同時也可以知道一個grid到底需要配置些什麼東西。



1.



首先我們知道表格肯定是二維的,橫著叫行,豎著叫列。跟設計數據庫,新建表一樣,我們要先設定這個表有幾列,每列叫啥名字,啥類型,咋顯示,這個表格的骨架也就出來了。



ext裡,這個列的定義,叫做ColumnModel,簡稱cm的就是它,它作為整個表格的列模型,是要首先建立起來的。



這裡我們建立一個三列的表格,第一列叫編號(code),第二列叫名稱(name),第三列叫描述(descn)。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



看到了吧?非常簡單的定義了三列,每列的header表示這列的名稱,dataIndex是跟後面的東西對應的,咱們暫且不提。現下只要知道有了三列就可以了。

2.



有了表格的骨架,現下我們要向裡邊添加數據了。這個數據當然也是二維了,為了簡便,我們學習examples裡的array-grid.js裡的模式,把數據直接寫到js裡。



var data = [

['1','name1','descn1'],

['2','name2','descn2'],

['3','name3','descn3'],

['4','name4','descn4'],

['5','name5','descn5']

];



很顯然,我們這裡定義了一個二維數據, (什麼?你不知道這是二維數組?快改行吧,這裡不是你該待的地方。)



這個有五條記錄的二維數組,顯示到grid裡就應該是五行,每行三列,正好對應這id,name,descn,在我們的腦子裡應該可以想像出grid顯示的結果了,為了讓想像變成顯示,我們還需要對原始數據做一下轉化。

3.



因為咱們希望grid不只能支援array,還可以支援json,支援xml,甚至支援咱們自己定義的數據格式,ext為咱們提供了一個橋 樑,Ext.data.Store,透過它我們可以把任何格式的數據轉化成grid可以使用的形式,這樣就不需要為每種數據格式寫一個grid的實現了。 現下咱們就來看看這個Ext.data.Store是如何轉換array的。



var ds = new Ext.data.Store({

proxy: new Ext.data.MemoryProxy(data),

reader: new Ext.data.ArrayReader({}, [

{name: 'id'},

{name: 'name'},

{name: 'descn'}

])

});

ds.load();



ds要對應兩個部分︰proxy和reader。proxy告訴我們從那裡獲得數據,reader告訴我們如何解析這個數據。



現下我們用的是Ext.data.MemoryProxy,它是專門用來解析js變量的。你可以看到,我們直接把data作為參數傳遞進去了。



Ext.data.ArrayReader專門用來解析數組,並且告訴我們它會按照定義的規範進行解析,每行讀取三個數據,第一個叫id,第二個叫 name,第三個descn。是不是有些眼熟,翻到前面cm定義的地方,哦,原來跟dataIndex是對應的。這樣cm就知道哪列應該顯示那條數據了。 唉,你要是能看明白這一點,那你實在是太聰明了。



記得要執行一次ds.load(),對數據進行初始化。



有兄弟可能要問了,要是我第一列數據不是id而是name,第二列數據不是name而是id咋辦?嗯,嗯,這個使用就用mapping來解決。改改變成這樣︰



var ds = new Ext.data.Store({

proxy: new Ext.data.MemoryProxy(data),

reader: new Ext.data.ArrayReader({}, [

{name: 'id', mapping: 1},

{name: 'name', mapping: 0},

{name: 'descn', mapping: 2}

])

});



這樣如截圖所見,id和name兩列的數據翻轉了。如此這般,無論數據排列順序如何,我們都可以使用mapping來控制對應關係,唯一需要注意的是,索引是從0開始的,所以對應第一列要寫成mapping:0,以此類推。

4.



哈哈,萬事俱備只欠東風,表格的列模型定義好了,原始數據和數據的轉換都做好了,剩下的只需要裝配在一起,我們的grid就出來了。



var grid = new Ext.grid.Grid('grid', {

ds: ds,

cm: cm

});

grid.render();



注意︰上頭是ext-1.x的寫法,Ext.grid.Grid的第一個參數是渲染的id,對應在html裡應該有一個 <div id="grid"></div>的東西,這樣grid才知道要把自己畫到那裡。



創建完grid以後,還要用grid.render()方法,讓grid開始渲染,這樣才能顯示出來。

5.



好了,把所有代碼組合到一起,看看效果吧。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



var data = [

['1','name1','descn1'],

['2','name2','descn2'],

['3','name3','descn3'],

['4','name4','descn4'],

['5','name5','descn5']

];



var ds = new Ext.data.Store({

proxy: new Ext.data.MemoryProxy(data),

reader: new Ext.data.ArrayReader({}, [

{name: 'id'},

{name: 'name'},

{name: 'descn'}

])

});

ds.load();



var grid = new Ext.grid.Grid('grid', {

ds: ds,

cm: cm

});

grid.render();



看看吧,這就是咱們搞出來的grid了。



html例子是lingo-sample/1.1.1目錄下的02-01.html,把這個目錄copy到ext-1.x的example目錄下,就可以直接打開觀看效果。

2.3. 上邊那個是1.x的,2.0稍微有些不同哦





首先,Ext.grid.Grid已經不見了,咱們需要用Ext.grid.GridPanel。需要傳遞的參數也有少許區別。



var grid = new Ext.grid.GridPanel({

el: 'grid',

ds: ds,

cm: cm

});



看到了嗎?負責指定渲染位置的id放到了{}裡邊,對應的名字是el。似乎ext2裡對這些參數進行了統一,比以前更整齊了。



因為其他地方都一樣,我就不多說了,html例子在是lingo-sample/2.0目錄下的02-01.html。



從截圖上看,少了斑馬條,下邊多了一條線,應該只是css有所不同吧。



預設情況下,兩個版本的grid都可以拖拽列,也可以改變列的寬度。不知道怎么禁用這兩個功能呢。



最大的不同應該是1.x裡預設支援的右鍵效果,在2.0裡不見了。



按shift和ctrl多選行的功能倒是都有。區別是,全選後,1.x必須按住ctrl才能取消,直接單擊其中一個行,不會取消全選功能,而2.0裡只需要任意點擊一行,就取消全選,只選中剛才點擊的那行。



哦,哦,那顏色不要也算是區別吧。

2.4. 按順序,咱們先要把常見功能講到,讓grid支援按列排序





其實很簡單,需要小小改動一下列模型。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id',sortable:true},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



如果你英語還可以,或者懂得查字典的話(軟體翻譯也算),那麼你就會知道,多出來的這個sortable屬性應該是可以排序的意思。現下咱們試一下改動後的效果。



看到了沒有?編號的標題上有個小小的箭頭,表格裡的數據也是按照編號做的逆序排列,如此簡單,我們就實現了按列排序。



很有趣的是,2.0加上sortable以後,1.x那種右鍵功能也跑回來了,不過它用的不是右鍵,而是下拉選單似的實現模式。



什麼?你問為什麼其他兩列無法排序?﹗嗯,好像是因為你還沒有給另兩列添加sortable屬性。



怎么加?﹗按編號那樣加就行了。



還是不會?﹗-_-。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id',sortable:true},

{header:'名稱',dataIndex:'name',sortable:true},

{header:'描述',dataIndex:'descn',sortable:true}

]);



這樣所有列都可以排序了。什麼?怎么取消排序?﹗-_-。

2.5. 讓單元格裡顯示紅色的字,圖片,按鈕,你還能想到什麼?





嘿,希望你跟我一樣,不願意只能在grid裡看到文字,至少不是單調的,毫無特色的文字。有些人就問了,如果我想改變一下單元格裡顯示內容,應該怎么辦呢?



非常不幸的是,ext的作者,偉大的jack早已經想到了,說真的,你沒想到的,他都想到了,不只想到了,他還做出來了。



唉,這就是區別啊。為啥你就不能動手做些東西呢?就知道向別人要這要那,唉。



首先,我宣佈,偶們的數據要擴充啦,每個人要加上一個性別字段。



var data = [

['1','male','name1','descn1'],

['2','female','name2','descn2'],

['3','male','name3','descn3'],

['4','female','name4','descn4'],

['5','male','name5','descn5']

];



男女搭配,干活不累撒。而且現下中國就是男多女少,我就還沒對象呢。征婚中,單身女性加(QQ)771490531詳談。



你可以試試不改其他的部分,顯示的結果是不會改變的,因為原始數據要經過ds的處理才能被grid使用,那麼下一步我們就開始修改ds,把性別加進去。



var ds = new Ext.data.Store({

proxy: new Ext.data.MemoryProxy(data),

reader: new Ext.data.ArrayReader({}, [

{name: 'id'},

{name: 'sex'},

{name: 'name'},

{name: 'descn'}

])

});



添加了一行{name: 'sex'},把數組的第二列映射為性別。現下grid可以感覺到sex了,嘿嘿。



不過grid還顯示不了性別這列,因為咱們還沒改cm。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



到現下其實都沒什麼新東西,但是你不覺得光看平板字,很難分出哪個是GG哪個是MM嗎?聽說過紅男綠女沒?要是男的都加上紅色,女的都變成綠色,那不是清楚多了。就像下面一樣。



怎么樣?是不是效果大不同了。你不會認為很難吧,嗯,確實,如果你對html和css完全搞不明白的話,勸你還是先去學學吧,對自己有信心的往下看。



var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex',renderer:function(value){

if (value == 'male') {

return "<span style='color:red;font-weight:bold;'>紅男</span>";

} else {

return "<span style='color:green;font-weight:bold;'>綠女</span>";

}

}},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



別被嚇到,這么一大段其實就是判斷是男是女,然後配上顏色。你要是覺得亂,也可以這么做。



function renderSex(value) {

if (value == 'male') {

return "<span style='color:red;font-weight:bold;'>紅男</span>";

} else {

return "<span style='color:green;font-weight:bold;'>綠女</span>";

}

}

var cm = new Ext.grid.ColumnModel([

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex',renderer:renderSex},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



實際上這個renderer屬性至關重要,它的值是一個function,哦,你說不知道js裡function可以這么用?那麼恭喜你,現下你知道了。



renderer會傳遞個參數進去,咱們grid裡看到的,是這個函數的返回值,怎么樣,神奇吧?



同志們,你們也應該看到了,返回html就可以,是html啊,html裡有的東西,你返回什麼就顯示什麼,顏色,鏈接,圖片,按鈕,只要你願意,整個網頁都可以返回回去。還有什麼做不到的?哦,你不會html,那沒轍,回去學吧。



咱們先來個圖片。



function renderSex(value) {

if (value == 'male') {

return "<span style='color:red;font-weight:bold;'>紅男</span><img src='user_male.png' />";

} else {

return "<span style='color:green;font-weight:bold;'>綠女</span><img src='user_female.png' />";

}

}



是不是太簡單了,下面咱們來玩點兒進階的。



function renderDescn(value, cellmeta, record, rowIndex, columnIndex, store) {

var str = "<input type='button' value='檢視詳細訊息' onclick='alert(\"" +

"這個單元格的值是︰" + value + "\\n" +

"這個單元格的配置是︰{cellId:" + cellmeta.cellId + ",id:" + cellmeta.id + ",css:" + cellmeta.css + "}\\n" +

"這個單元格對應行的record是︰" + record + ",一行的數據都在裡邊\\n" +

"這是第" + rowIndex + "行\\n" +

"這是第" + columnIndex + "列\\n" +

"這個表格對應的Ext.data.Store在這裡︰" + store + ",隨便用吧。" +

"\")'>";

return str;

}



來看看我們可以在render裡用到多少參數︰



1.



value是當前單元格的值

2.



cellmeta裡儲存的是cellId單元格id,id不知道是干啥的,似乎是列號,css是這個單元格的css樣式。

3.



record是這行的所有數據,你想要什麼,record.data["id"]這樣就獲得了。

4.



rowIndex是行號,不是從頭往下數的意思,而是計算了分頁以後的結果。

5.



columnIndex列號太簡單了。

6.



store,這個厲害,實際上這個是你構造表格時候傳遞的ds,也就是說表格裡所有的數據,你都可以隨便調用,唉,太厲害了。



有個同學就問啦︰EXT render的參數,是如何得到的呢。因為你講的那些都是EXT自己內部的。它是如何把這些參數傳遞給render的呢?



這個問題其實比較簡單,只是你們想複雜了。既然是函數,就肯定有調用它的地方,你找到GridView.js在裡邊搜索一下renderer,就會看到調用render的地方,這些參數都是在這裡傳進去的。



好,看看效果吧。



剩下的,就是發揮各位聰明才智的時候了,舞台已經搭好,看你如何作秀了。



html例子,1.x版本在lingo-sample/1.1.1目錄下的02-02.html,2.0的版本在lingo-sample/2.0目錄下的02-02.html。

2.6. 更進一步,自動行號和多選checkbox





實際上行號和多選checkbox都是renderer的延伸,當然多選checkbox更酷一點兒,兩者經常一起使用,所以讓我們放在一起討論好了。

2.6.1. 自動行號





只需要在cm中加上一行,這一行不會與ds中的任何數據對應,這也告訴我們可以憑空製作列,哈哈。



在之前的例子上改啦。



var cm = new Ext.grid.ColumnModel([

{header:'NO.',renderer:function(value, cellmeta, record, rowIndex){

return rowIndex + 1;

}},

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



如吾等所愿,不指定dataIndex,而是直接根據renderer返回rowIndex + 1,因為它是從0開始的,所以加個一。截圖如下。



1.x的例子在lingo-sample/1.1.1/02-03.html



很遺憾的是,2.0裡有自己的預設實現了,咱們不能展現自己的手工技藝了,還是乖乖使用jack提供的東東吧。



於是,在2.0裡cm就變成了這幅模樣。



var cm = new Ext.grid.ColumnModel([

new Ext.grid.RowNumberer(),

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



你絕對會同意我的意見,Jack's work is amazing.實在是太神奇了。看看截圖就知道。



2.0的例子在lingo-sample/2.0/02-03.html

2.6.2. 全選checkbox的時間了,請允許我讓2.0先上場。





因為2.0裡有checkboxSelectionModel,這樣完全可以證實用別人的輪子,比自己造輪子要方便。而且咱們造的輪子完全沒有jack圓。不信的話,看下面1.x裡的實現。



我們一直在修改cm,這次我們也要對它動刀了,不過SelectionModel既sm也要處理一下,這是為了改變單選和多選行的模式,以前這些可以靠shift或ctrl實現,而現下這些都要與checkbox關聯上了。



啦啦啦,先看圖片,後看代碼。



先看看具體多了什麼



var sm = new Ext.grid.CheckboxSelectionModel();



神奇的是這個sm身兼兩職,使用的時候既要放到cm裡,也要放到grid中。代碼如下。



var cm = new Ext.grid.ColumnModel([

new Ext.grid.RowNumberer(),

sm,

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



var grid = new Ext.grid.GridPanel({

el: 'grid',

ds: ds,

cm: cm,

sm: sm

});



然後你就可以得到效果啦,代碼在lingo-sample/2.0/02-04.html。

2.6.3. 1.x時代的全選checkbox。





理論上只需要給cm再加一列,像自動編號那樣,不對應數據,只顯示checkbox就可以了。難點就是checkbox與某一行被選擇的時候要對應上,用checkbox選擇上,sm裡也要讓這一行被選中,反之亦然。嗯,估計會比較複雜呢。



先放上顯示checkbox的代碼和截圖︰



var cm = new Ext.grid.ColumnModel([

{header:'NO.',renderer:function(value, cellmeta, record, rowIndex){

return rowIndex + 1;

}},

{header:'<input type="checkbox" onclick="selectAll(this)">',renderer:function(value, cellmeta, record, rowIndex){

return '<input type="checkbox" name="cb">';

}},

{header:'編號',dataIndex:'id'},

{header:'性別',dataIndex:'sex'},

{header:'名稱',dataIndex:'name'},

{header:'描述',dataIndex:'descn'}

]);



與sm對接的方面比較麻煩,好在extjs.com上已經有擴展了,或者你可以看看我們弄下來的。 看看1.x多選樹的截圖。

2.7. 分頁了嗎?分頁了嗎?如果還沒分就看這裡吧。





如果你有一千條訊息,一次都輸出到grid裡,然後讓客戶用下拉條一點兒一點兒去找吧。我們這裡可是要做一個分頁效果了,不用滾動螢幕,或者滾動一下就可以看到本頁顯示的數據,如果想看其他的只需要翻頁就可以了。同志們,加強客戶體驗呀。



實際上,grid控件挺耗性能的,據土豆講一個頁面上放3個grid就可以感覺到附應變慢,以前看過介紹,grid裡顯示數據過多,聽說是上千條,也會明顯變慢。



所以說分頁是必不可少滴,而且jack提供了方便集成分頁工具條的模式,不用一下實在是太浪費了。



兩步走,讓grid集成分頁。

2.7.1. 表面工作,先把分頁工具條弄出來。





從圖片可以清晰的看到,在grid下邊多出來一行東東,包括了前一頁,後一頁,第一頁,最後一頁,刷新,以及提示訊息。而我們不過寫了如下幾行代碼而已,神奇呀。



var gridFoot = grid.getView().getFooterPanel(true);



var paging = new Ext.PagingToolbar(gridFoot, ds, {

pageSize: 10,

displayInfo: true,

displayMsg: '顯示第 {0} 條到 {1} 條記錄,一共 {2} 條',

emptyMsg: '沒有記錄'

});



首先使用grid.getView().getFootPanel(true),獲得grid下邊那一條,嘿嘿,人家grid就設計的這么好,腳底下專門留了地方讓你放東西。我們老實不客氣,把Ext.PagingToolbar放到上邊,就可以顯示分頁工具條了。



這分頁工具條可不是個擺設,你按了前一頁,後一頁,整個grid都要有回應才對,要不咱們費勁弄這個么東西過來干嘛呀?所以,我們在構造 PagingToolbar的時候,不但告訴它,在gridFoot上顯示,還告訴它,進行分頁跳轉的時候,要對ds也進行操作。這個ds就是grid用 來獲取和顯示數據的,它一變整個grid都發生變化,嘿嘿~這就算共享數據模型了。厲害呀。



知道了分頁的玄機,讓我們揉揉眼睛,好好看看裡邊的參數。



1.



pageSize,是每頁顯示幾條數據。

2.



displayInfo,跟下面的配置有關,如果是false就不會顯示提示訊息。

3.



displayMsg,只有在displayInfo:true的時候才有效,用來顯示有數據的時候的提示訊息,中國人應該看得懂漢語,到時候{0},{1},{2}會自動變成對應的數據,咱們只需要想辦法把話說通就行了。

4.



emptyMsg,要是沒數據就顯示這個,jack實在太貼心了,連這些小處都考慮得如此精細。



最好要注意的是,這段代碼必須放在grid.render()之後,估計是因為動態生成grid的dom後,才能獲得對應的footPanel。



現下給出例子,1.x的例子在lingo-sample/1.1.1/02-05.html。

2.7.2. 2.0賜予我們更大的靈活性





其實,在下,一直,對於︰必須先渲染grid才能獲得footPanel這事非常憤恨,你想啊,本來分頁也應該屬於初始化的一部分,現下卻要先初始化 grid,配置完畢,渲染,回過頭來再從grid裡把footPanel拿出來,再咕噥分頁的配置。真,真,真郁悶呀。



所以2.0裡的模式,簡直大快民心。



var grid = new Ext.grid.GridPanel({

el: 'grid',

ds: ds,

cm: cm,

bbar: new Ext.PagingToolbar({

pageSize: 10,

store: ds,

displayInfo: true,

displayMsg: '顯示第 {0} 條到 {1} 條記錄,一共 {2} 條',

emptyMsg: "沒有記錄"

})

});

ds.load();



嘿嘿,加一個bbar的參數就可以了,bbar就是bottom bar啦,底端工具條。



不過還是要注意一點,與1.x中不同的是,如果配置了分頁工具條,ds.load()就必須在構造grid以後才能執行,否則分頁工具條會不起作用。看來分頁工具條會把自己和ds做一些關聯,來完成與grid共享數據模型的。



對了,還有一點不同,1.x中分頁條不會隨著瀏覽器的大小改變,自動放縮,這點在2.0中也解決了。



2.0的例子在lingo-sample/2.0/02-05.html。