第四节 QML交互、信号、动画与状态

第四节 QML交互、信号、动画与状态 本章让界面动起来、活起来。覆盖交互体系鼠标/触摸/键盘/焦点/拖拽、动画Animation/Behavior/Transition/状态机与性能注意事项。读完你应能写出流畅、可维护的交互界面。1. 交互体系总览QML 交互由三类对象协同层对象职责输入事件MouseArea / MultiPointTouchArea / Keys捕获鼠标/触摸/键盘状态focus / enabled / visible / Keys 附加属性决定谁能收到事件反馈属性变化 动画界面响应1.1 事件分发顺序重要事件到达窗口。从焦点对象focus: true 的叶子向根部冒泡——不QML 是向下的命中测试从根开始按 z 顺序 / 声明顺序找到最上层能接收事件的可视对象。MouseArea 在命中对象里从上往下z 大优先找第一个接受者消费事件。若 mouse.accepted false事件继续传递到下层。结论同级多个 MouseArea 重叠时z 大 / 后声明的先收到想阻止传递用 propagateComposedEvents: false默认或手动 mouse.accepted false。1.2 命中测试与可视性visible: false 或 opacity: 0 的对象不参与命中测试opacity 0 也不可点。enabled: false 的对象仍可见但不接收输入。父对象 enabled: false 时子对象也全部失效除非显式 enabled: true 覆盖——不能覆盖父禁用则子禁用。Rectangle { width: 200; height: 200 color: mouseArea.containsMouse ? #ffeeaa : #ffffff MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true } }2. 鼠标与触摸2.1 MouseArea 高级用法MouseArea { id: ma anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton hoverEnabled: true propagateComposedEvents: false onClicked: (mouse) { console.log(button:, mouse.button) // Qt.LeftButton 等 console.log(modifiers:, mouse.modifiers) // Qt.ControlModifier 等 console.log(pos:, mouse.x, mouse.y) } onDoubleClicked: (mouse) console.log(双击) onPressAndHold: (mouse) console.log(长按) onWheel: (wheel) { console.log(angleDelta:, wheel.angleDelta.x, wheel.angleDelta.y) console.log(pixelDelta:, wheel.pixelDelta.x, wheel.pixelDelta.y) console.log(modifiers:, wheel.modifiers) } }MouseEvent 常用字段x/y相对 MouseArea、button、buttons多键状态、modifiers、accepted。2.2 拖拽Rectangle { width: 80; height: 80 radius: 8 color: steelblue // 方案 AMouseArea.drag简单 MouseArea { anchors.fill: parent drag.target: parent drag.axis: Drag.XAndYAxis drag.minimumX: 0 drag.maximumX: 300 drag.minimumY: 0 drag.maximumY: 300 drag.threshold: 12 // 超过阈值才认为开始拖 onPressed: parent.color tomato onReleased: parent.color steelblue } // 方案 B拖拽数据交换跨容器/列表 Drag.active: dragArea.drag.active Drag.hotSpot: Qt.point(width / 2, height / 2) Drag.mimeData: { text/plain: 我是可拖拽对象 } states: State { when: dragArea.drag.active ParentChange { target: dragRoot; parent: rootOverlay } AnchorChanges { target: dragRoot; anchors.horizontalCenter: undefined; anchors.verticalCenter: undefined } } }方案 A 够用就用 A需要拖到列表/跨区域用 Drag DropAreaDropArea { anchors.fill: parent onEntered: (drag) parent.color #ffe0e0 onExited: parent.color #f0f0f0 onDropped: (drop) { console.log(received:, drop.getDataAsString(text/plain)) parent.color #e0ffe0 } }2.3 多点触控MultiPointTouchAreaMultiPointTouchArea { anchors.fill: parent maximumTouchPoints: 3 onTouchUpdated: (touchPoints) { for (var i 0; i touchPoints.length; i) { console.log(touch, i, touchPoints[i].x, touchPoints[i].y) } } }需要手势捏合缩放/旋转时自己用 PinchArea 或计算触摸点距离/角度现代 Qt 也支持 QQuickItem::touchEvent 在 C 侧实现。3. 键盘与焦点3.1 focus 焦点系统QML 的键盘事件只发给当前焦点对象focus: true。焦点规则同一作用域内只有一个对象 focus: true。父对象 focus: true 后子对象可继续 focus: true焦点下钻。窗口级 activeFocusItem 是真正拿到键盘的对象。Item { focus: true // 让本对象成为焦点需要窗口先给 Keys.onPressed: (event) { if (event.key Qt.Key_Enter || event.key Qt.Key_Return) { console.log(回车) event.accepted true // 消费事件阻止继续传播 } else if (event.key Qt.Key_Escape) { console.log(Esc) event.accepted true } else if (event.modifiers Qt.ControlModifier event.key Qt.Key_S) { console.log(CtrlS) event.accepted true } } }3.2 Keys 附加属性常用信号信号触发onPressed / onReleased按下/松开含修饰键onEnterPressed 等特定键快捷onReturnPressed、onEscapePressed、onBackPressedonUpPressed / onDownPressed / onLeftPressed / onRightPressed方向键onTabPressedTabonSpacePressed空格onDigit0Pressed … onDigit9Pressed数字键Qt5 中为 onDigit1PressedQt6 支持 onDigit0PressedonShortcutOverride拦截快捷键高级3.3 焦点示例方向键控制移动Rectangle { id: box width: 60; height: 60 radius: 8 color: dodgerblue focus: true Keys.onPressed: (event) { const step 20 switch (event.key) { case Qt.Key_Left: box.x - step; event.accepted true; break case Qt.Key_Right: box.x step; event.accepted true; break case Qt.Key_Up: box.y - step; event.accepted true; break case Qt.Key_Down: box.y step; event.accepted true; break } } Keys.onSpacePressed: box.color box.color dodgerblue ? coral : dodgerblue }3.4 输入控件与焦点TextField/TextArea 自带焦点管理focus: true 或 activeFocusOnTab 用于 Tab 键导航TextField { placeholderText: Tab 键可切换 activeFocusOnTab: true }4. 动画Animation4.1 动画类型总览类型用途NumberAnimation数值属性x/y/opacity/width…ColorAnimation颜色过渡Vector3dAnimation3D 向量PropertyAnimation通用属性动画任意可动画属性RotationAnimation旋转含方向最短路径ParallelAnimation并行多个动画SequentialAnimation串行多个动画PauseAnimation暂停ScriptAction动画中执行脚本Behavior属性变化时自动播放过渡Transition状态切换时的过渡动画4.2 基础动画写法两种方式 A独立 Animation 对象Rectangle { id: box width: 60; height: 60 color: coral NumberAnimation { id: moveAnim target: box property: x to: 250 duration: 800 easing.type: Easing.OutBounce } MouseArea { anchors.fill: parent onClicked: moveAnim.start() // 或 moveAnim.running true } }方式 B内联 NumberAnimation on 属性Rectangle { width: 60; height: 60 NumberAnimation on x { to: 250; duration: 800; easing.type: Easing.InOutQuad } }方式 B 在对象创建时自动播放可用 running: false 关闭或用 Component.onCompleted 触发。4.3 常用 easing 曲线曲线感觉Easing.Linear匀速Easing.InQuad / OutQuad慢快加速/ 快慢减速Easing.InOutQuad两头慢中间快默认 UI 舒适Easing.OutCubic / OutQuint强烈减速弹性进入感Easing.OutBack越过头一点再回弹Easing.OutBounce落地弹跳Easing.OutElastic橡皮筋式回弹Easing.InOutBack / InOutElastic复杂曲线慎用NumberAnimation on x { to: 300 duration: 1200 easing.type: Easing.OutBounce }可视化参考Qt 官方文档 Easing Curves 页有曲线图。实践中 UI 常用 OutQuad / InOutQuad物理感用 OutBack / OutBounce。4.4 并行 / 串行// 同时动 x 和 opacity ParallelAnimation { NumberAnimation { target: box; property: x; to: 300; duration: 600 } NumberAnimation { target: box; property: opacity; to: 0.2; duration: 600 } } // 先移动再变色 SequentialAnimation { NumberAnimation { target: box; property: x; to: 300; duration: 600 } PauseAnimation { duration: 200 } ColorAnimation { target: box; property: color; to: tomato; duration: 300 } }4.5 动画控制NumberAnimation { id: anim // ... running: false // 或 true 自动播放 loops: Animation.Infinite // 循环次数Infinite 无限 paused: false // 信号 onStarted: console.log(started) onStopped: console.log(stopped) onFinished: console.log(finished) }anim.start() / anim.stop() / anim.restart() / anim.pause() / anim.resume()4.6 循环示例心跳呼吸Rectangle { id: pulse width: 100; height: 100 radius: 50 color: #e63946 SequentialAnimation { running: true loops: Animation.Infinite NumberAnimation { target: pulse; property: scale to: 1.2; duration: 400 easing.type: Easing.InOutQuad } NumberAnimation { target: pulse; property: scale to: 1.0; duration: 400 easing.type: Easing.InOutQuad } } }4.7 Behavior —— 属性变化的自动动画Rectangle { width: 200; height: 100 color: #f0f0f0 Rectangle { id: dot width: 30; height: 30 radius: 15 color: dodgerblue Behavior on x { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } } Behavior on y { NumberAnimation { duration: 300; easing.type: Easing.OutQuad } } Behavior on color { ColorAnimation { duration: 200 } } MouseArea { anchors.fill: parent onClicked: { dot.x Math.random() * 150 dot.y Math.random() * 50 } } } }Behavior 的哲学不管谁改属性绑定、JS、外部都自动平滑过渡。适合风格化 UI但数据密集更新如实时图表时要禁用 Behavior否则每帧都触发动画导致卡顿。Behavior 可设置 enabled: false 临时关闭Behavior on x { enabled: !busy.value NumberAnimation { duration: 200 } }4.8 绑定 vs 动画的协作推荐模式数据用绑定驱动视觉过渡用动画修饰。例如Rectangle { x: targetX // 数据绑定 Behavior on x { NumberAnimation { duration: 200 } } // 过渡修饰 }5. 状态States与过渡Transitions5.1 States 基础状态是一组属性的快照。切换状态时被 PropertyChanges 修改的属性自动变化。Item { id: card width: 200; height: 100 Rectangle { id: bg anchors.fill: parent color: lightgray radius: 8 } states: [ State { name: active PropertyChanges { target: bg; color: steelblue; radius: 20 } PropertyChanges { target: card; width: 300 } }, State { name: hidden PropertyChanges { target: card; opacity: 0 } } ] MouseArea { anchors.fill: parent onClicked: card.state (card.state active ? : active) } }要点state: 是默认状态。PropertyChanges 只列出要改变的属性恢复默认状态时自动还原。可给 PropertyChanges 加 when 条件实现多条件状态不常用。5.2 State 最佳实践状态名语义化normal、hovered、pressed、disabled、active。状态本质是逻辑视图UI 进入某业务状态时切换。不要为临时动画开状态简单动画用 Behavior/Animation 即可。复杂组件可把 states 写进 qtquickcontrols2.conf 或自定义控件样式见 02-3。5.3 Transitions —— 状态切换动画Item { id: root width: 300; height: 200 Rectangle { id: box width: 60; height: 60 color: coral states: State { name: moved PropertyChanges { target: box; x: 200; color: steelblue } } transitions: Transition { // 过渡动画可加 from/to 限定 from: *; to: * NumberAnimation { properties: x; duration: 600; easing.type: Easing.OutQuad } ColorAnimation { duration: 400 } } MouseArea { anchors.fill: parent; onClicked: root.state root.state moved ? : moved } } }注意状态里改了哪些属性Transition 里就写对应属性的动画写错属性名动画不生效无报错坑。5.4 Transition 的进阶transitions: [ Transition { from: ; to: active NumberAnimation { properties: x,y; duration: 300; easing.type: Easing.OutQuad } }, Transition { from: active; to: NumberAnimation { properties: x,y; duration: 200; easing.type: Easing.InQuad } } ]reversible: true 可让同一 Transition 双向使用。ScriptAction 可在过渡中执行逻辑。6. 高级交互技巧6.1 长按 / 双击 / 右键组合MouseArea { anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onClicked: (mouse) { if (mouse.button Qt.RightButton) console.log(右键) else console.log(左键) } onDoubleClicked: console.log(双击) onPressAndHold: console.log(长按) }注意双击与单击会同时触发先单击后双击需要单击延迟时用 Timer 延迟判断详见下方。6.2 防抖 / 节流Timer// 防抖停止操作 300ms 后才执行 Timer { id: debounceTimer interval: 300 onTriggered: doSearch() } TextField { onTextChanged: debounceTimer.restart() } // 节流至少 500ms 执行一次 Timer { id: throttleTimer interval: 500 repeat: true running: slider.pressed onTriggered: recordValue(slider.value) }Qt.callLater()Qt6也可做下一次事件循环执行一次的去抖但它不能延迟 300ms。6.3 键盘导航循环示例Item { focus: true Keys.onTabPressed: (event) { // 自定义 Tab 导航focusIndex 循环 focusIndex (focusIndex 1) % 3 event.accepted true } }6.4 焦点恢复窗口失焦重获ApplicationWindow { onActiveChanged: { if (active) rootItem.forceActiveFocus() // 重新抢回焦点 } }6.5 触摸设备上的悬停移动端没有悬停hoverEnabled 在触摸平台可能永远 false。做悬停提示时要有触摸兜底如 pressed 时也显示提示。7. 动画与交互性能实践说明动画属性只动 transform 系x/y/opacity/scale/rotation 走 GPU 合成避免动 width/height触发布局重算减少动画中的对象数背景不透明度动画尽量放到单个元素Behavior 慎用于高频更新实时数据用直接赋值大批量动画用 Animator 系列NumberAnimator / OpacityAnimatorQtQuick 2在渲染线程运行列表滚动用 ListView 的 cacheBuffer控制预加载避免阴影/模糊动画开销大用预渲染贴图用 console.time() 排查console.time(anim); ... console.timeEnd(anim)检查帧率QT_QML_PROFILER / Creator Profiler 看 Frame Rate// 示例Animator渲染线程动画UI 线程不被占用 NumberAnimator { target: box property: opacity from: 1.0 to: 0.0 duration: 1000 running: true }⚠️ Animator 系列只能动画可渲染属性x/y/width/height/opacity/scale/rotation/color且无法 stop() 后精确回读中间值渲染线程状态。8. 综合示例卡片滑动切换结合全部知识import QtQuick Item { id: root width: 320; height: 420 property int currentIndex: 0 property var colors: [#e63946, #f4a261, #2a9d8f, #457b9d] Rectangle { id: card width: 280; height: 360 anchors.centerIn: parent radius: 16 color: colors[root.currentIndex % colors.length] clip: true Text { id: pageLabel anchors.centerIn: parent text: 第 (root.currentIndex 1) 页 font.pixelSize: 30 color: white font.bold: true } // 滑动切换模拟 MouseArea { anchors.fill: parent onPressed: (mouse) dragStartX mouse.x onReleased: (mouse) { var dx mouse.x - dragStartX if (Math.abs(dx) 40) { if (dx 0) root.currentIndex (root.currentIndex 1) % root.colors.length else root.currentIndex (root.currentIndex - 1 root.colors.length) % root.colors.length } } } } // 指示点 Row { anchors.bottom: parent.bottom anchors.horizontalCenter: parent.horizontalCenter spacing: 6 Repeater { model: root.colors.length Rectangle { width: 8; height: 8 radius: 4 color: index root.currentIndex ? white : #888888 Behavior on color { ColorAnimation { duration: 150 } } } } } // 状态过渡卡片颜色渐变 states: State { name: changed PropertyChanges { target: card; rotation: root.currentIndex % 2 0 ? 0 : 3 } } transitions: Transition { NumberAnimation { properties: rotation; duration: 300; easing.type: Easing.OutQuad } } // 切换时重新回到旋转 0 onCurrentIndexChanged: state }9. 本章小结交互MouseArea鼠标/触摸、Keys键盘、focus 体系、Drag/DropArea拖拽。事件按 z 顺序向下命中accepted 控制消费。动画NumberAnimation 等基础动画、Behavior属性过渡、States/Transition状态机。性能动 transform、慎 Behavior、用 Animator、列表虚拟化。10. 自测题opacity: 0 的对象还能被点击吗Behavior on x 和 NumberAnimation on x 有何区别状态切换时 Transition 不生效可能是什么原因为什么高频实时更新数据时不要用 Behavior如何实现单击延迟 300ms 区分单击/双击