Qt Quick 粒子系统(二):系统控制与生命周期管理

目录

    • 一、为什么需要系统控制
    • 二、开发环境与版本说明
    • 三、原理分析:三态模型与六种方法
      • [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 有三个核心属性来反映当前状态。其中 runningpaused可读写 的控制属性(可以直接绑定值或调用方法修改),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 仍为 trueresume() 可直接恢复;stop()runningfalse,需要 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。差异只在 textonClickedenabled 上。下面分别讲解每个按钮的逻辑。

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 是关键------只有系统在运行时才能暂停。如果系统已停止(runningfalse),暂停按钮变灰(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 的父级陷阱BaseRectdefault 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();弹窗关闭后 onClosedactive 设回 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 短暂变蓝随即恢复灰色(粒子清除后立即重新发射),runningpaused 不变。在 Running 状态下效果与 restart 相同。

点击 restart :粒子瞬间清除后重新发射(同一帧完成,无闪烁)。观察状态指示灯:empty 短暂变蓝随即恢复灰色,running 变为 truepaused 变为 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 秒所有粒子消亡、emptytrue,系统自动进入 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 ------ 包含完整的、可运行的代码

系列目录

相关推荐
Vertira4 小时前
如何对QT开发的软件进行打包[已解决]
开发语言·qt
大智兄5 小时前
128.配置qt(交叉)编译的路径---解决无法编译的问题
qt
Henry Zhu12310 小时前
Qt 元对象系统源码级理解
qt
读书札记202210 小时前
Qt中windeployqt.exe工具的使用:解决使用CMake创建的项目点击exe文件后系统提示0xc000007b的问题
开发语言·qt
luoyayun36111 小时前
Qt + FFmpeg 实战:实现音频格式转换功能
qt·ffmpeg·音频格式转换
Henry Zhu12312 小时前
Qt 信号槽、事件循环与线程通信源码级理解
开发语言·qt
郝学胜-神的一滴12 小时前
CMake 015:日志级别全解析
linux·开发语言·c++·qt·程序人生·软件构建·cmake
数据法师1 天前
QuickSay :基于 Qt 的轻量级快捷短语管理工具
开发语言·qt
小短腿的代码世界1 天前
行情快照与增量更新引擎:Qt在高频交易数据分发中的核心架构——你的行情推送为什么延迟了500ms?
开发语言·qt·架构
DogDaoDao1 天前
深入理解 Qt:从原理到实战的全景指南
开发语言·qt·程序员