为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
相关推荐
小涛不学习7 小时前
手写线程池(从0实现 ThreadPoolExecutor 核心思想)
windows
twc8297 小时前
大模型生成 QA Pairs 提升 RAG 应用测试效率的实践
服务器·数据库·人工智能·windows·rag·大模型测试
wenlonglanying9 小时前
Windows安装Rust环境(详细教程)
开发语言·windows·rust
polaris063010 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat
mldlds10 小时前
windows手动配置IP地址与DNS服务器以及netsh端口转发
服务器·windows·tcp/ip
取个名字太难了a10 小时前
DebugActiveProcess 调试流程分析(一)
windows
娇娇yyyyyy10 小时前
QT编程(15): Qt 按键事件和定时器事件
开发语言·qt
Java.熵减码农11 小时前
火绒安全软件误杀explorer.exe导致黑屏解决方法
windows
love530love11 小时前
不用聊天软件 OpenClaw 手机浏览器远程访问控制:Tailscale 配置、设备配对与常见问题全解
人工智能·windows·python·智能手机·tailscale·openclaw·远程访问控制
夏末蝉未鸣0111 小时前
Windows环境下载并安装milvus
windows·milvus