QSplashScreen animation
-
@VRonin said in QSplashScreen animation:
this
while(time.elapsed() < 5450){ QApplication::processEvents(); }
is useless.
Actually, not entirely useless, this garanties at least 5 seconds of splashscreen. And processEvents was so only way I managed to update the animation.
GUI operations are only allowed in the same thread the
QApplication
lives so the multi-thread is not feasible.I'd say the fastest way here is to move the splash to a separate binary altogether and use QProcess to control it
That was what I suspected. The question I have to that, is there a way to also close the seperated Process if the user terminated the app in an unusual way, e.g. forces a close via taskmanager?
-
So, I think I found a solution that works.
I'm not sure however, if it is the most elegant solution.I turned the SplshScreen animation into its own program, compiled that as an executable:
The Splash.exe and my main program have each a
QSharedMemory
object.
The QSharedMemory is aparently also freed, when I kill my main program via TaskManager.After 5 seconds, I check from my splash app if the Object was created/is maintained from my main program. If it isn't -> cancel program else wait for cancle from main program.
jhSplash::jhSplash(QWidget *parent) : QWidget(parent) { _MainApp = new QSharedMemory("myAppSingleInstance",this); this->setWindowFlags( Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::WindowTransparentForInput); this->setAttribute(Qt::WA_TranslucentBackground,true); m_Img = QImage(":/Logo.png"); this->resize(m_Img.size()); m_updateTimer = new QTimer(this); connect(m_updateTimer, SIGNAL(timeout()), this, SLOT(repaint())); m_updateTimer->start(6); this->show(); QTimer::singleShot(5000,this,[=]{m_CheckMemory = true;}); } jhSplash::~jhSplash() { } void jhSplash::paintEvent(QPaintEvent *event) { int penWidth = 6; QRectF oRect (0,0,this->width(), this->height()); QPainter painter(this); painter.setBackgroundMode(Qt::TransparentMode); painter.drawImage(0,0,m_Img); QString credits("Credits"); QFont f = painter.font(); f.setPixelSize(16); painter.setFont(f); QFontMetrics fMetric (painter.font()); QRect rec = fMetric.boundingRect(credits); painter.drawText((oRect.width()-rec.width())/2,(oRect.height()-rec.height()*1),credits); QPen pen; pen.setWidth(penWidth); pen.setColor(Qt::black); painter.setPen(pen); QRect arcRect(penWidth/2,penWidth/2,this->width()-penWidth,this->height()-penWidth); m_drawAngle = m_drawAngle +16; int startAngle = 0+m_drawAngle; int spanAngle = (5760 -960)*(-1); painter.drawArc(arcRect,startAngle,spanAngle); pen.setColor(Qt::red); painter.setPen(pen); startAngle = 0 + m_drawAngle; spanAngle = 960; painter.drawArc(arcRect,startAngle,spanAngle); if(m_CheckMemory){ if(!_MainApp->attach(QSharedMemory::ReadOnly)){ this->close(); } _MainApp->detach(); } QWidget::paintEvent(event); }
//Main Application main.cpp QElapsedTimer time; time.start(); QApplication::setAttribute(Qt::AA_Use96Dpi); QApplication a(argc, argv); QProcess splash; splash.start("path/to/Splash/Splash.exe"); uConfig w; if(!w.lock()) return -42; while(time.elapsed() < 5450){ } w.show(); splash.close(); QObject::connect(&a, &QApplication::aboutToQuit, &splash, &QProcess::close); return a.exec();
-
While I like your solution best, below an alternative based on a heartbeat socket:
Splash App
splashwidget.h
#ifndef SPLASHWIDGET_H #define SPLASHWIDGET_H #include <QWidget> #include <QTimer> #include <QLocalSocket> #include <QApplication> #include <QDataStream> class SplashWidget : public QWidget { Q_OBJECT Q_DISABLE_COPY(SplashWidget) enum {ContinueChar = 88}; public: SplashWidget(const QString& serverName, QWidget* parent= Q_NULLPTR) :QWidget(parent) ,m_serverName(serverName) { init(); } virtual ~SplashWidget() {} protected: void init(){ setWindowFlags( Qt::WindowStaysOnTopHint | Qt::FramelessWindowHint | Qt::WindowTransparentForInput); setAttribute(Qt::WA_TranslucentBackground,true); m_hbTimer = new QTimer(this); connect(m_hbTimer,&QTimer::timeout,this,&SplashWidget::resetTimer); m_hbTimer->setInterval(1000); // 1 sec m_hbReceiver = new QLocalSocket(this); connect(m_hbReceiver, static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error),qApp,&QApplication::quit); connect(m_hbReceiver, &QLocalSocket::disconnected,qApp,&QApplication::quit); connect(m_hbReceiver, &QLocalSocket::readyRead, this, &SplashWidget::resetTimer); connect(m_hbReceiver, &QLocalSocket::connected, m_hbTimer, static_cast<void(QTimer::*)()>(&QTimer::start)); m_hbReceiver->connectToServer(m_serverName,QIODevice::ReadOnly); } Q_SLOT void resetTimer(){ if(m_hbReceiver->bytesAvailable()>=sizeof(qint8)){ QDataStream hbin(m_hbReceiver); hbin.setVersion(QDataStream::Qt_5_0); qint8 readHb=0; hbin >> readHb; if(readHb == static_cast<qint8>(ContinueChar)) return m_hbTimer->start(); qApp->quit(); } } private: const QString m_serverName; QTimer* m_hbTimer; QLocalSocket* m_hbReceiver; }; #endif
main.cpp
#include "splashwidget.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication a(argc, argv); const QStringList argList = a.arguments(); if(argList.size()<2) return -1; if(argList.at(1).isEmpty()) return -1; SplashWidget w(argList.at(1)); w.show(); return a.exec(); }
Main Application
splashcontroller.h
#ifndef SPLASHCONTROLLER_H #define SPLASHCONTROLLER_H #include <QObject> #include <QUuid> #include <QLocalServer> #include <QLocalSocket> #include <QTimer> #include <QProcess> #include <QDataStream> class SplashController : public QObject { Q_OBJECT Q_DISABLE_COPY(SplashController) public: SplashController(QObject* parent = Q_NULLPTR) :QObject(parent) , m_localSocket(Q_NULLPTR) , m_localServer(Q_NULLPTR) , m_splashProcess(Q_NULLPTR) , m_splashProcessPath("SplashScreenApp") { m_hbTimer = new QTimer(this); m_hbTimer->setInterval(500); } QString splashProcessPath() const { return m_splashProcessPath; } void setSplashProcessPath(const QString &splashProcessPath) { m_splashProcessPath = splashProcessPath; } Q_SLOT void start() { if (m_localServer) return; const QString serverName = QUuid::createUuid().toString(); m_localSocket = Q_NULLPTR; m_localServer = new QLocalServer(this); if (!m_localServer->listen(serverName)) { m_localServer->deleteLater(); m_localServer = Q_NULLPTR; error(); } QObject::connect(m_localServer, &QLocalServer::newConnection, this, &SplashController::splashConnected); m_splashProcess = new QProcess(this); QObject::connect(m_splashProcess, &QProcess::errorOccurred, this, &SplashController::stopComponents); QObject::connect(m_splashProcess, &QProcess::errorOccurred, this, &SplashController::error); QObject::connect(m_splashProcess, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &SplashController::stopComponents); QObject::connect(m_splashProcess, static_cast<void(QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, &SplashController::error); m_splashProcess->start(m_splashProcessPath, QStringList(serverName)); } Q_SLOT void stop() { stopComponents(); finished(); } Q_SIGNAL void error(); Q_SIGNAL void finished(); protected: Q_SLOT void stopComponents() { if (!m_localServer) return; m_hbTimer->stop(); disconnect(m_splashProcess); disconnect(m_localServer); if (m_localSocket) disconnect(m_localSocket); m_splashProcess->deleteLater(); m_localServer->deleteLater(); m_splashProcess = Q_NULLPTR; m_localServer = Q_NULLPTR; m_localSocket = Q_NULLPTR; } Q_SLOT void splashConnected() { if (m_localSocket) return; m_localSocket = m_localServer->nextPendingConnection(); QObject::connect(m_localSocket, &QLocalSocket::disconnected, this, &SplashController::stopComponents); QObject::connect(m_localSocket, &QLocalSocket::disconnected, this, &SplashController::error); QObject::connect(m_localSocket, static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), this, &SplashController::stopComponents); QObject::connect(m_localSocket, static_cast<void(QLocalSocket::*)(QLocalSocket::LocalSocketError)>(&QLocalSocket::error), this, &SplashController::error); QObject::connect(m_hbTimer, &QTimer::timeout, this, &SplashController::spalshHB); m_hbTimer->start(); } Q_SLOT void spalshHB() { if (!m_localSocket) return; QDataStream hbin(m_localSocket); hbin.setVersion(QDataStream::Qt_5_0); hbin << static_cast<qint8>(88); } private: QTimer* m_hbTimer; QLocalServer* m_localServer; QLocalSocket* m_localSocket; QProcess* m_splashProcess; QString m_splashProcessPath; }; #endif // SPLASHCONTROLLER_H
an example of main.cpp
#include <QApplication> #include <QThread> #include "splashcontroller.h" int main(int argc, char *argv[]) { QApplication app(argc, argv); QThread splashThread; SplashController* splashScreen = new SplashController; splashScreen->setSplashProcessPath("C:/temp/SplashScreenApp"); splashScreen->moveToThread(&splashThread); QObject::connect(splashScreen, &SplashController::finished, &splashThread, &QThread::quit); QObject::connect(splashScreen, &SplashController::error, &splashThread, &QThread::quit); QObject::connect(splashScreen, &SplashController::finished, splashScreen, &SplashController::deleteLater); QObject::connect(splashScreen, &SplashController::error, splashScreen, &SplashController::deleteLater); QObject::connect(splashScreen, &SplashController::finished, [&splashScreen]()->void {splashScreen = Q_NULLPTR; }); QObject::connect(splashScreen, &SplashController::error, [&splashScreen]()->void {splashScreen = Q_NULLPTR; }); QObject::connect(&splashThread, &QThread::started, splashScreen, &SplashController::start); splashThread.start(); QTimer::singleShot(10000, [&splashScreen]()->void { QMetaObject::invokeMethod(splashScreen, "stop", Qt::QueuedConnection); }); QTimer::singleShot(10100, &app, &QApplication::quit); return app.exec(); }
Of course you'll have to subclass SplashWidget and implement your own paint
-
@VRonin Thanks for the detailed and complex answer! I'll defenitly look into your heartbeat approach a bit more!
I wanted to add a few lines, of code that I forgot in my post above.
The QShared Memory part of the main-app main.cpp:
QApplication::setAttribute(Qt::AA_Use96Dpi); QApplication a(argc, argv); //!New Part start QSharedMemory _singular("myAppSingleInstance"); if(_singular.attach(QSharedMemory::ReadOnly)){ _singular.detach(); return -42; }else{ _singular.create(1); } //!New Part End QProcess
And also a small addition, so that noone accidentally starts Splash.exe without a main App:
//Main Application, main.cpp QProcess splash; QStringList s{"openByApp"}; splash.start("path/to/Splash/Splash.exe",s); //Splash App, main.cpp QApplication a(argc, argv); if(a.arguments().last() != "openByApp"){ return -42; }
Also on a second thought, this would also be the perfect situtation to test Qt 5.9's brand new Technology Preview Module :
Qt Remote Objects
?It seems to fit, according to the quick description I read.
-
@J.Hilk said in QSplashScreen animation:
Also on a second thought, this would also be the perfect situtation to test Qt 5.9's brand new Technology Preview Module : Qt Remote Objects ?
My solution uses the principles behind this. It uses the "local" connection and, given the simplicity of the inter-process communication in this case, You probably do not need the overhead of that module. QSharedMemory is perfect in this case even though your current code probably doesn't handle opening the main app twice well
A side note for curious readers:
You might ask why we are using QSharedMemory or QLocalSocket instead of using stdin/stdout to communicate between processes and the answer is that on Windows stdin is not a socket so you can't use QSocketNotifier to get a sort of readyRead() in the child process. In this specific case you probably can have a thread in the child just stuck on a std::cin call until the server responds, but I'm not going to try it -
Not sure this will help. But I only wished to have messages animate for my splash. And this is what I do:
In my main I define a QApplication derived class:
MyAppObject app(argc, argv);
I construct the object, I have an init method that emits progress signal messages such as:
message="Reading configurations: " % settings.fileName(); emit initMessage(message);
Back in main, after app is declared but before app.init():
const QImage image(":/Images/Splash.png"); QPixmap pixmap = QPixmap::fromImage(image).scaled(QSize(300,300) ,Qt::AspectRatioMode::KeepAspectRatio ,Qt::SmoothTransformation); QSplashScreen *splash = new QSplashScreen(pixmap); splash->show(); // bind app init messsage signals/updates to splash message QObject::connect(&app, &ClassName::initMessage, [=]( const QString message){ int alignment = Qt::AlignCenter; const QColor &color = Qt::darkYellow; splash->showMessage(message, alignment, color); } // start the fun app.Init(); // everything is ready, kill off the splash splash->close(); // start the QApp return app.exec();
-
Hi guys - I'm going to pull this topic out of mothballs, just to ask...in the intervening years, has there been any developments that would make this task more straightforward? The customer wants an animated splash screen...will have to work on Android as well as desktops.
Thanks...
-
@mzimmers hi, as far as I know, there is no easier way in Qt than the QProcess one. Due to the nature of Qt and all GUI Stuff in one thread concept. Any long, consecutive, GUI Creation will cause freezes of the animation.
-> Standalone SplashScreen executable launched via QProcess.
Android is even more complicated. Because you actually have 2 different phases, the launch, which is purely OS handled and then the initialisation of your app where you can influence stuff.
You may have noticed when starting a bare bone application on android, it starts with a black screen transitioning into a white one with your application name written on it.
You where always able to replace the black screen with a static splash screen of your choice and only with android 12 they added the possibility to make this animated:
https://developer.android.com/develop/ui/views/launch/splash-screenIIRC the white one you could make animated in the past
-
@J-Hilk it occurs to me that there's more than one purpose for a splash screen:
- it displays some desired information for a minimum period of time
- it gives the user something to look at while the app is loading
What this would be (in non-technical terms) is: the app starts by playing a movie, while all the behind-the-scenes stuff is doing whatever it does. After some pre-set period of time, the movie player will accept a signal to end. This signal would come from the worker. The worker would then take over the display.
The difficulty here, IMO, is to get the movie player and the rest of the app to share one window. I'm not sure how I'd go about that.
I haven't built for Android in a few months, but as I remember, Qt Creator provides the hooks to install an animated splash screen for Android, no?