2014年6月28日 星期六

股票交易成本計算

今天在一本書上看到股票交易成本的計算, 想到剛學的 Google 試算表, 剛好拿來應用一下. 一算不得了, 手續費千分之 1.425, 交易稅千分之 3, 感覺好小的數字, 但其真正的威力要實際算過, 轉成交易成本百分比, 才會有較深刻的感覺.

股票買入與賣出都要交 0.1425% 的手續費, 這是賭場稅, 是各賭場 (證券行) 的營收來源. 股票賣出時要交證交稅 0.3%, 這是莊家稅, 也就是政府抽頭的部分. 因此一張股票買進時每股 100 元, 當日沖銷上漲 1 元賣掉 (101 元賣出), 要交的稅總共是 :

100*1000*0.1425% (買入手續費) + 101*1000*0.1425% (賣出手續費) +
101*1000*0.3% (賣出證交稅) = 589 元

賣出一張獲利 1000 元, 要交 589 元稅, 因此交易成本為 589/1000=58.9%, 也就是超過一半, 將近 6 成的獲利被賭場跟莊家抽走了. 真好賺,  不管你是賺是賠, 抽頭的躺著就能賺. 難怪政府要開放當日沖銷, 股民交易越頻繁, 政府跟證券行賺越多.

我假設某股票市價 100 元, 從賺 1 元賣出到賺 30 元作成試算表, 計算交易成本如下圖所示 :


可見當投資人獲利越高時, 交易成本占獲利比才會下降, 如果以接近漲停板 (7%) 當日賣出, 這時交易成本會降到 8.8% 左右. 如果不要做短線, 持有到賺 14% 以上時才賣出, 交易成本就會降到 5% 以下. 若能中長期持有到獲利 30% 才賣出, 那麼交易成本只佔獲利的 2.4%, 比起極短線當日沖的 58.8%, 相差約 25 倍, 很恐怖吧!

從另一個角度來看, 同樣是漲 10 元就賣出, 越高價的股票獲利率越低, 交易成本越高, 當然啦, 敢買千元股票的人, 怎麼可能只賺 10 元就跑呢?



自從開放當日沖銷助漲後, 股市紅通通,一舉突破 9000 點, 交易量放大, 政府與證券商各個眉開眼笑, 簡單的數學就能振興股市, 真是太厲害了. 安倍三箭怎能跟曾董事長比啊, 根本不需要三箭, 咱們一箭就雙鵰!

函數 :
每股獲利 (元)=B2-A2
交易稅=ROUND(B2*0.003*1000)/1000
手續費=ROUND((A2+B2)*0.001425*1000)/1000
交易成本 (元)=E2+F2
交易成本 (%)=ROUND(G2/C2*100*10)/10

只會要求禮貌

今之教育部, 相當於古時的禮部, 官員講話都很有禮貌, 也要求愛抗議愛丟鞋子的學生要有禮貌. 但他們啥也不會, "只會" 要求禮貌.

# 學者質疑十二年國教 教育部澄清

我看了真的無言, 驚覺自我感覺良好症會傳染 !
  1. 免試入學是要減輕學生壓力, 不要一試定江山 :
    那會考是幹啥用 ?
  2. 讓同學自己瞭解學習情況, 也讓學校對於「待加強」學生可進行補救教學 :
    就是去各大補習班補救? 都畢業了, 學校補救個 ~~~ 屁 !
  3. 十二年國教要讓學生就近入學 :
    怕被志願序扣分登記不上住家附近的高中, 只好捨近求遠, 高分低就.
  4. 第一次免試入學以第一志願報到率 83.06% :
    那是因為大家都不想再熬了, 念遠一點的學校沒關係.
  5. 十二年國教要讓學生五育均衡發展, 不要只會讀書
    那還要會考幹啥?
前天看報紙讀者投書, 大意是質疑 "不要只會" 讀書這句話, 吳寶春只會做麵包; 王建民只會打棒球; 李遠哲只會做研究 .... 一個做學問的料, 你罵他只會讀書? 這國家這樣辦教育, 真的沒希望了.

現在的教育大有問題. 光是看到會考 A 級稱為精熟級, 就知道從教育原理的核心開始爛掉了. 我對精熟就很感冒. 精熟表示啥? 就是 "訓練有素的狗", 不是嗎? 我們的教育原來重視的是不斷地背誦歷史事件與地理名詞, 演練數學題目, 不重視觀念與思辯, 一堆選擇題與是非題, 光是用猜的全部填 A 都可能得 60 分以上.

台灣學生在奧林匹亞數學大賽叱吒風雲, 但四十年來出了幾個數學家? 那個被我們笑得半死的笨笨的建構式數學, 老美用這套教出來的學生在中小學根本沒辦法跟我們比, 但是上了大學研究所後, 後勁就出來了. 問題出在哪裡? 我們只是訓練有素的狗罷了, 所以只能幫人代工. 我們的學生每天都在搞精熟, 好奇與發想能力早就被搞掉囉.

把對的人擺到對的位置上去, 這就是為國家培育人才的最簡單原則, 辦教育辦成這樣, 四十年過去了還是沒變, 我覺得, 禮部官員 "不要只會" 要求禮貌 !


2014年6月24日 星期二

ExtJS 4 測試 : GridPanel (一)

我的 ExtJS 4 學習走走停停, 今天被派去受 "安全程式碼" 的訓練, 為了避免無聊, 帶了半年前買的 "深入淺出 ExtJS" 去看, 不知不覺就把 GridPanel 看完了. 趁著記憶猶新, 就來簡單測試一番. 基本上參考舊作 jQuery UI 的 DataTables 外掛的測試步驟來測 :

# jQuery 套件 DataTables 的測試

ExtJS 有四種資料控件 (Data Controls), 都統一由 Ext.data 套件中的 Store 類別提供資料來源 :

  1. 下拉式選單 (Combobox)
  2. 表格 (Grid Panel)
  3. 樹狀結構 (Tree Panel)
  4. 圖表 (Chart)

其中 GridPanel 是 ExtJS 最受歡迎的功能之一. ExtJS 的表格是由 Ext.grid.Panel 類別之實體負責呈現, 它繼承自 Ext.panel.Panel 類別 :

Ext.panel.Panel
      |__ Ext.panel.Table
                  |__ Ext.grid.Panel

此類別有三個別名 :
  1. Ext.ListView
  2. Ext.grid.GridPanel
  3. Ext.list.ListView
亦即用哪一個都可以, 我建議用 Ext.grid.Panel, 比較好記, 其 API 參考 :

# http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.grid.Panel

GridPanel 相當於豪華版的 <table> 元素, 因此跟 <table> 一樣主要由兩個部分構成 :
  1. 欄位標題 columns
  2. 資料儲存 store
由於 GridPanel 源自 Panel 類別, 因此當然也可以設定上工具列 tbar 與下工具列 bbar. 基本用法是直接建立 Ext.grid.Panel 物件, 指定 columns, store, 與 renderTo 屬性即可, 也可以不用 renderTo 屬性, 但改呼叫 render() 方法.

var grid=Ext.create("Ext.grid.Panel", {
    columns:columns, 
    store:store,
    renderTo:"grid"   //id 為 grid 之 div
    });

或者 :

var grid=Ext.create("Ext.grid.Panel", {
    columns:columns, 
    store:store
    });
grid.render("grid");   //id 為 grid 之 div

其中, columns 是表格標頭 (欄位) 定義, 是一個具有 header 與 dataIndex 屬性的物件陣列. 表格中的欄位其顯示順序完全由此 columns 中的順序決定. 表格的原始資料可以是近端的二維陣列, 或是遠端的 JSON 或 XML 資料. 但 GridPanel 無法直接取用原始資料, 必須透過 Proxy 轉成 Store 物件才能被 GridPanel 所用. 二維陣列可以用 Ext.data.ArrayStore 來轉成 Store 物件, 利用 fields 屬性來與 columns 欄位相配對.

Ext.grid.Panel 通常從下列來源取得資料 :
  1. 二維陣列 (近端)
  2. Ajax JSON (遠端)
  3. Ajax XML (遠端)
首先來看看由程式本身的二維陣列所提供的近端資料, 如下列範例 1 所示 :

測試範例 1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_1.htm [看原始碼]  

  <div id="grid"></div>
  <script type="text/javascript">
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]

          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        title:"台股",
        columns:columns,
        store:store,
        renderTo:"grid",
        autoHeight:true,
        width:450
        });
      }); //end of onReady
  </script>

注意, 在 store 的 fields 屬性中, 各元素的順序必須與原始資料 data 的排列順序相同, 但不必與 columns 的元素順序相同, 因為它是透過 name 屬性之值與 columns 的 tableIndex 屬性配對.

這裡我們設定了 width 屬性, 將 GridPanel 寬度限制為 450px, 否則預設會占滿整個瀏覽器寬度. 每一個欄位預設會分配到 100px 的寬度, 剩下的就會在右方留下一個空的欄位. 如果要讓個欄位占滿整個表格, 不要留白, 則可以在 GridPanel 中加入 forceFit : true.

預設欄位寬度可以用滑鼠拖曳欄位中間的分隔線加以調整, 如果要指定欄位寬度, 必須在 columns 欄位定義中加入 width 屬性.  如果要禁止調整欄位寬度, 可在 columns 中加入 fixed : true 即可.

預設每一個欄位都可以排序, 當滑鼠移到欄位標題上時, 會出現一個小三角形, 點一下可以選擇排序方式與欄位. 直接點欄位標題則會以該欄資料進行排序 (正向/反向每按一次 toggle). 如果想禁止排序, 可以在 columns 欄位定義中加入 sortable : false, 則點該欄位標題時就不會排序了.

在下列範例 2 中, 我們在第一欄位定義中加入 sortable:false 使其無法排序, 同時也將 GrdiPanel 設為forceFit :

測試範例 2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_2.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",sortable:false},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


可見 forceFit: true 會讓所有欄位平均分配表格寬度, 而且第一欄位的正向與反向排序也被 sortable : false 給禁掉了, 沒辦法排序, 其他欄位則可.

前台的初始排序事實上可以呼叫 Store 物件的 sort() 方法並傳入欄位名稱與排序方式來達成 :

store.sort("id","desc");

測試範例 2-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_2_1.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      store.sort("id","desc");
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady


可見當網頁一載入時, 初始排序就依股票代號倒序排列了. 如果連續呼叫 sort() 的話, 會以最後一次呼叫來排序, 如下列範例 2-2 所示 :

測試範例 2-2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_2_2.htm [看原始碼]

      store.sort("id","desc");
      store.sort("close","asc");   //最後一個才有作用 (覆蓋前面的)


可見初始載入時是以收盤價升序排列.

所以若要同時排序多個欄位, 不能像上面那樣連續呼叫 sort(), 必須需傳入物件陣列, 以 property 屬性指定要排序之欄位 (依前後順序), 以 direction 指定排序方向 :

      store.sort([{property:"close",direction:"desc"},
                       {property:"id",direction:"asc"}]);  //先用 close 降序後, 再用 id 升序

如下列範例 2-3 所示 :

測試範例 2-3 : http://mybidrobot.allalla.com/extjstest/extjs_grid_2_3.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],   
                ["華擎","3515",108.0,175],
                ["訊連","5203",108.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      store.sort([{property:"close",direction:"desc"},
                  {property:"id",direction:"asc"}]);
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady



雖說同時排序, 其實是依序排序, 所以會先用 close 降序後, 相同部分再用 id 升序排列, 此處為了觀察方便, 我把 data 中的創見, 華擎, 訊連三家的收盤價全部改成 108 元, 所以以 close 先排序時三者同順序, 這時就由第二個排序欄位 id (股票代號) 來做升序排序.

事實上 Ext.data.ArrayStore 與 Ext.data.Store 類別都有一個 sorters 屬性來設定初始排序欄位, 與呼叫 sorter() 方法效果是一樣的. Store 的 load() 方法也可以用 autoLoad:true 屬性代替, 我們把範例 2-3 改寫為如下範例 2-4 :

測試範例 2-4 : http://mybidrobot.allalla.com/extjstest/extjs_grid_2_4.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",108.0,175],
                ["訊連","5203",108.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}],
          sorters:[{property:"close",direction:"desc"},
                  {property:"id",direction:"asc"}],
          autoLoad:true
          });
      //建立 GridPanel 
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true
        });
      }); //end of onReady

接下來範例三則是測試固定欄位寬度, 另外既然 GridPanel 繼承自 Panel 類別, 我們也可以加上表格標題 (title) 與外框 (frame) :

測試範例 3 : http://mybidrobot.allalla.com/extjstest/extjs_grid_3.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",width:80,fixed:true},
                   {header:"股票代號",dataIndex:"id",width:80,fixed:true},
                   {header:"收盤價 (元)",dataIndex:"close",width:90,fixed:true},
                   {header:"成交量 (張)",dataIndex:"volumn",width:90,fixed:true}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        title:"台股",
        columns:columns,
        store:store,
        renderTo:"grid",
        frame:true,
        width:450
        });
      }); //end of onReady


可見加入 frame:true 後, 表格外面多了一層外框, 欄位也設定了固定寬度 而且不能調整.

禁止調整欄位寬度除了在 columns 欄位定義中使用 fixed : true 外, 也可以在 GridPanel 中將 enableColumnResize 屬性設為 false. 另外, 表格的各欄位繪製完成後, 預設是可以用滑鼠拖曳改變排列順序的, 如果要禁止, 可以在 GridPanel 中將 enableColumnMove 屬性設為 false, 如下列範例 4 所示 :

測試範例 4 : http://mybidrobot.allalla.com/extjstest/extjs_grid_4.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        enableColumnMove:false,
        enableColumnResize:false
        });
      }); //end of onReady

可見不但欄位無法移動, 寬度也無法調整了. 欄位寬度設定還有一個 flex 屬性, 這是用來分配剩餘寬度比率的, 亦即, 當扣除全部有設 width 屬性的欄位寬度以及無 width 的預設 100px 後, 剩下的寬度就由 flex 所佔比率來瓜分, 比率就是該欄的 flex 除以全部 flex 總和, 如下面範例 4-1 所示 :

測試範例 4-1http://mybidrobot.allalla.com/extjstest/extjs_grid_4_1.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[{header:"股票名稱",dataIndex:"name",width:80},
                   {header:"股票代號",dataIndex:"id",flex:1},
                   {header:"收盤價 (元)",dataIndex:"close",flex:1},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        title:"台股",
        columns:columns,
        store:store,
        renderTo:"grid",
        frame:true,
        width:450
        });
      }); //end of onReady


此例中, 第一欄有設 width 為 80px, 第四欄沒有設, 故預設 100px, 剩下 450-80-100=270px 就由中間兩個欄位均分 (各佔 135px), 因為這兩欄的 flex 均為 1.

GridPanel 還有一個功能是, 可在表格中顯示列號, 當表格資料多時, 可以很快地定位某筆資料, 也可以知道現在總共顯示了幾筆資料. 這可以在 columns 欄位定義中, 呼叫 Ext.grid 的 RowNumberer() 方法產生一個列編號物件, 如下列範例 5 所示 :

測試範例 5 : http://mybidrobot.allalla.com/extjstest/extjs_grid_5.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450
        });
      }); //end of onReady


注意, 此例中的列編號欄的標題預設是空白, 其實 Ext.grid.RowNumberer 有 text 與 width 兩個屬性可以設定, 如果將範例 5 改成如下, 就會出現標題了 :

Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50});

如下列範例 5-0 所示 :

測試範例 5-0 : http://mybidrobot.allalla.com/extjstest/extjs_grid_5_0.htm [看原始碼]


在下面範例 5-1 中, 我們要測試表格的列選取模式. GridPanel 預設是單列選取, 故上面的每個範例都沒辦法按住 Ctrl 或 Shift 鍵用滑鼠做多選動作. 要控制選取模式必須設定 GirdPanel 的 selModel (或用簡寫 sm 亦可), 其值為一個 Ext.selection.RowModel 類別的實體物件.

ExtJS 4 的 selection 套件包含 5 個選取類別 : Model, CellModel, RowModel, CheckboxModel, 以及 TreeModel, 詳見 :

http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.selection.RowModel
http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.selection.CheckboxModel
# http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.selection.CellModel
http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.selection.TreeModel
http://docs.sencha.com/extjs/4.2.2/#!/api/Ext.selection.Model

這裡要用到的是 RowModel 類別, 其 mode 屬性可用來控制表格的列選取模式, 其值為字串 :
  1. "SINGLE" : 單選
  2. "SIMPLE" : 複選, 但只能一個一個選
  3. "MULTI" : 複選, 可配合 Ctrl 或 Shift 做區域複選
下面範例 5-1 為 SINGLE 模式, 5-2 為 SIMPLE 模式, 5-3 為 MULTI 模式.

測試範例 5-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_5_1.htm [看原始碼]
測試範例 5-2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_5_2.htm [看原始碼]
測試範例 5-3 : http://mybidrobot.allalla.com/extjstest/extjs_grid_5_3.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        selModel:Ext.create("Ext.selection.RowModel",{mode:"SINGLE"})
        });
      grid.on("itemclick",function(){
        var selected=grid.getSelectionModel().selected;
        var sel=[];
        for (var i=0; i<selected.getCount(); i++){
          var r=selected.get(i);
          var msg=r.get("name") + "," + r.get("id") + "," +
                        r.get("close") + "," + r.get("volumn");
          sel.push(msg);
          }
        Ext.Msg.alert("訊息","您選取的內容:<br>" + sel.join("<br>"));
        });
      }); //end of onReady


可見 SINGLE 是每次只能選一列, 點另一列時原先選取者會自動取消; 而 SIMPLE 是不會取消, 除非再點選一下已被選取者 (toggle); 而 MULTI 則是可按住 Ctrl 或 Shift 一次選一段區間.

注意, ExtJS 3 時所使用的 rowclick 事件在 4 版時已無作用, 必須使用 itemclick 事件.

除了放置列編號外, 也可以放置核取方塊欄, 這要將 GridPanel 的 selModel 設置為一個 CheckboxModel 物件 (預設選擇模式為 SINGLE), 如下列範例 6 所示 :

測試範例 6http://mybidrobot.allalla.com/extjstest/extjs_grid_6.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        selModel:Ext.create("Ext.selection.CheckboxModel")
        });
      }); //end of onReady




表格標頭上的核取方塊是全選/全取消動作, 已經內建不須自行處理. 我們將範例 6 加上 itemclick 事件監聽器, 分別設定 mode 為 SINGLE (預設), SIMPLE, 與 MULTI, 如下列範例 6-1, 6-2, 6-3 所示 :

測試範例 6-1http://mybidrobot.allalla.com/extjstest/extjs_grid_6_1.htm [看原始碼]
測試範例 6-2http://mybidrobot.allalla.com/extjstest/extjs_grid_6_2.htm [看原始碼]
測試範例 6-3http://mybidrobot.allalla.com/extjstest/extjs_grid_6_3.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        selModel:Ext.create("Ext.selection.CheckboxModel",{mode:"MULTI"})
        });
      grid.on("itemclick",function(){
        var selected=grid.getSelectionModel().selected;
        var sel=[];
        for (var i=0; i<selected.getCount(); i++){
          var r=selected.get(i);
          var msg=r.get("name") + "," + r.get("id") + "," +
                  r.get("close") + "," + r.get("volumn");
          sel.push(msg);
          }
        Ext.Msg.alert("訊息","您選取的內容:<br>" + sel.join("<br>"));
        });
      }); //end of onReady


既然我們已可選取表格中的各列, 那麼也可以將選取之列從表格中刪除, 實際也就是從 Store 中移除選取的列, 再更新顯示的 view 即可, 如下列範例 7 所示 :

測試範例 7http://mybidrobot.allalla.com/extjstest/extjs_grid_7.htm [看原始碼]

  <div id="grid"></div>
  <input type="button" id="delete" value="刪除">
      Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        selModel:Ext.create("Ext.selection.RowModel",{mode:"SIMPLE"})
        });
      Ext.get("delete").on("click",function(){
        var selected=grid.getSelectionModel().selected;
        var sel=[];
        for (var i=0; i<selected.getCount(); i++){
          var r=selected.get(i);
          sel.push(r);
          }
        store.remove(sel);
        grid.view.refresh();
        });

      }); //end of onReady


當然, CheckboxModel 也可以這麼做, 如範例 8 所示 :

測試範例 8http://mybidrobot.allalla.com/extjstest/extjs_grid_8.htm [看原始碼]

  <div id="grid"></div>
  <input type="button" id="delete" value="刪除">
  <script type="text/javascript">
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      store.load();
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        selModel:Ext.create("Ext.selection.CheckboxModel",{mode:"SIMPLE"})
        });
      Ext.get("delete").on("click",function(){
        var selected=grid.getSelectionModel().selected;
        var sel=[];
        for (var i=0; i<selected.getCount(); i++){
          var r=selected.get(i);
          sel.push(r);
          }
        store.remove(sel);
        grid.view.refresh();
        });

      }); //end of onReady
  </script>


接下來是此次測試的重點, 也就是分頁功能. ExtJS 4 提供了 Ext.PagingToolbar 類別來處理分頁功能, 前面提到 GridPanel 可以用 tbar 與 bbar 屬性來放置工具列, 我們試著用上面的本機陣列資料來測試 ExtJS 的前台分頁功能, 如下面範例 9 所示 :

測試範例 9http://mybidrobot.allalla.com/extjstest/extjs_grid_9.htm [看原始碼]
 
    Ext.QuickTips.init(); //啟動分頁按鈕提示功能
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.ArrayStore", {
          data:data,
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        bbar:Ext.create("Ext.PagingToolbar",{
          pageSize:2,
          store:store,
          displayInfo:true,
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄,共 {2} 列",
          emptyMsg:"沒有資料"
          })

        });
      store.load();
      }); //end of onReady


可見, 雖然設定了 pageSize=2, 但前台分頁功能並未實現, 而是一次顯示了全部六筆資料. 這說明 ExtJS 的 ArrayStore 是不支援前台分頁的. 如果要實現前台分頁, 必須使用 PagingMemoryProxy 這個擴充功能, 此擴充函式位置在 ExtJS 解壓縮目錄下的 examples\ux\data\PagingMemoryProxy.js, 如果是自備 ExtJS 函式庫, 建議在 extjs 目錄下建一個 ux 目錄, 然後將 PagingMemoryProxy.js 複製到 ux 下. 然後於網頁中匯入此 JS 檔即可, 例如 :

  <script type="text/javascript" src="../extjs/ux/PagingMemoryProxy.js"></script>

注意, 分頁工具列上的按鈕提示必須用 Ext.QuickTips.init() 指令予以啟動才會出現.

如下列範例 10 所示 :

測試範例 10http://mybidrobot.allalla.com/extjstest/extjs_grid_10.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          pageSize:2,
          proxy:{
            type:"pagingmemory",
            data:data,
            reader:{type:"array"}

            },
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true,
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄,共 {2} 列",
          emptyMsg:"沒有資料"
          })
        });
      store.load();  //初始載入 store (必須!)
      }); //end of onReady



可見使用 PagingMemoryProxy 就能達成前台分頁功能了. 參考 :

# understanding Ext.Loader.setPath('Ext.ux', '../ux/')

分頁工具列 PagingToolbar 的 xtype 為 pagingtoolbar, 所以也可以直接使用 xtype (ExtJS 4 建議盡量使用 xtype), 如下列範例 10-1 :

測試範例 10-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_10_1.htm [看原始碼]

    Ext.QuickTips.init(); //啟動分頁按鈕提示功能
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          pageSize:2,
          proxy:{
            type:"pagingmemory",
            data:data,
            reader:{type:"array"}
            },
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        bbar:{
          xtype:"pagingtoolbar",
          store:store,
          displayInfo:true,
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄,共 {2} 列",
          emptyMsg:"沒有資料"
          }
        });
      store.load();
      }); //end of onReady

表格分頁工具列除了可用 bbar 屬性設定外, 還可以使用 dockerItems 屬性, 使用方式幾乎一樣, 只是必須另外指定 dock 屬性, 而且其值為一個陣列, 如下列範例 10-2 所示 :

測試範例 10-2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_10_2.htm [看原始碼]

    Ext.QuickTips.init(); //啟動分頁按鈕提示功能
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"name"},
                   {header:"股票代號",dataIndex:"id"},
                   {header:"收盤價 (元)",dataIndex:"close"},
                   {header:"成交量 (張)",dataIndex:"volumn"}
                   ];
      //定義原始資料
      var data=[["台積電","2330",123.0,25119],
                ["中華電","2412",96.4,5249],
                ["中碳","1723",192.5,918],
                ["創見","2451",108.0,733],
                ["華擎","3515",118.5,175],
                ["訊連","5203",97.0,235]
                ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          pageSize:2,
          proxy:{
            type:"pagingmemory",
            data:data,
            reader:{type:"array"}
            },
          fields:[
            {name:"name"},
            {name:"id"},
            {name:"close"},
            {name:"volumn"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        dockedItems:[{
          xtype:"pagingtoolbar",
          store:store,
          dock:"bottom",
          displayInfo:true,
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄,共 {2} 列",
          emptyMsg:"沒有資料"
          }]
        });
      store.load();
      }); //end of onReady

此例中的 ExtJS 來源改用 Sencha 公司的 CDN 供檔, 注意, Sencha 已不再提供 4.2.1 的 CDN, 最高只到 4.2.0 版. CDN 提供了完整的 ExtJS 資源, 包括前端記憶體分頁程式 PagingMemoryProxy.js, 位置在 /examples/ux/data 下 :

  <link rel="stylesheet" href="http://cdn.sencha.com/ext-4.2.0-gpl/resources/css/ext-all.css">
  <script type="text/javascript" src="http://cdn.sencha.com/ext-4.2.0-gpl/ext-all.js"></script>
  <script type="text/javascript" src="http://cdn.sencha.com/ext-4.2.0-gpl/examples/ux/data/PagingMemoryProxy.js"></script>
  <script type="text/javascript" src="http://cdn.sencha.com/ext-4.2.0-gpl/locale/ext-lang-zh_TW.js"></script>

上面所有的範例, 資料來源都是近端的陣列資料, 接下來我們要測試遠端資料, 亦即利用 Ajax 非同步技術取得後端資料, 最常用的是 JSON 格式的資料, PHP 在此方面有 json_encode() 函式支援, 非常方便.

此處參考 "jQuery 套件 DataTables 的測試" 這篇舊作的範例 8 作法來稍作修改, 從 MySQL 資料庫的 stocks_list 資料表取的台股上市公司名稱與代號. 資料表 stocks_list 的製作方法詳見 "jQuery UI 的自動完成器 autocomplete 測試" 中的範例 4 說明.

首先來看產生 JSON 檔的 PHP 程式 :

<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$SQL="SELECT COUNT(*) FROM `stocks_list`";
$RS=mysql_query($SQL, $conn);
list($total)=mysql_fetch_row($RS); //紀錄總筆數
$SQL="SELECT * FROM `stocks_list`";
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$stock=array();
for ($i=0; $i<mysql_numrows($result); $i++) { //走訪紀錄集 (列)
     $row=mysql_fetch_array($result); //取得列陣列
     $stock_name=$row["stock_name"];
     $stock_id=$row["stock_id"];
     $stock[$i]=array("stock_name" => $stock_name,
                      "stock_id" => $stock_id);
     } //end of for
$arr=array("totalProperty" => $total, "root" => $stock);
echo json_encode($arr);  //將陣列轉成 JSON 資料格式傳回
?>

此程式會輸出如下 ExtJS 表格所需要 JSON 檔 :

{"totalProperty":"865","root":[{"stock_name":"\u5bcc\u90a6","stock_id":"0015"},
...., {"stock_name":"\u65fa\u65fa\u4fdd","stock_id":"2816"}]}

測試範例 11http://mybidrobot.allalla.com/extjstest/extjs_grid_11.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_all.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }

            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:450,
        forceFit:true,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true,
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄,共 {2} 列",
          emptyMsg:"沒有資料"
          })
        });
      store.load({params:{start:0,limit:20}}); //傳送起始參數
      }); //end of onReady


可見即使設定了 Store 的 pageSize=20, 但由於 PHP 總是輸出全部 865 筆資料, 所以表格的分頁功能也不會有效果, 雖然分頁工具列顯示分成 44 頁, 每頁 20 列, 事實上第一次載入時卻一次顯示全部資料, 按下一頁會顯示 21~865 列, 再按顯示 41~865 頁 ... 這不是我們預期的結果. 事實上, ExtJS 4 的 PagingToolBar 分頁功能會向後台傳送 page (頁次), start (起始列索引), 以及 limit (讀取列數) 三個參數, 後台的 PHP 程式必需利用此三參數輸出指定頁之資料, 不應該一次輸出全部資料, 要不然不管 Store 的 pageSize 設多少都不會如預期的只顯示指定頁之內容.

其次, 發現從遠端擷取時, 分頁工具列卻沒有完全中文化, 這是因為我沒有將中文化檔案 ext-lang-zh_TW.js 上傳到伺服器之故, 而 PagingToolbar 中我們只設定了 displayMsg 與 emptyMsg 兩個屬性而已, 所以前面的 "第~頁,共~頁" 就變成預設的英文了. 欲完全中文化, 可將 ExtJS 4.2 解壓縮 locale 目錄下的 ext-lang-zh_TW.js 上傳到伺服器, 或者加入下列屬性 :

beforePageText:"第",                 //取代 "page"
afterPageText:"頁, 共{0}頁",    //取代 of~, 其中 {0} 為總頁數

正確的 PHP 後台分頁程式如下, 先讀取 ExtJS 4 的 PagingToolBar 傳送的 start 與 limit 兩個參數, 再用 MySQL 的 LIMIT start,limit 語法讀取資料表中的指定頁次資料 :

<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com"; 
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$start=$_GET['start'];  //擷取 ExtJS 傳出參數 'start'
$limit=$_GET['limit'];  //擷取 ExtJS 傳出參數 'limit'
$SQL="SELECT COUNT(*) FROM `stocks_list`";
$RS=mysql_query($SQL, $conn);
list($total)=mysql_fetch_row($RS); //紀錄總筆數
$SQL="SELECT * FROM `stocks_list` LIMIT ".$start.",".$limit;
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$stock=array();
for ($i=0; $i<mysql_numrows($result); $i++) { //走訪紀錄集 (列)
     $row=mysql_fetch_array($result); //取得列陣列
     $stock_name=$row["stock_name"];
     $stock_id="<a href='http://tw.stock.yahoo.com/q/q?s=".
               $row["stock_id"]."' target='_blank'>".
               $row["stock_id"]."</a>";
     $stock[$i]=array("stock_name" => $stock_name,
                      "stock_id" => $stock_id);
     } //end of for
$arr=array("totalProperty" => $total, "root" => $stock);
echo json_encode($arr);  //將陣列轉成 JSON 資料格式傳回
?>

測試範例 12 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12.htm [看原始碼]

    Ext.QuickTips.init();  //啟動分頁按鈕提示
    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        limitParam:undefined,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          beforePageText:"第",              
          afterPageText:"頁, 共{0}頁",
          displayMsg:"顯示第 {0} 列到第 {1} 列紀錄, 共 {2} 列",
          emptyMsg:"沒有資料"
          })
        });
      store.load({params:{start:0,limit:20}});  //傳送起始參數
      }); //end of onReady


在此例中, 我們在後端 PHP 程式輸出 stock_id 時做了些改變, 加上 Yahoo 的超連結. 其次, 我們既匯入了中文化檔案 ext-lang-zh_TW.js, 也設置了 PagingToolbar 中文化屬性, 這時在後面的分頁工具列設定會蓋過前面匯入的中文化檔案, 如果刪除 PagingToolbar 的中文化設定, 則會採用中文化檔案.

注意, 使用後端分頁後, 若按一直下一頁, 則紀錄編號會顯示連續編號; 但若按了欄位標題進行過前台排序後, 再按下一頁就不會再顯示連續編號了, 固定顯示 1~20.

在上面的程式中, 我們在最後一行呼叫 store 的 load() 方法將遠端傳回之資料載入 Store 中, 並向伺服器傳送兩個參數 start 與 limit 之初始值, 這可以在 Store 中用 autoLoad 屬性取代, 並傳入 true 或含有 start 與 limit 屬性的物件 (即使傳入空物件 {}, 預設也是送出 start=0, limit=pageSize), 這與呼叫 load() 方法作用是一樣的, 如下列範例 12-1 所示 :

測試範例 12-1 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12_1.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},  //改用自動載入屬性指定參數初始值
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        limitParam:undefined,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady


可見中文化檔案的顯示文字較短. 事實上  ext-lang-zh_TW.js 對分頁工具列的處理如下 :

Ext.define("Ext.locale.zh_TW.toolbar.Paging", {
    override: "Ext.PagingToolbar",
    beforePageText: "第",
    afterPageText: "頁,共{0}頁",
    firstText: "第一頁",
    prevText: "上一頁",
    nextText: "下一頁",
    lastText: "最後頁",
    refreshText: "重新整理",
    displayMsg: "顯示{0} - {1}筆,共{2}筆",
    emptyMsg: '沒有任何資料'
});

另外要特別注意, pageSize 要設在 Store 中, 不是設在 PagingToolbar 中 (可設也可不設), 如果只設於 PagingToolbar 中, 分頁筆數只有初始載入時有效, 按下一頁時每頁筆數將會變成預設 25 筆, 如下列範例 12-2 所示 :

測試範例 12-2 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12_2.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        limitParam:undefined,
        bbar:Ext.create("Ext.PagingToolbar",{
          pageSize:20,  //此設定沒有分頁作用 (要在 Store 中設定)
          store:store,
          displayInfo:true
          })
        });
      store.load({params:{start:0,limit:20}});
      }); //end of onReady

此例只在 PagingToolbar 設定 pageSize 屬性, 結果只有網頁初始化時正確顯示 20 筆, 按下一頁卻顯示 26~50 筆, 而非預期的 21~40 筆. 原因就是當 Store 中沒有設定 pageSize 屬性時, ExtJS 預設就是 limit=25, 即每頁 25 筆. 因此要正確達成後台分頁功能有兩個條件, 一是前台必須在 Store 設定 pageSize, 二是後台必須用 SQL 輸出指定頁數之資料.

分頁工具列除了放在表格下方外, 也可以放在表格上方, 或者上下都放 (tbar 與 bbar 屬性均設定), 上下兩個分頁工具列會連動, 如下列範例 12-3 所示 :

測試範例 12-3 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12_3.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var codebase="http://mybidrobot.allalla.com/extjstest/";
      var store=Ext.create("Ext.data.Store", {
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        limitParam:undefined,
        tbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          }),
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      store.load({params:{start:0,limit:20}});
      }); //end of onReady


PagingToolbar 類別還有一個屬性 plugins 可以指定擴充組件, 這裡要來測試一下 ProgressBarPager 這個擴充元件, 此元件會在分頁工具列右側放置一個進度條, 並把 displayInfo 訊息顯示在上面. 這個 Ext.ux.ProgressBarPager 類別放在 ExtJS 4 解壓縮目錄 examples 下面, 我將其上傳到伺服器 extjs/ux/ 目錄下, 然後再匯入此 js 檔 :

<script type="text/javascript" src="../extjs/ux/ProgressBarPager.js"></script>

如下面範例 12-4 所示 :

測試範例 12-4 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12_4.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true,
          plugins:Ext.create("Ext.ux.ProgressBarPager")
          })
        });
      }); //end of onReady


動畫效果看起來還不錯.

最後我們來看一下後端排序. 前面我們已經利用 Store 類別的 sorters 屬性或 sort() 方法測過前端排序功能, 不論資料為本地陣列或後端所提供, GridView 所呈現的順序都是資料載入Store 後利用 sorters 屬性或 sort() 方法排序的結果. 亦即, 若資料由後端提供, 那麼後端程式可能已經用 SQL 的 ORDER BY 語法先對資料表擷取出來的紀錄排序, 傳送到前端後 Store 又會再排序一次, 這樣很複雜. 這裡所謂的後端排序是要禁止前端排序, 完全由後端來排序, 但可以由前端表格傳送排序參數給後端以控制排序欄位與方向.

GridPanel 的後端排序是透過將 Store 類別的 remoteSort 屬性設為 true 達成的 (預設為 false), 這樣會關閉前端排序功能 (按欄位標題無效), 開啟後端排序, 如下列範例 12-5 所示 :

測試範例 12-5http://mybidrobot.allalla.com/extjstest/extjs_grid_12_5.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_page.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true
          });
      //建立 GridPanel
      store.sort([{property:"close",direction:"desc"}]);
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,
        forceFit:true,
          remoteSort:true,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady

可見只要啟動了 remoteSort, 就會關閉前端排序功能了. 但光是將 remoteSort 設為 true 還無法達成後端排序, 所謂後端排序是指利用後端伺服器執行 SQL 的 ORDER BY 指令來排序. 上面範例 12-5 使用的後端程式為 get_stocks_list_page.php, 其 SQL 指令並未包含 ORDER BY, 而且我們也沒有向後端傳送排序參數, 就算我們修改 get_stocks_list_page.php 程式, 加入 ORDER BY 語法, 那也只是固定式的後端排序, 不是前端可控制的後端排序 (改排序欄位或方向時需修改後端程式).

但是我們要如何向後端傳送排序參數呢? 當使用 remoteSort 啟動後端排序時, ExtJS 的 Store/Proxy 會以 GET 方式向伺服器送出 page, start, limit 三個參數, 但是若要從前端控制後端的排序, 還必須送出 sort 與 dir 這兩個排序參數. 我在 "ExtJS 開發之練" 這本書的 P373 看到 store.load({params:{start:0,limit:5}}) 的用法, 亦即, 只要在 params 中添加 sort 與 dir, 應該就可以傳遞排序參數了. 這 load() 也可以在 Store 的 autoLoad 屬性中以 params 參數設定 :

          autoLoad:{
            params:{start:0,
                    limit:20,
                    sort:"stock_id",
                    dir:"desc"}
            }

而在後端必須修改 PHP 程式, 擷取 sort, dir, start, limit 這四個參數, 製作後端排序用之 SQL 指令, PHP 程式 get_stocks_list_sort.php 如下 :

<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$start=$_GET['start'];   //擷取 ExtJS 傳出參數 'start'
$limit=$_GET['limit'];    //擷取 ExtJS 傳出參數 'limit'
$sort=$_GET['sort'];    //擷取 ExtJS 傳出參數 'sort'
$dir=$_GET['dir'];        //擷取 ExtJS 傳出參數 'dir'
$SQL="SELECT COUNT(*) FROM `stocks_list`";
$RS=mysql_query($SQL, $conn);
list($total)=mysql_fetch_row($RS); //紀錄總筆數
$SQL="SELECT * FROM `stocks_list` ORDER BY ".$sort." ".$dir." ".
     "LIMIT ".$start.",".$limit; 

$result=mysql_query($SQL, $conn); //執行 SQL 指令
$stock=array();
for ($i=0; $i<mysql_numrows($result); $i++) { //走訪紀錄集 (列)
     $row=mysql_fetch_array($result); //取得列陣列
     $stock_name=$row["stock_name"];
     $stock_id="<a href='http://tw.stock.yahoo.com/q/q?s=".
               $row["stock_id"]."' target='_blank'>".
               $row["stock_id"]."</a>";
     $stock[$i]=array("stock_name" => $stock_name,
                      "stock_id" => $stock_id);
     } //end of for
$arr=array("totalProperty" => $total, "root" => $stock);
echo json_encode($arr);  //將陣列轉成 JSON 資料格式傳回
?>

# PHP isset() vs empty() vs is_null()

測試範例 12-6http://mybidrobot.allalla.com/extjstest/extjs_grid_12_6.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer"
,{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{
            params:{start:0,
                    limit:20,
                    sort:"stock_id",
                    dir:"desc"}
            },

          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_sort.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,pageSize:20,
        forceFit:true,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady


但是結果卻 NG! 很奇怪的是, 按下一頁時, 分頁工具列顯示正常, 但 GridView 卻不會換頁, 永遠顯示以 stock_id 倒序的第一頁, WHY? 用 Chrome 的 "工具/Javascript 工具台" (Cntl+Shift+J) 觀察 Network 部分, 發現 Store 的 Proxy 向伺服器發出的 QueryString 中, 初始載入時有正常發出 page, start, limit, sort, dir 四個參數, 但當按下一頁時, 卻只發出 page, start, limit 三個, sort 與 dir 不見了 :

初始載入時

按下一頁時

可見, 用 params 設定排序參數是行不通的, 只有初始載入時才有效 (我想可能是因為 autoLoad 只有初始化時才會傳送之故). 正確的做法是要把排序參數放在 Store 的 sorters 屬性中才對 :

          remoteSort:true,
          sorters:[{property:"stock_id",direction:"DESC"}]

Store 的 Proxy 會將 sorters 內的物件陣列以字串形式全部放在 sort 參數中送出 , 因此我們的後端程式必須對 sort 參數進行處理, 才能取出其中的排序參數. 處理的方式是先用 trim() 函式去除左右的陣列中括號, 剩下的 JSON 字串再用 json_decode() 函式轉成物件, 即可取出其中的屬性了. 修改後的 PHP 程式為 get_stocks_list_sort_new.php 如下 :

<?php
header('Content-Type: text/html;charset=UTF-8');
$host="abc.xyz.com";
$username="test";
$password="123";
$database="testdb";
$conn=mysql_connect($host, $username, $password); //建立連線
mysql_query("SET NAMES 'utf8'"); //設定查詢所用之字元集為 utf-8
mysql_select_db($database, $conn); //開啟資料庫
$start=$_GET['start'];  //擷取 ExtJS 傳出參數 'start'
$limit=$_GET['limit'];   //擷取 ExtJS 傳出參數 'limit'
$sort=$_GET['sort'];    //擷取 ExtJS 傳出參數 'sort'
$sort=trim($sort,"[");    //去除左括號
$sort=trim($sort,"]");    //去除右括號
$obj=json_decode($sort);          //轉成物件
$property=$obj->property;   //取出排序欄位
$dir=$obj->direction;            //取出排序方向
$SQL="SELECT COUNT(*) FROM `stocks_list`";
$RS=mysql_query($SQL, $conn);
list($total)=mysql_fetch_row($RS); //紀錄總筆數
$SQL="SELECT * FROM `stocks_list` ORDER BY ".$property." ".$dir." ".
     "LIMIT ".$start.",".$limit;
$result=mysql_query($SQL, $conn); //執行 SQL 指令
$stock=array();
for ($i=0; $i<mysql_numrows($result); $i++) { //走訪紀錄集 (列)
     $row=mysql_fetch_array($result); //取得列陣列
     $stock_name=$row["stock_name"];
     $stock_id="<a href='http://tw.stock.yahoo.com/q/q?s=".
               $row["stock_id"]."' target='_blank'>".
               $row["stock_id"]."</a>";
     $stock[$i]=array("stock_name" => $stock_name,
                      "stock_id" => $stock_id);
     } //end of for
$arr=array("totalProperty" => $total, "root" => $stock);
echo json_encode($arr);  //將陣列轉成 JSON 資料格式傳回
?>

測試範例 12-7 : http://mybidrobot.allalla.com/extjstest/extjs_grid_12_7.htm [看原始碼]

    Ext.onReady(function() {
      //定義表頭欄位
      var columns=[Ext.create("Ext.grid.RowNumberer",{text:"編號",width:50}),
                   {header:"股票名稱",dataIndex:"stock_name"},
                   {header:"股票代號",dataIndex:"stock_id"}
                   ];
      //轉成 Store 物件
      var store=Ext.create("Ext.data.Store", {
          autoLoad:{start:0,limit:20},
          pageSize:20,
          proxy:{
            type:"ajax",
            url:"get_stocks_list_sort_new.php",
            reader:{
              type:"json",
              totalProperty:"totalProperty",
              root:"root",
              idProperty:"stock_id"
              }
            },
          fields:[
            {name:"stock_name"},
            {name:"stock_id"}
            ],
          remoteSort:true,
          sorters:[{property:"stock_id",direction:"DESC"}]
          });
      //建立 GridPanel
      var grid=Ext.create("Ext.grid.Panel",{
        columns:columns,
        store:store,
        renderTo:"grid",
        width:520,pageSize:20,
        forceFit:true,
        bbar:Ext.create("Ext.PagingToolbar",{
          store:store,
          displayInfo:true
          })
        });
      }); //end of onReady

初始載入時

按下一頁時

按欄位標題 "股票名稱"

從 Chrome 觀察 Network 部分可知, 不論初始載入還是換頁, 或者按欄位標題, Store 都會送出 sorters 屬性值, 不會像範例 12-6 那樣無法換頁. 可見經過如此處理, 就能利用前端的 sorters 屬性設定控制後端的排序了, 亦即, 如果要改排序欄位或方向, 只要修改前端網頁即可, 不須修改後端程式. 此例僅控制一個欄位, 若要同時控制多欄排序, 則 sorters 就要添加多個排序參數, 而後端程式也必須隨同修改以處理所傳送的物件陣列, 擷取多組排序參數.

其次, 上面最後一張圖也顯示, 使用 sorters 屬性的好處是, 它不會因為 remoteSort 設為 true 而關閉前端排序, 按欄位標題時仍會傳送該欄位的排序字串, 也就是說, 不須修改網頁中的 sorters 屬性, 只要按欄位標題就能改變排序參數.

參考資料 :

# PHP 讓 json_encode() 指定回傳格式
# extjs4.0 分页问题 刷新后都是正确的 但是点击下一页后 limit就变成25了
# limit in Store requests always 25 
# extjs4.0分页时数据列表中总是显示所有的数据