【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 就变化了。

五、下载链接

全部代码下载链接:https://github.com/confidentFeng/QML_Demo/tree/master/ListViewPhone


参考:

《Qt Quick核心编程》第13章


posted @ 2020-09-01 11:44  fengMisaka  阅读(3862)  评论(0编辑  收藏  举报