
本文档基于一个完整的 AnimationDemo 项目,详细介绍了如何在 HarmonyOS 平台上使用 Qt 实现各种动画效果。项目实现了6种不同类型的动画演示(呼吸灯、渐变效果、移动动画、旋转动画、缩放动画、文字动画),展示了 Qt Quick 动画系统在 HarmonyOS 平台上的实际应用,为开发者提供了丰富的动画效果参考。
项目地址:https://gitcode.com/szkygc/HarmonyOs_PC-PGC/blob/main/AnimationDemo

AnimationDemo/
├── entry/src/main/
│ ├── cpp/
│ │ ├── main.cpp # 应用入口(HarmonyOS适配)
│ │ ├── main.qml # 主界面(6种动画演示)
│ │ ├── CMakeLists.txt # 构建配置
│ │ └── qml.qrc # QML资源文件
│ ├── module.json5 # 模块配置
│ └── resources/ # 资源文件
└── image/
└── 演示示例.gif # 演示动图ApplicationWindow (main.qml)
├── Column (主布局)
│ ├── Rectangle (标题栏)
│ │ └── Row
│ │ └── Text (标题)
│ ├── Rectangle (Demo选择器)
│ │ └── Row
│ │ ├── Text (标签)
│ │ └── ComboBox (动画选择器)
│ └── Rectangle (动画演示区域)
│ └── Item (动画容器)
│ ├── Rectangle (呼吸灯动画)
│ │ └── SequentialAnimation on alpha
│ ├── Rectangle (渐变效果动画)
│ │ ├── NumberAnimation (淡出)
│ │ └── NumberAnimation (淡入)
│ ├── Rectangle (移动动画)
│ │ └── SequentialAnimation on x
│ ├── Rectangle (旋转动画)
│ │ └── NumberAnimation on rotation
│ ├── Rectangle (缩放动画)
│ │ └── NumberAnimation on scaleValue
│ └── Item (文字动画)
│ └── NumberAnimation on yPositionqtmain()⚠️ 关键要点:HarmonyOS 真机上必须使用 qtmain() 而不是 main()!
// ✅ 正确写法
extern "C" int qtmain(int argc, char **argv)
{
// Qt 应用作为共享库加载,生命周期由 HarmonyOS 管理
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec(); // ⚠️ 重要:必须调用 exec() 启动事件循环
}
// ❌ 错误写法(桌面应用方式)
int main(int argc, char *argv[])
{
// 这种方式在 HarmonyOS 上会导致应用无法正常启动
}原因说明:
qtmain() 是 HarmonyOS Qt 插件的标准入口点⚠️ 关键要点:必须在创建 QGuiApplication 之前配置 QSurfaceFormat!
// Step 1: 配置 OpenGL ES 表面格式(必须在创建应用之前!)
QSurfaceFormat format;
// 设置 Alpha 通道(透明度)
format.setAlphaBufferSize(8); // 8 位 Alpha 通道
// 设置颜色通道(RGBA 32 位真彩色)
format.setRedBufferSize(8); // 8 位红色通道
format.setGreenBufferSize(8); // 8 位绿色通道
format.setBlueBufferSize(8); // 8 位蓝色通道
// 设置深度和模板缓冲区
format.setDepthBufferSize(24); // 24 位深度缓冲
format.setStencilBufferSize(8); // 8 位模板缓冲
// 指定渲染类型为 OpenGL ES(HarmonyOS要求)
format.setRenderableType(QSurfaceFormat::OpenGLES);
// 指定 OpenGL ES 版本为 3.0(推荐)
format.setVersion(3, 0);
// ⚠️ 关键:必须在创建 QGuiApplication 之前设置默认格式!
QSurfaceFormat::setDefaultFormat(format);
// Step 2: 创建 Qt 应用实例(必须在设置格式之后)
QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
QGuiApplication app(argc, argv);配置说明:
核心思想:通过 SequentialAnimation 实现透明度的循环变化,形成呼吸灯效果。
Rectangle {
visible: currentDemoIndex === 0
anchors.fill: parent
color: "#1E1E1E"
property int alpha: 20 // 透明度值(0-255)
Column {
anchors.centerIn: parent
spacing: 30
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "呼吸灯动画"
font.pixelSize: 36
font.bold: true
color: "#FFFFFF"
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 200
height: 200
radius: 100
color: Qt.rgba(0, 1, 0, parent.parent.alpha / 255.0) // 使用 alpha 值
border.color: "#FFFFFF"
border.width: 2
}
}
SequentialAnimation on alpha {
running: currentDemoIndex === 0 // 只在选中时运行
loops: Animation.Infinite // 无限循环
NumberAnimation {
from: 20
to: 255
duration: 2500
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: 255
to: 20
duration: 2500
easing.type: Easing.InOutQuad
}
}
}关键点:
alpha 属性上应用顺序动画Easing.InOutQuad 实现平滑的加速和减速running: currentDemoIndex === 0 控制动画运行核心思想:使用独立的 NumberAnimation 实现淡入淡出效果,通过按钮控制。
Rectangle {
id: fadeDemo
visible: currentDemoIndex === 1
anchors.fill: parent
color: "#F5F5F5"
property real targetOpacity: 1.0 // 目标透明度
Column {
anchors.centerIn: parent
spacing: 30
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "渐变效果动画"
font.pixelSize: 36
font.bold: true
color: "#333333"
}
Rectangle {
id: fadeTarget
anchors.horizontalCenter: parent.horizontalCenter
width: 300
height: 200
color: "#2196F3"
radius: 10
opacity: fadeDemo.targetOpacity // 绑定到目标透明度
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 20
Button {
text: "淡出"
width: 150
height: 60
font.pixelSize: 28
onClicked: {
fadeOutAnimation.start() // 启动淡出动画
}
}
Button {
text: "淡入"
width: 150
height: 60
font.pixelSize: 28
onClicked: {
fadeInAnimation.start() // 启动淡入动画
}
}
}
}
// 淡出动画
NumberAnimation {
id: fadeOutAnimation
target: fadeDemo
property: "targetOpacity"
from: 1.0
to: 0.0
duration: 1000
}
// 淡入动画
NumberAnimation {
id: fadeInAnimation
target: fadeDemo
property: "targetOpacity"
from: 0.0
to: 1.0
duration: 1000
}
}关键点:
NumberAnimation 创建独立的动画对象start() 方法手动启动动画opacity 绑定到 targetOpacity 属性核心思想:使用 SequentialAnimation 实现元素在左右两个位置之间的循环移动。
Rectangle {
id: moveDemo
visible: currentDemoIndex === 2
anchors.fill: parent
color: "#F5F5F5"
Text {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 30
text: "移动动画"
font.pixelSize: 36
font.bold: true
color: "#333333"
}
Rectangle {
id: movingLabel
width: 150
height: 60
color: "#3498db"
radius: 5
x: 0
y: (moveDemo.height - height) / 2 // 垂直居中
Text {
anchors.centerIn: parent
text: "移动的标签"
font.pixelSize: 24
color: "#FFFFFF"
}
SequentialAnimation on x {
id: moveAnimation
running: currentDemoIndex === 2 && moveDemo.visible
loops: Animation.Infinite
NumberAnimation {
from: 0
to: moveDemo.width - movingLabel.width // 移动到右边界
duration: 2000
easing.type: Easing.InOutQuad
}
NumberAnimation {
from: moveDemo.width - movingLabel.width
to: 0 // 移动回左边界
duration: 2000
easing.type: Easing.InOutQuad
}
}
}
// 当切换到移动动画时,重置位置并启动动画
onVisibleChanged: {
if (visible) {
movingLabel.x = 0
moveAnimation.restart()
}
}
}关键点:
x 属性上应用顺序动画moveDemo.width - movingLabel.width 计算右边界位置onVisibleChanged 在显示时重置位置核心思想:使用 NumberAnimation 实现元素的持续旋转。
Rectangle {
visible: currentDemoIndex === 3
anchors.fill: parent
color: "#F5F5F5"
property real rotation: 0 // 旋转角度
Column {
anchors.centerIn: parent
spacing: 30
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "旋转动画"
font.pixelSize: 36
font.bold: true
color: "#333333"
}
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
width: 200
height: 200
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: 150
height: 150
color: "#9C27B0"
radius: 10
rotation: parent.parent.parent.rotation // 绑定到旋转角度
Text {
anchors.centerIn: parent
text: "旋转"
font.pixelSize: 28
color: "#FFFFFF"
}
}
}
}
NumberAnimation on rotation {
running: currentDemoIndex === 3
loops: Animation.Infinite
from: 0
to: 360 // 360度旋转
duration: 5000
easing.type: Easing.Linear // 线性缓动,匀速旋转
}
}关键点:
rotation 属性上应用动画Easing.Linear 实现匀速旋转rotation 绑定到父元素的 rotation 属性核心思想:使用独立的 NumberAnimation 实现元素的缩放效果,支持手动控制。
Rectangle {
id: scaleDemo
visible: currentDemoIndex === 4
anchors.fill: parent
color: "#F5F5F5"
property real scaleValue: 1.0 // 缩放值
Column {
anchors.centerIn: parent
spacing: 30
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "缩放动画"
font.pixelSize: 36
font.bold: true
color: "#333333"
}
Rectangle {
id: scaleTarget
anchors.horizontalCenter: parent.horizontalCenter
width: 200 * scaleDemo.scaleValue // 使用缩放值
height: 200 * scaleDemo.scaleValue
color: "#FF5722"
radius: 10
Text {
anchors.centerIn: parent
text: "缩放"
font.pixelSize: 28
color: "#FFFFFF"
}
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: scaleAnimation.running ? "停止动画" : "开始动画"
width: 200
height: 60
font.pixelSize: 28
onClicked: {
if (scaleAnimation.running) {
scaleAnimation.stop() // 停止动画
} else {
// 根据当前缩放值决定目标值
if (scaleDemo.scaleValue === 1.0) {
scaleAnimation.from = 1.0
scaleAnimation.to = 2.0 // 放大到2倍
} else {
scaleAnimation.from = 2.0
scaleAnimation.to = 1.0 // 缩小到1倍
}
scaleAnimation.start()
}
}
}
}
NumberAnimation {
id: scaleAnimation
target: scaleDemo
property: "scaleValue"
duration: 1000
easing.type: Easing.InOutQuad
}
// 切换到缩放动画时,重置缩放值
onVisibleChanged: {
if (visible) {
scaleValue = 1.0
}
}
}关键点:
scaleValue 属性控制元素大小核心思想:使用 NumberAnimation 实现文字从下往上的移动效果。
Item {
id: textDemo
visible: currentDemoIndex === 5
anchors.fill: parent
property real yPosition: -parent.height // Y位置(初始在屏幕外)
Rectangle {
anchors.fill: parent
color: "#1E1E1E"
Text {
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: 30
text: "文字动画"
font.pixelSize: 36
font.bold: true
color: "#FFFFFF"
}
Text {
id: animatedText
anchors.horizontalCenter: parent.horizontalCenter
y: textDemo.height / 2 + textDemo.yPosition // 使用 yPosition
text: "欢迎使用Qt动画演示"
font.pixelSize: 32
color: "#00FF00"
}
Button {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 50
text: "重新播放"
width: 200
height: 60
font.pixelSize: 28
onClicked: {
textDemo.yPosition = -textDemo.height // 重置到屏幕外
textAnimation.start()
}
}
}
NumberAnimation {
id: textAnimation
target: textDemo
property: "yPosition"
from: -textDemo.height // 从屏幕外开始
to: 0 // 移动到屏幕中央
duration: 1000
easing.type: Easing.OutInQuad
}
// 切换到文字动画时,自动播放
onVisibleChanged: {
if (visible) {
yPosition = -height
Qt.callLater(function() {
textAnimation.start()
})
}
}
}关键点:
yPosition 属性控制文字的垂直位置-height(屏幕外)核心思想:使用 ComboBox 实现动画演示的切换。
// Demo名称列表
property var demoNames: [
"呼吸灯",
"渐变效果",
"移动动画",
"旋转动画",
"缩放动画",
"文字动画"
]
// 当前选中的动画demo索引
property int currentDemoIndex: 0
ComboBox {
id: demoComboBox
width: 300
height: 60
model: demoNames
currentIndex: 0
font.pixelSize: 26
onCurrentIndexChanged: {
currentDemoIndex = currentIndex // 更新当前索引
}
}关键点:
currentDemoIndex 绑定到 ComboBox 的 currentIndexcurrentDemoIndexvisible: currentDemoIndex === index 控制显示核心思想:正确处理窗口状态变化,确保动画在窗口状态改变后正常工作。
ApplicationWindow {
id: root
// 窗口状态变化处理(修复最大化黑屏问题)
onVisibilityChanged: {
if (visibility === Window.Windowed || visibility === Window.Maximized || visibility === Window.FullScreen) {
Qt.callLater(function() {
console.log("AnimationDemo: 窗口可见性变化,强制更新")
// 强制更新所有可见的动画组件
if (currentDemoIndex === 2) {
// 移动动画需要重新启动
movingLabel.x = 0
}
})
}
}
onWindowStateChanged: {
console.log("AnimationDemo: 窗口状态变化 - state:", windowState)
// 窗口状态变化时,延迟更新以确保尺寸已更新
Qt.callLater(function() {
console.log("AnimationDemo: 窗口尺寸:", width, "x", height)
})
}
}关键点:
onVisibilityChanged 处理窗口可见性变化Qt.callLater 确保在尺寸更新后再处理这是本文档最重要的发现!
在 entry/src/main/module.json5 文件中,deviceTypes 必须包含 "2in1":
{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": [
"default",
"tablet",
"2in1" // ⚠️ 必须添加!否则打包会失败
],
// ...
}
}错误信息:
hvigor ERROR: Failed :entry:default@PackageHap...
Ohos BundleTool [Error]: 10011001 Parse and check args invalid in hap mode.
Error Message: --json-path must be the config.json file or module.json file.原因分析:
"2in1" 设备类型deviceTypes 中缺少 "2in1",打包工具无法正确识别配置文件路径最佳实践:
"deviceTypes": [
"default", // 手机
"tablet", // 平板
"2in1" // ⚠️ PC/2合1设备(必须添加!)
]症状:选择动画后,动画效果没有显示。
原因:
running 属性没有正确设置currentDemoIndex 没有正确更新解决方案:
// ✅ 正确:使用条件运行
SequentialAnimation on alpha {
running: currentDemoIndex === 0 // 明确指定运行条件
loops: Animation.Infinite
// ...
}
// ❌ 错误:没有设置运行条件
SequentialAnimation on alpha {
loops: Animation.Infinite // 缺少 running 属性
// ...
}症状:切换动画时,新动画从错误的状态开始。
原因:
onVisibleChanged 没有正确处理解决方案:
Rectangle {
id: moveDemo
visible: currentDemoIndex === 2
// ✅ 正确:切换时重置状态
onVisibleChanged: {
if (visible) {
movingLabel.x = 0 // 重置位置
moveAnimation.restart() // 重新启动动画
}
}
}症状:动画运行卡顿,不流畅。
原因:
解决方案:
// ✅ 正确:使用合适的持续时间
NumberAnimation {
duration: 1000 // 1秒,性能良好
easing.type: Easing.InOutQuad
}
// ❌ 错误:持续时间过长
NumberAnimation {
duration: 10000 // 10秒,可能影响性能
}症状:窗口最大化后,动画不再运行。
原因:
解决方案:
ApplicationWindow {
id: root
// ✅ 正确:处理窗口状态变化
onVisibilityChanged: {
if (visibility === Window.Windowed || visibility === Window.Maximized) {
Qt.callLater(function() {
// 重新启动动画
if (currentDemoIndex === 2) {
movingLabel.x = 0
moveAnimation.restart()
}
})
}
}
}症状:点击停止按钮后,动画仍在运行。
原因:
SequentialAnimation on property 语法,无法直接停止解决方案:
// ✅ 正确:使用独立的动画对象
NumberAnimation {
id: scaleAnimation
target: scaleDemo
property: "scaleValue"
duration: 1000
}
Button {
onClicked: {
if (scaleAnimation.running) {
scaleAnimation.stop() // 可以停止
} else {
scaleAnimation.start() // 可以启动
}
}
}
// ❌ 错误:使用 on property 语法,无法停止
NumberAnimation on scaleValue {
duration: 1000
// 无法直接停止
}症状:
hvigor ERROR: Failed :entry:default@PackageHap...
Error Message: --json-path must be the config.json file or module.json file.原因:module.json5 中的 deviceTypes 缺少 "2in1"。
解决方案:
// entry/src/main/module.json5
{
"module": {
"deviceTypes": [
"default",
"tablet",
"2in1" // ⚠️ 必须添加!
]
}
}// 1. 属性动画(PropertyAnimation)
PropertyAnimation {
target: myItem
property: "x"
from: 0
to: 100
duration: 1000
}
// 2. 数字动画(NumberAnimation)
NumberAnimation on opacity {
from: 0.0
to: 1.0
duration: 1000
}
// 3. 顺序动画(SequentialAnimation)
SequentialAnimation {
NumberAnimation { from: 0; to: 100; duration: 500 }
NumberAnimation { from: 100; to: 0; duration: 500 }
}
// 4. 并行动画(ParallelAnimation)
ParallelAnimation {
NumberAnimation { target: item; property: "x"; to: 100 }
NumberAnimation { target: item; property: "y"; to: 100 }
}// 线性缓动(匀速)
easing.type: Easing.Linear
// 二次缓动(加速/减速)
easing.type: Easing.InOutQuad
// 弹性缓动(弹性效果)
easing.type: Easing.OutElastic
// 回弹缓动(回弹效果)
easing.type: Easing.OutBounce// 启动动画
animation.start()
// 停止动画
animation.stop()
// 暂停动画
animation.pause()
// 恢复动画
animation.resume()
// 重新启动动画
animation.restart()// 使用 running 属性控制运行
SequentialAnimation on alpha {
running: currentDemoIndex === 0 // 条件运行
loops: Animation.Infinite
// ...
}
// 使用 visible 属性控制显示
Rectangle {
visible: currentDemoIndex === 0
// ...
}Rectangle {
id: demo
// 显示时启动动画
onVisibleChanged: {
if (visible) {
animation.start()
} else {
animation.stop()
}
}
// 组件完成时初始化
Component.onCompleted: {
// 初始化代码
}
}// ✅ 正确:使用合适的持续时间
NumberAnimation {
duration: 1000 // 1秒
}
// ✅ 正确:避免同时运行过多动画
// 只运行当前显示的动画
// ✅ 正确:使用硬件加速
// Qt Quick 默认使用硬件加速AnimationDemo/
├── AppScope/
│ └── app.json5 # 应用配置
├── entry/
│ ├── build-profile.json5 # 构建配置
│ ├── src/
│ │ ├── main/
│ │ │ ├── cpp/
│ │ │ │ ├── main.cpp # C++ 入口(qtmain)
│ │ │ │ ├── main.qml # QML UI(6种动画演示)
│ │ │ │ ├── qml.qrc # 资源文件
│ │ │ │ └── CMakeLists.txt
│ │ │ ├── module.json5 # ⚠️ 必须包含 "2in1"
│ │ │ └── ets/ # ArkTS 代码
│ │ └── ohosTest/
│ └── libs/ # Qt 库文件
├── image/
│ └── 演示示例.gif # 演示动图
└── build-profile.json5 # 根构建配置cmake_minimum_required(VERSION 3.5.0)
project(QtForHOSample)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})
list(APPEND CMAKE_FIND_ROOT_PATH ${QT_PREFIX})
include_directories(${NATIVERENDER_ROOT_PATH}
${NATIVERENDER_ROOT_PATH}/include)
find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS
Concurrent Gui Network Qml Quick QuickControls2
Widgets QuickTemplates2 QmlWorkerScript)
add_library(entry SHARED main.cpp qml.qrc)
target_link_libraries(entry PRIVATE
Qt${QT_VERSION_MAJOR}::Concurrent
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Qml
Qt${QT_VERSION_MAJOR}::Quick
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::QuickControls2
Qt${QT_VERSION_MAJOR}::QuickTemplates2
Qt${QT_VERSION_MAJOR}::QmlWorkerScript
Qt${QT_VERSION_MAJOR}::QOpenHarmonyPlatformIntegrationPlugin # HarmonyOS 插件
){
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": [
"default",
"tablet",
"2in1" // ⚠️ 必须添加!否则打包会失败
],
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceType": [
"default",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
]
}
}通过本项目的开发实践,我们总结了以下关键要点:
qtmain() 作为入口函数,而不是 main()deviceTypes 必须包含 "2in1",否则打包会失败SequentialAnimation 实现顺序动画,适合循环效果NumberAnimation 实现属性动画,支持手动控制running 属性控制动画运行,实现条件运行onVisibleChanged 处理动画生命周期,确保状态正确ComboBox 实现动画切换,提升用户体验这些经验对于在 HarmonyOS 平台上开发 Qt 应用至关重要,特别是涉及动画效果的场景。希望本文档能帮助开发者避免常见陷阱,快速上手 Qt for HarmonyOS 开发,并创建出流畅美观的动画效果。