From 09287f3d9e9e88271de2bfd5388dae5a53e8c6f5 Mon Sep 17 00:00:00 2001 From: Sine Striker <trueful@163.com> Date: 周四, 07 12月 2023 15:03:24 +0800 Subject: [PATCH] Add host event filter --- src/core/contexts/win32windowcontext.cpp | 334 ++++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 252 insertions(+), 82 deletions(-) diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index 09c2b83..c5c1cbd 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -1,19 +1,30 @@ #include "win32windowcontext_p.h" +#include "qwkcoreglobal_p.h" #include <optional> #include <QtCore/QHash> #include <QtCore/QAbstractNativeEventFilter> -#include <QtCore/QCoreApplication> #include <QtCore/QOperatingSystemVersion> +#include <QtCore/QScopeGuard> +#include <QtGui/QGuiApplication> #include <QtCore/private/qsystemlibrary_p.h> #include <QtGui/private/qhighdpiscaling_p.h> - -#include "qwkcoreglobal_p.h" +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) +# include <QtGui/private/qguiapplication_p.h> +#endif +#include <QtGui/qpa/qplatformwindow.h> +#if QT_VERSION < QT_VERSION_CHECK(6, 2, 0) +# include <QtGui/qpa/qplatformnativeinterface.h> +#else +# include <QtGui/qpa/qplatformwindow_p.h> +#endif #include <shellscalingapi.h> #include <dwmapi.h> + +Q_DECLARE_METATYPE(QMargins) namespace QWK { @@ -25,8 +36,15 @@ static WNDPROC g_qtWindowProc = nullptr; // Original Qt window proc function + static struct QWK_Hook { + QWK_Hook() { + qApp->setAttribute(Qt::AA_DontCreateNativeWidgetSiblings); + } + } g_hook{}; + struct DynamicApis { decltype(&::DwmFlush) pDwmFlush = nullptr; + decltype(&::DwmIsCompositionEnabled) pDwmIsCompositionEnabled = nullptr; decltype(&::GetDpiForWindow) pGetDpiForWindow = nullptr; decltype(&::GetSystemMetricsForDpi) pGetSystemMetricsForDpi = nullptr; decltype(&::GetDpiForMonitor) pGetDpiForMonitor = nullptr; @@ -44,6 +62,8 @@ QSystemLibrary dwmapi(QStringLiteral("dwmapi.dll")); pDwmFlush = reinterpret_cast<decltype(pDwmFlush)>(dwmapi.resolve("DwmFlush")); + pDwmIsCompositionEnabled = reinterpret_cast<decltype(pDwmIsCompositionEnabled)>( + dwmapi.resolve("DwmIsCompositionEnabled")); } ~DynamicApis() = default; @@ -137,6 +157,12 @@ return hwnd2str(reinterpret_cast<WId>(hwnd)); } + static inline bool isWin8OrGreater() { + static const bool result = + QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8; + return result; + } + static inline bool isWin8Point1OrGreater() { static const bool result = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1; @@ -149,11 +175,25 @@ return result; } - static inline quint32 getDpiForWindow(HWND hwnd) { - Q_ASSERT(hwnd); - if (!hwnd) { - return USER_DEFAULT_SCREEN_DPI; + static inline bool isDwmCompositionEnabled() { + if (isWin8OrGreater()) { + return true; } + const DynamicApis &apis = DynamicApis::instance(); + if (!apis.pDwmIsCompositionEnabled) { + return false; + } + BOOL enabled = FALSE; + return SUCCEEDED(apis.pDwmIsCompositionEnabled(&enabled)) && enabled; + } + + static inline void triggerFrameChange(HWND hwnd) { + ::SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER | + SWP_FRAMECHANGED); + } + + static inline quint32 getDpiForWindow(HWND hwnd) { const DynamicApis &apis = DynamicApis::instance(); if (apis.pGetDpiForWindow) { // Win10 return apis.pGetDpiForWindow(hwnd); @@ -173,10 +213,6 @@ } static inline quint32 getResizeBorderThickness(HWND hwnd) { - Q_ASSERT(hwnd); - if (!hwnd) { - return 0; - } const DynamicApis &apis = DynamicApis::instance(); if (apis.pGetSystemMetricsForDpi) { const quint32 dpi = getDpiForWindow(hwnd); @@ -187,11 +223,47 @@ } } - static inline std::optional<MONITORINFOEXW> getMonitorForWindow(HWND hwnd) { - Q_ASSERT(hwnd); - if (!hwnd) { - return std::nullopt; + static inline quint32 getTitleBarHeight(HWND hwnd) { + const auto captionHeight = [hwnd]() -> int { + const DynamicApis &apis = DynamicApis::instance(); + if (apis.pGetSystemMetricsForDpi) { + const quint32 dpi = getDpiForWindow(hwnd); + return apis.pGetSystemMetricsForDpi(SM_CYCAPTION, dpi); + } else { + return ::GetSystemMetrics(SM_CYCAPTION); + } + }(); + return captionHeight + getResizeBorderThickness(hwnd); + } + + static inline void updateInternalWindowFrameMargins(HWND hwnd, QWindow *window) { + const auto margins = [hwnd]() -> QMargins { + const int titleBarHeight = getTitleBarHeight(hwnd); + if (isWin10OrGreater()) { + return {0, -titleBarHeight, 0, 0}; + } else { + const int frameSize = getResizeBorderThickness(hwnd); + return {-frameSize, -titleBarHeight, -frameSize, -frameSize}; + } + }(); + const QVariant marginsVar = QVariant::fromValue(margins); + window->setProperty("_q_windowsCustomMargins", marginsVar); +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) + if (QPlatformWindow *platformWindow = window->handle()) { + if (const auto ni = QGuiApplication::platformNativeInterface()) { + ni->setWindowProperty(platformWindow, QStringLiteral("_q_windowsCustomMargins"), + marginsVar); + } } +#else + if (const auto platformWindow = + dynamic_cast<QNativeInterface::Private::QWindowsWindow *>(window->handle())) { + platformWindow->setCustomMargins(margins); + } +#endif + } + + static inline MONITORINFOEXW getMonitorForWindow(HWND hwnd) { // Use "MONITOR_DEFAULTTONEAREST" here so that we can still get the correct // monitor even if the window is minimized. HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); @@ -201,6 +273,16 @@ return monitorInfo; }; + static inline void moveToDesktopCenter(HWND hwnd) { + const auto monitorInfo = getMonitorForWindow(hwnd); + RECT windowRect{}; + ::GetWindowRect(hwnd, &windowRect); + const auto newX = (RECT_WIDTH(monitorInfo.rcMonitor) - RECT_WIDTH(windowRect)) / 2; + const auto newY = (RECT_HEIGHT(monitorInfo.rcMonitor) - RECT_HEIGHT(windowRect)) / 2; + ::SetWindowPos(hwnd, nullptr, newX, newY, 0, 0, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); + } + static inline bool isFullScreen(HWND hwnd) { Q_ASSERT(hwnd); if (!hwnd) { @@ -208,13 +290,11 @@ } RECT windowRect{}; ::GetWindowRect(hwnd, &windowRect); - const std::optional<MONITORINFOEXW> mi = getMonitorForWindow(hwnd); // Compare to the full area of the screen, not the work area. - return (windowRect == mi.value_or(MONITORINFOEXW{}).rcMonitor); + return (windowRect == getMonitorForWindow(hwnd).rcMonitor); } - static inline bool isWindowNoState(HWND hwnd) - { + static inline bool isWindowNoState(HWND hwnd) { Q_ASSERT(hwnd); if (!hwnd) { return false; @@ -266,13 +346,13 @@ case HTBORDER: return Win32WindowContext::FixedBorder; default: - break; + break; // unreachable } return Win32WindowContext::Outside; } static bool isValidWindow(HWND hWnd, bool checkVisible, bool checkTopLevel) { - if (::IsWindow(hWnd) == FALSE) { + if (!::IsWindow(hWnd)) { return false; } const LONG_PTR styles = ::GetWindowLongPtrW(hWnd, GWL_STYLE); @@ -283,15 +363,15 @@ if (exStyles & WS_EX_TOOLWINDOW) { return false; } - RECT rect = {0, 0, 0, 0}; - if (::GetWindowRect(hWnd, &rect) == FALSE) { + RECT rect{}; + if (!::GetWindowRect(hWnd, &rect)) { return false; } if ((rect.left >= rect.right) || (rect.top >= rect.bottom)) { return false; } if (checkVisible) { - if (::IsWindowVisible(hWnd) == FALSE) { + if (!::IsWindowVisible(hWnd)) { return false; } } @@ -314,11 +394,14 @@ public: bool nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) override { + Q_UNUSED(eventType); + // It has been observed that the pointer that Qt gives us is sometimes null on some // machines. We need to guard against it in such scenarios. if (!result) { return false; } + if (lastMessageHandled) { *result = static_cast<QT_NATIVE_EVENT_RESULT_TYPE>(lastMessageResult); return true; @@ -332,11 +415,11 @@ static inline void install() { instance = new WindowsNativeEventFilter(); - qApp->installNativeEventFilter(instance); + installNativeEventFilter(instance); } static inline void uninstall() { - qApp->removeNativeEventFilter(instance); + removeNativeEventFilter(instance); delete instance; instance = nullptr; } @@ -419,13 +502,15 @@ // TODO: Determine whether to show system menu // ... - // Since Qt does the necessary processing of the message afterward, we still need to + // Since Qt does the necessary processing of the WM_NCCALCSIZE afterward, we still need to // continue dispatching it. + if (handled && message != WM_NCCALCSIZE) { + return result; + } return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); } - Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate) - : AbstractWindowContext(window, delegate) { + Win32WindowContext::Win32WindowContext() : AbstractWindowContext() { } Win32WindowContext::~Win32WindowContext() { @@ -440,11 +525,13 @@ } } - bool Win32WindowContext::setup() { - auto winId = m_windowHandle->winId(); - + bool Win32WindowContext::setupHost() { // Install window hook + auto winId = m_windowHandle->winId(); auto hWnd = reinterpret_cast<HWND>(winId); + + // Inform Qt we want and have set custom margins + updateInternalWindowFrameMargins(hWnd, m_windowHandle); // Store original window proc if (!g_qtWindowProc) { @@ -514,7 +601,7 @@ const auto &get = [](const int virtualKey) -> bool { return (::GetAsyncKeyState(virtualKey) < 0); }; - const bool buttonSwapped = (::GetSystemMetrics(SM_SWAPBUTTON) != FALSE); + const bool buttonSwapped = ::GetSystemMetrics(SM_SWAPBUTTON); if (get(VK_LBUTTON)) { result |= (buttonSwapped ? MK_RBUTTON : MK_LBUTTON); } @@ -657,9 +744,10 @@ // actually lost the hover state. So we filter out these superfluous mouse leave // events here to avoid this issue. DWORD dwScreenPos = ::GetMessagePos(); + POINT screenPoint{GET_X_LPARAM(dwScreenPos), GET_Y_LPARAM(dwScreenPos)}; + ::ScreenToClient(hWnd, &screenPoint); QPoint qtScenePos = - fromNativeLocalPosition(m_windowHandle, QPoint(GET_X_LPARAM(dwScreenPos), - GET_Y_LPARAM(dwScreenPos))); + fromNativeLocalPosition(m_windowHandle, {screenPoint.x, screenPoint.y}); auto dummy = CoreWindowAgent::Unknown; if (isInSystemButtons(qtScenePos, &dummy)) { // We must record whether the last WM_MOUSELEAVE was filtered, because if @@ -784,6 +872,18 @@ bool Win32WindowContext::customWindowHandler(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result) { switch (message) { + case WM_SHOWWINDOW: { + if (!centered) { + // If wParam is TRUE, the window is being shown. + // If lParam is zero, the message was sent because of a call to the ShowWindow + // function. + if (wParam && lParam == 0) { + centered = true; + moveToDesktopCenter(hWnd); + } + } + break; + } case WM_NCCALCSIZE: { // Windows鏄牴鎹繖涓秷鎭殑杩斿洖鍊兼潵璁剧疆绐楀彛鐨勫鎴峰尯锛堢獥鍙d腑鐪熸鏄剧ず鐨勫唴瀹癸級 // 鍜岄潪瀹㈡埛鍖猴紙鏍囬鏍忋�佺獥鍙h竟妗嗐�佽彍鍗曟爮鍜岀姸鎬佹爮绛塛indows绯荤粺鑷鎻愪緵鐨勯儴鍒� @@ -871,8 +971,8 @@ // simply return 0, which means "preserve the entire old client area // and align it with the upper-left corner of our new client area". const auto clientRect = - ((wParam == FALSE) ? reinterpret_cast<LPRECT>(lParam) - : &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(lParam))->rgrc[0]); + wParam ? &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(lParam))->rgrc[0] + : reinterpret_cast<LPRECT>(lParam); if (isWin10OrGreater()) { // Store the original top margin before the default window procedure applies the // default frame. @@ -938,9 +1038,7 @@ // we have to use another way to judge this if we are running // on Windows 7 or Windows 8. if (isWin8Point1OrGreater()) { - const std::optional<MONITORINFOEXW> monitorInfo = - getMonitorForWindow(hWnd); - const RECT monitorRect = monitorInfo.value().rcMonitor; + const RECT monitorRect = getMonitorForWindow(hWnd).rcMonitor; // This helper can be used to determine if there's an // auto-hide taskbar on the given edge of the monitor // we're currently on. @@ -1008,7 +1106,7 @@ // of the upper-left non-client area. It's confirmed that this issue exists // from Windows 7 to Windows 10. Not tested on Windows 11 yet. Don't know // whether it exists on Windows XP to Windows Vista or not. - *result = wParam == FALSE ? FALSE : WVR_REDRAW; + *result = wParam ? WVR_REDRAW : FALSE; return true; } case WM_NCHITTEST: { @@ -1087,45 +1185,50 @@ // window can behave just like a normal Win32 window even if we now // doesn't have a title bar at all. Most importantly, the flicker and // jitter during window resizing is totally gone now. The disadvantage - // is we have to draw a top frame border ourself. Previously I thought + // is we have to draw a top frame border ourselves. Previously I thought // we have to do the black magic in WM_PAINT just like what Windows // Terminal does, however, later I found that if we choose a proper // color, our homemade top border can almost have exactly the same // appearance with the system's one. - const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; + [[maybe_unused]] const auto &hitTestRecorder = qScopeGuard([this, result]() { + lastHitTestResult = getHitWindowPart(int(*result)); // + }); + + POINT nativeGlobalPos{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; POINT nativeLocalPos = nativeGlobalPos; ::ScreenToClient(hWnd, &nativeLocalPos); - auto clientRect = RECT{ 0, 0, 0, 0 }; + RECT clientRect{0, 0, 0, 0}; ::GetClientRect(hWnd, &clientRect); - const auto clientWidth = RECT_WIDTH(clientRect); - const auto clientHeight = RECT_HEIGHT(clientRect); + auto clientWidth = RECT_WIDTH(clientRect); + auto clientHeight = RECT_HEIGHT(clientRect); - const QPoint qtScenePos = fromNativeLocalPosition(m_windowHandle, QPoint(nativeLocalPos.x, nativeLocalPos.y)); + QPoint qtScenePos = fromNativeLocalPosition( + m_windowHandle, QPoint(nativeLocalPos.x, nativeLocalPos.y)); - const bool isFixedSize = /*isWindowFixedSize()*/false; // ### FIXME - const bool isTitleBar = isInTitleBarDraggableArea(qtScenePos); - const bool dontOverrideCursor = false; // ### TODO + bool isFixedSize = m_delegate->isHostSizeFixed(m_host); + bool isTitleBar = isInTitleBarDraggableArea(qtScenePos); + bool dontOverrideCursor = false; // ### TODO CoreWindowAgent::SystemButton sysButtonType = CoreWindowAgent::Unknown; if (!isFixedSize && isInSystemButtons(qtScenePos, &sysButtonType)) { - // Firstly, we set the hit test result to a default value to be able to detect whether we - // have changed it or not afterwards. + // Firstly, we set the hit test result to a default value to be able to detect + // whether we have changed it or not afterwards. *result = HTNOWHERE; - // Even if the mouse is inside the chrome button area now, we should still allow the user - // to be able to resize the window with the top or right window border, this is also the - // normal behavior of a native Win32 window (but only when the window is not maximized/ - // fullscreened/minimized, of course). + // Even if the mouse is inside the chrome button area now, we should still allow + // the user to be able to resize the window with the top or right window border, + // this is also the normal behavior of a native Win32 window (but only when the + // window is not maximized/fullscreen/minimized, of course). if (isWindowNoState(hWnd)) { static constexpr const int kBorderSize = 2; - const bool isTop = (nativeLocalPos.y <= kBorderSize); - const bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize)); + bool isTop = (nativeLocalPos.y <= kBorderSize); + bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize)); if (isTop || isRight) { if (dontOverrideCursor) { - // The user doesn't want the window to be resized, so we tell Windows we are - // in the client area so that the controls beneath the mouse cursor can still - // be hovered or clicked. + // The user doesn't want the window to be resized, so we tell + // Windows we are in the client area so that the controls beneath + // the mouse cursor can still be hovered or clicked. *result = (isTitleBar ? HTCAPTION : HTCLIENT); } else { if (isTop && isRight) { @@ -1139,8 +1242,9 @@ } } if (*result == HTNOWHERE) { - // OK, we are now really inside one of the chrome buttons, tell Windows the exact role of our button. - // The Snap Layout feature introduced in Windows 11 won't work without this. + // OK, we are now really inside one of the chrome buttons, tell Windows the + // exact role of our button. The Snap Layout feature introduced in Windows + // 11 won't work without this. switch (sysButtonType) { case CoreWindowAgent::WindowIcon: *result = HTSYSMENU; @@ -1157,35 +1261,38 @@ case CoreWindowAgent::Close: *result = HTCLOSE; break; - case CoreWindowAgent::Unknown: - break; + default: + break; // unreachable } } if (*result == HTNOWHERE) { - // OK, it seems we are not inside the window resize area, nor inside the chrome buttons, - // tell Windows we are in the client area to let Qt handle this event. + // OK, it seems we are not inside the window resize area, nor inside the + // chrome buttons, tell Windows we are in the client area to let Qt handle + // this event. *result = HTCLIENT; } return true; } - // OK, we are not inside any chrome buttons, try to find out which part of the window - // are we hitting. + // OK, we are not inside any chrome buttons, try to find out which part of the + // window are we hitting. - const bool max = IsMaximized(hWnd); - const bool full = isFullScreen(hWnd); - const int frameSize = getResizeBorderThickness(hWnd); - const bool isTop = (nativeLocalPos.y < frameSize); + bool max = IsMaximized(hWnd); + bool full = isFullScreen(hWnd); + int frameSize = getResizeBorderThickness(hWnd); + bool isTop = (nativeLocalPos.y < frameSize); if (isWin10OrGreater()) { // This will handle the left, right and bottom parts of the frame // because we didn't change them. - const LRESULT originalHitTestResult = ::DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam); + LRESULT originalHitTestResult = ::DefWindowProcW(hWnd, WM_NCHITTEST, 0, lParam); if (originalHitTestResult != HTCLIENT) { - // Even if the window is not resizable, we still can't return HTCLIENT here because - // when we enter this code path, it means the mouse cursor is outside the window, - // that is, the three transparent window resize area. Returning HTCLIENT will confuse - // Windows, we can't put our controls there anyway. - *result = ((isFixedSize || dontOverrideCursor) ? HTBORDER : originalHitTestResult); + // Even if the window is not resizable, we still can't return HTCLIENT here + // because when we enter this code path, it means the mouse cursor is + // outside the window, that is, the three transparent window resize area. + // Returning HTCLIENT will confuse Windows, we can't put our controls there + // anyway. + *result = ((isFixedSize || dontOverrideCursor) ? HTBORDER + : originalHitTestResult); return true; } if (full) { @@ -1205,7 +1312,9 @@ // Return HTCLIENT instead of HTBORDER here, because the mouse is // inside our homemade title bar now, return HTCLIENT to let our // title bar can still capture mouse events. - *result = ((isFixedSize || dontOverrideCursor) ? (isTitleBar ? HTCAPTION : HTCLIENT) : HTTOP); + *result = ((isFixedSize || dontOverrideCursor) + ? (isTitleBar ? HTCAPTION : HTCLIENT) + : HTTOP); return true; } if (isTitleBar) { @@ -1213,6 +1322,7 @@ return true; } *result = HTCLIENT; + return true; } else { if (full) { *result = HTCLIENT; @@ -1274,13 +1384,73 @@ return true; } *result = HTCLIENT; + return true; } - return true; } default: break; } + if (!isWin10OrGreater()) { + switch (message) { + case WM_NCUAHDRAWCAPTION: + case WM_NCUAHDRAWFRAME: { + // These undocumented messages are sent to draw themed window + // borders. Block them to prevent drawing borders over the client + // area. + *result = FALSE; + return true; + } + case WM_NCPAINT: { + // 杈规闃村奖澶勪簬闈炲鎴峰尯鐨勮寖鍥达紝鍥犳濡傛灉鐩存帴闃绘闈炲鎴峰尯鐨勭粯鍒讹紝浼氬鑷磋竟妗嗛槾褰变涪澶� + + if (!isDwmCompositionEnabled()) { + // Only block WM_NCPAINT when DWM composition is disabled. If + // it's blocked when DWM composition is enabled, the frame + // shadow won't be drawn. + *result = FALSE; + return true; + } else { + break; + } + } + case WM_NCACTIVATE: { + if (isDwmCompositionEnabled()) { + // DefWindowProc won't repaint the window border if lParam (normally a HRGN) + // is -1. See the following link's "lParam" section: + // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-ncactivate + // Don't use "*result = 0" here, otherwise the window won't respond to the + // window activation state change. + *result = ::DefWindowProcW(hWnd, WM_NCACTIVATE, wParam, -1); + } else { + if (wParam) { + *result = FALSE; + } else { + *result = TRUE; + } + } + return true; + } + case WM_SETICON: + case WM_SETTEXT: { + // Disable painting while these messages are handled to prevent them + // from drawing a window caption over the client area. + const auto oldStyle = static_cast<DWORD>(::GetWindowLongPtrW(hWnd, GWL_STYLE)); + // Prevent Windows from drawing the default title bar by temporarily + // toggling the WS_VISIBLE style. + const DWORD newStyle = (oldStyle & ~WS_VISIBLE); + ::SetWindowLongPtrW(hWnd, GWL_STYLE, static_cast<LONG_PTR>(newStyle)); + triggerFrameChange(hWnd); + const LRESULT originalResult = ::DefWindowProcW(hWnd, message, wParam, lParam); + ::SetWindowLongPtrW(hWnd, GWL_STYLE, static_cast<LONG_PTR>(oldStyle)); + triggerFrameChange(hWnd); + *result = originalResult; + return true; + } + default: + break; + } + } return false; } -} \ No newline at end of file +} -- Gitblit v1.9.1