QSplashScreen: Enforce visibility by processing events

A splash screen is usually shown during initialization, before the main
window is shown. At that point, an application might not spin an event
loop. The documentation therefore recommends to run processEvents()
periodically, in order to ensure that the splash screen is actually
shown. Async window managers (e.g. XCB) might require more than one call
to processEvents() for the splash screen to be shown. It can't be
guaranteed, that a specific number of calls to processEvents() make the
splash screen visible. Calls to processEvents() are not considered
good practice in application code.

QSplashScreen::finish() uses a copy of QTest::qWaitForWindowExposed(),
to automatically close the splash screen, when the main window is
shown.

=> Re-write the static inline waitForWindowExposed to wait for the
widget's windowHandle()->isVisible(). That ensures visibility to the
window system.
The previous version waited for isExposed() to become true. That
ensured exposure to the application, but not visibility.

=> Intercept QEvent::Show and process events, until the splash screen
becomes visible.

=> Remove references to processEvents() as a requirement to show the
splash screen from the documentation. It remains a requirement for
processing mouse and keyboard events.

=> Re-activate tst_QSplashScreen and test visibility in the constructor
test function.

Drive by:
- implement switch in event override
- factor out paint event handling in a function
- update documentation about calling processEvents() to enable mouse
handling.

Fixes: QTBUG-119225
Pick-to: 6.5
Change-Id: I07910705246b4e3234e01cc4896f5bc4b2c56772
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
(cherry picked from commit 7835f9e60157987b4aafc9b860f925d775151b7c)
Reviewed-by: Qt Cherry-pick Bot <cherrypick_bot@qt-project.org>
(cherry picked from commit b41fc4bedd50760d838e7bacb5b4b648806551de)
This commit is contained in:
Axel Spoerl 2023-12-11 10:57:20 +01:00 committed by Qt Cherry-pick Bot
parent 01546825ae
commit 87c81931b9
3 changed files with 49 additions and 24 deletions

View File

@ -32,6 +32,8 @@ public:
int currAlign; int currAlign;
inline QSplashScreenPrivate(); inline QSplashScreenPrivate();
void handlePaintEvent();
}; };
/*! /*!
@ -67,9 +69,9 @@ public:
\snippet qsplashscreen/main.cpp 1 \snippet qsplashscreen/main.cpp 1
The user can hide the splash screen by clicking on it with the The user can hide the splash screen by clicking on it with the
mouse. Since the splash screen is typically displayed before the mouse. For mouse handling to work, call QApplication::processEvents()
event loop has started running, it is necessary to periodically periodically during startup.
call QCoreApplication::processEvents() to receive the mouse clicks.
It is sometimes useful to update the splash screen with messages, It is sometimes useful to update the splash screen with messages,
for example, announcing connections established or modules loaded for example, announcing connections established or modules loaded
@ -201,18 +203,21 @@ void QSplashScreen::clearMessage()
repaint(); repaint();
} }
// A copy of Qt Test's qWaitForWindowExposed() and qSleep(). static bool waitForWidgetMapped(QWidget *widget, int timeout = 1000)
inline static bool waitForWindowExposed(QWindow *window, int timeout = 1000)
{ {
enum { TimeOutMs = 10 }; enum { TimeOutMs = 10 };
auto isMapped = [widget](){
return widget->windowHandle() && widget->windowHandle()->isVisible();
};
QElapsedTimer timer; QElapsedTimer timer;
timer.start(); timer.start();
while (!window->isExposed()) { while (!isMapped()) {
const int remaining = timeout - int(timer.elapsed()); const int remaining = timeout - int(timer.elapsed());
if (remaining <= 0) if (remaining <= 0)
break; break;
QCoreApplication::processEvents(QEventLoop::AllEvents, remaining); QCoreApplication::processEvents(QEventLoop::AllEvents, remaining);
QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); QCoreApplication::sendPostedEvents();
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
Sleep(uint(TimeOutMs)); Sleep(uint(TimeOutMs));
#else #else
@ -220,7 +225,7 @@ inline static bool waitForWindowExposed(QWindow *window, int timeout = 1000)
nanosleep(&ts, nullptr); nanosleep(&ts, nullptr);
#endif #endif
} }
return window->isExposed(); return isMapped();
} }
/*! /*!
@ -233,7 +238,7 @@ void QSplashScreen::finish(QWidget *mainWin)
if (mainWin) { if (mainWin) {
if (!mainWin->windowHandle()) if (!mainWin->windowHandle())
mainWin->createWinId(); mainWin->createWinId();
waitForWindowExposed(mainWin->windowHandle()); waitForWidgetMapped(mainWin);
} }
close(); close();
} }
@ -311,18 +316,33 @@ void QSplashScreen::drawContents(QPainter *painter)
} }
} }
void QSplashScreenPrivate::handlePaintEvent()
{
Q_Q(QSplashScreen);
QPainter painter(q);
painter.setRenderHints(QPainter::SmoothPixmapTransform);
painter.setLayoutDirection(q->layoutDirection());
if (!pixmap.isNull())
painter.drawPixmap(QPoint(), pixmap);
q->drawContents(&painter);
}
/*! \reimp */ /*! \reimp */
bool QSplashScreen::event(QEvent *e) bool QSplashScreen::event(QEvent *e)
{ {
if (e->type() == QEvent::Paint) { Q_D(QSplashScreen);
Q_D(QSplashScreen);
QPainter painter(this); switch (e->type()) {
painter.setRenderHints(QPainter::SmoothPixmapTransform); case QEvent::Paint:
painter.setLayoutDirection(layoutDirection()); d->handlePaintEvent();
if (!d->pixmap.isNull()) break;
painter.drawPixmap(QPoint(), d->pixmap); case QEvent::Show:
drawContents(&painter); waitForWidgetMapped(this);
break;
default:
break;
} }
return QWidget::event(e); return QWidget::event(e);
} }

View File

@ -29,6 +29,7 @@ add_subdirectory(qscrollbar)
add_subdirectory(qsizegrip) add_subdirectory(qsizegrip)
add_subdirectory(qslider) add_subdirectory(qslider)
add_subdirectory(qspinbox) add_subdirectory(qspinbox)
add_subdirectory(qsplashscreen)
add_subdirectory(qsplitter) add_subdirectory(qsplitter)
add_subdirectory(qstackedwidget) add_subdirectory(qstackedwidget)
add_subdirectory(qstatusbar) add_subdirectory(qstatusbar)

View File

@ -4,6 +4,7 @@
#include <QTest> #include <QTest>
#include <QSplashScreen> #include <QSplashScreen>
#include <QTimer>
class tst_QSplashScreen : public QObject class tst_QSplashScreen : public QObject
{ {
@ -11,7 +12,7 @@ class tst_QSplashScreen : public QObject
private slots: private slots:
void checkCloseTime(); void checkCloseTime();
void checkScreenConstructor(); void checkConstructorAndShow();
}; };
class CloseEventSplash : public QSplashScreen class CloseEventSplash : public QSplashScreen
@ -20,7 +21,7 @@ public:
CloseEventSplash(const QPixmap &pix) : QSplashScreen(pix), receivedCloseEvent(false) {} CloseEventSplash(const QPixmap &pix) : QSplashScreen(pix), receivedCloseEvent(false) {}
bool receivedCloseEvent; bool receivedCloseEvent;
protected: protected:
void closeEvent(QCloseEvent *event) void closeEvent(QCloseEvent *event) override
{ {
receivedCloseEvent = true; receivedCloseEvent = true;
QSplashScreen::closeEvent(event); QSplashScreen::closeEvent(event);
@ -35,23 +36,26 @@ void tst_QSplashScreen::checkCloseTime()
QVERIFY(!splash.receivedCloseEvent); QVERIFY(!splash.receivedCloseEvent);
QWidget w; QWidget w;
splash.show(); splash.show();
QTimer::singleShot(500, &w, SLOT(show())); QTimer::singleShot(10, &w, &QWidget::show);
QVERIFY(!splash.receivedCloseEvent); QVERIFY(!splash.receivedCloseEvent);
splash.finish(&w); splash.finish(&w);
QVERIFY(splash.receivedCloseEvent); QVERIFY(splash.receivedCloseEvent);
// We check the window handle because if this is not valid, then // We check the window handle because if this is not valid, then
// it can't have been exposed // it can't have been exposed
QVERIFY(w.windowHandle()); QVERIFY(w.windowHandle());
QVERIFY(w.windowHandle()->isExposed()); QVERIFY(w.windowHandle()->isVisible());
} }
void tst_QSplashScreen::checkScreenConstructor() void tst_QSplashScreen::checkConstructorAndShow()
{ {
for (const auto screen : QGuiApplication::screens()) { QPixmap pix(100, 100);
QSplashScreen splash(screen); pix.fill(Qt::red);
for (auto *screen : QGuiApplication::screens()) {
QSplashScreen splash(screen, pix);
splash.show(); splash.show();
QCOMPARE(splash.screen(), screen); QCOMPARE(splash.screen(), screen);
QVERIFY(splash.windowHandle()); QVERIFY(splash.windowHandle());
QVERIFY(splash.windowHandle()->isVisible());
QCOMPARE(splash.windowHandle()->screen(), screen); QCOMPARE(splash.windowHandle()->screen(), screen);
} }
} }