列表里的自定义行基本上是由委托组件画出来的。Qt Quick Controls 把它做成了四个:ItemDelegate 负责纯点击,CheckDelegate、RadioDelegate、SwitchDelegate 各自多带一种选择状态,这篇把四个委托用法都展示一遍。
这四个委托的差异
这四个组件是同源的,CheckDelegate、RadioDelegate、SwitchDelegate 都继承自 ItemDelegate,而 ItemDelegate 又继承自 AbstractButton。所以它们都拥有点击属性,而正的差别在选中属性上:
ItemDelegate 没有选中属性,用 highlighted 来表示 "这是当前行",一般绑到列表的当前项上;CheckDelegate 带一个布尔属性 checked,适合多选;RadioDelegate 也带 checked属性,想要带有排它属性,需绑定 ButtonGroup组件;SwitchDelegate 同也有 checked 属性,只是外观不同而已。
Demo 1 点击列表项

这个示例展示 ItemDelegate 的精简用法。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片:列表垂直方向铺满剩余空间
Rectangle {
id: panel
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
ListView {
id: listView
anchors.fill: parent
anchors.margins: 8
clip: true
spacing: 4
ScrollBar.vertical: ScrollBar {}
model: 8
delegate: ItemDelegate {
required property int index
text: "选项 " + (index + 1)
width: ListView.view.width
height: 44
highlighted: ListView.isCurrentItem
onClicked: ListView.view.currentIndex = index
}
}
}
}
}
关键逻辑解析
核心就三件事。第一,model: 8 告诉列表要生成 8 行;第二,每行的文字用"选项"加上 index 拼出来就行;第三,也是最关键的------把 highlighted 绑到 ListView.isCurrentItem 上,再在点击时把当前行设为 currentIndex,这样点哪一行哪一行就高亮,列表的"当前选中项"就管起来了。
Demo 2 复选列表项

带有八项兴趣标签,可以多选,底部的获取已选项按钮 可以将当前所选用一句话展示出来。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
FadeInAnimation {
id: root
// 记录已勾选的项
property var selectedItems: []
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片:列表垂直方向铺满剩余空间
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 2
ScrollBar.vertical: ScrollBar {}
model: ["阅读", "音乐", "摄影", "旅行", "编程", "运动", "美食", "绘画"]
delegate: CheckDelegate {
id: checkItem
required property string modelData
width: ListView.view.width
height: 42
// 指示器移到最左侧,文本紧随其后;右侧留白,滚动条不再遮挡交互区
indicator: Item {} // 屏蔽默认的右侧勾选框
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 6
spacing: 10
Rectangle {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignVCenter
radius: 4
border.width: 1.5
border.color: checkItem.checked ? "#1976D2" : "#b6bec9"
color: checkItem.checked ? "#1976D2" : "transparent"
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: "✓"
color: "white"
font.pixelSize: 12
font.bold: true
visible: checkItem.checked
}
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: checkItem.modelData
elide: Text.ElideRight
font.pointSize: 11
color: "#333"
}
}
onCheckedChanged: {
if (checked) {
root.selectedItems.push(modelData)
} else {
root.selectedItems = root.selectedItems.filter(item => item !== modelData)
}
}
}
}
// ... 省略底部结果栏:一个按钮 + 一行结果文字 ...
}
}
}
}
关键逻辑解析
自绘勾选框的思路很直接:先用 indicator: Item {} 把默认的右边勾选框藏起来,然后在 contentItem 里自己画一个放在左边。
画出来的这个勾选框,它的边框颜色、填充颜色、对勾显不显示,全部跟着 checkItem.checked 走。也就是说,"选中状态"本身还是委托在管,我们只是换了个外观,再配上 120 毫秒的颜色过渡动画,选中和取消的切换就很顺滑。
选中的集合用一个数组来维护,在 onCheckedChanged 里操作:勾上了就把当前项 push 进去,取消了就用 filter 把它筛掉。这里有个新手容易踩的坑------取消的时候不能直接在原数组上删(比如用 splice),必须重新赋值 给 selectedItems。因为 QML 里的 var 属性只有被整个重新赋值时,才会触发界面刷新,原地修改数组是没用的。
底部的按钮只是在点的时候才把选中的内容拼成一句话展示,避免每勾一下都刷新一遍结果文字。
Demo 3 单选列表项

同样是自绘,但把方框(CheckDelegate)换成圆圈(RadioDelegate),并且保证一组里只有一个被选中。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
FadeInAnimation {
id: root
// 单选组:保证同一时刻只能选中一个
property string currentSelection: ""
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片:列表垂直方向铺满剩余空间
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
spacing: 8
ListView {
id: listView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 2
ScrollBar.vertical: ScrollBar {}
model: ["极速达(次日到)", "标准配送(3-5天)", "经济配送(5-7天)", "门店自提", "同城闪送", "预约送达"]
delegate: RadioDelegate {
id: radioItem
required property string modelData
required property int index
width: ListView.view.width
height: 42
checked: index === 0
ButtonGroup.group: radioGroup
// 指示器移到最左侧,文本紧随其后;右侧留白,滚动条不再遮挡交互区
indicator: Item {} // 屏蔽默认的右侧圆点
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 6
spacing: 10
Rectangle {
Layout.preferredWidth: 20
Layout.preferredHeight: 20
Layout.alignment: Qt.AlignVCenter
radius: width / 2
border.width: 1.5
border.color: radioItem.checked ? "#1976D2" : "#b6bec9"
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: 10
height: 10
radius: width / 2
color: "#1976D2"
visible: radioItem.checked
}
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: radioItem.modelData
elide: Text.ElideRight
font.pointSize: 11
color: "#333"
}
}
onCheckedChanged: {
if (checked)
root.currentSelection = modelData
}
}
}
// ... 省略底部结果栏:一行结果文字 ...
}
}
}
// 单选组对象:供每个 RadioDelegate 通过 ButtonGroup.group 挂载
ButtonGroup {
id: radioGroup
}
}
关键逻辑解析
单选和复选的自绘思路基本一样:先把默认指示器藏掉,再自己画一个放在左边。
真正实现"单选"的关键是 ButtonGroup。在外面定义一个 ButtonGroup { id: radioGroup },然后每个 RadioDelegate 都通过 ButtonGroup.group: radioGroup 挂到这个组上,组里的成员就自动互斥了------点一个,其他的自动取消选中,完全不用自己写逻辑。
当前选中的值在 onCheckedChanged 里更新就好,因为有 ButtonGroup 保证互斥,所以不用操心"取消其他项"这件事。
Demo 4 开关列表项

带有9项系统设置,每行带有SwitchDelegate。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Basic
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 内容卡片:列表垂直方向铺满剩余空间
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
ListView {
id: listView
anchors.fill: parent
anchors.margins: 8
clip: true
spacing: 2
ScrollBar.vertical: ScrollBar {}
model: ListModel {
ListElement { label: "飞行模式"; isOn: false }
ListElement { label: "Wi-Fi"; isOn: true }
ListElement { label: "蓝牙"; isOn: true }
ListElement { label: "移动数据"; isOn: true }
ListElement { label: "定位服务"; isOn: false }
ListElement { label: "护眼模式"; isOn: false }
ListElement { label: "深色模式"; isOn: false }
ListElement { label: "自动旋转"; isOn: true }
ListElement { label: "省电模式"; isOn: false }
}
delegate: SwitchDelegate {
id: switchItem
required property string label
required property bool isOn
width: ListView.view.width
height: 44
checked: isOn
// 指示器移到最左侧,文本紧随其后;右侧留白,滚动条不再遮挡交互区
indicator: Item {} // 屏蔽默认的右侧开关
contentItem: RowLayout {
anchors.fill: parent
anchors.leftMargin: 12
anchors.rightMargin: 6
spacing: 10
// 自绘开关:轨道 + 滑块(checked 时滑块右移)
Rectangle {
Layout.preferredWidth: 40
Layout.preferredHeight: 22
Layout.alignment: Qt.AlignVCenter
radius: height / 2
color: switchItem.checked ? "#1976D2" : "#cfd6df"
Behavior on color { ColorAnimation { duration: 150 } }
Rectangle {
width: 18
height: 18
radius: width / 2
y: (parent.height - height) / 2
x: switchItem.checked ? parent.width - width - 2 : 2
color: "white"
Behavior on x { NumberAnimation { duration: 150; easing.type: Easing.OutQuad } }
}
}
Text {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
text: switchItem.label
elide: Text.ElideRight
font.pointSize: 11
color: "#333"
}
}
}
}
}
}
}
关键逻辑解析
开关也是自绘的,结构很简单:一个大矩形当轨道,里面套一个小矩形当滑块。
滑块的位置由 checked 状态决定------开着的时候滑到右边,关着的时候滑到左边。这里用了 Behavior on x 给 x 坐标加了动画,所以拨动开关时滑块是平滑滑动的,不是直接跳过去。轨道的颜色也跟着 checked 变,同样配了颜色过渡动画,整体效果就很灵动。
模型里的 isOn 字段用来存初始状态,委托通过 checked: isOn 把它读进来。不过要注意,这只是初始值 ------用户拨动开关之后,checked 变了,但模型里的 isOn 不会自动跟着变。如果需要把改动同步回数据源,得自己在 onCheckedChanged 里手动写回去。
四个委托怎么选
| 维度 | ItemDelegate |
CheckDelegate |
RadioDelegate |
SwitchDelegate |
|---|---|---|---|---|
| 选择状态 | 无,只有 highlighted |
checked,各行独立 |
checked,靠 ButtonGroup 排他 |
checked,开 / 关语义 |
| 能否多选 | 不适用 | 能 | 不能 | 每行独立,互不影响 |
| 默认指示器 | 无 | 勾选框 | 圆点 | 开关 |
| 典型场景 | 菜单项、导航项、纯点击列表 | 多选筛选、标签选择 | 配送方式、单选设置 | 系统设置、功能开关 |
如何选择?
选哪个委托,就两个判断:
第一,这一行需不需要"记住选中状态"?不需要就用 ItemDelegate,它只有一个 highlighted 用来表示"我是当前行",点一下触发个动作就行,比如菜单项、导航列表。
如果需要记住选中状态,再看第二件事:这组选项之间是不是互斥的?只能选一个的用 RadioDelegate,可以选多个的用 CheckDelegate。至于功能开关那种"开/关"语义的,就用 SwitchDelegate,视觉上最直观。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_itemdelegate