QML进阶指南:状态动画与模型视图实战详解

1. 引言

掌握 QML 的基础语法之后,很多开发者会卡在同一个瓶颈:界面能画出来,但交互生硬、列表卡顿、状态切换靠一堆 if-else 硬编码。要跨过这道坎,核心是两块能力:

  • 状态与动画体系 :用 State 描述"界面有哪几种形态",用 Transition 描述"形态之间怎么过渡",用 Behavior 描述"某个属性变化时自动怎么动",用 NumberAnimation 等动画元素描述"具体怎么动"。
  • 模型视图体系 :用 ListModel 管理数据,用 DelegateModel 做分组、过滤、排序,用 TableView 展示二维表格数据。

本文按"概念 → 原理 → 可运行示例 → 常见坑"的顺序展开,每个知识点都配完整代码,建议边读边在 Qt Creator 里跑一遍。

2. 状态与动画体系

QML 的动画体系可以分成四层,理解这个分层,后面就不会混淆各个元素的职责:

flowchart TD A[属性值发生变化] --> B{是否有 State 定义} B -- 有 --> C[State 记录目标属性值] B -- 无 --> D[直接赋值] C --> E{是否有 Transition} E -- 有 --> F[Transition 定义过渡动画] E -- 无 --> G[瞬间切换] D --> H{是否有 Behavior} H -- 有 --> I[Behavior 自动播放动画] H -- 无 --> J[瞬间变化] F --> K[Animation 具体动画曲线] I --> K

一句话总结:State 管"变成什么样",Transition 管"状态切换时怎么变",Behavior 管"任意属性变化时怎么变",Animation 管"变化的曲线和时长"。

3. State

3.1 基本用法

State 的本质是一组"属性覆盖"。当 state 属性等于某个 State 的 name 时,该 State 中 PropertyChanges 定义的属性值就会生效;离开该状态时,属性自动恢复到默认值。

qml 复制代码
import QtQuick 2.15

Rectangle {
    id: root
    width: 400; height: 300
    color: "#f5f5f5"

    Rectangle {
        id: box
        width: 100; height: 100
        x: 20; y: 20
        color: "#4a90d9"
        radius: 8
    }

    states: [
        State {
            name: "moved"
            PropertyChanges { target: box; x: 260; y: 160; color: "#e94b3c" }
        }
    ]

    MouseArea {
        anchors.fill: parent
        onClicked: root.state = (root.state === "moved" ? "" : "moved")
    }
}

点击后方块跳到右下角并变红,再点一次回到原位。注意这里没有任何动画,是瞬间切换------这正是下一节 Transition 要解决的问题。

3.2 关键细节

  • 默认状态名为空字符串 :state: "" 表示回到默认状态,不要写成 "default"。
  • PropertyChanges 只覆盖属性,不删除属性:离开状态后属性回到 State 之外声明的值。
  • 可以修改任意对象的属性 :通过 target 指定,不限于当前组件。
  • StateChangeScript :需要在进入状态时执行逻辑,可以嵌入脚本,但建议优先用 onStateChanged 信号,可读性更好。
  • when 条件 :State { name: "expanded"; when: root.expanded },用布尔表达式自动激活状态,比手动赋值更声明式。

3.3 用when替代手动赋值

qml 复制代码
Rectangle {
    id: root
    property bool expanded: false
    width: expanded ? 400 : 200
    height: 200

    states: State {
        name: "expanded"
        when: root.expanded
        PropertyChanges { target: root; width: 400 }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.expanded = !root.expanded
    }
}

使用 when 后,状态与业务布尔量绑定,避免了"状态名拼错"这类低级错误。

4. Transition

4.1 基本用法

Transition 定义在 transitions 列表中,通过 from 和 to 指定适用的状态切换方向,"*" 表示任意状态。

qml 复制代码
Rectangle {
    id: root
    width: 400; height: 300
    color: "#f5f5f5"

    Rectangle {
        id: box
        width: 100; height: 100
        x: 20; y: 20
        color: "#4a90d9"
        radius: 8
    }

    states: State {
        name: "moved"
        PropertyChanges { target: box; x: 260; y: 160; color: "#e94b3c" }
    }

    transitions: Transition {
        from: "*"; to: "*"
        NumberAnimation {
            properties: "x,y"
            duration: 400
            easing.type: Easing.InOutQuad
        }
        ColorAnimation { properties: "color"; duration: 400 }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: root.state = (root.state === "moved" ? "" : "moved")
    }
}

现在方块会平滑移动并渐变颜色。Transition 只负责状态切换时的动画,普通属性赋值不会触发它,这是与 Behavior 最本质的区别。

4.2 精确控制方向

qml 复制代码
transitions: [
    Transition {
        from: ""; to: "moved"
        NumberAnimation { properties: "x,y"; duration: 300; easing.type: Easing.OutCubic }
    },
    Transition {
        from: "moved"; to: ""
        NumberAnimation { properties: "x,y"; duration: 600; easing.type: Easing.OutBounce }
    }
]

去程快、回程带回弹,这种"非对称动画"在真实产品中非常常见,用两条 Transition 就能表达。

4.3 常见坑

  • Transition 不生效 :最常见原因是 from/to 写错,或者状态名拼写不一致。调试时先用 from: "*"; to: "*" 验证。
  • 动画属性名写错 :properties: "x,y" 中的属性名必须与 PropertyChanges 中修改的属性一致,否则该属性会瞬间跳变。
  • Transition 与 Behavior 冲突:同一属性同时被两者控制时,行为不确定,应只保留一种。

5. Behavior

5.1 基本用法

Behavior 挂在某个属性上,只要该属性值发生变化(无论来自状态切换、绑定更新还是代码赋值),就自动播放指定动画。

qml 复制代码
Rectangle {
    id: root
    width: 400; height: 300
    color: "#f5f5f5"

    Rectangle {
        id: box
        width: 100; height: 100
        y: 100
        x: 20
        color: "#4a90d9"
        radius: 8

        Behavior on x {
            NumberAnimation { duration: 500; easing.type: Easing.OutBack }
        }
    }

    MouseArea {
        anchors.fill: parent
        onClicked: box.x = (box.x < 200 ? 260 : 20)
    }
}

这里没有 State,也没有 Transition,但点击后 x 依然平滑移动------因为 Behavior 拦截了属性变化。

5.2 临时禁用Behavior

有时需要"瞬间归位",可以临时关闭:

qml 复制代码
Behavior on x { enabled: false; NumberAnimation { duration: 500 } }

或者在代码中动态设置 box.Behavior.enabled = false(Qt 5.15+ 支持通过 Behavior 附加属性访问)。更通用的做法是给 Behavior 加一个 id,通过 id.enabled 控制。

5.3 Behavior与Transition

对比项 Behavior Transition
触发条件 任意属性变化 仅状态切换
作用范围 单个属性 一组属性
适用场景 简单、独立的属性动画 多属性协同的复杂过渡
可维护性 分散在属性旁,直观 集中管理,便于统一调整

经验法则:单属性、随时可能变化 → Behavior;多属性、与状态强相关 → Transition。

6. NumberAnimation

6.1 NumberAnimation详解

NumberAnimation 是最常用的动画类型,用于数值属性(x、y、width、opacity、rotation 等)。

qml 复制代码
Rectangle {
    id: root
    width: 400; height: 300
    color: "#f5f5f5"

    Rectangle {
        id: ball
        width: 60; height: 60
        radius: 30
        color: "#4a90d9"
        x: 20; y: 20
    }

    NumberAnimation {
        id: moveAnim
        target: ball
        property: "x"
        from: 20
        to: 300
        duration: 800
        easing.type: Easing.InOutQuad
        loops: 1
    }

    MouseArea {
        anchors.fill: parent
        onClicked: moveAnim.start()
    }
}

关键属性说明:

  • target / property:动画作用对象与属性;也可以用 properties: "x,y" 同时驱动多个属性。
  • from / to:起止值,省略 from 时从当前值开始。
  • duration:毫秒。
  • easing.type:缓动曲线,常用 Linear、InQuad、OutCubic、OutBack、OutBounce。
  • loops:循环次数,Animation.Infinite 表示无限循环。

6.2 动画控制方法

qml 复制代码
moveAnim.start()
moveAnim.stop()
moveAnim.pause()
moveAnim.resume()
moveAnim.complete()   // 直接跳到终点

配合 onFinished 信号可以做链式动画:

qml 复制代码
NumberAnimation {
    id: anim1
    target: ball; property: "x"; to: 300; duration: 500
    onFinished: anim2.start()
}
NumberAnimation {
    id: anim2
    target: ball; property: "y"; to: 200; duration: 500
}

6.3 其他常用动画

类型 作用 典型属性
NumberAnimation 数值属性 x、y、width、opacity
ColorAnimation 颜色属性 color、borderColor
RotationAnimation 旋转 rotation、angle
PropertyAnimation 通用属性(含非数值) 任意可动画属性
SequentialAnimation 串行组合 内部嵌套多个动画
ParallelAnimation 并行组合 内部嵌套多个动画
6.3.1 动画元素选型
动画类型 触发场景 能否嵌套 是否支持 loops 典型误用
NumberAnimation 驱动 x、y、width、height、opacity、rotation 等数值属性变化 可以,作为 SequentialAnimation / ParallelAnimation 的子元素 支持,可设 loops: Animation.Infinite 用 property 写多个属性名(应改用 properties: "x,y");对非数值属性使用导致无效
ColorAnimation color、borderColor、backgroundColor 等颜色属性渐变 可以,可嵌套在组合动画中 支持 对非颜色属性使用;忘记颜色值需为可插值的 "#RRGGBB" 格式
RotationAnimation rotation、angle 等旋转属性,需要指定旋转方向时 可以,可嵌套 支持 用 NumberAnimation 代替导致旋转方向不可控;未设置 direction 时跨 0/360 度出现反向旋转
PropertyAnimation 通用属性动画,包括非数值类型(如 point、size、rect) 可以,可嵌套 支持 本可用 NumberAnimation 却一律用 PropertyAnimation,牺牲了类型检查与可读性
SequentialAnimation 多个动画按顺序依次播放,如先移动再淡出 可以,内部可嵌套任意动画,也可再嵌套组合动画 支持,作用于整个序列 把本应并行的动画塞进序列,导致总时长翻倍;子动画各自设置 loops 造成逻辑混乱
ParallelAnimation 多个动画同时播放,如边移动边旋转边变色 可以,内部可嵌套任意动画 支持,作用于整个并行组 子动画 duration 不一致时误以为整体同步结束;与 Behavior 叠加导致同一属性被重复驱动

选型口诀可以记成四句话:数值属性找 Number,颜色渐变找 Color,旋转方向找 Rotation,拿不准类型就用 Property。 需要"先做什么再做什么"时用 SequentialAnimation 串起来,需要"同时做什么"时用 ParallelAnimation 并起来,两者还可以互相嵌套表达复杂节奏。loops 只写在真正需要循环的那一层 ,写在组合动画上比写在子动画上更可控。最后别忘了 Behavior 与 Transition 是"触发器",动画元素是"执行者",先确定由谁触发,再决定用哪种动画元素,选型就不会乱。

6.4 组合动画示例

qml 复制代码
ParallelAnimation {
    id: combo
    NumberAnimation { target: ball; property: "x"; to: 300; duration: 600 }
    NumberAnimation { target: ball; property: "opacity"; to: 0.3; duration: 600 }
    RotationAnimation { target: ball; property: "rotation"; to: 360; duration: 600 }
}

三个动画同时进行,形成"边移动边淡出边旋转"的效果。

7. 模型视图总览

QML 的模型视图体系遵循 MVC 思想,但更强调"数据驱动 UI":

flowchart LR M[Model 数据] --> D[Delegate 渲染模板] D --> V[View 视图容器] M --> DM[DelegateModel 分组/过滤/排序] DM --> V
  • ListModel :最常用的内存模型,用 ListElement 声明数据。
  • DelegateModel:在 ListModel 之上做分组(groups)、过滤(filterOnGroup)、排序(itemsIndex),适合复杂列表。
  • TableView :二维表格视图,配合 TableModel 或自定义模型使用。

8. ListModel

8.1 声明式定义

qml 复制代码
ListModel {
    id: fruitModel
    ListElement { name: "苹果"; price: 5.5; stock: 100 }
    ListElement { name: "香蕉"; price: 3.2; stock: 200 }
    ListElement { name: "橙子"; price: 4.8; stock: 150 }
}

8.2 动态增删改查

qml 复制代码
// 追加
fruitModel.append({ "name": "葡萄", "price": 8.0, "stock": 80 })

// 插入
fruitModel.insert(0, { "name": "西瓜", "price": 2.5, "stock": 50 })

// 修改
fruitModel.setProperty(1, "price", 3.5)

// 读取
console.log(fruitModel.get(0).name)

// 删除
fruitModel.remove(2)

// 清空
fruitModel.clear()

注意:append 传入的是 JS 对象,字段名必须与已有 ListElement 的 role 一致,否则新字段不会出现在 delegate 中。

8.3 与ListView配合

qml 复制代码
ListView {
    width: 300; height: 400
    model: fruitModel
    spacing: 4

    delegate: Rectangle {
        width: ListView.view.width
        height: 60
        color: index % 2 === 0 ? "#ffffff" : "#f0f0f0"

        Row {
            anchors.verticalCenter: parent.verticalCenter
            anchors.left: parent.left
            anchors.leftMargin: 12
            spacing: 16
            Text { text: name; font.pixelSize: 16 }
            Text { text: "¥" + price.toFixed(1); color: "#e94b3c" }
            Text { text: "库存 " + stock; color: "#888" }
        }

        MouseArea {
            anchors.fill: parent
            onClicked: fruitModel.setProperty(index, "stock", stock - 1)
        }
    }
}

delegate 中可以直接访问 role 名(name、price、stock)以及 index。修改模型后视图会自动刷新,无需手动调用更新。

8.4 性能提示

  • 大量数据时避免在 delegate 中使用复杂绑定和 console.log。
  • 固定高度的 delegate 显式设置 height,避免布局计算开销。
  • 超过几千条数据时考虑 ObjectModel 或 C++ 模型(QAbstractListModel)。

9. DelegateModel

9.1 作用

ListModel 只能"原样展示"。当需要"按分类分组显示""只显示未完成的待办""按价格排序"时,DelegateModel 提供了 groups、filterOnGroup、itemsIndex 等能力,且不修改原始数据。

9.2 分组示例

qml 复制代码
ListModel {
    id: taskModel
    ListElement { title: "写周报"; category: "work"; done: false }
    ListElement { title: "买牛奶"; category: "life"; done: false }
    ListElement { title: "修复 Bug"; category: "work"; done: true }
    ListElement { title: "健身"; category: "life"; done: false }
}

DelegateModel {
    id: taskDelegateModel
    model: taskModel

    groups: [
        DelegateModelGroup { name: "work"; includeByDefault: false },
        DelegateModelGroup { name: "life"; includeByDefault: false }
    ]

    filterOnGroup: "work"

    delegate: Rectangle {
        width: 300; height: 50
        color: done ? "#e0e0e0" : "#ffffff"
        Text {
            anchors.centerIn: parent
            text: title
            font.strikeout: done
        }
    }

    Component.onCompleted: {
        for (var i = 0; i < taskModel.count; i++) {
            var item = taskModel.get(i)
            taskDelegateModel.items.get(i).inGroups = item.category
        }
    }
}

通过 filterOnGroup 切换 "work" / "life",即可实现分类筛选,原始 ListModel 不受影响。

9.3 排序

qml 复制代码
DelegateModel {
    id: sortedModel
    model: fruitModel
    delegate: Text { text: name + " ¥" + price }

    function sortByPrice() {
        items.setGroups(0, items.count, "items")
        // 简化示例:借助 JS 排序后重建索引
        var arr = []
        for (var i = 0; i < items.count; i++) arr.push(items.get(i))
        arr.sort(function(a, b) { return a.model.price - b.model.price })
        for (var j = 0; j < arr.length; j++) items.move(j, arr[j].itemsIndex)
    }
}

实际项目中排序逻辑较复杂时,更推荐在 C++ 侧用 QSortFilterProxyModel 处理,QML 只负责展示。

9.4 使用建议

  • 分组数量少、逻辑简单 → DelegateModel 足够。
  • 需要多级分组、动态增删组 → 考虑 C++ 代理模型。
  • DelegateModel 的 items 是只读视图,修改数据仍要回到原始 model。

10. TableView

10.1 基本结构

TableView 位于 QtQuick.Controls 中(Qt 5.12+ 提供 TableView,Qt 6 中为 TableView + TableModel)。它需要列定义和行数据。

qml 复制代码
import QtQuick 2.15
import QtQuick.Controls 2.15
import Qt.labs.qmlmodels 1.0

ApplicationWindow {
    visible: true
    width: 600; height: 400

    TableView {
        anchors.fill: parent
        columnSpacing: 1
        rowSpacing: 1
        clip: true

        model: TableModel {
            TableModelColumn { display: "name" }
            TableModelColumn { display: "price" }
            TableModelColumn { display: "stock" }

            rows: [
                { "name": "苹果", "price": 5.5, "stock": 100 },
                { "name": "香蕉", "price": 3.2, "stock": 200 },
                { "name": "橙子", "price": 4.8, "stock": 150 }
            ]
        }

        delegate: Rectangle {
            implicitWidth: 180
            implicitHeight: 40
            color: row % 2 === 0 ? "#ffffff" : "#f5f5f5"
            border.color: "#e0e0e0"

            Text {
                anchors.centerIn: parent
                text: display
            }
        }
    }
}

10.2 关键点解析

  • TableModelColumn :声明每一列对应的字段,display 是列名,delegate 中通过 display 取值。
  • delegate 中的 row / column:可直接访问当前行列索引,用于斑马纹、条件样式。
  • implicitWidth / implicitHeight:TableView 依赖 delegate 的隐式尺寸计算列宽行高,必须设置。
  • clip: true:内容超出时裁剪,避免溢出。

10.3 自定义列宽与表头

qml 复制代码
TableView {
    id: table
    anchors.fill: parent
    columnWidthProvider: function (column) {
        return column === 0 ? 200 : 120
    }
    rowHeightProvider: function (row) { return 40 }
    // ...
}

Row {
    anchors.top: parent.top
    Repeater {
        model: ["名称", "价格", "库存"]
        Rectangle {
            width: index === 0 ? 200 : 120
            height: 36
            color: "#4a90d9"
            Text {
                anchors.centerIn: parent
                text: modelData
                color: "white"
            }
        }
    }
}

10.4 与ListModel结合

TableView 也可以直接使用 ListModel,此时 delegate 中通过 role 名取值:

qml 复制代码
TableView {
    anchors.fill: parent
    model: fruitModel
    delegate: Rectangle {
        implicitWidth: 150
        implicitHeight: 40
        Text { anchors.centerIn: parent; text: name + " / " + price }
    }
}

但这种方式无法区分列,通常只适合单列展示。多列场景建议使用 TableModel 或 C++ 的 QAbstractTableModel。

10.5 常见坑

  • 不显示内容 :忘记设置 delegate 的 implicitWidth/implicitHeight,导致尺寸为 0。
  • 滚动卡顿 :delegate 过于复杂,或未开启 reuseItems(Qt 6 默认开启)。
  • 列宽不生效 :同时设置了 columnWidthProvider 和 delegate 的 width,后者会覆盖前者。
  • Qt 5 与 Qt 6 差异 :Qt 6 中 TableView 的 API 有调整,迁移时注意 Qt.labs.qmlmodels 的导入。

11. 综合实战

把前面知识串起来:用 ListModel 存任务,用 State + Transition 实现侧边栏展开收起,用 Behavior 让卡片高度平滑变化实现带动画任务看板

qml 复制代码
import QtQuick 2.15
import QtQuick.Controls 2.15

ApplicationWindow {
    visible: true
    width: 800; height: 500
    title: "任务看板"

    ListModel {
        id: tasks
        ListElement { title: "需求评审"; done: false }
        ListElement { title: "接口联调"; done: true }
        ListElement { title: "编写文档"; done: false }
    }

    Rectangle {
        id: sidebar
        width: 200; height: parent.height
        color: "#2c3e50"

        states: State {
            name: "collapsed"
            PropertyChanges { target: sidebar; width: 60 }
        }

        transitions: Transition {
            NumberAnimation { properties: "width"; duration: 300; easing.type: Easing.InOutQuad }
        }

        Text {
            anchors.centerIn: parent
            text: sidebar.state === "collapsed" ? ">>" : "任务列表"
            color: "white"
        }

        MouseArea {
            anchors.fill: parent
            onClicked: sidebar.state = (sidebar.state === "collapsed" ? "" : "collapsed")
        }
    }

    ListView {
        anchors.left: sidebar.right
        anchors.right: parent.right
        anchors.top: parent.top
        anchors.bottom: parent.bottom
        anchors.margins: 16
        model: tasks
        spacing: 8

        delegate: Rectangle {
            width: ListView.view.width
            height: done ? 40 : 70
            color: done ? "#ecf0f1" : "#ffffff"
            border.color: "#dcdcdc"
            radius: 6

            Behavior on height {
                NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
            }

            Text {
                anchors.verticalCenter: parent.verticalCenter
                anchors.left: parent.left
                anchors.leftMargin: 12
                text: title
                font.strikeout: done
                font.pixelSize: 16
            }

            MouseArea {
                anchors.fill: parent
                onClicked: tasks.setProperty(index, "done", !done)
            }
        }
    }
}

这个例子覆盖了:State 描述侧边栏两种宽度、Transition 平滑切换、Behavior 让卡片高度随完成状态动画变化、ListModel 驱动列表、delegate 中访问 role 与 index。

12. 总结与学习建议

  • 先分清职责:State 描述形态,Transition 描述状态间过渡,Behavior 描述单属性自动动画,Animation 描述具体曲线。混淆它们是新手最常见的错误。
  • 模型视图先跑通再优化:ListModel + ListView 能覆盖 80% 场景;需要分组过滤再上 DelegateModel;二维数据用 TableModel + TableView。
  • 性能意识要提前:delegate 保持轻量,显式设置尺寸,大数据量交给 C++ 模型。
  • 多读官方示例 :Qt 安装目录下的 examples/quick 中有大量可运行 demo,比看文档更直观。

建议按本文顺序动手实现每个示例,再尝试把"任务看板"扩展成支持分组筛选(DelegateModel)和表格统计(TableView)的完整应用,这样两块进阶知识就能真正串起来。

相关推荐
qeen872 小时前
【Linux】动静态库的制作使用及ELF文件的宏观理解
linux·运维·库
xie881018022 小时前
正则里的点号不匹配换行:多行模式的开关
运维·服务器·测试工具
是jin奥2 小时前
Claude Code + GitHub 自动化工作流整理
运维·自动化·github
wefg12 小时前
【Docker】基本指令
运维·docker·容器
日常筹谋记3 小时前
数据中心逆变设备运维|通讯对接、动环接入与多系统联动方案科普
运维·网络
强哥聊算力3 小时前
《算力江湖·Intel帝国往事》第三集:安迪·格鲁夫的铁血时代:Intel为什么能把对手逼到墙角?
运维·人工智能·科技·算法·ai·gpu算力
正在走向自律3 小时前
Linux 服务器部署 DeepSeek 大模型完全指南(2026 版)
linux·运维·服务器·deepseek·部署deepseek·安装ollama·配置api服务
mosaicwang3 小时前
使用XSHELL的ssh隧道/代理连接服务器
运维·xshell·ssh隧道
JoyCong19983 小时前
ToDesk鸿蒙PC端又焕新!远控更稳、操作更准,跨端办公如本地
运维·华为·智能手机·harmonyos·远程工作