ListView 是 QML 里最常用的列表组件,小到一个菜单、大到通讯录,基本都离不开它。默认的 ListView 不好看,但通过自定义 delegate、header、section 这些属性,可以做出很像样的列表效果。再配合 ListModel 的增删改方法,就能实现完整的数据交互。
这篇文章介绍2个简单的demo:第一个例子做一个带头像、在线状态、分组的联系人列表;第二个例子做一个可增删的价格表,演示 ListModel 和输入框怎么配合。
自定义样式:联系人列表
一个联系人列表,带头像占位、在线状态颜色、按状态分组。

演示代码
qml
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 画一个浅灰色的分组条。列表会自动把相同状态的联系人归到一组下面,不用自己手动排序。
适用场景:联系人列表、好友列表、任何需要按状态/分类展示的列表。
数据交互:增删改操作
一个水果价格表,顶部输入名称和价格点添加,列表里每一项都能单独删除。

演示代码
qml
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++ 模型来处理,后面的文章会讲到。
适用场景:简单的待办清单、购物车、录入类小工具。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_listview