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 ------ 包含完整的、可运行的代码

系列目录

相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner5 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz10 天前
QML Hello World 入门示例
qt
xcyxiner13 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner14 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
桥田智能16 天前
桥田智能 QT-650S:面向白车身焊装的 800kg 重载快换解决方案
开发语言·qt·系统架构
森G16 天前
75、服务器源码解析---------云视频服务项目
linux·服务器·网络·c++·qt