Handle Windows theme changes in central theme listener

Instead of having each window listen for theme changes we
now have a single theme listener window, similar to the
way we handle screen management.

This reduces the amount of redundant callbacks to QWSI
for theme changes somewhat, but Windows still emits
several redundant events even for a single window.
We want the theme change event to be synchronous, so
there is no obvious way to debounce these events,
besides the clearing of the message queue that we
already do in this patch.

Since we don't know exactly what part of the theme
changed we can't bail out of the theme change event
to QWSI based on the color scheme being the same, as
the accent color or other parts of the theme might
have changed.

Change-Id: I827fa50effadf8a8e674a03ddc72958c60310f48
Reviewed-by: Oliver Wolff <oliver.wolff@qt.io>
Reviewed-by: Zhao Yuhang <2546789017@qq.com>
This commit is contained in:
Tor Arne Vestbø 2025-04-09 20:48:10 +02:00
parent 3a21b4bcaf
commit fdfed82675
5 changed files with 86 additions and 32 deletions

View File

@ -178,7 +178,7 @@ static const char *findWMstr(uint msg)
{ 0x0014, "WM_ERASEBKGND" },
{ 0x0015, "WM_SYSCOLORCHANGE" },
{ 0x0018, "WM_SHOWWINDOW" },
{ 0x001A, "WM_WININICHANGE" },
{ 0x001A, "WM_SETTINGCHANGE" },
{ 0x001B, "WM_DEVMODECHANGE" },
{ 0x001C, "WM_ACTIVATEAPP" },
{ 0x001D, "WM_FONTCHANGE" },
@ -410,6 +410,7 @@ static const char *findWMstr(uint msg)
{ 0x0317, "WM_PRINT" },
{ 0x0318, "WM_PRINTCLIENT" },
{ 0x0319, "WM_APPCOMMAND" },
{ 0x0320, "WM_DWMCOLORIZATIONCOLORCHANGED" },
{ 0x031A, "WM_THEMECHANGED" },
{ 0x0358, "WM_HANDHELDFIRST" },
{ 0x0359, "WM_HANDHELDFIRST + 1" },
@ -804,6 +805,10 @@ QString decodeMSG(const MSG& msg)
if (const char *logoffOption = sessionMgrLogOffOption(uint(wParam)))
parameters += QLatin1StringView(logoffOption);
break;
case WM_SETTINGCHANGE:
parameters = "wParam"_L1 + wParamS + " lParam("_L1
+ QString::fromWCharArray(reinterpret_cast<LPCWSTR>(lParam)) + u')';
break;
default:
parameters = "wParam"_L1 + wParamS + " lParam"_L1 + lParamS;
break;

View File

@ -75,6 +75,7 @@ Q_LOGGING_CATEGORY(lcQpaAccessibility, "qt.qpa.accessibility")
Q_LOGGING_CATEGORY(lcQpaUiAutomation, "qt.qpa.uiautomation")
Q_LOGGING_CATEGORY(lcQpaTrayIcon, "qt.qpa.trayicon")
Q_LOGGING_CATEGORY(lcQpaScreen, "qt.qpa.screen")
Q_LOGGING_CATEGORY(lcQpaTheme, "qt.qpa.theme")
int QWindowsContext::verbose = 0;
@ -195,6 +196,9 @@ QWindowsContext::~QWindowsContext()
if (d->m_powerDummyWindow)
DestroyWindow(d->m_powerDummyWindow);
if (QWindowsTheme *theme = QWindowsTheme::instance())
theme->destroyThemeChangeWindow();
d->m_screenManager.destroyWindow();
unregisterWindowClasses();
@ -1062,11 +1066,6 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message,
#endif
case QtWindows::SettingChangedEvent: {
QWindowsWindow::settingsChanged();
// Only refresh the window theme if the user changes the personalize settings.
if ((wParam == 0) && (lParam != 0) // lParam sometimes may be NULL.
&& (wcscmp(reinterpret_cast<LPCWSTR>(lParam), L"ImmersiveColorSet") == 0)) {
QWindowsTheme::handleSettingsChanged();
}
return d->m_screenManager.handleScreenChanges();
}
default:
@ -1231,10 +1230,6 @@ bool QWindowsContext::windowsProc(HWND hwnd, UINT message,
QWindowSystemInterface::handleCloseEvent(platformWindow->window());
return true;
case QtWindows::ThemeChanged: {
QWindowsThemeCache::clearThemeCache(platformWindow->handle());
// Switch from Aero to Classic changes margins.
if (QWindowsTheme *theme = QWindowsTheme::instance())
theme->windowsThemeChanged(platformWindow->window());
return true;
}
case QtWindows::CompositionSettingsChanged:

View File

@ -29,6 +29,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcQpaAccessibility)
Q_DECLARE_LOGGING_CATEGORY(lcQpaUiAutomation)
Q_DECLARE_LOGGING_CATEGORY(lcQpaTrayIcon)
Q_DECLARE_LOGGING_CATEGORY(lcQpaScreen)
Q_DECLARE_LOGGING_CATEGORY(lcQpaTheme)
class QWindow;
class QPlatformScreen;

View File

@ -41,6 +41,7 @@
#include <private/qhighdpiscaling_p.h>
#include <private/qwinregistry_p.h>
#include <QtCore/private/qfunctions_win_p.h>
#include <QtGui/private/qwindowsthemecache_p.h>
#include <algorithm>
@ -480,6 +481,39 @@ static inline QPalette *menuBarPalette(const QPalette &menuPalette, bool light)
const char *QWindowsTheme::name = "windows";
QWindowsTheme *QWindowsTheme::m_instance = nullptr;
extern "C" LRESULT QT_WIN_CALLBACK qThemeChangeObserverWndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) {
case WM_SETTINGCHANGE:
// Only refresh the theme if the user changes the personalize settings
if (!((wParam == 0) && (lParam != 0) // lParam sometimes may be NULL.
&& (wcscmp(reinterpret_cast<LPCWSTR>(lParam), L"ImmersiveColorSet") == 0)))
break;
Q_FALLTHROUGH();
case WM_THEMECHANGED:
case WM_SYSCOLORCHANGE:
case WM_DWMCOLORIZATIONCOLORCHANGED:
qCDebug(lcQpaTheme) << "Handling theme change due to"
<< qUtf8Printable(decodeMSG(MSG{hwnd, message, wParam, lParam, 0, {0, 0}}).trimmed());
QWindowsTheme::handleThemeChange();
MSG msg; // Clear the message queue, we've already reacted to the change
while (PeekMessage(&msg, hwnd, 0, 0, PM_REMOVE));
// FIXME: Despite clearing the message queue above, Windows will send
// us redundant theme change events for our single window. We want the
// theme change delivery to be synchronous, so we can't easily debounce
// them by peeking into the QWSI event queue, but perhaps there are other
// ways.
break;
default:
break;
}
return DefWindowProc(hwnd, message, wParam, lParam);
}
QWindowsTheme::QWindowsTheme()
{
m_instance = this;
@ -489,6 +523,16 @@ QWindowsTheme::QWindowsTheme()
std::fill(m_palettes, m_palettes + NPalettes, nullptr);
refresh();
refreshIconPixmapSizes();
auto className = QWindowsContext::instance()->registerWindowClass(
QWindowsContext::classNamePrefix() + QLatin1String("ThemeChangeObserverWindow"),
qThemeChangeObserverWndProc);
// HWND_MESSAGE windows do not get the required theme events,
// so we use a real top-level window that we never show.
m_themeChangeObserver = CreateWindowEx(0, reinterpret_cast<LPCWSTR>(className.utf16()),
nullptr, WS_TILED, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
nullptr, nullptr, GetModuleHandle(nullptr), nullptr);
Q_ASSERT(m_themeChangeObserver);
}
QWindowsTheme::~QWindowsTheme()
@ -498,6 +542,13 @@ QWindowsTheme::~QWindowsTheme()
m_instance = nullptr;
}
void QWindowsTheme::destroyThemeChangeWindow()
{
qCDebug(lcQpaTheme) << "Destroying theme change window";
DestroyWindow(m_themeChangeObserver);
m_themeChangeObserver = nullptr;
}
static inline QStringList iconThemeSearchPaths()
{
const QFileInfo appDir(QCoreApplication::applicationDirPath() + "/icons"_L1);
@ -594,7 +645,7 @@ Qt::ColorScheme QWindowsTheme::effectiveColorScheme()
void QWindowsTheme::requestColorScheme(Qt::ColorScheme scheme)
{
s_colorSchemeOverride = scheme;
handleSettingsChanged();
handleThemeChange();
}
Qt::ContrastPreference QWindowsTheme::contrastPreference() const
@ -603,23 +654,27 @@ Qt::ContrastPreference QWindowsTheme::contrastPreference() const
: Qt::ContrastPreference::NoPreference;
}
void QWindowsTheme::handleSettingsChanged()
void QWindowsTheme::handleThemeChange()
{
QWindowsThemeCache::clearAllThemeCaches();
const auto oldColorScheme = s_colorScheme;
s_colorScheme = Qt::ColorScheme::Unknown; // make effectiveColorScheme() query registry
const auto newColorScheme = effectiveColorScheme();
const bool colorSchemeChanged = newColorScheme != oldColorScheme;
s_colorScheme = newColorScheme;
if (!colorSchemeChanged)
return;
auto integration = QWindowsIntegration::instance();
integration->updateApplicationBadge();
if (integration->darkModeHandling().testFlag(QWindowsApplication::DarkModeStyle)) {
QWindowsTheme::instance()->refresh();
QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>();
s_colorScheme = effectiveColorScheme();
if (s_colorScheme != oldColorScheme) {
// Only propagate color scheme changes if the scheme actually changed
auto integration = QWindowsIntegration::instance();
integration->updateApplicationBadge();
for (QWindowsWindow *w : std::as_const(QWindowsContext::instance()->windows()))
w->setDarkBorder(s_colorScheme == Qt::ColorScheme::Dark);
}
for (QWindowsWindow *w : std::as_const(QWindowsContext::instance()->windows()))
w->setDarkBorder(s_colorScheme == Qt::ColorScheme::Dark);
// But always reset palette and fonts, and signal the theme
// change, as other parts of the theme could have changed,
// such as the accent color.
QWindowsTheme::instance()->refresh();
QWindowSystemInterface::handleThemeChange<QWindowSystemInterface::SynchronousDelivery>();
}
void QWindowsTheme::clearPalettes()
@ -784,12 +839,6 @@ QPlatformSystemTrayIcon *QWindowsTheme::createPlatformSystemTrayIcon() const
}
#endif
void QWindowsTheme::windowsThemeChanged(QWindow * window)
{
refresh();
QWindowSystemInterface::handleThemeChange(window);
}
static int fileIconSizes[FileIconSizeCount];
void QWindowsTheme::refreshIconPixmapSizes()

View File

@ -35,7 +35,7 @@ public:
void requestColorScheme(Qt::ColorScheme scheme) override;
Qt::ContrastPreference contrastPreference() const override;
static void handleSettingsChanged();
static void handleThemeChange();
const QPalette *palette(Palette type = SystemPalette) const override
{ return m_palettes[type]; }
@ -47,7 +47,6 @@ public:
QIcon fileIcon(const QFileInfo &fileInfo, QPlatformTheme::IconOptions iconOptions = {}) const override;
QIconEngine *createIconEngine(const QString &iconName) const override;
void windowsThemeChanged(QWindow *window);
void displayChanged() { refreshIconPixmapSizes(); }
QList<QSize> availableFileIconSizes() const { return m_fileIconSizes; }
@ -83,9 +82,14 @@ private:
static inline Qt::ColorScheme s_colorScheme = Qt::ColorScheme::Unknown;
static inline Qt::ColorScheme s_colorSchemeOverride = Qt::ColorScheme::Unknown;
friend class QWindowsContext;
QPalette *m_palettes[NPalettes];
QFont *m_fonts[NFonts];
QList<QSize> m_fileIconSizes;
HWND m_themeChangeObserver = nullptr;
void destroyThemeChangeWindow();
};
QT_END_NAMESPACE