From 8c35eb9b70c2f8db333bbcb869dd3bb26f690bef Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Wed, 9 Nov 2022 13:22:58 +0800 Subject: [PATCH] win: add more dpi hacks & fix some bugs Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- src/core/framelesshelper_win.cpp | 33 ++++ src/core/utils_win.cpp | 226 ++++++++++++++++++++++++++-- src/widgets/widgetssharedhelper.cpp | 13 +- 3 files changed, 256 insertions(+), 16 deletions(-) diff --git a/src/core/framelesshelper_win.cpp b/src/core/framelesshelper_win.cpp index c633fcd7..d6e26a06 100644 --- a/src/core/framelesshelper_win.cpp +++ b/src/core/framelesshelper_win.cpp @@ -35,6 +35,8 @@ #include "winverhelper_p.h" #include "framelesshelper_windows.h" +EXTERN_C BOOL WINAPI EnableChildWindowDpiMessage2(const HWND hWnd, const BOOL fEnable); + FRAMELESSHELPER_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcFramelessHelperWin, "wangwenx190.framelesshelper.core.impl.win") @@ -73,6 +75,7 @@ FRAMELESSHELPER_STRING_CONSTANT(TrackMouseEvent) FRAMELESSHELPER_STRING_CONSTANT(FindWindowW) FRAMELESSHELPER_STRING_CONSTANT(UnregisterClassW) FRAMELESSHELPER_STRING_CONSTANT(DestroyWindow) +FRAMELESSHELPER_STRING_CONSTANT(EnableChildWindowDpiMessage) [[maybe_unused]] static constexpr const char kFallbackTitleBarErrorMessage[] = "FramelessHelper is unable to create the fallback title bar window, and thus the snap layout feature will be disabled" " unconditionally. You can ignore this error and continue running your application, nothing else will be affected, " @@ -97,6 +100,17 @@ struct Win32Helper Q_GLOBAL_STATIC(Win32Helper, g_win32Helper) +[[nodiscard]] static inline QString hwnd2str(const WId windowId) +{ + return FRAMELESSHELPER_STRING_LITERAL("0x") + + QString::number(windowId, 16).toUpper(); +} + +[[nodiscard]] static inline QString hwnd2str(const HWND hwnd) +{ + return hwnd2str(reinterpret_cast(hwnd)); +} + [[nodiscard]] static inline LRESULT CALLBACK FallbackTitleBarWindowProc (const HWND hWnd, const UINT uMsg, const WPARAM wParam, const LPARAM lParam) { @@ -508,6 +522,9 @@ void FramelessHelperWin::addWindow(const SystemParameters ¶ms) qApp->installNativeEventFilter(g_win32Helper()->nativeEventFilter.data()); } g_win32Helper()->mutex.unlock(); + DEBUG.noquote() << "The DPI of window" << hwnd2str(windowId) << "is: QDpi(" + << Utils::getWindowDpi(windowId, true) << ',' + << Utils::getWindowDpi(windowId, false) << ")."; // Some Qt internals have to be corrected. Utils::maybeFixupQtInternals(windowId); // Qt maintains a frame margin internally, we need to update it accordingly @@ -519,6 +536,18 @@ void FramelessHelperWin::addWindow(const SystemParameters ¶ms) Utils::updateWindowFrameMargins(windowId, false); // Tell DWM we don't use the window icon/caption/sysmenu, don't draw them. Utils::hideOriginalTitleBarElements(windowId); + // We don't need this hack on Win10 1607 and newer, the PMv2 DPI awareness + // mode will take care of it for us by default. + if (WindowsVersionHelper::isWin10OrGreater() + && !WindowsVersionHelper::isWin10RS1OrGreater()) { + // Without this hack, child windows can't get DPI change messages, + // which means only top level windows can scale to the correct size. + if (EnableChildWindowDpiMessage2(reinterpret_cast(windowId), TRUE) == FALSE) { + if (GetLastError() != ERROR_CALL_NOT_IMPLEMENTED) { + WARNING << Utils::getSystemErrorMessage(kEnableChildWindowDpiMessage); + } + } + } if (WindowsVersionHelper::isWin10RS1OrGreater()) { // Tell DWM we may need dark theme non-client area (title bar & frame border). FramelessHelper::Core::setApplicationOSThemeAware(); @@ -1096,6 +1125,10 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me } break; #endif case WM_DPICHANGED: { + const UINT dpiX = LOWORD(wParam); + const UINT dpiY = HIWORD(wParam); + DEBUG.noquote() << "New DPI for window" << hwnd2str(hWnd) + << ": QDpi(" << dpiX << ',' << dpiY << ")."; // Sync the internal window frame margins with the latest DPI. Utils::updateInternalWindowFrameMargins(data.params.getWindowHandle(), true); // For some unknown reason, Qt sometimes won't re-paint the window contents after diff --git a/src/core/utils_win.cpp b/src/core/utils_win.cpp index 52bfcc7d..ca7df252 100644 --- a/src/core/utils_win.cpp +++ b/src/core/utils_win.cpp @@ -87,6 +87,11 @@ _GetSystemMetricsForDpi( _In_ UINT dpi ); +UINT WINAPI +_GetWindowDPI( + _In_ HWND hWnd +); + UINT WINAPI _GetDpiForWindow( _In_ HWND hWnd @@ -149,6 +154,32 @@ _AreDpiAwarenessContextsEqual( _In_ _DPI_AWARENESS_CONTEXT dpiContextB ); +BOOL WINAPI +_EnableChildWindowDpiMessage( + _In_ HWND hWnd, + _In_ BOOL fEnable +); + +BOOL WINAPI +_EnablePerMonitorDialogScaling( + VOID +); + +int WINAPI +_GetDpiMetrics( + _In_ int nIndex, + _In_ UINT dpi +); + +BOOL WINAPI +_AdjustWindowRectExForDpi( + _Inout_ LPRECT lpRect, + _In_ DWORD dwStyle, + _In_ BOOL bMenu, + _In_ DWORD dwExStyle, + _In_ UINT dpi +); + EXTERN_C_END EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API BOOL WINAPI @@ -395,6 +426,150 @@ IsDarkModeAllowedForApp(VOID) return pIsDarkModeAllowedForApp(); } +EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API BOOL WINAPI +EnableChildWindowDpiMessage2(const HWND hWnd, const BOOL fEnable) +{ + using EnableChildWindowDpiMessagePtr = decltype(&::_EnableChildWindowDpiMessage); + static const auto pEnableChildWindowDpiMessage = []() -> EnableChildWindowDpiMessagePtr { + FRAMELESSHELPER_USE_NAMESPACE + FRAMELESSHELPER_STRING_CONSTANT(user32) + FRAMELESSHELPER_STRING_CONSTANT(EnableChildWindowDpiMessage) + // EnableChildWindowDpiMessage() was once a public API, so we can load it by name, + // but it got removed in some later SDK versions, so we can't link to it directly. + // I haven't check the accurate time point of its removal. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kEnableChildWindowDpiMessage))) { + return pFunc; + } + // EnableChildWindowDpiMessage() was a private API when first introduced. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, MAKEINTRESOURCEA(2704)))) { + return pFunc; + } + return nullptr; + }(); + if (!pEnableChildWindowDpiMessage) { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED); + return FALSE; + } + return pEnableChildWindowDpiMessage(hWnd, fEnable); +} + +EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API BOOL WINAPI +EnablePerMonitorDialogScaling2(VOID) +{ + using EnablePerMonitorDialogScalingPtr = decltype(&::_EnablePerMonitorDialogScaling); + static const auto pEnablePerMonitorDialogScaling = []() -> EnablePerMonitorDialogScalingPtr { + FRAMELESSHELPER_USE_NAMESPACE + FRAMELESSHELPER_STRING_CONSTANT(user32) + FRAMELESSHELPER_STRING_CONSTANT(EnablePerMonitorDialogScaling) + // EnablePerMonitorDialogScaling() was once a public API, so we can load it by name, + // but it got removed in some later SDK versions, so we can't link to it directly. + // I haven't check the accurate time point of its removal. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kEnablePerMonitorDialogScaling))) { + return pFunc; + } + // EnablePerMonitorDialogScaling() was a private API when first introduced. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, MAKEINTRESOURCEA(2577)))) { + return pFunc; + } + return nullptr; + }(); + if (!pEnablePerMonitorDialogScaling) { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED); + return FALSE; + } + return pEnablePerMonitorDialogScaling(); +} + +EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API UINT WINAPI +GetDpiForWindow2(const HWND hWnd) +{ + using GetDpiForWindowPtr = decltype(&::_GetDpiForWindow); + static const auto pGetDpiForWindow = []() -> GetDpiForWindowPtr { + FRAMELESSHELPER_USE_NAMESPACE + FRAMELESSHELPER_STRING_CONSTANT(user32) + FRAMELESSHELPER_STRING_CONSTANT(GetDpiForWindow) + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kGetDpiForWindow))) { + return pFunc; + } + // GetDpiForWindow() was named "GetWindowDPI" before made public. + FRAMELESSHELPER_STRING_CONSTANT(GetWindowDPI) + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kGetWindowDPI))) { + return pFunc; + } + // GetDpiForWindow() was a private API when first introduced. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, MAKEINTRESOURCEA(2707)))) { + return pFunc; + } + return nullptr; + }(); + if (!pGetDpiForWindow) { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED); + return 0; + } + return pGetDpiForWindow(hWnd); +} + +EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API int WINAPI +GetSystemMetricsForDpi2(const int nIndex, const UINT dpi) +{ + using GetSystemMetricsForDpiPtr = decltype(&::_GetSystemMetricsForDpi); + static const auto pGetSystemMetricsForDpi = []() -> GetSystemMetricsForDpiPtr { + FRAMELESSHELPER_USE_NAMESPACE + FRAMELESSHELPER_STRING_CONSTANT(user32) + FRAMELESSHELPER_STRING_CONSTANT(GetSystemMetricsForDpi) + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kGetSystemMetricsForDpi))) { + return pFunc; + } + // GetSystemMetricsForDpi() was named "GetDpiMetrics" before made public. + FRAMELESSHELPER_STRING_CONSTANT(GetDpiMetrics) + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kGetDpiMetrics))) { + return pFunc; + } + return nullptr; + }(); + if (!pGetSystemMetricsForDpi) { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED); + return 0; + } + return pGetSystemMetricsForDpi(nIndex, dpi); +} + +EXTERN_C [[nodiscard]] FRAMELESSHELPER_CORE_API BOOL WINAPI +AdjustWindowRectExForDpi2(LPRECT lpRect, const DWORD dwStyle, + const BOOL bMenu, const DWORD dwExStyle, const UINT dpi) +{ + using AdjustWindowRectExForDpiPtr = decltype(&::_AdjustWindowRectExForDpi); + static const auto pAdjustWindowRectExForDpi = []() -> AdjustWindowRectExForDpiPtr { + FRAMELESSHELPER_USE_NAMESPACE + FRAMELESSHELPER_STRING_CONSTANT(user32) + FRAMELESSHELPER_STRING_CONSTANT(AdjustWindowRectExForDpi) + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, kAdjustWindowRectExForDpi))) { + return pFunc; + } + // AdjustWindowRectExForDpi() was a private API when first introduced. + if (const auto pFunc = reinterpret_cast( + SysApiLoader::resolve(kuser32, MAKEINTRESOURCEA(2580)))) { + return pFunc; + } + return nullptr; + }(); + if (!pAdjustWindowRectExForDpi) { + SetLastError(ERROR_CALL_NOT_IMPLEMENTED); + return FALSE; + } + return pAdjustWindowRectExForDpi(lpRect, dwStyle, bMenu, dwExStyle, dpi); +} + Q_DECLARE_METATYPE(QMargins) FRAMELESSHELPER_BEGIN_NAMESPACE @@ -510,6 +685,11 @@ FRAMELESSHELPER_STRING_CONSTANT(GetCurrentProcess) FRAMELESSHELPER_STRING_CONSTANT(GetProcessDpiAwareness) FRAMELESSHELPER_STRING_CONSTANT(IsProcessDPIAware) FRAMELESSHELPER_STRING_CONSTANT(AreDpiAwarenessContextsEqual) +FRAMELESSHELPER_STRING_CONSTANT(GetWindowDPI) +FRAMELESSHELPER_STRING_CONSTANT(AdjustWindowRectExForDpi) +FRAMELESSHELPER_STRING_CONSTANT(GetDpiMetrics) +FRAMELESSHELPER_STRING_CONSTANT(EnablePerMonitorDialogScaling) +FRAMELESSHELPER_STRING_CONSTANT(EnableChildWindowDpiMessage) struct Win32UtilsHelperData { @@ -661,13 +841,16 @@ struct SYSTEM_METRIC return 0; } const UINT realDpi = Utils::getWindowDpi(windowId, horizontal); - if (API_USER_AVAILABLE(GetSystemMetricsForDpi)) { + { const UINT dpi = (scaled ? realDpi : USER_DEFAULT_SCREEN_DPI); - return API_CALL_FUNCTION4(GetSystemMetricsForDpi, index, dpi); - } else { - const qreal dpr = (scaled ? qreal(1) : (qreal(realDpi) / qreal(USER_DEFAULT_SCREEN_DPI))); - return qRound(qreal(GetSystemMetrics(index)) / dpr); + if (const int result = GetSystemMetricsForDpi2(index, dpi)) { + return result; + } } + // GetSystemMetrics() will always return a scaled value, so if we want to get an unscaled + // one, we have to calculate it ourself. + const qreal dpr = (scaled ? qreal(1) : (qreal(realDpi) / qreal(USER_DEFAULT_SCREEN_DPI))); + return qRound(qreal(GetSystemMetrics(index)) / dpr); } [[maybe_unused]] [[nodiscard]] static inline @@ -1301,11 +1484,13 @@ quint32 Utils::getWindowDpi(const WId windowId, const bool horizontal) return USER_DEFAULT_SCREEN_DPI; } const auto hwnd = reinterpret_cast(windowId); - if (API_USER_AVAILABLE(GetDpiForWindow)) { - const UINT dpi = API_CALL_FUNCTION4(GetDpiForWindow, hwnd); - if (dpi > 0) { + { + if (const UINT dpi = GetDpiForWindow2(hwnd)) { return dpi; - } else { + } + // ERROR_CALL_NOT_IMPLEMENTED: the function is not available on + // current platform, not an error. + if (GetLastError() != ERROR_CALL_NOT_IMPLEMENTED) { WARNING << getSystemErrorMessage(kGetDpiForWindow); } } @@ -1542,6 +1727,9 @@ bool Utils::isWindowFrameBorderVisible() { static const bool result = []() -> bool { const FramelessConfig * const config = FramelessConfig::instance(); + if (config->isSet(Option::UseCrossPlatformQtImplementation)) { + return false; + } if (config->isSet(Option::ForceShowWindowFrameBorder)) { return true; } @@ -1662,6 +1850,18 @@ void Utils::setAeroSnappingEnabled(const WId windowId, const bool enable) void Utils::tryToEnableHighestDpiAwarenessLevel() { + // We don't need this hack when running on Win10 1607 and newer, the PMv2 + // DPI awareness mode will take care of it for us by default. + if (WindowsVersionHelper::isWin10OrGreater() + && !WindowsVersionHelper::isWin10RS1OrGreater()) { + // This function need to be called before any dialogs are created, so + // to be safe we call it here. + if (EnablePerMonitorDialogScaling2() == FALSE) { + if (GetLastError() != ERROR_CALL_NOT_IMPLEMENTED) { + WARNING << getSystemErrorMessage(kEnablePerMonitorDialogScaling); + } + } + } bool isHighestAlready = false; const DpiAwareness currentAwareness = getDpiAwarenessForCurrentProcess(&isHighestAlready); DEBUG << "Current DPI awareness mode:" << currentAwareness; @@ -1679,7 +1879,7 @@ void Utils::tryToEnableHighestDpiAwarenessLevel() } const DWORD dwError = GetLastError(); // "ERROR_ACCESS_DENIED" means set externally (mostly due to manifest file). - // Any attempt to change the DPI awareness level through API will always fail, + // Any attempt to change the DPI awareness mode through API will always fail, // so we treat this situation as succeeded. if (dwError == ERROR_ACCESS_DENIED) { DEBUG << kDpiNoAccessErrorMessage; @@ -1720,7 +1920,7 @@ void Utils::tryToEnableHighestDpiAwarenessLevel() return true; } // "E_ACCESSDENIED" means set externally (mostly due to manifest file). - // Any attempt to change the DPI awareness level through API will always fail, + // Any attempt to change the DPI awareness mode through API will always fail, // so we treat this situation as succeeded. if (hr == E_ACCESSDENIED) { DEBUG << kDpiNoAccessErrorMessage; @@ -1827,8 +2027,8 @@ bool Utils::shouldAppsUseDarkMode_windows() // it works well. // However, reverse engineering of Win11's Task Manager reveals that Microsoft still // uses this function internally to determine the system theme, and the Task Manager - // can correctly respond to the theme change event indeed. Is it fixed silently - // in some unknown Windows versions? To be checked. + // can correctly respond to the theme change event indeed. But strangely, I've checked + // that it's still broken on Win11 22H2. What's going on here? if (WindowsVersionHelper::isWin10RS5OrGreater() && !WindowsVersionHelper::isWin1019H1OrGreater()) { return (ShouldAppsUseDarkMode() != FALSE); diff --git a/src/widgets/widgetssharedhelper.cpp b/src/widgets/widgetssharedhelper.cpp index a3ebc4af..4b309e22 100644 --- a/src/widgets/widgetssharedhelper.cpp +++ b/src/widgets/widgetssharedhelper.cpp @@ -262,9 +262,16 @@ void WidgetsSharedHelper::handleScreenChanged(QScreen *screen) void WidgetsSharedHelper::updateContentsMargins() { #ifdef Q_OS_WINDOWS - m_targetWidget->setContentsMargins(0, - ((Utils::windowStatesToWindowState(m_targetWidget->windowState()) == Qt::WindowNoState) - ? kDefaultWindowFrameBorderThickness : 0), 0, 0); + const auto margins = [this]() -> QMargins { + if (!Utils::isWindowFrameBorderVisible() || WindowsVersionHelper::isWin11OrGreater()) { + return {}; + } + if (Utils::windowStatesToWindowState(m_targetWidget->windowState()) != Qt::WindowNoState) { + return {}; + } + return {0, kDefaultWindowFrameBorderThickness, 0, 0}; + }(); + m_targetWidget->setContentsMargins(margins); #endif }