Qt Quick 粒子系统(一):架构总览与四层模型
目录
- 一、为什么需要粒子系统
- 二、开发环境与版本说明
- 三、原理分析:四层架构
- 3.1 ParticleSystem——容器与调度器
- 3.2 Emitter——发射器
- 3.3 ParticlePainter——渲染器
- 3.4 Affector——影响器
- 3.5 粒子的完整生命周期
- 四、代码实现:Concept_ParticleSystem.qml 逐段解析
- 4.1 完整代码
- 4.2 页面结构:BaseRect + PageTitle
- 4.3 粒子特效层
- 4.4 信息卡片层:ListModel + Repeater
- 4.5 入场动画:延迟渐入效果
- 4.6 动画生命周期管理
- 4.7 文字渲染
- 4.8 项目骨架:导航与页面管理
- 4.9 通用组件复用
- 五、运行效果
- 六、常见问题与边界条件
- 七、总结与下篇预告
一、为什么需要粒子系统
在 QML 开发中,我们经常遇到这样的需求:游戏中的爆炸火花、天气应用的雪花飘落、音乐播放器的频谱背景、按钮点击后的光点扩散。这些效果有一个共同特征——大量微小元素各自独立运动,形成整体的视觉表现。
如果用传统的 QML 动画实现,每个元素都需要独立的NumberAnimation或Behavior,100 个元素就是 100 个动画对象,性能和代码量都会急剧恶化。而Qt Quick 粒子系统模块(QtQuick.Particles)正是为这类场景设计的:它在 GPU 层面批量管理成百上千个粒子,开发者只需声明发射规则和行为约束,系统自动处理生命周期、运动计算和渲染。
在正式学习之前,先对比三种常见方案的适用边界:
| 方案 | 适用场景 | 性能上限 | 开发复杂度 |
|---|---|---|---|
| QML 动画(NumberAnimation 等) | 单个元素的简单动效 | <20个对象(仅供参考) | 低 |
| Canvas 手绘 | 自定义绘制逻辑 | 中等 | 高 |
| ParticleSystem 模块 | 大量独立运动粒子 | 5000-10000粒子(仅供参考) | 中 |
| ShaderEffect | GPU 全自定义 | 最高 | 很高 |
本文是系列第一篇,目标是建立 Qt Quick 粒子系统的全局认知框架——理解它的四层架构,知道每个组件的职责和协作关系,最终用 10 行代码写出一个最小粒子系统。
二、开发环境与版本说明
本文所有代码基于以下环境验证(验证日期:2026-06-05):
- Qt 版本:6.8.2(
QtQuick.Particles模块从 Qt 5 引入,Qt 6 中沿用,本文以 Qt 6 为准) - 编译器:MinGW 64-bit
- 操作系统:Windows 11
- 构建工具:CMake 3.29
如果你使用 Qt 6.5 及以上版本,代码无需修改即可运行。Qt 5.x 版本也可以使用粒子系统模块,但import路径和部分 API 存在差异(如import QtQuick.Particles 2.15),本文统一以 Qt 6 为准。
项目的 CMake 配置中,关键部分如下:
# 启用 QML 模块 qt_add_qml_module(appqml_particlesystem URI qml_particlesystem VERSION 1.0 QML_FILES Main.qml Concept_ParticleSystem.qml # ... 其他 QML 文件 RESOURCES res.qrc )粒子系统本身不需要额外的 C++ 代码,所有逻辑都在 QML 中完成。main.cpp仅负责创建QQmlApplicationEngine并加载入口 QML 文件:
QQmlApplicationEngine engine;engine.loadFromModule("qml_particlesystem","Main");三、原理分析:四层架构
Qt Quick 粒子系统的设计可以抽象为四层,每层各司其职:
3.1 ParticleSystem——容器与调度器
ParticleSystem是所有粒子元素的宿主容器。它本身不发射、不渲染、不影响粒子,而是负责:
- 生命周期管理:跟踪每个粒子的诞生、运动和消亡
- 全局调度:协调 Emitter、Painter、Affector 之间的数据流
- 状态控制:通过
running、paused、empty等属性管理系统运行状态
一个粒子系统中可以包含多个 Emitter、多个 Painter 和多个 Affector,它们通过父子关系或system属性关联到同一个 ParticleSystem:
// 方式一:父子关系(推荐,简洁) ParticleSystem { ImageParticle { ... } Emitter { ... } } // 方式二:显式 system 绑定(组件分散在不同层级时使用) ParticleSystem { id: mySystem } ImageParticle { system: mySystem } Emitter { system: mySystem }3.2 Emitter——发射器
Emitter决定粒子从哪里来、以什么方式来。它的核心职责包括:
- 发射区域:通过
shape属性限定粒子的出生位置(矩形、椭圆、线条等) - 发射频率:
emitRate控制每秒发射的粒子数量 - 初始属性:
lifeSpan(生命周期)、size(大小)、velocity(速度)、acceleration(加速度) - 运动方向:粒子的扩散方向由
Direction子组件控制,包括AngleDirection(角度方向)、PointDirection(点方向)、TargetDirection(目标方向)等,后续文章将详细讲解 - 粒子分组:
group属性将发射的粒子归入指定组
一个 ParticleSystem 可以有多个 Emitter,分别发射不同样式的粒子。
3.3 ParticlePainter——渲染器
ParticlePainter决定粒子长什么样。Qt Quick 提供了两种实现:
- ImageParticle:使用图片作为粒子外观,GPU 批量渲染,性能最优。支持着色、旋转、变形、透明度等属性。
- ItemParticle:使用任意 QML 组件作为粒子外观,灵活度最高,支持交互(点击、拖拽),但每个粒子是独立的 Item,性能较低。
两者通过groups属性指定负责渲染哪些粒子组,可以同时使用。
3.4 Affector——影响器
Affector在粒子发射后、消亡前,对其属性进行二次修改。常见效果包括:
Gravity:重力加速度Friction:速度衰减Attractor:向目标点吸引Wander:随机游走Turbulence:湍流扰动
Affector 同样通过groups属性限定作用范围,可以叠加使用。
3.5 粒子的完整生命周期
将四层串联起来,一个粒子从诞生到消亡的完整流程是:
理解这个流程是后续学习的基础——Emitter 控制"从哪来",Direction 控制"往哪飞",Affector 控制"受什么力",Painter 控制"长什么样"。
四、代码实现:Concept_ParticleSystem.qml 逐段解析
项目中Concept_ParticleSystem.qml是第一个示例页面,它同时做了两件事:展示四层架构的知识卡片,以及用粒子特效作为背景。下面先看完整代码,再逐段解析关键设计。
4.1 完整代码
import QtQuick import QtQuick.Particles import QtQuick.Layouts import "common" BaseRect { id: root PageTitle { titleText: "ParticleSystem - 粒子系统" } // ── 粒子特效层 ────────────────────────────────── Rectangle { Layout.fillWidth: true Layout.fillHeight: true Layout.margins: 10 color: "transparent" radius: 8 ParticleSystem { id: particleSystem anchors.fill: parent running: root.isCurrentItem ImageParticle { source: "qrc:/images/star.png" alpha: 0.5 } Emitter { anchors.centerIn: parent width: infoColumn.width height: infoColumn.height emitRate: 30 lifeSpan: 3000 size: 10 sizeVariation: 10 } Wander { system: particleSystem xVariance: 100 yVariance: 100 pace: 100 } } // ── 信息卡片层 ────────────────────────────── ColumnLayout { id: infoColumn anchors.fill: parent anchors.margins: 15 spacing: 15 visible: false ListModel { id: infoModel ListElement { title: "## ParticleSystem 是所有粒子元素的容器" titleColor: "#4ECDC4" content: "- 管理粒子生命周期、渲染和交互\n- 核心属性: running, paused, empty\n- 核心方法: start(), stop(), pause(), resume(), reset(), restart()" } ListElement { title: "## Emitter - 发射器" titleColor: "#FFE66D" content: "- 发射逻辑粒子到 ParticleSystem\n- 核心属性: emitRate, lifeSpan, size, velocity, acceleration, shape, group\n- 核心方法: burst(count), pulse(duration)" } ListElement { title: "## ParticlePainter - 渲染器" titleColor: "#FF6B6B" content: "- 指定如何绘制粒子\n- 子类: ImageParticle(图像渲染), ItemParticle(QML组件渲染)\n- 核心属性: groups, system" } ListElement { title: "## Affector - 影响器" titleColor: "#87CEEB" content: "- 在粒子生命周期任意时刻修改其属性\n- 子类: Age, Attractor, Friction, Gravity, Turbulence, Wander, SpriteGoal\n- 核心属性: acceleration, velocity, position, relative" } } // ── 入场动画 Repeater ──────────────────── Repeater { id: infoRepeater model: infoModel ColumnLayout { id: delegateItem spacing: 5 Layout.fillWidth: true opacity: 0 transform: Translate { id: trans; y: 20 } NumberAnimation { id: entryAnim target: delegateItem property: "opacity" from: 0; to: 1 duration: 400 easing.type: Easing.OutCubic } NumberAnimation { id: slideAnim target: trans property: "y" from: 20; to: 0 duration: 400 easing.type: Easing.OutCubic } Timer { id: delayTimer interval: 500 + index * 120 onTriggered: { entryAnim.start(); slideAnim.start() } } Connections { target: root function onIsCurrentItemChanged() { if (root.isCurrentItem) { delegateItem.opacity = 0 trans.y = 20 delayTimer.restart() } else { entryAnim.stop() slideAnim.stop() delayTimer.stop() } } } Component.onCompleted: { if (root.isCurrentItem) delayTimer.start() } // ── 文字渲染 ────────────────────── Text { Layout.fillWidth: true color: model.titleColor text: model.title textFormat: Text.MarkdownText wrapMode: Text.WordWrap } Text { Layout.fillWidth: true color: "#aaa" text: model.content textFormat: Text.MarkdownText wrapMode: Text.WordWrap } Rectangle { Layout.fillWidth: true Layout.topMargin: 5 height: 1 color: "#333" visible: index < infoModel.count - 1 } } } Item { Layout.fillHeight: true } } } }4.2 页面结构:BaseRect + PageTitle
页面继承BaseRect而非直接使用Rectangle。BaseRect提供了两个关键能力:
- 自动布局:内部使用
ColumnLayout,子元素自动纵向排列 - 页面激活感知:暴露
isCurrentItem属性,由 StackLayout 自动赋值
PageTitle是通用标题组件,18px 白色加粗居中显示。
4.3 粒子特效层
粒子特效层的核心是ParticleSystem,包含三个子组件:
running: root.isCurrentItem——这是整个项目的核心模式。当用户切换到其他页面时,StackLayout 会将isCurrentItem设为false,粒子系统自动停止,避免不可见时浪费 GPU。切换回来时自动恢复。
Emitter尺寸绑定infoColumn——发射区域不是固定大小,而是绑定到下方文字区域的尺寸。这样粒子从文字区域的范围内涌出,形成"文字背后有粒子"的视觉效果。这是一个常见的设计技巧:让发射区域跟随内容自适应。
alpha: 0.5——粒子半透明,避免遮挡前景文字。粒子系统和 UI 内容共存时,透明度控制是关键。
Wander随机漂移——粒子不仅从中心扩散,还带有布朗运动般的漂移效果。xVariance: 100和yVariance: 100控制漂移幅度,pace: 100控制漂移频率。
4.4 信息卡片层:ListModel + Repeater
ColumnLayout(infoColumn)设为visible: false,它本身不显示——只为Emitter提供尺寸参考(width: infoColumn.width)。实际的文字显示由Repeater的 delegate 完成。
ListModel定义了四层架构的描述数据,每条包含标题颜色和内容。Repeater为每条数据生成一个ColumnLayoutdelegate,包含标题 Text、内容 Text 和分割线 Rectangle。
4.5 入场动画:延迟渐入效果
每张卡片都有一个精心设计的入场动画,分三个阶段:
- 延迟:
Timer的interval: 500 + index * 120,第一张卡片延迟 500ms,第二张 620ms,第三张 740ms……形成逐张依次出现的效果 - 渐入:
entryAnim将opacity从 0 动画到 1 - 上滑:
slideAnim将Translate.y从 20 动画到 0,卡片从下方滑入
Easing.OutCubic缓动曲线让动画有"快速进入、缓慢停下"的质感,比线性动画更自然。
4.6 动画生命周期管理
动画必须和页面激活状态同步。Connections监听root.isCurrentItemChanged信号:
- 页面激活时:重置 opacity 和位置,重启延迟 Timer,动画从头播放
- 页面离开时:停止所有动画和 Timer,避免后台无意义执行
Component.onCompleted处理首次加载的边界情况——如果页面在组件创建时就已经是当前页(比如应用启动时默认显示第一页),直接启动动画。
4.7 文字渲染
Text.MarkdownText让标题中的##语法被正确渲染为二级标题样式。每张卡片之间用 1px 的深灰分割线隔开,最后一张卡片不显示分割线(visible: index < infoModel.count - 1)。
4.8 项目骨架:导航与页面管理
所有示例页面都在Main.qml中通过ListView+StackLayout组织:
Window { RowLayout { ListView { id: navList Layout.preferredWidth: 180 model: ListModel { ListElement { name: "系统概述"; category: "基本概念" } ListElement { name: "系统控制"; category: "基本概念" } // ... 更多页面 } } StackLayout { id: contentStack Concept_ParticleSystem {} // 第 0 页 Concept_SystemControl {} // 第 1 页 // ... 更多页面 } } }ListView提供左侧分类导航,StackLayout管理右侧内容区。点击导航项时contentStack.currentIndex切换,BaseRect 的isCurrentItem自动跟随变化,粒子系统和动画的启停全部自动化——开发者在每个页面中只需声明running: root.isCurrentItem即可。
4.9 通用组件复用
项目定义了几个通用组件,保证页面风格一致:
| 组件 | 文件 | 职责 |
|---|---|---|
| BaseRect | common/BaseRect.qml | 页面基础容器,管理 isCurrentItem |
| PageTitle | common/PageTitle.qml | 页面标题,18px 白色加粗居中 |
| BottomNote | common/BottomNote.qml | 底部说明文字,深灰背景 |
| SubTitleRow | common/SubTitleRow.qml | 小节副标题 |
| ColorButton | common/ColorButton.qml | 彩色按钮组件 |
这些组件将布局和样式从业务逻辑中抽离,让每个示例页面只需关注粒子系统本身的代码。
五、运行效果
运行项目后,主界面如下:
- 左侧:180px 宽的导航栏,按分类分组(基本概念、发射区域、扩散方向、粒子随机性、粒子影响器、尾迹发射器、粒子系统示例)——规划分类,目前还在做
- 右侧:StackLayout 内容区,显示当前选中的粒子示例
- 粒子行为:切换页面时,当前页的粒子系统自动运行,离开时自动停止
点击「系统概述」进入第一个示例页面,可以看到四层架构的概览信息和背景粒子效果:
运行截图说明:页面展示 ParticleSystem / Emitter / ParticlePainter / Affector 四层组件的职责说明,背景有 Wander 随机漂移的星形粒子。
项目规划有 7 个分类、30+ 个示例页面,覆盖了 Qt Quick 粒子系统的完整功能。后续文章将逐个模块深入讲解。
六、常见问题与边界条件
Q:Qt 5 项目能否使用粒子系统?
可以。Qt 5 的QtQuick.Particles模块功能基本一致,但 import 语句需要指定版本号:
// Qt 5 写法 import QtQuick.Particles 2.15 // Qt 6 写法(推荐) import QtQuick.ParticlesQ:ParticleSystem 和 QML 的 Behavior/NumberAnimation 动画有什么区别?
Behavior和NumberAnimation适合单个元素的属性动画(如按钮缩放、颜色渐变)。ParticleSystem 适合大量独立运动对象的场景——它在引擎层面批量处理粒子数据,避免了为每个粒子创建独立动画对象的开销。
简单判断标准:如果动画对象超过 20 个,考虑 ParticleSystem。
Q:粒子系统的性能上限是多少?
取决于设备 GPU 能力、粒子大小和渲染方式。使用ImageParticle(GPU 批量渲染)时,中端设备通常可支持数千个活跃粒子。使用ItemParticle(QML 组件渲染)时,由于每个粒子都是独立的 QML Item,建议控制在 100 个以内。
性能优化的关键手段:
- 不可见时停止粒子系统(
running: isCurrentItem) - 使用
maximumEmitted限制最大活跃粒子数 - 优先使用 ImageParticle 而非 ItemParticle
Q:粒子系统的running属性默认是什么?
默认为true。如果在页面切换场景中使用,务必绑定为false或绑定到页面可见性属性,避免不可见时仍消耗 GPU 资源。
七、总结与下篇预告
本文建立了 Qt Quick 粒子系统的四层架构认知:
- ParticleSystem是容器和调度器,管理全局生命周期
- Emitter是发射器,决定粒子从哪来、怎么来
- ParticlePainter是渲染器,决定粒子长什么样(ImageParticle 批量渲染 / ItemParticle 组件渲染)
- Affector是影响器,决定粒子发射后受什么力
记住这个四层模型:容器、发、画、改——容器管理一切,发射器创造粒子,渲染器绘制粒子,影响器改造粒子。
下一篇将深入 ParticleSystem 的状态控制,详细讲解start()/stop()/pause()/resume()/reset()/restart()六种方法的行为差异,以及running/paused/empty三态模型的工作机制。
资源下载:qml_particlesystem —— 包含完整的、可运行的代码
系列目录
- 本文:Qt Quick 粒子系统(一):架构总览与四层模型
- 下一篇:Qt Quick 粒子系统(二):系统控制与生命周期管理
