目录
- 一、为什么需要系统控制
- 二、开发环境与版本说明
- 三、原理分析:三态模型与六种方法
- 3.1 三个核心属性
- 3.2 六种方法的行为差异
- 3.3 empty 属性的实用价值
- 四、代码实现:Concept_SystemControl.qml 逐段解析
- 4.1 粒子特效层
- 4.2 控制按钮面板
- 4.3 状态指示灯
- 4.4 页面容器:BaseRect 与弹窗加载
- 4.5 Empty 状态演示弹窗:EmptyStatePopup.qml
- 五、运行效果
- 六、边界条件说明
- 6.1 方法调用的合法组合
- 6.2 empty 属性的时机
- 6.3 性能边界
- 七、总结与下篇预告
一、为什么需要系统控制
上一篇我们建立了 Qt Quick 粒子系统的四层架构认知——ParticleSystem 容器、Emitter 发射器、ParticlePainter 渲染器、Affector 影响器,并通过 Concept_ParticleSystem.qml 跑通了第一个粒子效果。但那个示例中粒子从诞生到消亡全自动运行,开发者无法干预。
实际项目中很少有"开了就不管"的场景——页面切换时粒子还在跑,GPU 白白消耗;用户点击暂停按钮,粒子应该冻结在原地;爆炸效果播完后,需要清除残余粒子重新来过。
这些需求都指向 ParticleSystem 的状态管理能力:它不是一根"开/关"的拨杆,而是一台有完整控制面板的机器。本文的目标是彻底理解这台控制面板的每一个按钮——running/paused/empty三态模型,以及start()/stop()/pause()/resume()/reset()/restart()六种方法的行为差异。
二、开发环境与版本说明
本文所有代码基于以下环境验证(验证日期:2026-06-08):
- Qt 版本:6.8.2(最低要求 Qt 6.5,参见 CMakeLists.txt 中
qt_standard_project_setup(REQUIRES 6.5)) - 编译器:MinGW 64-bit
- 操作系统:Windows 11
- 构建工具:CMake 3.29
三、原理分析:三态模型与六种方法
3.1 三个核心属性
ParticleSystem 有三个核心属性来反映当前状态。其中running和paused是可读写的控制属性(可以直接绑定值或调用方法修改),empty是只读的状态属性:
| 属性 | 类型 | 读写 | 含义 |
|---|---|---|---|
| running | bool | 读写 | 系统是否正在运行(发射新粒子 + 更新已有粒子) |
| paused | bool | 读写 | 系统是否暂停(粒子冻结在当前位置,不发射、不更新) |
| empty | bool | 只读 | 是否没有活跃粒子(所有已发射的粒子都已消亡) |
这三个属性的组合构成了粒子系统的状态空间。用一张状态图来表示:
三个状态的含义:
| 状态 | running | paused | 粒子行为 |
|---|---|---|---|
| Stopped | false | false | 冻结在原地,下次 start 时清除 |
| Running | true | false | 持续发射 + 更新 |
| Paused | true | true | 冻结,不发射不更新 |
注意一个容易混淆的点:stop()后running变为false,粒子冻结在原地——既不会继续运动,也不会立即消失。下次调用start()时,这些冻结的粒子会被立即清除。而reset()在 Running 状态下调用时,粒子被清除后系统仍在 Running 状态并继续发射——与restart()效果相同。
3.2 六种方法的行为差异
这是本文最核心的内容。六种方法看似简单,但行为差异微妙,选错方法会导致意料之外的效果:
| 方法 | running | paused | 已有粒子 | 新粒子 | 典型场景 |
|---|---|---|---|---|---|
start() | → true | → false | 清除旧粒子 | 开始发射 | 首次启动、从停止恢复 |
stop() | → false | → false | 冻结在原地 | 停止发射 | 离开页面(下次 start 时清除),Paused 下也可调用 |
pause() | 不变 | → true | 冻结在原地 | 停止发射 | 暂停动画,保留状态 |
resume() | 不变 | → false | 恢复运动 | 恢复发射 | 从暂停恢复 |
reset() | 不变 | 不变 | 立即清除 | 继续发射 | Running 状态下清除并重新开始(同 restart)。⚠️ 仅在 running=true 且 paused=false 时有效 |
restart() | → true | → false | 立即清除(无闪烁) | 重新发射 | 重新开始效果,任何状态可调用 |
关键区别用三组对比来说明:
stop()vsreset():stop()后系统进入 Stopped 状态,粒子冻结在原地,下次start()时才清除。reset()后系统仍在 Running 状态,粒子被清除并立即重新发射——在 Running 状态下reset()和restart()效果相同。
pause()vsstop():两者都会让粒子冻结,但状态不同——pause()后running仍为true,resume()可直接恢复;stop()后running为false,需要start()恢复,且会清除旧粒子。
restart()vsreset():在 Running 状态下两者效果相同——清除粒子并重新发射。区别在于restart()可以在任何状态下调用(Stopped / Running / Paused),而reset()仅在 Running 状态下有效。因此restart()的适用范围更广。
3.3 empty 属性的实用价值
empty属性在代码中经常被忽略,但它在效果编排中非常有用。项目中EmptyStatePopup.qml实现了一个完整的 empty 状态演示弹窗,核心逻辑如下:
ParticleSystem { id: particleSys anchors.fill: parent running: popup.visible Emitter { id: burstEmitter anchors.centerIn: parent emitRate: 0 // 默认不发射,仅通过 burst() 触发 lifeSpan: 1000 size: 14 velocity: AngleDirection { angle: 0 angleVariation: 360 magnitude: 100 magnitudeVariation: 40 } } } // burst(100) 一次性发射 100 个粒子 Button { text: "burst(100)" onClicked: { particleSys.reset() particleSys.start() burstEmitter.burst(100) } }这个模式的核心逻辑是:emitRate: 0使 Emitter 默认不发射粒子,burst(100)一次性发射 100 个粒子后不再有新粒子产生。当所有粒子自然消亡后empty变为true,系统自动进入 Stopped 状态(running变为false)——这是框架的内建行为,不是代码主动调用stop()。开发者可以通过监听empty状态来判断效果是否播放完毕。Emitter 是 ParticleSystem 的子组件,burst()发射的粒子归属于particleSys,所以通过读取particleSys.empty就能知道所有粒子是否已消亡。完整的 empty 演示弹窗实现见下文 4.5 节。
四、代码实现:Concept_SystemControl.qml 逐段解析
项目中Concept_SystemControl.qml是一个交互式的状态控制演示页面。它做了两件事:上方展示粒子效果,下方提供控制按钮和状态指示灯。整体布局采用RowLayout+ColumnLayout,左侧显示状态,右侧放置按钮。
说明:为了便于理解和分析,以下代码进行了简化展示,完整代码见文章结尾的【资源下载】。
4.1 粒子特效层
ParticleSystem { id: particleSystem anchors.fill: parent running: root.isCurrentItem ImageParticle { source: "qrc:/images/star.png" color: "#4ECDC4" colorVariation: 0.2 } Emitter { anchors.centerIn: parent emitRate: 80 lifeSpan: 2000 size: 12 velocity: AngleDirection { angle: 0 angleVariation: 360 magnitude: 80 } } }这段代码有几个值得注意的设计:
running: root.isCurrentItem——这是整个项目的核心模式。root继承自BaseRect,它通过StackLayout.isCurrentItem自动感知当前页面是否被选中。当用户切换到其他页面时,isCurrentItem变为false,粒子系统自动停止;切换回来时自动恢复。这个模式让开发者无需手动管理粒子系统的生命周期。验证方式:切换到其他页面时,观察状态指示灯running变为false(红色),粒子停止发射;切换回来时恢复为true(绿色)。
emitRate: 80——每秒发射 80 个粒子,配合lifeSpan: 2000(2 秒),意味着屏幕上大约有 160 个活跃粒子同时存在。这个数量对 ImageParticle 来说完全没有性能压力。
AngleDirection的 360 度扩散——angle: 0配合angleVariation: 360,粒子向四面八方均匀扩散。magnitude: 80控制扩散速度为每秒 80 像素。
4.2 控制按钮面板
控制面板采用RowLayout+ColumnLayout嵌套,左侧显示状态指示灯,右侧放置控制按钮:
Rectangle { Layout.fillWidth: true Layout.preferredHeight: 100 Layout.margins: 10 color: "#333" radius: 8 RowLayout { anchors.fill: parent anchors.margins: 10 spacing: 8 // 左侧:状态指示灯 ColumnLayout { spacing: 8 Text { /* running 指示灯 */ } Text { /* paused 指示灯 */ } Text { /* empty 指示灯 */ } } // 右侧:控制按钮 RowLayout { spacing: 10 Item { Layout.fillWidth: true } // 左侧填充,按钮居右 Button { /* start/stop */ } Button { /* pause/resume */ } Button { /* reset */ } Button { /* restart */ } Item { Layout.fillWidth: true } // 右侧填充 Button { /* Empty 状态演示 */ } } } }五个按钮共享同一套自定义样式:background用深灰底色 + 圆角,contentItem用白色文字居中。按下时颜色变深(#555),悬停时稍亮(#666),默认状态为 #444。差异只在text、onClicked和enabled上。下面分别讲解每个按钮的逻辑。
start/stop 按钮的动态文案:按钮文字根据particleSystem.running动态切换——系统运行时显示"stop",停止时显示"start"。这是一个常见的交互设计模式:用状态驱动 UI 文案,用户一眼就知道点击后会发生什么。
pause/resume 按钮的启用条件:
Button { text: particleSystem.paused ? "resume" : "pause" enabled: particleSystem.running opacity: enabled ? 1.0 : 0.5 onClicked: { if (particleSystem.paused) { particleSystem.resume() } else { particleSystem.pause() } } }enabled: particleSystem.running是关键——只有系统在运行时才能暂停。如果系统已停止(running为false),暂停按钮变灰(opacity: 0.5)。这避免了用户在停止状态下误点暂停导致的困惑。
reset 和 restart 按钮:这两个按钮始终可用,代码结构与 start/stop 按钮相同,区别只在onClicked逻辑:
Button { text: "reset" onClicked: particleSystem.reset() } Button { text: "restart" onClicked: particleSystem.restart() }reset()和restart()都是一行调用,不需要条件判断。在 Running 状态下两者效果相同——清除粒子并重新发射。区别是restart()在任何状态(Stopped / Running / Paused)下都可调用,而reset()仅在 Running 状态下有效。
Empty 状态演示按钮:最后一个按钮以蓝色高亮显示,点击后打开EmptyStatePopup弹窗。弹窗通过Loader按需加载,关闭后自动销毁,不占用额外资源。弹窗的具体实现见 4.5 节。
4.3 状态指示灯
三个Text组件通过ColumnLayout纵向排列在控制面板左侧,实时显示running/paused/empty的状态:
ColumnLayout { spacing: 8 Text { color: particleSystem.running ? "#2ECC71" : "#E74C3C" text: "running: " + particleSystem.running font.pixelSize: 11 font.bold: true } Text { color: particleSystem.paused ? "#F39C12" : "#888" text: "paused: " + particleSystem.paused font.pixelSize: 11 font.bold: true } Text { color: particleSystem.empty ? "#3498DB" : "#888" text: "empty: " + particleSystem.empty font.pixelSize: 11 font.bold: true } }颜色编码采用了语义化的选择:running为绿色(#2ECC71)表示健康运行,红色(#E74C3C)表示停止;paused为橙色(#F39C12)表示暂停状态;empty为蓝色(#3498DB)表示无活跃粒子。
需要特别注意empty指示灯的行为:正常运行时它始终是灰色(false),因为emitRate: 80持续发射新粒子,屏幕上始终有活跃粒子存在。只有在以下情况下它才会短暂变蓝:
stop()后:粒子冻结,empty不会立即变蓝;下次start()时清除旧粒子后变蓝(随即新粒子发射又变回灰色)reset()时:粒子清除的瞬间短暂变蓝,随即新粒子发射又变回灰色(同 restart)restart()时:粒子清除的瞬间短暂变蓝,随即新粒子发射又变回灰色
在演示中可以观察到:点击 restart 或 reset 时蓝色闪一下就消失(因为清除后立即重新发射),而点击 stop 后粒子冻结、empty 不变蓝,再次点击 start 时蓝色闪一下随即恢复灰色。
4.4 页面容器:BaseRect 与弹窗加载
所有示例页面都继承自BaseRect,它提供了两个关键能力:
Rectangle { id: root Layout.fillWidth: true Layout.fillHeight: true color: "#1a1a1a" default property alias content: contentColumn.children property bool isCurrentItem: root.StackLayout ? root.StackLayout.isCurrentItem : false ColumnLayout { id: contentColumn anchors.fill: parent spacing: 0 } }isCurrentItem属性:通过root.StackLayout.isCurrentItem自动获取当前页面是否被 StackLayout 选中。当 StackLayout 的currentIndex变化时,这个属性自动更新,粒子系统的running绑定随之联动。
default property alias content:将contentColumn.children设为默认属性,这样子页面可以直接在BaseRect {}内部声明子元素,无需显式写ColumnLayout。
Popup的父级陷阱:BaseRect的default property会把所有子项塞进ColumnLayout,而Popup不能是ColumnLayout的子项(会导致Cannot assign object to list property错误)。解决方案是用Loader按需加载弹窗,并显式指定parent为窗口的contentItem:
Loader { id: emptyPopupLoader active: false sourceComponent: EmptyStatePopup { parent: root.Window.window.contentItem Component.onCompleted: open() onClosed: emptyPopupLoader.active = false } } function openEmptyPopup() { emptyPopupLoader.active = true }active: false表示 Loader 初始不加载组件;调用openEmptyPopup()时设为true,触发组件创建并自动open();弹窗关闭后onClosed将active设回false,组件销毁,不留残余。
4.5 Empty 状态演示弹窗:EmptyStatePopup.qml
EmptyStatePopup.qml是一个独立的Popup组件,专门演示empty属性的行为。
粒子系统配置:
ParticleSystem { id: particleSys anchors.fill: parent running: popup.visible ImageParticle { source: "qrc:/images/star.png" color: "#FFE66D" colorVariation: 0.3 } Emitter { id: burstEmitter anchors.centerIn: parent emitRate: 0 lifeSpan: 1000 size: 14 sizeVariation: 8 velocity: AngleDirection { angle: 0 angleVariation: 360 magnitude: 100 magnitudeVariation: 40 } } }emitRate: 0是关键——Emitter 默认不发射粒子,只通过burst()手动触发。lifeSpan: 1000表示每个粒子存活 1 秒,所以burst(100)后约 1 秒所有粒子消亡,empty变为true。
burst 按钮:
Button { text: "burst(100)" onClicked: { particleSys.reset() particleSys.start() burstEmitter.burst(100) } }按钮的onClicked依次执行三步:reset()清除残留粒子 →start()启动系统 →burst(100)发射 100 个新粒子。这三步保证无论当前系统处于什么状态(运行中、已停止、已暂停),点击后都能重新开始演示。running: popup.visible保证弹窗打开时系统运行、关闭时自动停止。所有粒子自然消亡后,系统自动进入 Stopped 状态(框架内建行为)。
界面组成:弹窗内包含粒子演示区(深色背景 + 居中状态文字)、running/empty两个状态指示灯、一个burst(100)操作按钮,以及底部的原理说明。中心状态文字根据empty属性动态切换——粒子存活时显示绿色 “emitting…”,全部消亡后显示蓝色 “empty”。
五、运行效果
运行项目后,点击左侧导航栏的「系统控制」进入本示例页面。
初始状态:粒子系统自动运行,星形粒子从中心向四面八方扩散,状态指示灯显示running: true(绿色),empty为灰色(false,因为始终有活跃粒子)。
点击 stop:粒子系统停止发射,已有粒子冻结在原地。观察状态指示灯:running变为false(红色),而empty保持灰色不变(粒子冻结在原地,未消亡)。再次点击 start 时,冻结的粒子会被立即清除并重新开始发射。
点击 pause:所有粒子冻结在当前位置,不发射新粒子。观察状态指示灯:paused变为true(橙色),running保持true(绿色)。此时点击 resume 恢复运动。
点击 reset:粒子瞬间清除后重新发射。观察状态指示灯:empty短暂变蓝随即恢复灰色(粒子清除后立即重新发射),running和paused不变。在 Running 状态下效果与 restart 相同。
点击 restart:粒子瞬间清除后重新发射(同一帧完成,无闪烁)。观察状态指示灯:empty短暂变蓝随即恢复灰色,running变为true,paused变为false。
点击 Empty 状态演示:打开演示弹窗,点击burst(100)发射 100 个粒子。观察弹窗中的状态指示灯:粒子存活时empty为灰色(false),中心显示绿色 “emitting…”;约 1 秒后所有粒子消亡,empty变为蓝色(true),中心切换为蓝色 “empty”,系统自动进入 Stopped 状态。这是理解empty属性最直观的方式。
运行截图说明:上方区域展示粒子效果,下方深色面板左侧显示三个状态指示灯,右侧提供四个控制按钮(start/stop、pause/resume、reset、restart)和一个 Empty 状态演示按钮。通过操作按钮可以直观感受六种方法的行为差异。
六、边界条件说明
6.1 方法调用的合法组合
不是所有方法都能随意组合。以下是实际测试中观察到的行为:
| 当前状态 | 可调用的方法 | 不可调用 / 无效的方法 |
|---|---|---|
| Running(运行中) | stop / pause / reset / restart(reset 和 restart 效果相同) | — |
| Stopped(已停止) | start / restart | pause / resume(running 为 false,无意义) / reset(需 running=true 且 paused=false) |
| Paused(已暂停) | resume / restart(进入 Running) / stop(进入 Stopped,粒子仍冻结) | reset(paused=true,不满足条件) |
代码中的pause按钮通过enabled: particleSystem.running在 UI 层面规避了非法调用——系统停止时按钮变灰,用户无法点击。
6.2 empty 属性的时机
empty在不同操作下的变化时机不同:
| 操作 | empty 变化 | 原因 |
|---|---|---|
stop() | 不变(粒子冻结,未消亡) | 粒子冻结在原地,需等下次start()或手动reset() |
reset() | 短暂变 true 后立即变 false | 粒子清除后重新发射(同 restart) |
restart() | 短暂变 true 后立即变 false | 清除后立即重新发射 |
| 正常运行 | 始终 false | emitRate 持续发射,始终有活跃粒子 |
在主页面中,lifeSpan为 2000ms,所以stop()后约 2 秒empty才变true。在 Empty 状态演示弹窗中,lifeSpan为 1000ms,burst(100)后约 1 秒所有粒子消亡、empty变true,系统自动进入 Stopped 状态——可以观察弹窗中的empty指示灯和中心状态文字来验证这个时机。
6.3 性能边界
- 页面不可见时务必停止粒子系统:
running: root.isCurrentItem模式自动处理了这一点 - 短暂隐藏用
pause():比如对话框弹出时,pause()比stop()更合适,因为恢复时不需要重新发射,粒子从冻结位置无缝继续 - 需要彻底释放资源时用
stop():stop()后粒子冻结但仍占用 GPU,需要等下次start()才清除;reset()/restart()会立即清除并重新发射 emitRate与稳态粒子数:连续发射场景下,稳态粒子数 ≈emitRate × lifeSpan / 1000。本示例中80 × 2000 / 1000 = 160个,对 ImageParticle 来说没有性能压力。注意burst()发射不适用此公式(一次性发射,非持续发射)
七、总结与下篇预告
本文详细讲解了 ParticleSystem 的三态模型和六种方法:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 离开页面 | stop() | 粒子冻结,下次 start 时清除 |
| 短暂暂停 | pause() | 粒子冻结,resume 恢复 |
| 重新开始效果 | restart() | 清除+重新发射,任何状态均可调用 |
| 重新开始效果 | reset() | Running 状态下效果同 restart |
记住选择策略:短暂停留用pause,离开页面用stop,重新来过用restart。
下一篇将深入 Emitter 的发射逻辑,讲解emitRate连续发射、burst()脉冲发射和pulse()定时脉冲三种发射模式的行为差异和适用场景。
资源下载:qml_particlesystem —— 包含完整的、可运行的代码
系列目录
- 上一篇:Qt Quick 粒子系统(一):架构总览与四层模型
- 本文:Qt Quick 粒子系统(二):系统控制与生命周期管理
- 下一篇:Qt Quick 粒子系统(三):发射器深度解析