QML 列表中的四种委托:ItemDelegate、CheckDelegate、RadioDelegate、SwitchDelegate

列表里的自定义行基本上是由委托组件画出来的。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,视觉上最直观。


已验证环境

相关推荐
实心儿儿2 小时前
Qt — 显示类控件
qt
江湖人称菠萝包19 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter9-Qt Charts
笔记·qt·qt5
扶尔魔ocy19 小时前
【QT android】环境安装及第一个QT项目
qt·andriod开发
江湖人称菠萝包21 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter10-Data Visualization
笔记·qt·qt5
Cx330❀1 天前
Qt 常用控件属性与底层机制详解:从窗口半透明、浮点数陷阱到 QSS 与焦点策略
qt·ui·图形渲染
Quz1 天前
QML DelegateChooser:多条件组合与按列选择委托
qt
Quz1 天前
QML DelegateChooser:按角色选择委托与按索引选择委托
qt
Cx330❀1 天前
Qt 常用 UI 控件详解:从QPushButton到QLabel 核心用法解析
qt·ui·图形渲染
实心儿儿2 天前
Qt — 信号和槽
qt