GridView 网格搭好之后,数据一变动,视图就得跟着动起来。那么新增的项从哪进场、删掉的项怎么退场、被挤动的项目怎么动态挪位?
这篇用两个简单的例子说说滚动控制和定位。
增删动画
演示代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ListModel {
id: tagModel
ListElement { tagName: "风景"; tagColor: "#2ecc71" }
ListElement { tagName: "人像"; tagColor: "#3498db" }
ListElement { tagName: "街拍"; tagColor: "#e67e22" }
ListElement { tagName: "美食"; tagColor: "#e74c3c" }
ListElement { tagName: "夜景"; tagColor: "#9b59b6" }
ListElement { tagName: "建筑"; tagColor: "#1abc9c" }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 操作按钮
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
text: "随机新增"
font.pointSize: 10
onClicked: {
var colors = ["#16a085", "#2980b9", "#8e44ad", "#c0392b", "#d35400", "#27ae60"]
var names = ["星空", "海洋", "森林", "沙漠", "雪山", "草原"]
var i = Math.floor(Math.random() * names.length)
tagModel.insert(Math.floor(Math.random() * tagModel.count), {
"tagName": names[i],
"tagColor": colors[i]
})
}
}
Button {
text: "删除首项"
font.pointSize: 10
enabled: tagModel.count > 0
onClicked: tagModel.remove(0)
}
Button {
text: "打乱顺序"
font.pointSize: 10
onClicked: {
// 未定义 move 过渡:被移动项直接跳到新格,邻居走 displaced 滑过去
for (var i = 0; i < 3; i++) {
var from = Math.floor(Math.random() * tagModel.count)
var to = Math.floor(Math.random() * tagModel.count)
tagModel.move(from, to, 1)
}
}
}
Item { Layout.fillWidth: true }
Text {
text: "共 " + tagModel.count + " 项"
font.pointSize: 10
color: "#666"
}
}
// 内容卡片
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
GridView {
id: grid
anchors.fill: parent
anchors.margins: 10
clip: true
cellWidth: 90
cellHeight: 70
model: tagModel
// 新增的项从 (100,100) 位置滑入并淡入
add: Transition {
NumberAnimation { properties: "x,y"; from: 100; duration: 300; easing.type: Easing.OutBack }
NumberAnimation { property: "opacity"; from: 0; duration: 300 }
}
// 删除的项淡出
remove: Transition {
NumberAnimation { property: "opacity"; to: 0; duration: 250 }
}
// 被挤动的项滑到新格子
displaced: Transition {
NumberAnimation { properties: "x,y"; duration: 300; easing.type: Easing.OutCubic }
}
delegate: Item {
id: cell
required property int index
required property string tagName
required property string tagColor
// remove 过渡期间本 delegate 仍存活,但 index 已过期,置位后忽略重复点击
property bool removing: false
width: grid.cellWidth
height: grid.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: 8
color: cell.tagColor
Column {
anchors.centerIn: parent
spacing: 4
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: cell.tagName
color: "white"
font.pointSize: 12
font.bold: true
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "#" + (cell.index + 1)
color: "#FFFFFFAA"
font.pointSize: 9
}
}
MouseArea {
anchors.fill: parent
onClicked: {
// 点谁删谁,观察 remove 和 displaced 的配合
// 动画期间 index 不再更新,先校验范围防止删错项
if (cell.removing || cell.index < 0 || cell.index >= tagModel.count)
return
cell.removing = true
tagModel.remove(cell.index)
}
}
}
}
}
}
Text {
Layout.fillWidth: true
text: "点击任意标签可删除该项"
font.pointSize: 10
color: "#999"
}
}
}
add 属性只管新的项如何进场,remove 属性只管被删的项怎么退场,displaced 属性统一负责那些被挤动的项。
删除这里有个细节,移除动画期间,delegate 还没被销毁,但 index 已经不可靠了,所以 delegate 里加了 removing 作为标记,点过一次就忽略后续点击,删除前再校验一次索引范围,避免连点删错项。
适用场景:可增删的标签云、可编辑的宫格列表、带动画的数据看板。
动态操作
可以直接定位到某一具体的项,或者用键盘或鼠标点击能够切换选中项,模型也能随时追加和删除。如图所示:
演示代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
id: root
property int nextId: 14
// 模型就是一个普通数组,1~14 不用逐条声明 ListElement
property var numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
// 操作区
RowLayout {
Layout.fillWidth: true
spacing: 8
SpinBox {
id: jumpBox
from: 0
to: Math.max(0, root.numbers.length - 1)
value: 12
font.pointSize: 10
}
Button {
text: "定位到该格"
font.pointSize: 10
enabled: root.numbers.length > 0
onClicked: {
grid.positionViewAtIndex(jumpBox.value, GridView.Contain)
grid.currentIndex = jumpBox.value
}
}
Button {
text: "末尾追加"
font.pointSize: 10
onClicked: {
// 换成新数组会触发模型整体 reset,currentIndex 会被清空,
// 所以先记下来,等视图处理完更新再恢复
var keep = grid.currentIndex
root.numbers = root.numbers.concat(++root.nextId)
Qt.callLater(function() {
grid.positionViewAtIndex(root.numbers.length - 1, GridView.Contain)
if (keep >= 0)
grid.currentIndex = keep
})
}
}
Button {
text: "删除选中"
font.pointSize: 10
enabled: grid.currentIndex >= 0 && grid.currentIndex < root.numbers.length
onClicked: {
var i = grid.currentIndex
root.numbers = root.numbers.slice(0, i).concat(root.numbers.slice(i + 1))
// reset 后恢复选中:优先原位置,删的是末尾项则前移一格
Qt.callLater(function() {
grid.currentIndex = Math.min(i, root.numbers.length - 1)
})
}
}
Item { Layout.fillWidth: true }
}
// 内容卡片
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: "#fafafa"
radius: 8
border.color: "#e0e0e0"
border.width: 1
GridView {
id: grid
anchors.fill: parent
anchors.margins: 10
clip: true
focus: true
cellWidth: 66
cellHeight: 66
model: root.numbers
delegate: Item {
id: cell
required property int index
required property var modelData
width: grid.cellWidth
height: grid.cellHeight
Rectangle {
anchors.fill: parent
anchors.margins: 5
radius: 8
color: cell.GridView.isCurrentItem ? "#1296FF" : "#FFFFFF"
border.color: "#e0e0e0"
border.width: 1
Behavior on color { ColorAnimation { duration: 120 } }
Text {
anchors.centerIn: parent
text: cell.modelData
color: cell.GridView.isCurrentItem ? "white" : "#333"
font.pointSize: 14
font.bold: true
}
}
MouseArea {
anchors.fill: parent
onClicked: {
grid.currentIndex = cell.index
grid.forceActiveFocus()
}
}
}
}
}
// 状态提示
Text {
Layout.fillWidth: true
text: "共 %1 项 | 当前 index:%2 | 点击格子或用方向键切换选中项"
.arg(root.numbers.length)
.arg(grid.currentIndex)
font.pointSize: 10
color: "#666"
}
}
}
positionViewAtIndex(index, mode) 接口把指定项滚动进视野中,第二个参数是停靠方式,这里用的 GridView.Contain 表示让整项完整可见,项比视图还大时就贴到顶部。想让它停在中间或用 GridView.Center,想只保证可见用 GridView.Visible。
currentIndex 记住当前选中项,GridView.isCurrentItem 这个附加属性让 delegate 知道自己是不是被选中的那个。选中的格子填成蓝色、文字变白,颜色变化加了一段 Behavior 过渡。方向键切换不用自己写按键处理,只要 focus: true,GridView 会自动响应上下左右并移动 currentIndex;点格子也会把选中项设过去并抢回焦点,鼠标和键盘两边能接着用。
模型用的是普通 JS 数组。追加和删除都是换成一份新数组,这会让 GridView 整体重置、currentIndex 被清空,所以代码在Qt.callLater里再恢复选中项并滚动到末尾。
过渡属性速查
| 属性 | 作用 |
|---|---|
add | 新加入项自身的入场 |
remove | 被移除项自身的退场 |
move | 被移动项自身的位置变化 |
displaced | 被任意操作挤动的邻居 |
addDisplaced / moveDisplaced / removeDisplaced | 按增、移、删分别指定挤动动画 |
已验证环境:
- Qt 版本:Qt 6.12
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_gridview