QML GridView 动态:增删过渡与滚动定位
目录增删动画演示代码动态操作演示代码过渡属性速查工程下载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统一负责那些被挤动的项三者互不干扰。add 里面用 NumberAnimation 把 x、y从 100 动画到最终位置同时把 opacity透明度 从 0 拉到 1新的项就是从远处滑进来然后再显示出来的remove 把透明度降到 0 让被删项淡出displaced 则给x、y加一段让相邻的项被挤开后的动画是平滑过渡的。删除这里有个细节remove过渡期间 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: { // 换成新数组会触发模型整体 resetcurrentIndex 会被清空 // 所以先记下来等视图处理完更新再恢复 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: trueGridView 会自动响应上下左右并移动 currentIndex点格子也会把选中项设过去并抢回焦点鼠标和键盘两边能接着用。模型用的是普通 JS 数组。追加和删除都是换成一份新数组这会让 GridView 整体重置、currentIndex 被清空所以代码在Qt.callLater里再恢复选中项并滚动到末尾。适用场景带选中态的选择网格、键盘操作的图片浏览、可动态加载的宫格列表。过渡属性速查属性作用add新加入项自身的入场remove被移除项自身的退场move被移动项自身的位置变化displaced被任意操作挤动的邻居addDisplaced/moveDisplaced/removeDisplaced按增、移、删分别指定挤动动画工程下载我的 Qt 版本Qt 6.12操作系统Windows 11GitHubQML 示例合集V2.0 - qml_gridview