1. 程式人生 > 實用技巧 >【QML Model-View】ListView(一)

【QML Model-View】ListView(一)

一、前言:MVC

Model-View-Controller (MVC) 是源自 SmallTalk 的一個設計模式,在構建使用者介面時經常用到。作為一種經典到不能再經典的架構模式,MVC 大行其道有其必然的道理。通過把職責、 性質相近的成分歸結在一起,不相近的進行隔離,MVC 將系統分解為模型、檢視、控制器 三部分,每一部分都相對獨立,職責單一,在實現過程中可以專注於自身的核心邏輯。MVC 是對系統複雜性的一種合理的梳理與切分,它的思想實質就是“關注點分離”。

  • 模型(Model)代表資料,通過精心設計的介面向外部提供服務,而內部實現,拜託,誰也別想管我,哪怕我自甘墮落成為一團漿糊。
  • 檢視(View)是呈現給使用者看的視覺化介面,文字列表、圖文混合,想怎麼著就怎麼著。
  • 控制器(Controller )就是個中間人,它從模型拉資料給檢視,資料變化時通知檢視更新,使用者想針對資料乾點什麼,比如刪除、更改、排序等,它也通知模型來響應這種變化。

Qt 中的 Model-View 程式設計框架,對 Controller 部分做了改動,引入了 Delegate 的概念, 合起來就是 Model-View-Delegate。模型還是負責提供資料,檢視則負責提供一個舞臺、基本的佈局管理和 Item 建立等工作,剩下的就由 Delegate 負責實現。

下圖來自於 Qt 幫助,可以說明 Qt 中的 Model-View-Delegate 框架。


在 Qt Quick 中,Model-View 程式設計變得更加簡單,不簡單也對不起 Quick 這個詞兒不是。 ListView、TableView、GridView、PathView 等預定義的檢視大多數時候可以滿足你的需要, Model 則有現成的 ListModel、XmlListModel 可用,而 Delegate 的實現則受益於 Qt Quick 的設計理念,組合一些基礎的 Item 就行,可以構建出很好的可視效果。

二、ListView的簡單使用

ListView 用來顯示一個條目列表,條目對應的資料來自於Model,而每個條目的外觀則由 Delegate 決定。我們可以將 Delegate 看成如何展示 Item 的一個模板。Android 手機上常見 的聯絡人介面,其實就是使用 ListView 實現的,而且 Android 的 ListView 和 Qt Quick 的 ListView 使用同樣的模式:Model、View、Item Template (Delegate)。


我們先以 Qt Quick 內建 Model 為例,把使用 ListView 的方方面面都介紹一下,然後再看如何使用在 C++ 中實現自定義的 Model。

我構建了一個簡單的手機列表,展示手機的型號、價格、製造商。使用上下鍵可以切換不同的手機,選中的手機有一個淺藍色的高亮背景,同時字型放大,文字顏色變為紅色。程式碼 phone_list_simple.qml:

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1

Rectangle {
    width: 360
    height: 300
    color: "#EEEEEE"

    // 1.定義delegate,內嵌三個Text物件來展示Model定義的ListElement的三個role
    Component {
        id: phoneDelegate
        Item {
            id: wrapper
            width: parent.width
            height: 30
            
            // 實現了滑鼠點選高亮的效果
            MouseArea {
                anchors.fill: parent;
                onClicked: wrapper.ListView.view.currentIndex = index
            }
            
            // 內嵌三個Text物件,水平佈局
            RowLayout {
                anchors.left: parent.left
                anchors.verticalCenter: parent.verticalCenter
                spacing: 8

                Text { 
                    id: col1;
                    text: name;
                    // 是否是當前條目
                    color: wrapper.ListView.isCurrentItem ? "red" : "black"
                    font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
                    Layout.preferredWidth: 120
                }
                
                Text { 
                    text: cost; 
                    color: wrapper.ListView.isCurrentItem ? "red" : "black"
                    font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
                    Layout.preferredWidth: 80
                }
                
                Text { 
                    text: manufacturer; 
                    color: wrapper.ListView.isCurrentItem ? "red" : "black"
                    font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18
                    Layout.fillWidth: true
                }
            }
        }
    } // phoneDelegate-END
    
    // 2.定義ListView
    ListView {
        id: listView
        anchors.fill: parent

        // 使用先前設定的delegate
        delegate: phoneDelegate
        
        // 3.ListModel專門定義列表資料的,它內部維護一個 ListElement 的列表。
        model: ListModel {
            id: phoneModel

            // 一個 ListElement 物件就代表一條資料
            ListElement{
                name: "iPhone 3GS"
                cost: "1000"
                manufacturer: "Apple"
            }
            ListElement{
                name: "iPhone 4"
                cost: "1800"
                manufacturer: "Apple"
            }            
            ListElement{
                name: "iPhone 4S"
                cost: "2300"
                manufacturer: "Apple"
            } 
            ListElement{
                name: "iPhone 5"
                cost: "4900"
                manufacturer: "Apple"
            }    
            ListElement{
                name: "B199"
                cost: "1590"
                manufacturer: "HuaWei"
            }  
            ListElement{
                name: "MI 2S"
                cost: "1999"
                manufacturer: "XiaoMi"
            }         
            ListElement{
                name: "GALAXY S5"
                cost: "4699"
                manufacturer: "Samsung"
            }                                                  
        }

        // 背景高亮
        focus: true
        highlight: Rectangle{
            color: "lightblue"
        }
    }
}

執行 “qmlscene phone_list_simple.qml” 命令,可以看到如下圖所示的效果。


為了示例簡單,我直接在宣告 ListView 物件時為 model 屬性初始化了一個 ListModel。ListModel 是專門定義列表資料的,它內部維護一個 ListElement 的列表。一個 ListElement 物件就代表一條資料。

  • 使用 ListElement 定義的資料條目可能是簡單的,比如只有一個人名;也可能是複雜的,比如還有這個人的出生年月、性別;共同構成一個 ListElement 的一個或多個數據信息被稱為 role,它包含一個名字(role-name)和一個值(role-value)。

  • role 的定義就像 QML 物件屬性定義那樣簡單,語法是這樣的:<role-name>: <role-value>,其中 role-name 必須以小寫字母開頭,role-value 必須是簡單的常量,如字串、布林值、數字或列舉值。

  • 在 ListElement 中定義的 role,可以在 Delegate 中通過 role-name 來訪問。示例定義的 ListElement 包含三個 role:name、cost、manufacturer,而 Delegate 則使用 Row 管理三個 Text 物件來展現這三個 role, Text 物件的 text 屬性被繫結到 role-name 上。


ListView 的 delegate 屬性型別是 Component,我在 phone_list_simple.qml 中定義了 id 為 phoneDelegate 的 Component。phoneDelegate 的頂層是 RowLayout,RowLayout內嵌三個 Text 物件來展示 Model 定義的 ListElement 的三個 role。

  • ListView 給 delegate 暴露了一個 index 屬性,代表當前 delegate 例項對應的 Item 的索引位置,必要時可以使用它來訪問資料。
  • 示例中實現了滑鼠點選高亮的效果:給 delegate 添加了一個 MouseArea 元素,在 onClicked 訊號處理器中設定 ListView 的 currentlndex 屬性。

ListView 定義了 delayRemove、isCurrentltem、nextSection、previousSection、section、view 等附加屬性,以及 add、remove 兩個附加訊號,可以在 delegate 中直接訪問。不過要注意的是,只有 delegate 的頂層 Item 才可以直接使用這些附加屬性和訊號,非頂層 Item 則需通過頂層Item的id來訪問這些附加屬性。

  • 示例中的 delegate 元件,頂層 Item 是一個 Item 物件, 用於展示 name、cost、manufacturer 的 Text 物件通過 wrapper.ListView.isCurrentltem判斷本 delegate 例項呈現的資料是否是當前條目,如果是,則改變文字大小和顏色。注意,我們是通過類名直接訪問附加屬性的。

  • 示例中當前選中條目有一個淺藍色的背景,它由 ListView 的 highlight 屬性指定的 Component 提供,它的 Z 序小於 delegate 例項化出來的 Item 物件。示例通過給 highlight 初始 化一個 Rectangle 定義了高亮背景,如果你想實現複雜的高亮效果,也可以專門定義一個 Component。

  • 與高亮效果相關的,還有很多屬性,比如 highlightFollowsCurrentltem 屬性指定高亮背景是否跟隨當前條目,預設值為 true,你用滑鼠點選某個 Item 時,高亮背景會經過一個平滑的動畫後移動到新的 Item 下面。你可以設定它為 false 來禁用這種動畫。

三、header

通過為 ListView 的 header 屬性設定一個 Component,,用方向鍵瀏覽 Item 或者用滑鼠在 ListView 內拖動時,表頭隨著拖動可能會變得不可見。

表頭在某些應用場景下可以讓資料的可讀性更好。比如前面的手機資訊示例,如果添加了表頭,別人一看就知道每一列的資料含義。phone_list_header.qml 是修改後的檔案,內容如下:

import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Layouts 1.1

Rectangle {
    width: 360;
    height: 300;
    color: "#EEEEEE";
    
    // 1.定義header
    Component {
        id: headerView;
        Item {
            width: parent.width;
            height: 30;
            RowLayout {
                anchors.left: parent.left;
                anchors.verticalCenter: parent.verticalCenter;
                spacing: 8

                Text { 
                    text: "Name";
                    font.bold: true; 
                    font.pixelSize: 20;
                    Layout.preferredWidth: 120;
                }
                // 省略。。。
            }            
        }
    }       
    
    // 2.定義delegate
    Component {
        id: phoneDelegate;
        Item {
            id: wrapper;
            width: parent.width;
            height: 30;
            
            MouseArea {
                anchors.fill: parent;
                onClicked: {
                    wrapper.ListView.view.currentIndex = index;
                    console.log("index=", index);
                    }
            }      
            
            RowLayout {
                anchors.left: parent.left;
                anchors.verticalCenter: parent.verticalCenter;
                spacing: 8;
                Text { 
                    id: col1;
                    text: name; 
                    color: wrapper.ListView.isCurrentItem ? "red" : "black";
                    font.pixelSize: wrapper.ListView.isCurrentItem ? 22 : 18;
                    Layout.preferredWidth: 120;
                }
                // 省略。。。
            }
        }
    }
    
    // 3.定義model
    Component {
        id: phoneModel;
        ListModel {
            ListElement{
                name: "iPhone 3GS";
                cost: "1000";
                manufacturer: "Apple";
            }
            // 省略。。。
        }
    }    
    
    // 4.定義ListView
    ListView {
        id: listView;
        anchors.fill: parent;

        delegate: phoneDelegate;
        model: phoneModel.createObject(listView);
        header: headerView;
        focus: true;
        highlight: Rectangle{
            color: "lightblue";
        }
    }
}

效果如下圖所示。


headerView 是我定義的表頭元件,與 delegate 元件定義類似,使用三個 Text 物件分別來描述每一列資料的含義,設定字型大小,讓字型變粗,還設定了每一列的寬度。ListView 的 headerltem 屬性儲存了本 ListView 使用的、由 header 元件創建出來的 Item。

四、footer

footer 屬性允許我們指定 ListView 的頁尾,footerltem 儲存了 footer 元件創建出來的 Item 物件,這個 Item會被新增到 ListView 的末尾,在所有可見的 Item 之後。

用 footer 可以幹什麼呢?隨你吧。我這裡的示例只是簡單地在footer內放置了一個 Text物件,顯示當前選中的Item的資料。有點兒像狀態列。

Rectangle {
    width: 360;
    height: 300;
    color: "#EEEEEE";
    
    // 省略header。。。
    
    // 2. 定義footer
    Component {
        id: footerView;
        Text {
            width: parent.width;
            font.italic: true;
            color: "blue";
            height: 30;
            verticalAlignment: Text.AlignVCenter;
        }
    }
    
    // 省略delegate和model。。。
    
    // 5.定義ListView
    ListView {
        id: listView;
        anchors.fill: parent;

        delegate: phoneDelegate;
        model: phoneModel.createObject(listView);
        header: headerView;
        footer: footerView;
        focus: true;
        highlight: Rectangle{
            color: "lightblue";
        }
        
        onCurrentIndexChanged:{
            if( listView.currentIndex >=0 ){
                var data = listView.model.get(listView.currentIndex);
                listView.footerItem.text = data.name + " , " + data.cost + " , " + data.manufacturer;
            }
        }
    }        
}    

效果如下圖所示。


為了使 footer 能夠跟隨當前 Item 發生變化,我為 listView 定義了 onCurrentlndexChanged 訊號處理器,因為 currentlndexChanged 訊號不帶引數,所以只能再次訪問 currentlndex 屬性來獲取當前 Item 的索引,然後通過 ListModel 的 get() 方法獲取到對應的資料物件,最後呢, 我把 name、cost、manufacturer 三個 role 拼接在一塊賦值給 footerltem。於是乎,當你點選一 個 Item 或者使用上下鍵瀏覽 Item 時,footer 就變化了。


參考:

《Qt Quick核心程式設計》第6章