【QML Model-View】ListView-簡單使用(一)

音視頻開發老舅 發佈 2024-04-27T07:59:31.822870+00:00

1、前言:MVCModel-View-Controller (MVC) 是源自 SmallTalk 的一個設計模式,在構建用戶界面時經常用到。作為一種經典到不能再經典的架構模式,MVC 大行其道有其必然的道理。

1、前言: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 就行,可以構建出很好的可視效果。


2、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"
        }
    }
}
QT開發交流+貲料君羊:714620761

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

為了示例簡單,我直接在聲明 ListView 對象時為 model 屬性初始化了一個 ListModel。ListModel 是專門定義列表數據的,它內部維護一個 ListElement 的列表。一個 ListElement 對象就代表一條數據。

QT開發交流+貲料君羊:714620761

  • 使用 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 來禁用這種動畫。

3、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。


4、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 就變化了。

關鍵字: