为QML程序添加启动Logo:提升用户体验

为QML程序添加启动Logo:提升用户体验

一、背景与问题分析

1、为什么需要启动Logo?

在实际开发中,很多QML应用程序在启动时需要执行一些耗时的初始化操作,比如:

  • 加载大型资源文件
  • 初始化复杂的业务逻辑
  • 连接数据库或网络服务
  • 等待后端服务准备就绪

问题表现:用户点击应用后,屏幕会黑屏或空白10几秒钟,给用户带来"应用卡死"或"无响应"的负面体验。

解决方案:通过显示启动Logo界面,我们能够:

  • 向用户反馈应用正在正常启动
  • 展示品牌形象和视觉元素
  • 提供进度反馈,降低用户的等待焦虑
  • 提升应用的专业度和用户体验

二、实现原理详解

1、技术思路

我们的解决方案基于以下关键点:

  1. 双窗口架构:启动窗口 + 主界面窗口
  2. 透明背景技术:实现自定义形状的启动界面
  3. 异步加载机制:在后台初始化时保持界面响应
  4. 动态切换技术:平滑过渡到主界面

2、窗口标志位说明

qml 复制代码
flags: Qt.SplashScreen | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
  • Qt.SplashScreen:将窗口标记为启动屏幕
  • Qt.FramelessWindowHint:移除窗口边框和标题栏
  • Qt.WindowStaysOnTopHint:确保启动界面始终在最前面

三、效果

四、完整实现步骤

第一步:项目环境搭建

复制张贴代码段的内容在PowerShell终端即可

1、主程序main.cpp
bash 复制代码
@'
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQuickWindow>
#include <QTimer>
#include <QtQml/QQmlEngine>
#include <QQmlContext>

int main(int argc, char *argv[])
{
    QGuiApplication  app(argc, argv);

    app.setApplicationName("MyApp");
    app.setApplicationVersion("1.0.0");
    app.setOrganizationName("MyCompany");
    app.setOrganizationDomain("mycompany.com");

    QQmlApplicationEngine engine;
    // 模拟一些耗时的初始化工作
    QTimer::singleShot(1000, [&engine]() {
        QMetaObject::invokeMethod(engine.rootObjects().first(), "showStage",Qt::DirectConnection,QVariant(0.2),QVariant("进度1"));
        QTimer::singleShot(2000, [&engine]() {
            QMetaObject::invokeMethod(engine.rootObjects().first(), "showStage",Qt::DirectConnection,QVariant(0.4),QVariant("进度2"));
            QTimer::singleShot(1000, [&engine]() {
                QMetaObject::invokeMethod(engine.rootObjects().first(), "showStage",Qt::DirectConnection,QVariant(0.8),QVariant("进度3"));
                QTimer::singleShot(1000, [&engine]() {
                    QMetaObject::invokeMethod(engine.rootObjects().first(), "showMainPage");
                });
            });

        });
    });
    // 加载启动界面
    engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
    if (engine.rootObjects().isEmpty())
        return -1;
    return app.exec();
}
'@ | Out-File -FilePath main.cpp -Encoding utf8

代码解释

  • 使用QTimer::singleShot模拟异步初始化过程
  • 通过QMetaObject::invokeMethod调用QML中的函数更新进度
  • 分阶段更新进度,让用户了解启动状态
2、主QML文件main.qml
bash 复制代码
@'
import QtQuick 2.15
import QtQuick.Window 2.15
import QtQuick.Controls 2.15

Window {
    id: mainWindow
    width: 800
    height: 600
    visible: true
    flags: Qt.SplashScreen | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint  // 无边框,类似启动画面
    color: "transparent"  // 透明背景

    // 启动 Logo 界面
    Rectangle {
        id: startupScreen
        anchors.fill: parent
        color: "transparent"  // 深蓝色背景
        z: 10  // 确保在最上层

        // GIF 启动动画
        AnimatedImage {
            id: startupLogo
            source: "startup-logo.gif"
            width: 300
            height: 300
            anchors.centerIn: parent
            playing: true
            // Logo 背景(可选,根据需要添加)
            Rectangle {
                id: logoBackground
                anchors.fill: parent
                color: "transparent"  // 透明或纯色背景
                radius: 20  // 圆角(可选)
                z: -1  // 在 Logo 后面
            }
        }
        // 加载提示
        Text {
            id: loadingText
            text: qsTr("正在加载...")
            color: "#bdc3c7"
            font.pixelSize: 25
            anchors {
                top: startupLogo.bottom
                horizontalCenter: startupLogo.horizontalCenter
                topMargin: 10
            }
        }
        // 进度指示器
        ProgressBar {
            id: loadingProgress
            width: 300
            anchors {
                top: loadingText.bottom
                horizontalCenter: loadingText.horizontalCenter
                topMargin: 20
            }
            from: 0.0
            to: 100.0
            value: 0.0
            indeterminate: false
            visible: true
        }
    }
    // 主界面容器 - 初始隐藏
    Loader {
        id: mainInterfaceLoader
        active: false
        source: "MainPage.qml"
        onLoaded: {
            // 主界面加载完成后,调整窗口属性
            mainWindow.flags = Qt.Window | Qt.WindowStaysOnTopHint // 恢复正常窗口样式
            mainWindow.color = "#f0f0f0"  // 设置正常背景色
            mainWindow.visible=false
        }
    }
    function showStage(percent,name) {
        loadingProgress.value=percent*100
        loadingText.text=name
    }

    // 显示主界面的函数
    function showMainPage() {
        console.log("初始化完成,切换到主界面")
        startupLogo.visible = false
        mainInterfaceLoader.active = true
    }
}
'@ | Out-File -FilePath main.qml -Encoding utf8
3、主界面MainPage.qml
bash 复制代码
@'
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15

Window {
    id: mainInterface
    width: 800
    height: 600
    visible: true
    title: qsTr("我的应用")
    // 简单的主界面内容
    Rectangle {
        anchors.fill: parent
        color: "#ffffff"
        ColumnLayout {
            anchors.centerIn: parent
            spacing: 20
            Text {
                text: qsTr("欢迎使用")
                font.pixelSize: 32
                font.bold: true
                Layout.alignment: Qt.AlignHCenter
            }
            Button {
                text: qsTr("开始")
                Layout.alignment: Qt.AlignHCenter

                onClicked: {
                    console.log("应用已启动")
                }
            }
        }
    }
}
'@ | Out-File -FilePath MainPage.qml -Encoding utf8
4、项目配置文件QmlLogo.pro
bash 复制代码
$content = @'
QT += quick quickcontrols2
QT += qml
SOURCES += main.cpp
resources.files = main.qml MainPage.qml startup-logo.gif
resources.prefix = /
RESOURCES += resources
DISTFILES +=  MainPage.qml
'@

$utf8NoBom = New-Object System.Text.UTF8Encoding $false
$writer = New-Object System.IO.StreamWriter("QmlLogo.pro", $false, $utf8NoBom)
$writer.Write($content)
$writer.Close()
1、Logo生成脚本 gen_startup_logo.py
bash 复制代码
@'
from PIL import Image, ImageEnhance
import requests
from io import BytesIO
import math
import os

def download_image(url, save_path=None):
    try:
        response = requests.get(url)
        response.raise_for_status()
        image = Image.open(BytesIO(response.content)).resize((200,200), Image.Resampling.BILINEAR)
        if save_path:
            image.save(save_path)        
        return image
        
    except Exception as e:
        print(f"{e}")
        return None

def create_blinking_gif(image, output_gif_path, blink_count=16, duration=80, 
                       min_brightness=0.2, max_brightness=1.0, blink_pattern="sine"):    
    try:
        if image.mode != 'RGBA':
            image = image.convert('RGBA')
        frames = []
        
        for i in range(blink_count):
            if blink_pattern == "sine":
                brightness_factor = min_brightness + (max_brightness - min_brightness) * (0.5 + 0.5 * math.sin(i * math.pi / 4))
            elif blink_pattern == "square":
                brightness_factor = max_brightness if i % 2 == 0 else min_brightness
            elif blink_pattern == "triangle":
                phase = (i % 8) / 4.0
                brightness_factor = min_brightness + (max_brightness - min_brightness) * (1 - abs(phase - 1))
            else:
                brightness_factor = min_brightness + (max_brightness - min_brightness) * (0.5 + 0.5 * math.sin(i * math.pi / 4))
            enhancer = ImageEnhance.Brightness(image)
            frame = enhancer.enhance(brightness_factor)
            frames.append(frame)
        
        frames[0].save(
            output_gif_path,
            save_all=True,
            append_images=frames[1:],
            duration=duration,
            loop=0,
            optimize=True
        )        
        print(f"{output_gif_path}")
        return True
        
    except Exception as e:
        print(f"{e}")
        return False

if __name__ == "__main__":
    pikachu_url = "https://www.pngall.com/wp-content/uploads/5/Pikachu-PNG-Images.png"
    
    original_image_path = "pikachu_original.png"
    blinking_gif_path = "startup-logo.gif"
    pikachu_image = download_image(pikachu_url, original_image_path)
    
    if pikachu_image:
        success = create_blinking_gif(
            pikachu_image,
            blinking_gif_path,
            blink_count=16,
            duration=80,
            min_brightness=0.2,
            max_brightness=1.0,
            blink_pattern="sine")
        
        if success:            
            if os.path.exists(blinking_gif_path):
                file_size = os.path.getsize(blinking_gif_path) / 1024  # KB
                print(f"{file_size:.2f} KB")
'@ | Out-File -FilePath gen_startup_logo.py -Encoding utf8
2、运行脚本
bash 复制代码
python gen_startup_logo.py

第三步:编译与部署

1. 设置编译环境

打开 x64 Native Tools Command Prompt for VS 2022 (或其他对应版本):

切换到项目目录

2. 编译项目
bash 复制代码
C:\Qt\6.9.2\msvc2022_64\bin\qmake.exe .
nmake
3. 部署应用程序
bash 复制代码
cd release
# 使用windeployqt6自动拷贝依赖
C:\Qt\6.9.2\msvc2022_64\bin\windeployqt6.exe QmlLogo.exe
4. 运行测试
bash 复制代码
QmlLogo.exe
相关推荐
郝学胜-神的一滴3 小时前
Qt QPushButton 样式完全指南:从基础到高级实现
linux·开发语言·c++·qt·程序人生
love530love5 小时前
【笔记】xFormers版本与PyTorch、CUDA对应关系及正确安装方法详解
人工智能·pytorch·windows·笔记·python·深度学习·xformers
洛克希德马丁6 小时前
Qt 配置Webassemble环境
开发语言·qt·webassembly·emscripten·emsdk
自由的好好干活6 小时前
C#桌面框架与Qt对比及选型(国产操作系统开发视角)
开发语言·qt·c#
加上音乐6 小时前
windows—wsl2—docker配置代理以push/pull
windows·docker·容器
KWTXX7 小时前
制作qt小型交互音乐播放器
qt
我要升天!7 小时前
QT -- 初识
开发语言·qt
CodeCraft Studio7 小时前
Excel处理控件Aspose.Cells教程:如何使用C#在Excel中添加、编辑和更新切片器
ui·c#·excel·aspose·excel切片器·创建表格切片器
2501_916007479 小时前
如何在 Windows 电脑上调试 iOS 设备上的 Safari?完整方案与实战经验分享
android·windows·ios·小程序·uni-app·iphone·safari