目录
-
- 一、为什么需要系统控制
- 二、开发环境与版本说明
- 三、原理分析:三态模型与六种方法
-
- [3.1 三个核心属性](#3.1 三个核心属性)
- [3.2 六种方法的行为差异](#3.2 六种方法的行为差异)
- [3.3 empty 属性的实用价值](#3.3 empty 属性的实用价值)
- [四、代码实现:Concept_SystemControl.qml 逐段解析](#四、代码实现:Concept_SystemControl.qml 逐段解析)
-
- [4.1 粒子特效层](#4.1 粒子特效层)
- [4.2 控制按钮面板](#4.2 控制按钮面板)
- [4.3 状态指示灯](#4.3 状态指示灯)
- [4.4 页面容器:BaseRect 与弹窗加载](#4.4 页面容器:BaseRect 与弹窗加载)
- [4.5 Empty 状态演示弹窗:EmptyStatePopup.qml](#4.5 Empty 状态演示弹窗:EmptyStatePopup.qml)
- 五、运行效果
- 六、边界条件说明
-
- [6.1 方法调用的合法组合](#6.1 方法调用的合法组合)
- [6.2 empty 属性的时机](#6.2 empty 属性的时机)
- [6.3 性能边界](#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() vs reset() :stop() 后系统进入 Stopped 状态,粒子冻结在原地,下次 start() 时才清除。reset() 后系统仍在 Running 状态,粒子被清除并立即重新发射------在 Running 状态下 reset() 和 restart() 效果相同。
pause() vs stop() :两者都会让粒子冻结,但状态不同------pause() 后 running 仍为 true,resume() 可直接恢复;stop() 后 running 为 false,需要 start() 恢复,且会清除旧粒子。
restart() vs reset() :在 Running 状态下两者效果相同------清除粒子并重新发射。区别在于 restart() 可以在任何状态下调用(Stopped / Running / Paused),而 reset() 仅在 Running 状态下有效。因此 restart() 的适用范围更广。
3.3 empty 属性的实用价值
empty 属性在代码中经常被忽略,但它在效果编排中非常有用。项目中 EmptyStatePopup.qml 实现了一个完整的 empty 状态演示弹窗,核心逻辑如下:
qml
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 粒子特效层
qml
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 嵌套,左侧显示状态指示灯,右侧放置控制按钮:
qml
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 按钮的启用条件:
qml
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 逻辑:
qml
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 的状态:
qml
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,它提供了两个关键能力:
qml
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:
qml
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 属性的行为。
粒子系统配置:
qml
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 按钮:
qml
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 粒子系统(三):发射器深度解析