QML SwipeDelegate 滑动委托

SwipeDelegate 委托组件本质上是在 ItemDelegate 上加了一层滑动功能。这篇介绍两种用法,一是左右两侧各实现一个操作(一侧归档、一侧删除),二是用 behind 属性让两侧拖拉出同一块内容面板。

先认识一下 swipe 分组

SwipeDelegate 继承自 ItemDelegate,所以 textcontentItembackground 这些属性它全都有,用法也一样。它多出来的东西都装在 swipe 这个分组里,管理滑动相关的一切。

不用一口气把所有属性都记住,先搞懂三件核心的事就够了。

第一件:露出来的是哪几块

滑动时从行底下露出来的内容,有三种挂法:

  • swipe.left ------ 挂在左边的那块,往右拖的时候露出来
  • swipe.right ------ 挂在右边的那块,往左拖的时候露出来
  • swipe.behind ------ 左右共用同一块,往哪边拖都露出它

这里有个容易搞反的地方:名字说的是那块内容的位置 ,不是拖动方向。swipe.left 是"位于左边的东西",所以得往右拖才能看见它,别记反了。

这三块内容都藏在 contentItembackground 的后面,所以视觉上是"行被拖走、底下的东西露出来"的效果。

其中 swipe.behind 比较特殊------它一个顶俩,左右两边都用同一块内容。也正因为如此,behindleft / right 不能同时用,选了一种就不能写另一种。

第二件:怎么知道滑到什么程度了

两个只读属性用来读滑动状态:

  • swipe.position ------ 当前滑到哪了,范围 -1 到 1。-1 是左边滑到底,1 是右边滑到底,0 是收起来的状态
  • swipe.complete ------ 有没有滑到底并且松手。只有它为 true 时,露出来的那块才能响应点击

第二点很重要:操作按钮必须滑到底、松了手,才能点得动。没滑到底的时候点上去是没反应的。

剩下的 swipe.enabled(能不能滑)和 swipe.transition(松手回弹用什么动画)都是辅助属性,用到的时候再查就行。

第三件:点击事件有个小坑

露出来的那块东西要响应点击,用的是两个附加属性:SwipeDelegate.pressed(判断是否按下)和 SwipeDelegate.onClicked(处理点击)。

这里有个官方文档特意提醒的坑:这两个属性必须用那块内容自己的 id 来限定 ,比如 deleteLabel.SwipeDelegate.pressed

为什么呢?因为那块内容的 background 是它的子元素,点击事件会先被子元素接住。如果你直接写 SwipeDelegate.pressed 而不限定 id,背景色能正常变化,但写在 Label 上的 onClicked 永远不会触发。这个坑调试起来挺费时间的,记一下就好。

Demo 1 左右滑动操作

下面的示例中,左右两侧各实现一个操作(左侧是归档,右侧删除)

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
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: 4
                ScrollBar.vertical: ScrollBar {}

                model: ListModel {
                    ListElement { title: "选项 1"; msg: "已归档" }
                    ListElement { title: "选项 2"; msg: "已归档" }
                    ListElement { title: "选项 3"; msg: "已归档" }
                    ListElement { title: "选项 4"; msg: "已归档" }
                    ListElement { title: "选项 5"; msg: "已归档" }
                    ListElement { title: "选项 6"; msg: "已归档" }
                    ListElement { title: "选项 7"; msg: "已归档" }
                }

                delegate: SwipeDelegate {
                    id: swipeDelegate
                    required property string title
                    required property string msg
                    required property int index

                    text: title
                    width: listView.width
                    height: 46
                    // 裁剪移出界外的文本,避免左滑时文字残留在可视区
                    clip: true

                    // 删除行前先收起,再真正从 model 移除
                    SequentialAnimation {
                        id: seqAnimation
                        PropertyAction { target: swipeDelegate; property: "ListView.delayRemove"; value: true }
                        NumberAnimation { target: swipeDelegate; property: "height"; to: 0; duration: 250 }
                        PropertyAction { target: swipeDelegate; property: "ListView.delayRemove"; value: false }
                    }

                    ListView.onRemove: seqAnimation.start()

                    swipe.right: Label {
                        id: deleteLabel
                        text: "删除"
                        color: "white"
                        horizontalAlignment: Label.AlignHCenter
                        verticalAlignment: Label.AlignVCenter
                        width: 88
                        height: parent.height
                        anchors.right: parent.right
                        SwipeDelegate.onClicked: listView.model.remove(swipeDelegate.index)

                        background: Rectangle {
                            color: deleteLabel.SwipeDelegate.pressed ? Qt.darker("tomato", 1.1) : "tomato"
                        }
                    }

                    swipe.left: Label {
                        id: archiveLabel
                        text: "归档"
                        color: "white"
                        horizontalAlignment: Label.AlignHCenter
                        verticalAlignment: Label.AlignVCenter
                        width: 88
                        height: parent.height
                        SwipeDelegate.onClicked: {
                            swipeDelegate.text = swipeDelegate.msg
                            swipeDelegate.swipe.close()
                        }

                        background: Rectangle {
                            color: archiveLabel.SwipeDelegate.pressed ? Qt.darker("green", 1.1) : "green"
                        }
                    }
                }
            }
        }
    }
}

关键逻辑解析

正常情况下,直接从列表模型删除条目,行会立刻消失。这里用 ListView.delayRemove 优化体验。

触发 ListView.onRemove 时会执行一组顺序动画:先用 PropertyActiondelayRemove 设为 true,再做行高过渡,实现条目缓慢压扁收起;动画结束后恢复 delayRemove,列表才真正移除该项。

归档逻辑更简单,点击修改文案,调用 swipe.close() 收回滑出的操作面板即可。

按钮按下变色依靠附加属性 SwipeDelegate.pressed。这里有个常见坑:必须带上控件id限定 ,例如 deleteLabel.SwipeDelegate.pressed,不能直接写 SwipeDelegate.pressed。 按钮背景属于子元素,事件会优先被子元素捕获。不加id限定,虽然背景色能变化,但 Label 的 onClicked 会失效。官方文档也提示过这点,能省下不少调试时间。

还有两个细节:委托要开启 clip: true,否则左滑时文字超出边界会留下残影;操作按钮设置固定宽度,这个宽度决定左滑时正文的偏移距离,宽度太小滑动效果会不明显。

Demo 2 滑动露出背景

以下示例不论往哪边拖拉,露出来的都是同一块内容面板。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
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: 4
                ScrollBar.vertical: ScrollBar {}

                model: 8
                delegate: SwipeDelegate {
                    required property int index

                    text: "选项 " + (index + 1)
                    width: ListView.view.width
                    height: 46

                    // 注意:behind 不能与 left / right 同时使用
                    // 无论向左还是向右滑动,都露出同一块位于后方的组件
                    swipe.behind: Label {
                        text: "behind"
                        color: "#fff"
                        font.bold: true
                        horizontalAlignment: Label.AlignHCenter
                        verticalAlignment: Label.AlignVCenter
                        width: parent.width
                        height: parent.height

                        background: Rectangle {
                            color: "#1296FF"
                        }
                    }
                }
            }
        }
    }
}

关键逻辑解析

swipe.behind 可以理解为共用底层面板:无论向左还是向右滑动,露出的都是同一块内容。但它有个限制,不能和 left、right属性同时使用,一旦启用 behind,就无法分别配置左右两侧的操作区域。

这种方式一般只用来做视觉效果,不绑定点击事件,面板尺寸会和父条目保持一致。

对比 Demo 1 就能很好区分:如果左右操作不同,比如左侧归档、右侧删除,就分开配置左右面板,各自绑定点击事件;如果两侧交互一致,或是仅需要滑动背景效果,直接用 behind 会更简洁。

两个示例对比

维度 左右分侧操作 behind 背景
用的属性 swipe.left + swipe.right swipe.behind
露出内容 两侧各自不同 两侧相同
能否继续拖过边界 不能,拖到头就停 能,可以一直拖、循环露出同一块
是否接点击 接了,点了执行操作 没接,只是视觉反馈
典型场景 归档 / 删除 / 更多操作 滑动提示、加载中的背景

选哪种,其实就一个判断:两侧要做的事是不是一样。

不一样就用 swipe.left + swipe.right 分侧挂,各自接自己的点击事件,像归档、删除、标记已读这种操作都属于这一类。一样的话,或者只是想让滑动有个视觉反馈、不需要点,就用 swipe.behind,写起来更简单。

另外记住一条硬约束:behindleft / right 不能同时出现在同一个委托上,只能二选一。


已验证环境

  • Qt 版本:Qt 6.8.2 / Qt 6.11.1
  • 操作系统:Windows 11
  • GitHub:https://github.com/qiu-zhi/QML-Minimal-Demos
相关推荐
Quz2 小时前
QML 列表中的四种委托:ItemDelegate、CheckDelegate、RadioDelegate、SwitchDelegate
qt
实心儿儿2 小时前
Qt — 显示类控件
qt
江湖人称菠萝包19 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter9-Qt Charts
笔记·qt·qt5
扶尔魔ocy19 小时前
【QT android】环境安装及第一个QT项目
qt·andriod开发
江湖人称菠萝包1 天前
【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·图形渲染