七、QT Quick模型与视图(一):ListView、ListModel与委托全解析
本文深入解析QT Quick模型-视图-委托架构,带你掌握ListView基础、ListModel动态操作、委托设计及高亮选择,快速构建高效列表界面。
七、QT Quick模型与视图(一):ListView、ListModel与委托全解析  -MakerLi

第7章:模型与视图(一)

ListView基础、ListModel、Delegate设计、高亮与选择


本章将深入探讨QT Quick中用于数据展示的核心组件——模型与视图框架。通过ListView及其相关组件,学习如何高效、美观地呈现列表数据。


一、核心概念:模型-视图-委托 (MVD)


QT Quick的模型-视图-委托(MVD)架构,将数据的存储、显示和可视化呈现方式彻底分离,实现了高度解耦与灵活性,让数据管理、界面展示和交互逻辑各自独立,便于维护和扩展。


  • 数据模型(Model):负责存储和管理数据,完全不关心数据如何显示。常见的模型包括ListModel(动态列表模型)、XmlListModel(XML数据模型)以及C++端的QAbstractItemModel类。
  • 视图(View):负责显示模型中的数据,同时管理视图的布局、滚动等交互,还自带虚拟化优化提升性能。常用视图有ListView(列表视图)、GridView(网格视图)、TableView(表格视图)。
  • 委托(Delegate):定义每个数据项在视图中的可视化外观和交互行为,是一个QML组件,视图会为每个数据项单独实例化一个委托。

二、ListView 基础


ListView是QT Quick中用于显示垂直或水平列表数据的核心组件,它会将模型中的数据按顺序排列,同时处理滚动、虚拟化等性能优化,即使是大数据集也能流畅运行。


2.1 核心属性说明

ListView的核心属性决定了它的功能和表现:

  • model:指定要显示的数据模型,可以是ListModel、数字(表示生成对应数量的空项)或JavaScript数组。
  • delegate:定义每个数据项外观的QML组件,是列表项的“模板”。
  • orientation:控制列表方向,默认是垂直(ListView.Vertical),也可设置为水平(ListView.Horizontal)。
  • spacing:设置委托项之间的间距,单位为像素。
  • currentIndex:标记当前选中项的索引,-1表示未选中任何项。
  • highlight:定义当前选中项的高亮显示组件,与委托分离,便于单独控制。

2.2 基本示例

下面是一个简单的ListView示例,显示20个交替背景的列表项:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    width: 300
    height: 400
    visible: true

    ListView {
        anchors.fill: parent
        model: 20  // 简单模型:生成20个空项
        delegate: Rectangle {
            width: ListView.view.width
            height: 50
            color: index % 2 ? "#F5F5F5" : "#FFFFFF"
            border.color: "#E0E0E0"
            
            Text {
                anchors.centerIn: parent
                text: "列表项 " + (index + 1)
                font.pixelSize: 16
                color: "#333333"
            }
        }
    }
}


三、ListModel 动态模型


ListModel是QT Quick中最简单的动态数据模型,完全在QML中定义和修改,无需编写C++代码,适合中小型数据集。每个数据项由ListElement组成,每个元素可以包含多个命名“角色”(即数据字段)。


3.1 定义与使用

我们可以直接在QML中定义ListModel,并在ListView中引用它:

import QtQuick 2.15

// 定义水果模型
ListModel {
    id: fruitModel
    
    ListElement {
        name: "苹果"
        color: "red"
        price: 5.5
        icon: "🍎"
    }
    ListElement {
        name: "香蕉"
        color: "yellow"
        price: 3.2
        icon: "🍌"
    }
    ListElement {
        name: "葡萄"
        color: "purple"
        price: 8.0
        icon: "🍇"
    }
}

// 在ListView中使用模型
ListView {
    width: 200
    height: 300
    model: fruitModel
    delegate: Text { 
        text: icon + " " + name + " - ¥" + price 
        color: model.color
    }
}


3.2 动态操作数据

ListModel支持动态增删改查数据项,常用方法如下:

// 添加新水果项
fruitModel.append({"name": "橙子", "color": "orange", "price": 4.5, "icon": "🍊"})

// 在索引1的位置插入新项
fruitModel.insert(1, {"name": "草莓", "color": "pink", "price": 12.0, "icon": "🍓"})

// 修改索引1项的价格
fruitModel.setProperty(1, "price", 10.5)

// 移除索引0的项
fruitModel.remove(0)

// 清空所有数据
fruitModel.clear()

// 获取模型中数据项的数量
var count = fruitModel.count


四、Delegate 委托设计


委托是列表项的“模板”,定义了每个数据项的外观和交互行为。它在ListView的上下文执行,可以直接访问模型数据、当前项索引等信息。


4.1 委托中的可用属性

在委托组件中,你可以直接使用这些属性来获取上下文信息:

  • model:当前数据项的所有角色数据,比如model.name可以获取当前项的名称字段。
  • index:当前项在模型中的索引,从0开始计数。
  • ListView.view:引用当前委托所属的ListView对象。
  • ListView.isCurrentItem:布尔值,标记当前项是否为ListView的选中项。

4.2 高级委托示例

下面是一个模拟联系人列表的高级委托,包含鼠标交互效果、头像和状态显示:

Component {
    id: contactDelegate
    
    Rectangle {
        id: delegateRoot
        width: ListView.view.width
        height: 70
        color: ListView.isCurrentItem ? "#E3F2FD" : (index % 2 ? "#FAFAFA" : "white")
        border.color: "#E0E0E0"
        radius: 5
        
        // 鼠标交互:点击选中、 hover时改变边框颜色
        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onClicked: {
                delegateRoot.ListView.view.currentIndex = index
                console.log("选中:", model.name)
            }
            onEntered: parent.border.color = "#2196F3"
            onExited: parent.border.color = "#E0E0E0"
        }
        
        Row {
            anchors.fill: parent
            anchors.margins: 10
            spacing: 15
            
            // 头像:用名字首字母和背景色生成
            Rectangle {
                width: 50
                height: 50
                radius: 25
                color: model.avatarColor
                anchors.verticalCenter: parent.verticalCenter
                
                Text {
                    anchors.centerIn: parent
                    text: model.name.charAt(0)
                    font.bold: true
                    font.pixelSize: 20
                    color: "white"
                }
            }
            
            // 联系人信息区域
            Column {
                anchors.verticalCenter: parent.verticalCenter
                spacing: 5
                
                Text {
                    text: model.name
                    font.bold: true
                    font.pixelSize: 16
                    color: "#333333"
                }
                
                Text {
                    text: model.phone
                    font.pixelSize: 14
                    color: "#666666"
                }
            }
            
            // 在线状态显示
            Item {
                width: 60
                height: parent.height
                
                Text {
                    anchors.right: parent.right
                    anchors.verticalCenter: parent.verticalCenter
                    text: model.online ? "在线" : "离线"
                    color: model.online ? "#4CAF50" : "#9E9E9E"
                    font.pixelSize: 12
                }
            }
        }
    }
}


五、高亮与选择


ListView提供了独立的高亮组件来标识当前选中项,它与委托组件分离,便于单独控制高亮效果,还能设置移动动画让高亮跟随选中项平滑切换。


5.1 基本高亮实现

下面是一个简单的高亮示例,为选中项添加浅蓝色背景和边框,并设置移动动画:

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15


Rectangle {
    id: root
    width: StackView.view ? StackView.view.width : 700
    height: StackView.view ? StackView.view.height : 800
    color: "#F0F2F5"


    // 顶部返回栏
    Rectangle {
        id: headerBar
        anchors { left: parent.left; right: parent.right; top: parent.top }
        height: 56
        color: "#FFFFFF"
        z: 10
        Rectangle {
            anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
            height: 1; color: "#E0E0E0"
        }
        RowLayout {
            anchors { fill: parent; leftMargin: 16; rightMargin: 16 }
            Button {
                text: "← 返回"
                flat: true
                font.pixelSize: 14
                onClicked: { StackView.view.pop() }
            }
            Text {
                text: "ListView 模型与视图"
                font.pixelSize: 16; font.bold: true; color: "#1A1A2E"
            }
            Item { Layout.fillWidth: true }
        }
    }
    // 数据模型
    ListModel {
        id: myModel
        ListElement { name: "项目 Alpha"; progress: 75; status: "进行中" }
        ListElement { name: "项目 Beta"; progress: 30; status: "起步" }
        ListElement { name: "项目 Gamma"; progress: 100; status: "已完成" }
        ListElement { name: "项目 Delta"; progress: 50; status: "进行中" }
        ListElement { name: "项目 Epsilon"; progress: 90; status: "收尾" }
    }


    // 代理组件
    Component {
        id: myDelegate


        Rectangle {
            width: ListView.view.width
            height: 60
            color: index % 2 === 0 ? "#FFFFFF" : "#F8F9FA"


            RowLayout {
                anchors { fill: parent; leftMargin: 16; rightMargin: 16 }
                spacing: 12


                Rectangle {
                    width: 8; height: 8; radius: 4
                    color: status === "已完成" ? "#4CAF50" : (status === "进行中" ? "#2196F3" : "#FF9800")
                }


                Text {
                    text: name
                    font.pixelSize: 14
                    font.bold: true
                    color: "#1A1A2E"
                    Layout.preferredWidth: 120
                }


                Rectangle {
                    Layout.fillWidth: true
                    height: 8
                    radius: 4
                    color: "#E0E0E0"


                    Rectangle {
                        anchors { left: parent.left; top: parent.top; bottom: parent.bottom }
                        width: parent.width * progress / 100
                        radius: 4
                        color: status === "已完成" ? "#4CAF50" : (status === "进行中" ? "#2196F3" : "#FF9800")
                    }
                }


                Text {
                    text: progress + "%"
                    font.pixelSize: 13
                    color: "#666666"
                    Layout.preferredWidth: 36
                }


                Rectangle {
                    width: 56; height: 24; radius: 12
                    color: status === "已完成" ? "#E8F5E9" : (status === "进行中" ? "#E3F2FD" : "#FFF3E0")


                    Text {
                        anchors.centerIn: parent
                        text: status
                        font.pixelSize: 11
                        color: status === "已完成" ? "#4CAF50" : (status === "进行中" ? "#2196F3" : "#FF9800")
                    }
                }
            }


            // 分割线
            Rectangle {
                anchors { left: parent.left; right: parent.right; bottom: parent.bottom }
                height: 1
                color: "#EEEEEE"
            }
        }
    }


    ListView {
        id: listView
        anchors {
            top: headerBar.bottom
            left: parent.left
            right: parent.right
            bottom: parent.bottom
            leftMargin: 20
            rightMargin: 20
            topMargin: 20
        }
        clip: true
        model: myModel
        delegate: myDelegate
        spacing: 0


        // 定义高亮组件
        highlight: Rectangle {
            color: "#E3F2FD"
            border.color: "#2196F3"
            border.width: 2
            radius: 5
            width: listView.width - 10
            x: 5
        }


        // 高亮跟随选中项移动,设置动画时长
        highlightFollowsCurrentItem: true
        highlightMoveDuration: 200  // 移动动画时长(毫秒)
        highlightResizeDuration: 100 // 大小变化动画时长
    }
}



5.2 自定义高亮组件

你还可以设计更复杂的高亮组件,比如添加左侧指示条和选中标记:

Component {
    id: customHighlight
    
    Item {
        width: ListView.view.width
        height: 70
        
        // 半透明背景高亮
        Rectangle {
            anchors.fill: parent
            anchors.margins: 2
            color: "#FFF3E0"
            border.color: "#FF9800"
            border.width: 2
            radius: 8
            opacity: 0.8
        }
        
        // 左侧橙色指示条
        Rectangle {
            width: 6
            height: parent.height
            color: "#FF9800"
            radius: 3
        }
        
        // 右侧选中标记
        Text {
            anchors.right: parent.right
            anchors.rightMargin: 15
            anchors.verticalCenter: parent.verticalCenter
            text: "✓"
            color: "#FF9800"
            font.bold: true
            font.pixelSize: 20
        }
    }
}

// 在ListView中使用自定义高亮
ListView {
    highlight: customHighlight
    highlightRangeMode: ListView.ApplyRange  // 确保高亮始终可见
    preferredHighlightBegin: 100
    preferredHighlightEnd: 300
}


5.3 多选模式实现

默认ListView是单选模式,我们可以通过自定义逻辑实现多选功能:

ListView {
    id: multiSelectList
    width: 300
    height: 400
    
    // 存储选中项的索引数组
    property var selectedIndexes: []
    
    model: ListModel {
        // 模型数据定义
    }
    
    delegate: Rectangle {
        width: parent.width
        height: 50
        // 根据是否选中设置背景色
        color: {
            if (multiSelectList.selectedIndexes.indexOf(index) !== -1) {
                return "#C8E6C9"  // 选中状态颜色
            }
            return index % 2 ? "#F5F5F5" : "white"
        }
        
        Text {
            anchors.centerIn: parent
            text: model.text
        }
        
        MouseArea {
            anchors.fill: parent
            onClicked: {
                var idx = multiSelectList.selectedIndexes.indexOf(index)
                if (idx === -1) {
                    // 未选中则添加到选中列表
                    multiSelectList.selectedIndexes.push(index)
                } else {
                    // 已选中则移除
                    multiSelectList.selectedIndexes.splice(idx, 1)
                }
                // 强制更新委托显示
                multiSelectList.modelChanged()
            }
        }
    }
}


六、综合示例:学生名单列表


结合本章所学,我们来实现一个完整的学生名单管理界面,包含数据增删、选中高亮、成绩分级显示等功能:

import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    width: 500
    height: 600
    visible: true
    title: "学生名单管理系统"
    
    // 数据模型
    ListModel {
        id: studentModel
        ListElement { name: "张三"; id: "2023001"; score: 85; major: "计算机科学" }
        ListElement { name: "李四"; id: "2023002"; score: 92; major: "软件工程" }
        ListElement { name: "王五"; id: "2023003"; score: 78; major: "人工智能" }
        ListElement { name: "赵六"; id: "2023004"; score: 88; major: "数据科学" }
        ListElement { name: "钱七"; id: "2023005"; score: 95; major: "网络安全" }
    }
    
    // 主界面
    Column {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15
        
        // 标题
        Text {
            text: "📚 学生名单"
            font.bold: true
            font.pixelSize: 28
            color: "#4CAF50"
        }
        
        // 操作栏
        Row {
            spacing: 10
            Button {
                text: "添加学生"
                onClicked: {
                    studentModel.append({
                        "name": "新学生",
                        "id": "2023" + (100 + studentModel.count),
                        "score": Math.floor(Math.random() * 40) + 60,
                        "major": "未指定"
                    })
                }
                background: Rectangle {
                    color: "#4CAF50"
                    radius: 5
                }
                contentItem: Text {
                    text: parent.text
                    color: "white"
                    horizontalAlignment: Text.AlignHCenter
                    verticalAlignment: Text.AlignVCenter
                }
            }
            
            Button {
                text: "删除选中"
                enabled: listView.currentIndex !== -1
                onClicked: studentModel.remove(listView.currentIndex)
            }
            
            Text {
                anchors.verticalCenter: parent.verticalCenter
                text: "共 " + studentModel.count + " 名学生"
                color: "#666666"
            }
        }
        
        // 列表视图
        ListView {
            id: listView
            width: parent.width
            height: parent.height - 100
            model: studentModel
            spacing: 5
            clip: true
            
            // 高亮组件
            highlight: Rectangle {
                color: "#E8F5E9"
                border.color: "#4CAF50"
                border.width: 2
                radius: 8
                width: listView.width
            }
            highlightMoveDuration: 150
            
            // 委托组件
            delegate: Rectangle {
                id: delegateItem
                width: listView.width
                height: 80
                radius: 8
                color: ListView.isCurrentItem ? "transparent" : (index % 2 ? "#F9F9F9" : "white")
                border.color: "#E0E0E0"
                border.width: 1
                
                MouseArea {
                    anchors.fill: parent
                    onClicked: listView.currentIndex = index
                    onDoubleClicked: console.log("双击:", model.name)
                }
                
                Row {
                    anchors.fill: parent
                    anchors.margins: 15
                    spacing: 20
                    
                    // 学号
                    Column {
                        anchors.verticalCenter: parent.verticalCenter
                        Text {
                            text: "学号"
                            font.pixelSize: 12
                            color: "#999999"
                        }
                        Text {
                            text: model.id
                            font.bold: true
                            font.pixelSize: 16
                            color: "#2196F3"
                        }
                    }
                    
                    // 姓名
                    Column {
                        anchors.verticalCenter: parent.verticalCenter
                        width: 100
                        Text {
                            text: "姓名"
                            font.pixelSize: 12
                            color: "#999999"
                        }
                        Text {
                            text: model.name
                            font.bold: true
                            font.pixelSize: 18
                            color: "#333333"
                        }
                    }
                    
                    // 专业
                    Column {
                        anchors.verticalCenter: parent.verticalCenter
                        width: 120
                        Text {
                            text: "专业"
                            font.pixelSize: 12
                            color: "#999999"
                        }
                        Text {
                            text: model.major
                            font.pixelSize: 14
                            color: "#666666"
                        }
                    }
                    
                    // 成绩
                    Column {
                        anchors.verticalCenter: parent.verticalCenter
                        Text {
                            text: "成绩"
                            font.pixelSize: 12
                            color: "#999999"
                        }
                        Rectangle {
                            width: 60
                            height: 30
                            radius: 15
                            color: {
                                if (model.score >= 90) return "#4CAF50";
                                if (model.score >= 80) return "#8BC34A";
                                if (model.score >= 70) return "#FFC107";
                                return "#F44336";
                            }
                            
                            Text {
                                anchors.centerIn: parent
                                text: model.score
                                font.bold: true
                                color: "white"
                            }
                        }
                    }
                }
                
                // 右侧操作按钮
                Button {
                    anchors.right: parent.right
                    anchors.rightMargin: 15
                    anchors.verticalCenter: parent.verticalCenter
                    text: "编辑"
                    flat: true
                    onClicked: {
                        listView.currentIndex = index
                        editDialog.open()
                    }
                }
            }
            
            // 空列表提示
            Label {
                anchors.centerIn: parent
                text: "暂无学生数据\n点击上方按钮添加"
                visible: studentModel.count === 0
                horizontalAlignment: Text.AlignHCenter
                color: "#999999"
            }
        }
    }
}