多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Qt启动动画兼容图片与视频的工程实现与性能优化

Qt启动动画兼容图片与视频的工程实现与性能优化 给 Qt 软件启动动画加上一段开场同时兼容图片和视频两种方式是我这两年在好几个桌面端项目里反复做过的活。起因基本都一样客户或者产品经理看了默认那个灰底白字的 QSplashScreen觉得太素希望软件一打开就像正经商业软件那样有一段几秒的开场画面。听起来是个小需求但真正动手就会发现里面有几个绕不开的坎——视频窗口为什么黑屏、动图为什么卡在最后一帧、主窗口为什么先闪一下白底再出现、装到别人机器上为什么解码失败。这些问题查文档基本查不到只能自己踩。这篇文章就把我踩过的坑一次性讲清楚。内容覆盖技术选型逻辑、图片与 GIF 的几种实现路径、QMediaPlayer 配 QGraphicsVideoItem 播视频的完整代码、主窗口接管时机的时序控制以及启动耗时和内存占用的实测对比。代码以 Qt 5.15.2 为基准同时标注 Qt 6 的差异点因为现在不少项目已经切到 Qt 6而 QtMultimedia 在 6.x 上做过一次大重构后端和信号名都变了照着 5.x 的教程抄会直接编译不过。适合手上正要做启动动画、或者做出来效果不对想排查的同学。1. 启动动画的定位与技术选型思路1.1 启动动画到底该不该做它解决什么问题先说个容易被忽略的事实启动动画本身不解决任何功能问题它解决的是感知问题。一个 Qt 客户端冷启动如果要把配置文件读一遍、数据库连上、插件扫描完、缓存预热好很容易就是 800 毫秒到 3 秒。这段时间里如果屏幕上什么都没有用户第一反应是卡死了第二反应是再点一次图标然后开出两个进程。所以启动动画的核心价值有三个第一用一个明确的视觉信号告诉用户程序在动别急第二遮住主窗口构建时那段不体面的空白和闪烁第三顺便承载品牌信息比如 logo、版本号、公司名。理解了这三点后面很多设计决策就顺了——比如为什么启动动画必须比初始化流程先出现、为什么它必须无条件在规定时间内退场。反过来说如果你的程序冷启动只要 200 毫秒那加启动动画就是负优化用户还没看清画面主界面就蹦出来了反而显得闪一下。这类程序老老实实优化启动速度更实在。我的判断标准是从点击图标到主窗口可交互超过 600 毫秒才值得上启动动画。1.2 图片方案与视频方案的能力边界对比图片和视频这两条路不是哪个高级用哪个而是能力边界完全不同选错了会把自己坑死。下面这张表是我在实际项目里总结出来的对照数字都是量级参考不是精确值维度静态图片 (PNG)动态图 (GIF)视频 (H.264 MP4)依赖模块QtGuiQtGuiQtMultimedia MultimediaWidgets资源体积50 KB ~ 500 KB1 MB ~ 8 MB1 MB ~ 5 MB首帧显示延迟 20 ms 50 ms300 ms ~ 900 ms单核 CPU 占用接近 05% ~ 15%硬解 5%软解 15% ~ 30%透明背景支持 alpha支持 alpha基本不支持能否带声音否否可以但启动画面建议静音部署复杂度极低极低中依赖系统解码器观感上限一般一般高可做完整片头看这张表能得出很直接的结论如果只是想让启动画面别那么寒酸PNG 加一行进度文字就够了如果想做出轻微的呼吸感、旋转 loadingGIF 是最划算的选择代价是体积膨胀如果产品真的要求开场三秒短片有镜头运动有转场那只有视频这条路但要接受启动延迟增加和部署环境不确定性。我个人的默认策略是PNG 打底GIF 进阶视频只在明确要求下才上。原因很现实——视频方案在客户机器上翻车的概率比另外两种加起来还高而且排查成本极大你没法远程看他的解码器状态。1.3 工程目录与模块依赖规划开工前先把目录和依赖定好后面改起来省事。我习惯的目录结构是这样myapp/ ├── src/ │ ├── main.cpp │ ├── mainwindow.cpp │ ├── splash/ │ │ ├── splashmanager.h │ │ ├── splashmanager.cpp │ │ ├── imagesplash.h │ │ ├── gifsplash.h │ │ └── videosplash.h ├── resources/ │ ├── app.qrc │ ├── splash.png │ ├── loading.gif │ └── intro.mp4 └── myapp.pro把 splash 相关的类全部收在一个子目录里好处是将来要换方案直接替换这个目录不用翻遍整个工程。对应的 qmake 配置QT core gui widgets QT multimedia multimediawidgets CONFIG c11如果用的是 CMake对应写法是find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Multimedia MultimediaWidgets) target_link_libraries(myapp PRIVATE Qt6::Widgets Qt6::Multimedia Qt6::MultimediaWidgets)注意multimediawidgets 这个模块很容易被漏掉。QVideoWidget 和 QGraphicsVideoItem 都属于它而不是 multimedia。只加 multimedia 的话编译时报的就是 unknown module 或者找不到 QVideoWidget 头文件。还有一个坑Qt 5.15 起部分离线安装包默认不勾选 QtMultimedia 组件装完发现QT multimedia报 unknown module。这时候要重新跑一遍维护工具MaintenanceTool在组件列表里把 Multimedia 勾上。别硬扛这个模块没法手动补。2. 图片方式从静态 PNG 到动态 GIF 的完整实现2.1 用 QSplashScreen 三行代码拉起静态启动图Qt 自带的 QSplashScreen 是被严重低估的一个类。很多人以为它只能显示一张图其实它内置了消息文字、进度绘制、点击穿透、置顶显示这几件事而且它天生就是启动画面这个语义finish()方法会自动等主窗口显示后再关闭自己时序处理得很干净。最简版本长这样#include QApplication #include QSplashScreen #include QPixmap #include QThread int main(int argc, char *argv[]) { QApplication app(argc, argv); QPixmap pixmap(:/resources/splash.png); QSplashScreen splash(pixmap, Qt::WindowStaysOnTopHint); splash.setWindowFlag(Qt::FramelessWindowHint); splash.show(); // 手动吐一次事件循环让窗口真正绘制出来 app.processEvents(); // 模拟一段耗时初始化 QThread::msleep(800); MainWindow w; w.show(); splash.finish(w); // 主窗口显示后再关掉启动画面 return app.exec(); }这里有个细节必须强调splash.show()之后如果不调app.processEvents()紧接着就跑耗时初始化那么启动画面根本没机会绘制等初始化完才一起出现等于白做。processEvents()的作用是把当前积压的绘制事件先处理掉让窗口真正上屏。另一个细节是splash.finish(w)的位置。它的内部实现在主窗口显示之前会一直等所以顺序一定是先w.show()再splash.finish(w)。写反了会出现启动画面先消失、主窗口后出现的空档视觉上就是闪一下黑。2.2 QSplashScreen 的进阶玩法进度文字与自绘进度条QSplashScreen 支持showMessage()显示一行文字但它默认的字体和颜色很不讲究白底图上直接就是白字看不清。要好看一点得重写drawContents()自己画。下面这个类我用了好几个项目直接拿走能用#include QSplashScreen #include QPainter class ProgressSplash : public QSplashScreen { public: explicit ProgressSplash(const QPixmap pixmap) : QSplashScreen(pixmap, Qt::WindowStaysOnTopHint) { setWindowFlag(Qt::FramelessWindowHint); } void setProgress(int value) { m_progress qBound(0, value, 100); repaint(); // 关键repaint 而不是 update立刻重绘 } void setTip(const QString text) { m_tip text; repaint(); } protected: void drawContents(QPainter *painter) override { QSplashScreen::drawContents(painter); painter-setRenderHint(QPainter::Antialiasing, true); // 底槽 QRect barRect(40, height() - 72, width() - 80, 10); painter-setPen(Qt::NoPen); painter-setBrush(QColor(255, 255, 255, 55)); painter-drawRoundedRect(barRect, 5, 5); // 进度条本体 if (m_progress 0) { QRect fillRect barRect; fillRect.setWidth(barRect.width() * m_progress / 100); painter-setBrush(QColor(0, 160, 233)); painter-drawRoundedRect(fillRect, 5, 5); } // 提示文字 if (!m_tip.isEmpty()) { painter-setPen(QColor(240, 240, 240)); QFont f painter-font(); f.setPointSize(9); painter-setFont(f); painter-drawText(QRect(40, height() - 56, width() - 80, 24), Qt::AlignLeft | Qt::AlignVCenter, m_tip); } } private: int m_progress 0; QString m_tip; };drawContents()是 QSplashScreen 预留的虚函数参数里的 painter 已经平移到了图片的坐标系直接画就行。关键点更新进度一定要用repaint()而不是update()。update()只是投递一个重绘请求要等事件循环跑起来才执行。而启动阶段主线程往往被初始化代码占着事件循环根本没在转用update()会出现进度条一动不动初始化完直接跳满的尴尬效果。2.3 GIF 动图方案与自绘 SplashWidgetQSplashScreen 本身只认 QPixmap播不了 GIF。要播动图就得自己写一个 Widget用 QMovie 驱动。这类需求我做过好几次下面这个 GifSplash 是比较稳的版本#include QWidget #include QMovie #include QPainter #include QScreen #include QGuiApplication class GifSplash : public QWidget { Q_OBJECT public: explicit GifSplash(const QString gifPath, const QSize size, QWidget *parent nullptr) : QWidget(parent, Qt::SplashScreen | Qt::FramelessWindowHint) { // 透明背景让 GIF 的 alpha 通道生效 setAttribute(Qt::WA_TranslucentBackground); setAttribute(Qt::WA_DeleteOnClose); // 屏蔽鼠标事件防止用户点击干扰 setAttribute(Qt::WA_TransparentForMouseEvents); m_movie new QMovie(gifPath, QByteArray(), this); m_movie-setCacheMode(QMovie::CacheAll); m_movie-setScaledSize(size); connect(m_movie, QMovie::frameChanged, this, [this](int) { update(); }); resize(size); centerOnPrimaryScreen(); } void startPlay() { m_movie-start(); } void centerOnPrimaryScreen() { if (QScreen *screen QGuiApplication::primaryScreen()) { const QRect geo screen-availableGeometry(); move(geo.center() - rect().center()); } } protected: void paintEvent(QPaintEvent *) override { QPainter p(this); p.setRenderHint(QPainter::SmoothPixmapTransform, true); p.drawPixmap(rect(), m_movie-currentPixmap()); } private: QMovie *m_movie nullptr; };这里有三个设计点值得说明。WA_TranslucentBackground是为了让 GIF 的透明区域真的透明配合无边框窗口使用WA_TransparentForMouseEvents是为了让启动画面期间用户的点击穿透到桌面避免误触发setCacheMode(QMovie::CacheAll)是把解码后的帧全部缓存到内存里代价是内存占用上升收益是播放期间不再有解码抖动。QMovie 默认loopCount是 -1也就是无限循环。启动动画一般不需要循环播完就退场所以要么setLoopCount(1)要么干脆不管它——因为退场时机由主窗口的初始化进度决定跟播放次数无关。动图素材这块我踩过一个坑网上下的 GIF 经常是 24 位色加索引调色板在深色主题的启动背景上边缘会有明显的锯齿和白边。解决办法是把 GIF 背景做成和启动画面同色而不是透明或者干脆用 APNGQt 的 QMovie 也支持 APNG只是文件大不少。2.4 资源打包与 qrc 的那些细节不管图片还是视频都建议走 Qt 资源系统qrc而不是相对路径读文件。原因有三个第一发布时不用管资源目录结构全打进可执行文件或者外部 rcc第二不存在工作目录被改导致找不到文件的问题第三访问速度比磁盘 IO 快。qrc 文件写法RCC qresource prefix/resources file aliassplash.pngsplash.png/file file aliasloading.gifloading.gif/file file aliasintro.mp4intro.mp4/file /qresource /RCC然后在 pro 里加RESOURCES app.qrc。这里有个必须提醒的事视频文件打进 qrc 之后QMediaPlayer 不一定能直接播。原因是 QMediaPlayer 的底层后端Windows 上是 Media FoundationLinux 上是 GStreamer走的是本地文件路径或流对qrc:/这种虚拟路径支持不一致。实测在 Windows Qt 5.15 上QMediaPlayer是读不了 qrc 里的 mp4的会静默失败mediaStatus停在NoMedia。所以视频素材要么放到可执行文件旁边的 resources 目录用QCoreApplication::applicationDirPath()拼绝对路径要么在程序启动时把 qrc 里的视频先拷贝到临时目录再播。我一般选前者简单直接const QString videoPath QCoreApplication::applicationDirPath() /resources/intro.mp4;注意QDir::currentPath()和applicationDirPath()不是一回事。前者是进程的工作目录双击图标启动时可能是桌面或者别的什么位置后者才是 exe 所在目录。启动动画读资源一定要用后者。3. 视频方式QMediaPlayer 播放启动短片3.1 模块依赖与后端环境准备视频这条路的第一道门槛不是代码是环境。QMediaPlayer 是个壳真正解码的是底层后端不同平台差异巨大必须先搞清楚。Windows 上Qt 5.15 默认使用WMFWindows Media Foundation后端也就是系统自带的解码框架。好处是 H.264 MP4 开箱即播不用带任何额外 DLL坏处是只支持系统装的解码器某些冷门编码比如 H.265、VP9在老系统上直接播不了。所以视频素材一律用 H.264 编码的 MP4这是兼容性最好的选择。Linux 上是 GStreamer 后端需要确保系统装了gstreamer1.0-plugins-good、gstreamer1.0-plugins-bad、gstreamer1.0-libav这几个包。少一个都可能出现有声音没画面或者完全黑屏的现象。国内发行版如果没有预装apt 一行搞定。Qt 6 这边情况变了。Qt 6.5 起QtMultimedia 引入了FFmpeg 后端跨平台行为统一了很多不再依赖系统解码器。这是个大改进如果你的项目能上 Qt 6.5视频方案的部署问题基本消失了。但如果你在 Qt 6.0 到 6.4 之间那还是平台后端问题照旧。Qt 版本Windows 后端Linux 后端备注Qt 5.15WMFGStreamer依赖系统解码器Qt 6.0 ~ 6.4WMFGStreamer依赖系统解码器Qt 6.5FFmpegFFmpeg自带解码能力推荐3.2 QGraphicsVideoItem 构建无边框视频窗口视频播放的显示载体有两条路QVideoWidget 和 QGraphicsVideoItem。前者更简单后者更灵活。做启动画面我强烈推荐 QGraphicsVideoItem原因是QVideoWidget 是个原生窗口native window没法做透明、没法叠加普通 QWidget、图形效果也基本不生效。而 QGraphicsVideoItem 走的是 QGraphicsScene 体系可以自由叠加图层、做缩放和裁剪配合setAspectRatioMode还能控制填充模式。下面是完整的 VideoSplash 实现#include QWidget #include QGraphicsScene #include QGraphicsView #include QGraphicsVideoItem #include QMediaPlayer #include QScreen #include QGuiApplication #include QGraphicsOpacityEffect #include QPropertyAnimation class VideoSplash : public QWidget { Q_OBJECT public: explicit VideoSplash(const QString filePath, QWidget *parent nullptr) : QWidget(parent, Qt::SplashScreen | Qt::FramelessWindowHint) { setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_ShowWithoutActivating); m_scene new QGraphicsScene(this); m_item new QGraphicsVideoItem; m_item-setAspectRatioMode(Qt::KeepAspectRatioByExpanding); m_scene-addItem(m_item); m_view new QGraphicsView(m_scene, this); m_view-setFrameShape(QFrame::NoFrame); m_view-setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view-setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_view-setStyleSheet(background:transparent;border:0;); m_player new QMediaPlayer(this); m_player-setVideoOutput(m_item); m_player-setVolume(0); // 启动动画静音 #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) m_player-setMedia(QUrl::fromLocalFile(filePath)); connect(m_player, QMediaPlayer::errorOccurred, this, [this](QMediaPlayer::Error, const QString msg) { emit failed(msg); }); #else m_player-setMedia(QMediaContent(QUrl::fromLocalFile(filePath))); connect(m_player, QOverloadQMediaPlayer::Error::of(QMediaPlayer::error), this, [this](QMediaPlayer::Error) { emit failed(m_player-errorString()); }); #endif connect(m_player, QMediaPlayer::mediaStatusChanged, this, [this](QMediaPlayer::MediaStatus status) { if (status QMediaPlayer::EndOfMedia) emit finished(); }); } void start(const QSize size) { resize(size); m_item-setSize(size); m_scene-setSceneRect(0, 0, size.width(), size.height()); m_view-resize(size); centerOnPrimaryScreen(); show(); m_player-play(); } void fadeOutAndClose(int durationMs 300) { auto *effect new QGraphicsOpacityEffect(this); setGraphicsEffect(effect); auto *anim new QPropertyAnimation(effect, opacity, this); anim-setDuration(durationMs); anim-setStartValue(1.0); anim-setEndValue(0.0); anim-setEasingCurve(QEasingCurve::OutCubic); connect(anim, QPropertyAnimation::finished, this, QWidget::close); anim-start(QAbstractAnimation::DeleteWhenStopped); } signals: void finished(); void failed(const QString message); private: void centerOnPrimaryScreen() { if (QScreen *screen QGuiApplication::primaryScreen()) { const QRect geo screen-availableGeometry(); move(geo.center() - rect().center()); } } QGraphicsScene *m_scene nullptr; QGraphicsView *m_view nullptr; QGraphicsVideoItem *m_item nullptr; QMediaPlayer *m_player nullptr; };几个关键点解释一下。KeepAspectRatioByExpanding的意思是视频按比例缩放直到铺满整个 item 区域超出部分裁剪掉。启动动画一般希望画面铺满全屏或者铺满一个固定尺寸的窗口周围不留黑边所以用 Expanding 比 KeepAspectRatio 更合适。WA_ShowWithoutActivating这个属性容易被忽略。它让启动窗口显示时不抢焦点。因为启动阶段主窗口还在构造抢焦点会导致后续主窗口显示时焦点状态混乱特别是带输入框的界面。fadeOutAndClose里用了 QGraphicsOpacityEffect。这里要提醒一句图形特效对视频渲染不一定生效。因为视频帧在某些后端上是直接送到窗口的渲染层不走 QWidget 的绘制管线。实测在 Qt 5.15 WMF 后端上淡出动画对 QVideoWidget 完全无效对 QGraphicsVideoItem 时好时坏。如果淡出效果是硬需求稳妥的做法是用一个纯色的顶层 Widget 做遮罩淡出而不是给视频本身加特效。3.3 播放时序控制从首帧到主窗口接管视频方案最难的部分不是播放是什么时候让主窗口接管。这里有个反直觉的点视频播放本身占用主线程的事件循环如果同时做重初始化视频会卡帧用户看到的就是一顿一顿的片头比不做还难看。我的处理方式是分片初始化把初始化拆成一组小任务用 QTimer 逐个执行每个任务之间让事件循环喘口气。#include QApplication #include QTimer #include QVector #include functional int main(int argc, char *argv[]) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif QApplication app(argc, argv); const QString videoPath QCoreApplication::applicationDirPath() /resources/intro.mp4; VideoSplash splash(videoPath); MainWindow window; bool handoverDone false; auto handover []() { if (handoverDone) return; handoverDone true; window.show(); splash.fadeOutAndClose(); }; // 视频自然播完 QObject::connect(splash, VideoSplash::finished, handover); // 视频出错兜底退场 QObject::connect(splash, VideoSplash::failed, [](const QString msg) { qWarning() splash video failed: msg; handover(); }); // 分片初始化任务 QVectorstd::functionvoid() tasks { [] { /* 读配置文件 */ }, [] { /* 打开数据库连接 */ }, [] { /* 扫描插件目录 */ }, [] { /* 预热缓存 */ }, }; auto *pump new QTimer(app); int index 0; QObject::connect(pump, QTimer::timeout, []() { if (index tasks.size()) { tasks[index](); } else { pump-stop(); handover(); // 任务跑完正常交接 } }); // 硬性超时4 秒必须退场绝不让用户干等 QTimer::singleShot(4000, handover); splash.start(QSize(960, 540)); // 16:9匹配视频比例 pump-start(30); // 30ms 一片足够视频流畅播放 return app.exec(); }这段代码里有三个兜底缺一不可。第一个兜底是视频播放失败——用户机器上解码器缺失、文件损坏都可能触发这时候不能卡死直接进主界面。第二个兜底是初始化任务跑完——如果任务比视频短提前交接不用等视频播完。第三个兜底是硬性超时——如果某个任务卡住了比如数据库连不上在等超时4 秒后强制交接保证用户体验。pump-start(30)这个 30 毫秒也很讲究。太短了比如 0 或 1 毫秒事件循环被初始化任务挤占视频掉帧太长了比如 200 毫秒初始化总时长被拉长用户等得久。30 毫秒大约对应 33 帧的节奏够用。3.4 视频素材的制作与编码参数建议素材这关我自己也折腾过很久最后固定下来一套 ffmpeg 参数基本没出过问题ffmpeg -i raw_intro.mp4 \ -c:v libx264 \ -profile:v main \ -level 4.0 \ -pix_fmt yuv420p \ -g 15 \ -bf 0 \ -crf 23 \ -an \ -movflags faststart \ intro.mp4逐个参数说下为什么。-profile:v main加上-pix_fmt yuv420p是兼容性底线几乎所有后端都能解-g 15把关键帧间隔设成 15 帧意味着拖到任意位置最多 15 帧就能出画对首帧显示速度有帮助-bf 0关掉 B 帧解码器不用做双向预测起播更快-crf 23是画质和体积的平衡点-an去掉音轨启动画面不需要声音还能省几百 KB-movflags faststart是最重要的一条它把 moov atom记录视频元数据的那块从文件末尾挪到文件开头播放器不用扫完整个文件就能开始解码。另外几个经验值视频时长控制在2 到 4 秒再长用户就不耐烦了分辨率不要超过窗口实际尺寸你播 4K 素材但窗口只有 960x540等于白白浪费解码算力文件体积控制在3 MB 以内超过这个数加载 IO 会明显拖慢起播。提示如果说手上只有一张静态图但需要做出视频感可以用 ffmpeg 的 zoompan 滤镜做缓慢推镜几行命令就能生成一段有镜头运动的短片比真拍一段省事得多。4. 两种方案硬碰硬性能实测与坑位排查4.1 启动耗时、内存与 CPU 占用实测我在一台 i5-8250U、8GB 内存、Windows 10、SSD 的笔记本上做过一组对比测试测试方法是把主窗口初始化固定在 1500 毫秒记录从main()第一行到主窗口可交互的耗时以及启动动画期间的峰值资源占用。测试项静态 PNG (1280x720)GIF (640x360, 45帧)MP4 (960x540, 3秒)首帧可见耗时18 ms42 ms610 ms内存峰值增量约 12 MB约 48 MB约 65 MB启动动画期间 CPU 2%8% ~ 14%硬解 3%软解 22%主窗口接管总耗时1520 ms1545 ms1680 ms冷启动二次波动很小小较大首次解码器初始化慢几个数据背后的解释。静态图的 12 MB 是 1280x720 的位图解码结果正好是1280 × 720 × 4 字节 ≈ 3.6 MB加上 Qt 内部的纹理拷贝。GIF 的 48 MB 是因为CacheAll把 45 帧全缓存了如果换成CacheNone内存能降到 15 MB 左右代价是 CPU 上去、偶尔掉帧。视频的 65 MB 主要来自解码缓冲和显存占用硬解时 CPU 很低但显存吃得多。还有一个容易被忽略的指标冷启动第一次的视频解码器初始化特别慢。因为 WMF 或 FFmpeg 要加载一堆 DLL 和初始化硬件加速上下文第一次可能要 800 毫秒以上第二次就降到 400 毫秒左右。这就意味着用户每次开机后第一次打开你的软件片头动画会明显延迟出现。这个只能接受没法优化。4.2 常见问题速查表下面这张表是我这些年遇到过的所有启动动画相关问题的汇总按出现频率排序现象根本原因解决办法视频黑屏但有声音缺解码后端或缺 gstreamer 插件Windows 换 H.264 MP4Linux 装 gstreamer1.0-libav视频完全没反应无报错qrc 路径 QMediaPlayer 读不了改用 applicationDirPath() 拼绝对路径mediaStatus 卡在 NoMedia同上或者文件权限被拒打印 errorString() 定位检查文件与 rcc 的存在启动画面根本没出现没调 processEvents()初始化把事件循环堵死show() 之后立刻 processEvents()进度条不动结束后直接跳满用了 update() 而不是 repaint()换成 repaint() 强制同步重绘GIF 播完停在最后一帧QMovie 循环相关设置setLoopCount(-1) 或干脆不管靠主窗口接管GIF 边缘有白边GIF 调色板不支持真透明用同色背景替代透明或改 APNG/PNG 序列高 DPI 下图片糊没开 High DPI 缩放也没提供高分素材设 AA_EnableHighDpiScaling准备 2x 素材多显示器时启动画面跑到副屏直接 move 到固定坐标用 primaryScreen()-availableGeometry() 计算居中主窗口出现前闪一下白主窗口背景未设置显示瞬间才绘制设主窗口背景色、或在 show 前先 resizemove主窗口显示后启动画面还在finish() 和 show() 顺序反了先 window.show()再 splash.finish(window)启动画面被其它窗口盖住没有置顶标志构造时传 Qt::WindowStaysOnTopHint视频淡出效果无效QGraphicsOpacityEffect 对视频层不生效改用一个纯色遮罩 Widget 做淡出用户点击启动画面触发了主界面操作没有屏蔽鼠标事件setAttribute(Qt::WA_TransparentForMouseEvents)4.3 高 DPI、多屏与无边框的三个隐蔽坑这三个问题单独拎出来说因为它们的表现都不在启动动画这个关键词上很容易查偏方向。第一个是高 DPI。在 150% 缩放的显示器上如果你没开AA_EnableHighDpiScalingQt 会把 1280x720 的图片按逻辑像素处理实际显示出来是 853x480 逻辑像素撑满屏幕看起来就是整体模糊加锯齿。反过来如果你开了缩放但只提供了 1x 素材Qt 会做上采样同样模糊。正确做法是开缩放 提供 2x 素材qrc 里写成splash2x.pngQt 会自动按需选择。第二个是多屏。QSplashScreen默认会显示在主屏中央但如果用户在设置里把主屏改成了副屏或者显示器排列是上下结构直接move(固定坐标)就会跑到屏幕外面去。统一用这段代码算位置QScreen *screen QGuiApplication::primaryScreen(); const QRect geo screen-availableGeometry(); // 排除任务栏 move(geo.center() - rect().center());注意是availableGeometry()而不是geometry()前者排除了任务栏区域后者包含用错了启动画面会被任务栏遮住一点。第三个是无边框窗口的焦点问题。设置Qt::FramelessWindowHint之后Windows 上这个窗口可能不会自动出现在任务栏也不会自动获得焦点。对于启动画面来说这其实是好事但有个副作用如果启动画面显示期间用户用 AltTab 切换可能切不到这个窗口视觉上就是启动画面不见了但程序还没起来。要避免这种情况可以给启动画面加Qt::Tool属性或者干脆接受这个行为——毕竟启动画面只存在几秒钟。5. 把启动动画封装成可复用组件5.1 SplashManager 接口设计前面三套实现如果每次都往 main() 里塞工程会越来越乱。我一般会封一个 SplashManager 出来把方案选择、超时兜底、进度上报、淡出交接全部收进去对外只暴露几个方法// splashmanager.h #pragma once #include QObject #include QSize #include QPointer class SplashManager : public QObject { Q_OBJECT public: enum Mode { Image, Gif, Video }; explicit SplashManager(QObject *parent nullptr); ~SplashManager() override; // maxMs 是硬性超时到点无条件交接 bool start(Mode mode, const QString resPath, const QSize size, int maxMs 4000); void setProgress(int value); // 0~100仅图片模式有效 void setMessage(const QString text); void handoverTo(QWidget *mainWindow); // 交给主窗口做淡出 signals: void finished(); private: void onNativeFinished(); QPointerQWidget m_splash; QPointerQWidget m_target; QTimer *m_timeout nullptr; bool m_done false; };核心逻辑是start()根据 Mode 创建对应的 splash 对象并显示同时启动一个maxMs的超时定时器handoverTo()记录目标窗口等 splash 自然结束或超时后显示目标窗口并关闭 splashm_done布尔量保证交接只发生一次避免超时和自然结束同时触发导致两次 show。这个m_done标志看起来多余实际上是必需的。我最早就是没加结果视频 3 秒结束和 4 秒超时几乎同时触发主窗口被 show 了两次在某些平台上会闪一下。5.2 与主窗口初始化流程的协作方式SplashManager 只负责动画初始化任务流是另一套东西。我习惯再做一个 Bootstrap 类把两者串起来class Bootstrap : public QObject { Q_OBJECT public: void run(SplashManager *splash, QWidget *mainWindow) { QVectorstd::functionvoid() tasks buildTaskList(); auto *pump new QTimer(this); int index 0; connect(pump, QTimer::timeout, this, []() mutable { if (index tasks.size()) { tasks[index](); splash-setProgress(index * 100 / tasks.size()); } else { pump-stop(); splash-handoverTo(mainWindow); } }); pump-start(30); } private: QVectorstd::functionvoid() buildTaskList(); };把任务列表和 UI 分开之后加一个初始化步骤就是往buildTaskList()里加一行完全不用碰动画代码。这套结构我在三个项目里复用没改过。有一点要提醒分片任务里不要做 UI 操作。因为任务是在事件循环的回调里跑的这时候界面可能还没完全就绪碰 UI 容易出各种偶发问题。任务里只做数据层面的准备UI 相关全部留到主窗口的构造函数或者showEvent()里。5.3 一些实战体会做启动动画这几年我最大的感受是它是个细节活不是功能活。功能上半小时就能跑通但要做得不露破绽让用户看不出有启动延迟、看不出有窗口切换得反复调。几个印象比较深的教训。给一个工业控制软件做启动动画客户要求用视频现场机器是集成显卡加老驱动硬解直接失败回落到软解1080p 视频把 CPU 干到 40%整个启动过程拖到 8 秒。最后是把视频降到 720p、时长压到 2.5 秒才勉强能用。那次之后我定了个规矩视频素材的分辨率上限是主窗口设计尺寸绝不做超采样。还有一个是给触屏设备做的启动画面用 GIF 做了个旋转 loading。上线后用户反馈说点屏幕没反应。排查半天发现是 GIF 有 60 多帧CacheAll缓存了 200 多 MB在 2GB 内存的设备上直接把内存打满系统开始疯狂 swap。改成CacheNone加降低帧率之后正常了。这件事告诉我移动端和嵌入式设备上GIF 的帧数和分辨率必须严格控CacheAll这个选项要慎用。再有就是退出环节。有次做视频启动动画用户从启动画面直接点了关闭按钮想退出程序结果视频还在播主窗口没构造完退出流程走到一半崩了。后来在 splash 上加了按键拦截启动期间按 Esc 或者点关闭一律忽略等主窗口起来之后再交给它处理。这种边缘场景不做测试基本想不到。如果这几套代码你打算直接拿去用我建议先把图片方案跑通确认整个启动流程的事件循环、进程退出、窗口切换都正常了再往上加 GIF最后才是视频。因为视频方案的失败模式最多如果一开始就上视频出了问题你会分不清是流程问题还是解码问题排查成本会翻倍。
返回列表