SwipeDelegate 委托组件本质上是在 ItemDelegate 上加了一层滑动功能。这篇介绍两种用法,一是左右两侧各实现一个操作(一侧归档、一侧删除),二是用 behind 属性让两侧拖拉出同一块内容面板。
先认识一下 swipe 分组
SwipeDelegate 继承自 ItemDelegate,所以 text、contentItem、background 这些属性它全都有,用法也一样。它多出来的东西都装在 swipe 这个分组里,管理滑动相关的一切。
不用一口气把所有属性都记住,先搞懂三件核心的事就够了。
第一件:露出来的是哪几块
滑动时从行底下露出来的内容,有三种挂法:
swipe.left------ 挂在左边的那块,往右拖的时候露出来swipe.right------ 挂在右边的那块,往左拖的时候露出来swipe.behind------ 左右共用同一块,往哪边拖都露出它
这里有个容易搞反的地方:名字说的是那块内容的位置 ,不是拖动方向。
swipe.left是"位于左边的东西",所以得往右拖才能看见它,别记反了。
这三块内容都藏在 contentItem 和 background 的后面,所以视觉上是"行被拖走、底下的东西露出来"的效果。
其中 swipe.behind 比较特殊------它一个顶俩,左右两边都用同一块内容。也正因为如此,behind 和 left / 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 时会执行一组顺序动画:先用 PropertyAction 把 delayRemove 设为 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,写起来更简单。
另外记住一条硬约束:behind 和 left / right 不能同时出现在同一个委托上,只能二选一。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:
https://github.com/qiu-zhi/QML-Minimal-Demos