QML ListView 入门:自定义样式与数据交互

0 阅读4分钟

ListView 是 QML 里最常用的列表组件,小到一个菜单、大到通讯录,基本都离不开它。默认的 ListView 不好看,但通过自定义 delegate、header、section 这些属性,可以做出很像样的列表效果。再配合 ListModel 的增删改方法,就能实现完整的数据交互。

这篇文章介绍2个简单的demo:第一个例子做一个带头像、在线状态、分组的联系人列表;第二个例子做一个可增删的价格表,演示 ListModel 和输入框怎么配合。

自定义样式:联系人列表

一个联系人列表,带头像占位、在线状态颜色、按状态分组。

1.gif

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

// Demo:ListView 样式(头像、分组、状态)
FadeInAnimation {

    ListModel {
        id: contactModel
        ListElement { name: "老王"; avatar: "qrc:/images/avatar1.png"; status: "在线" }
        ListElement { name: "张三"; avatar: ""; status: "在线" }
        ListElement { name: "李四"; avatar: ""; status: "在线" }
        ListElement { name: "联系人4"; avatar: "qrc:/images/avatar2.png"; status: "离线" }
        ListElement { name: "联系人5"; avatar: "qrc:/images/avatar3.png"; status: "忙碌" }
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        ListView {
            id: listView
            model: contactModel
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true

            header: Rectangle {
                width: listView.width
                height: 30
                color: "#2D82B5"
                Text {
                    anchors.centerIn: parent
                    text: "联系人列表"
                    font.pixelSize: 15
                    color: "white"
                }
            }

            delegate: Rectangle {
                width: listView.width
                height: 50
                color: ListView.isCurrentItem ? "#BCE6FF" : "white"

                MouseArea {
                    anchors.fill: parent
                    onClicked: parent.ListView.view.currentIndex = index
                }

                Row {
                    anchors.fill: parent
                    anchors.margins: 5
                    spacing: 10

                    Item {
                        width: 40; height: 40
                        anchors.verticalCenter: parent.verticalCenter

                        Image {
                            id: avatarImage
                            anchors.fill: parent
                            source: avatar
                            visible: status === Image.Ready
                            fillMode: Image.PreserveAspectCrop
                            Rectangle {
                                anchors.fill: parent
                                color: "#999999A0"
                                visible: model.status === "离线"
                            }
                        }

                        Rectangle {
                            visible: avatarImage.status !== Image.Ready
                            anchors.fill: parent
                            radius: 20
                            color: "#E0E0E0"
                            Text {
                                anchors.centerIn: parent
                                text: name[0]
                                font.pixelSize: 20
                            }
                        }
                    }

                    Column {
                        anchors.verticalCenter: parent.verticalCenter
                        spacing: 4
                        Text { text: name; font.pixelSize: 14; font.bold: true }
                        Text {
                            text: status
                            font.pixelSize: 12
                            color: {
                                if (status === "在线") return "#4CAF50"
                                if (status === "离线") return "#9E9E9E"
                                return "#F44336"
                            }
                        }
                    }
                }
            }

            section.property: "status"
            section.delegate: Rectangle {
                width: parent.width
                height: 26
                color: "#F5F5F5"
                Text {
                    anchors.verticalCenter: parent.verticalCenter
                    anchors.left: parent.left
                    anchors.leftMargin: 10
                    text: section
                    font.bold: true
                    font.pixelSize: 12
                    color: "#2D82B5"
                }
            }
        }
    }
}

这个例子里有三个值得注意的地方。首先是 header 属性,给列表加了一个蓝色标题栏,它会跟着列表一起滚动,适合放列名或者分类标题。

然后是 delegate 的设计。每一行左边是头像区域,这里做了两层:有图片时显示 Image,图片加载失败或者路径为空时,显示一个灰色圆形,里面放名字的首字母当占位。离线状态的头像还盖了一层半透明灰色蒙版,视觉上一眼就能区分在线和离线。右边是姓名和状态,状态文字的颜色根据 status 字段动态变化,在线绿色、离线灰色、忙碌红色。选中行用 ListView.isCurrentItem 来判断,点哪一行哪一行就变蓝。

最后是分组功能,用 section.property: "status" 告诉 ListView 按 status 字段分组,再配上 section.delegate 画一个浅灰色的分组条。列表会自动把相同状态的联系人归到一组下面,不用自己手动排序。

适用场景:联系人列表、好友列表、任何需要按状态/分类展示的列表。

数据交互:增删改操作

一个水果价格表,顶部输入名称和价格点添加,列表里每一项都能单独删除。

2.gif

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "./components"

// Demo:ListView 数据交互(增删改、输入框绑定)
FadeInAnimation {

    ListModel {
        id: fruitModel
        ListElement { fruitName: "苹果"; price: "8.0" }
        ListElement { fruitName: "香蕉"; price: "3.5" }
        ListElement { fruitName: "橙子"; price: "6.0" }
        ListElement { fruitName: "草莓"; price: "10.0" }
    }

    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        // 添加输入区
        RowLayout {
            Layout.fillWidth: true
            spacing: 6

            CustomTextField {
                id: nameInput
                placeholderText: "名称"
                Layout.fillWidth: true
            }

            CustomTextField {
                id: priceInput
                placeholderText: "价格"
                Layout.preferredWidth: 60
            }

            IconButton {
                text: "添加"
                iconSource: "qrc:/icons/add.png"
                showBackground: true
                backgroundColor: "#BBDEFB"
                onClicked: {
                    if (nameInput.text && priceInput.text) {
                        fruitModel.append({ "fruitName": nameInput.text, "price": priceInput.text })
                        nameInput.text = ""
                        priceInput.text = ""
                    }
                }
            }
        }

        // 列表
        ListView {
            id: listView
            Layout.fillWidth: true
            Layout.fillHeight: true
            model: fruitModel
            clip: true
            spacing: 4

            delegate: Rectangle {
                width: listView.width
                height: 40
                radius: 4
                color: "#f5f5f5"

                RowLayout {
                    anchors.fill: parent
                    anchors.margins: 8
                    spacing: 8

                    Text {
                        text: fruitName
                        Layout.fillWidth: true
                        font.pixelSize: 13
                    }
                    Text {
                        text: price + " 元"
                        color: "#E67E22"
                        font.pixelSize: 13
                    }
                    Button {
                        text: "删除"
                        implicitHeight: 26
                        implicitWidth: 44
                        font.pixelSize: 11
                        onClicked: fruitModel.remove(index)
                    }
                }
            }
        }
    }
}

ListModel 自带 append() 和 remove() 两个方法,操作起来很直接。添加的时候调用 append() 传一个 JS 对象进去,键名要和 ListElement 里的字段名对上;删除的时候调用 remove(index),传当前行的索引就行。

输入框加了个简单的非空判断,名称和价格都填了才允许添加,加完自动清空输入框。列表项里的删除按钮直接绑 fruitModel.remove(index),点一下那行就没了,ListView 会自动刷新,不用手动通知。

这个例子的数据量不大,直接在 QML 里用 ListModel 就够了。如果数据来自后端或者数据量很大,就得用 C++ 模型来处理,后面的文章会讲到。

适用场景:简单的待办清单、购物车、录入类小工具。


已验证环境: