From a0bede6ff6a700a6eea9702c49d378f07de22f63 Mon Sep 17 00:00:00 2001 From: Sine Striker <trueful@163.com> Date: 周三, 06 12月 2023 12:11:37 +0800 Subject: [PATCH] Fix title bar hover bug --- src/core/contexts/win32windowcontext.cpp | 714 +++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 files changed, 592 insertions(+), 122 deletions(-) diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index 7c50d84..8800a7f 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -1,45 +1,70 @@ #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 <QtCore/QTimer> +#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 { - static constexpr const auto kAutoHideTaskBarThickness = quint8{ 2 }; // The thickness of an auto-hide taskbar in pixels. + static constexpr const auto kAutoHideTaskBarThickness = + quint8{2}; // The thickness of an auto-hide taskbar in pixels. using WndProcHash = QHash<HWND, Win32WindowContext *>; // hWnd -> context - Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash); + Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash) 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; DynamicApis() { QSystemLibrary user32(QStringLiteral("user32.dll")); - pGetDpiForWindow = reinterpret_cast<decltype(pGetDpiForWindow)>(user32.resolve("GetDpiForWindow")); - pGetSystemMetricsForDpi = reinterpret_cast<decltype(pGetSystemMetricsForDpi)>(user32.resolve("GetSystemMetricsForDpi")); + pGetDpiForWindow = + reinterpret_cast<decltype(pGetDpiForWindow)>(user32.resolve("GetDpiForWindow")); + pGetSystemMetricsForDpi = reinterpret_cast<decltype(pGetSystemMetricsForDpi)>( + user32.resolve("GetSystemMetricsForDpi")); QSystemLibrary shcore(QStringLiteral("shcore.dll")); - pGetDpiForMonitor = reinterpret_cast<decltype(pGetDpiForMonitor)>(shcore.resolve("GetDpiForMonitor")); + pGetDpiForMonitor = + reinterpret_cast<decltype(pGetDpiForMonitor)>(shcore.resolve("GetDpiForMonitor")); QSystemLibrary dwmapi(QStringLiteral("dwmapi.dll")); pDwmFlush = reinterpret_cast<decltype(pDwmFlush)>(dwmapi.resolve("DwmFlush")); + pDwmIsCompositionEnabled = reinterpret_cast<decltype(pDwmIsCompositionEnabled)>( + dwmapi.resolve("DwmIsCompositionEnabled")); } ~DynamicApis() = default; @@ -53,124 +78,142 @@ Q_DISABLE_COPY_MOVE(DynamicApis) }; - static inline constexpr bool operator==(const POINT &lhs, const POINT &rhs) noexcept - { + static inline constexpr bool operator==(const POINT &lhs, const POINT &rhs) noexcept { return ((lhs.x == rhs.x) && (lhs.y == rhs.y)); } - static inline constexpr bool operator!=(const POINT &lhs, const POINT &rhs) noexcept - { + static inline constexpr bool operator!=(const POINT &lhs, const POINT &rhs) noexcept { return !operator==(lhs, rhs); } - static inline constexpr bool operator==(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator==(const SIZE &lhs, const SIZE &rhs) noexcept { return ((lhs.cx == rhs.cx) && (lhs.cy == rhs.cy)); } - static inline constexpr bool operator!=(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator!=(const SIZE &lhs, const SIZE &rhs) noexcept { return !operator==(lhs, rhs); } - static inline constexpr bool operator>(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator>(const SIZE &lhs, const SIZE &rhs) noexcept { return ((lhs.cx * lhs.cy) > (rhs.cx * rhs.cy)); } - static inline constexpr bool operator>=(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator>=(const SIZE &lhs, const SIZE &rhs) noexcept { return (operator>(lhs, rhs) || operator==(lhs, rhs)); } - static inline constexpr bool operator<(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator<(const SIZE &lhs, const SIZE &rhs) noexcept { return (operator!=(lhs, rhs) && !operator>(lhs, rhs)); } - static inline constexpr bool operator<=(const SIZE &lhs, const SIZE &rhs) noexcept - { + static inline constexpr bool operator<=(const SIZE &lhs, const SIZE &rhs) noexcept { return (operator<(lhs, rhs) || operator==(lhs, rhs)); } - static inline constexpr bool operator==(const RECT &lhs, const RECT &rhs) noexcept - { - return ((lhs.left == rhs.left) && (lhs.top == rhs.top) && (lhs.right == rhs.right) && (lhs.bottom == rhs.bottom)); + static inline constexpr bool operator==(const RECT &lhs, const RECT &rhs) noexcept { + return ((lhs.left == rhs.left) && (lhs.top == rhs.top) && (lhs.right == rhs.right) && + (lhs.bottom == rhs.bottom)); } - static inline constexpr bool operator!=(const RECT &lhs, const RECT &rhs) noexcept - { + static inline constexpr bool operator!=(const RECT &lhs, const RECT &rhs) noexcept { return !operator==(lhs, rhs); } - static inline constexpr QPoint point2qpoint(const POINT &point) - { - return QPoint{ int(point.x), int(point.y) }; + static inline constexpr QPoint point2qpoint(const POINT &point) { + return QPoint{int(point.x), int(point.y)}; } - static inline constexpr POINT qpoint2point(const QPoint &point) - { - return POINT{ LONG(point.x()), LONG(point.y()) }; + static inline constexpr POINT qpoint2point(const QPoint &point) { + return POINT{LONG(point.x()), LONG(point.y())}; } - static inline constexpr QSize size2qsize(const SIZE &size) - { - return QSize{ int(size.cx), int(size.cy) }; + static inline constexpr QSize size2qsize(const SIZE &size) { + return QSize{int(size.cx), int(size.cy)}; } - static inline constexpr SIZE qsize2size(const QSize &size) - { - return SIZE{ LONG(size.width()), LONG(size.height()) }; + static inline constexpr SIZE qsize2size(const QSize &size) { + return SIZE{LONG(size.width()), LONG(size.height())}; } - static inline constexpr QRect rect2qrect(const RECT &rect) - { - return QRect{ QPoint{ int(rect.left), int(rect.top) }, QSize{ int(RECT_WIDTH(rect)), int(RECT_HEIGHT(rect)) } }; + static inline constexpr QRect rect2qrect(const RECT &rect) { + return QRect{ + QPoint{int(rect.left), int(rect.top) }, + QSize{int(RECT_WIDTH(rect)), int(RECT_HEIGHT(rect))} + }; } - static inline constexpr RECT qrect2rect(const QRect &qrect) - { - return RECT{ LONG(qrect.left()), LONG(qrect.top()), LONG(qrect.right()), LONG(qrect.bottom()) }; + static inline constexpr RECT qrect2rect(const QRect &qrect) { + return RECT{LONG(qrect.left()), LONG(qrect.top()), LONG(qrect.right()), + LONG(qrect.bottom())}; } - static inline /*constexpr*/ QString hwnd2str(const WId windowId) - { + static inline /*constexpr*/ QString hwnd2str(const WId windowId) { // NULL handle is allowed here. - return QLatin1String("0x") + QString::number(windowId, 16).toUpper().rightJustified(8, u'0'); + return QLatin1String("0x") + + QString::number(windowId, 16).toUpper().rightJustified(8, u'0'); } - static inline /*constexpr*/ QString hwnd2str(const HWND hwnd) - { + static inline /*constexpr*/ QString hwnd2str(HWND hwnd) { // NULL handle is allowed here. 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; + static const bool result = + QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1; return result; } static inline bool isWin10OrGreater() { - static const bool result = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10; + static const bool result = + QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10; return result; } - static inline quint32 getDpiForWindow(const 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.pGetDpiForWindow) { // Win10 + if (!apis.pDwmIsCompositionEnabled) { + return false; + } + BOOL enabled = FALSE; + return SUCCEEDED(apis.pDwmIsCompositionEnabled(&enabled)) && enabled; + } + + static inline void triggerFrameChange(HWND hwnd) { + Q_ASSERT(hwnd); + if (!hwnd) { + return; + } + ::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) { + Q_ASSERT(hwnd); + if (!hwnd) { + return 0; + } + const DynamicApis &apis = DynamicApis::instance(); + if (apis.pGetDpiForWindow) { // Win10 return apis.pGetDpiForWindow(hwnd); } else if (apis.pGetDpiForMonitor) { // Win8.1 - const HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); - UINT dpiX{ USER_DEFAULT_SCREEN_DPI }; - UINT dpiY{ USER_DEFAULT_SCREEN_DPI }; + HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + UINT dpiX{USER_DEFAULT_SCREEN_DPI}; + UINT dpiY{USER_DEFAULT_SCREEN_DPI}; apis.pGetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); return dpiX; } else { // Win2K - const HDC hdc = ::GetDC(nullptr); + HDC hdc = ::GetDC(nullptr); const int dpiX = ::GetDeviceCaps(hdc, LOGPIXELSX); const int dpiY = ::GetDeviceCaps(hdc, LOGPIXELSY); ::ReleaseDC(nullptr, hdc); @@ -178,7 +221,7 @@ } } - static inline quint32 getResizeBorderThickness(const HWND hwnd) { + static inline quint32 getResizeBorderThickness(HWND hwnd) { Q_ASSERT(hwnd); if (!hwnd) { return 0; @@ -186,28 +229,77 @@ const DynamicApis &apis = DynamicApis::instance(); if (apis.pGetSystemMetricsForDpi) { const quint32 dpi = getDpiForWindow(hwnd); - return apis.pGetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) + apis.pGetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); + return apis.pGetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) + + apis.pGetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); } else { return ::GetSystemMetrics(SM_CXSIZEFRAME) + ::GetSystemMetrics(SM_CXPADDEDBORDER); } } - static inline std::optional<MONITORINFOEXW> getMonitorForWindow(const HWND hwnd) - { + static inline quint32 getTitleBarHeight(HWND hwnd) { + Q_ASSERT(hwnd); + if (!hwnd) { + return 0; + } + 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) { + Q_ASSERT(hwnd); + Q_ASSERT(window); + if (!hwnd || !window) { + return; + } + 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 std::optional<MONITORINFOEXW> getMonitorForWindow(HWND hwnd) { Q_ASSERT(hwnd); if (!hwnd) { return std::nullopt; } // Use "MONITOR_DEFAULTTONEAREST" here so that we can still get the correct // monitor even if the window is minimized. - const HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); MONITORINFOEXW monitorInfo{}; monitorInfo.cbSize = sizeof(monitorInfo); ::GetMonitorInfoW(monitor, &monitorInfo); return monitorInfo; }; - static inline bool isFullScreen(const HWND hwnd) { + static inline bool isFullScreen(HWND hwnd) { Q_ASSERT(hwnd); if (!hwnd) { return false; @@ -217,6 +309,22 @@ 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); + } + + static inline bool isWindowNoState(HWND hwnd) { + Q_ASSERT(hwnd); + if (!hwnd) { + return false; + } +#if 0 + WINDOWPLACEMENT wp{}; + wp.length = sizeof(wp); + ::GetWindowPlacement(hwnd, &wp); + return ((wp.showCmd == SW_NORMAL) || (wp.showCmd == SW_RESTORE)); +#else + const auto style = static_cast<DWORD>(::GetWindowLongPtrW(hwnd, GWL_STYLE)); + return (!(style & (WS_MINIMIZE | WS_MAXIMIZE))); +#endif } static inline QPoint fromNativeLocalPosition(const QWindow *window, const QPoint &point) { @@ -255,38 +363,37 @@ case HTBORDER: return Win32WindowContext::FixedBorder; default: - break; + break; // unreachable } return Win32WindowContext::Outside; } - static bool isValidWindow(WId windowId, bool checkVisible, bool checkTopLevel) { - const auto hwnd = reinterpret_cast<HWND>(windowId); - if (::IsWindow(hwnd) == FALSE) { + static bool isValidWindow(HWND hWnd, bool checkVisible, bool checkTopLevel) { + if (::IsWindow(hWnd) == FALSE) { return false; } - const LONG_PTR styles = ::GetWindowLongPtrW(hwnd, GWL_STYLE); + const LONG_PTR styles = ::GetWindowLongPtrW(hWnd, GWL_STYLE); if (styles & WS_DISABLED) { return false; } - const LONG_PTR exStyles = ::GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + const LONG_PTR exStyles = ::GetWindowLongPtrW(hWnd, GWL_EXSTYLE); if (exStyles & WS_EX_TOOLWINDOW) { return false; } RECT rect = {0, 0, 0, 0}; - if (::GetWindowRect(hwnd, &rect) == FALSE) { + if (::GetWindowRect(hWnd, &rect) == FALSE) { return false; } if ((rect.left >= rect.right) || (rect.top >= rect.bottom)) { return false; } if (checkVisible) { - if (::IsWindowVisible(hwnd) == FALSE) { + if (::IsWindowVisible(hWnd) == FALSE) { return false; } } if (checkTopLevel) { - if (::GetAncestor(hwnd, GA_ROOT) != hwnd) { + if (::GetAncestor(hWnd, GA_ROOT) != hWnd) { return false; } } @@ -296,7 +403,7 @@ // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1556 // In QWindowsContext::windowsProc(), the messages will be passed to all global native event // filters, but because we have already filtered the messages in the hook WndProc function for - // convenience, Qt does not know we may have already process the messages and thus will call + // convenience, Qt does not know we may have already processed the messages and thus will call // DefWindowProc(). Consequently, we have to add a global native filter that forwards the result // of the hook function, telling Qt whether we have filtered the events before. Since Qt only // handles Windows window messages in the main thread, it is safe to do so. @@ -304,11 +411,14 @@ public: bool nativeEventFilter(const QByteArray &eventType, void *message, QT_NATIVE_EVENT_RESULT_TYPE *result) override { - // 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. + 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; @@ -322,11 +432,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; } @@ -409,13 +519,16 @@ // 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 (message != WM_NCCALCSIZE && handled) { + return result; + } return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); } - Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate) - : AbstractWindowContext(window, delegate) { + Win32WindowContext::Win32WindowContext(QObject *host, WindowItemDelegate *delegate) + : AbstractWindowContext(host, delegate) { } Win32WindowContext::~Win32WindowContext() { @@ -431,10 +544,16 @@ } bool Win32WindowContext::setup() { - auto winId = m_windowHandle->winId(); + if (!m_windowHandle) { + return false; + } // 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) { @@ -454,6 +573,8 @@ // Save window handle mapping g_wndProcHash->insert(hWnd, this); + + QTimer::singleShot(0, m_windowHandle, [hWnd]() { triggerFrameChange(hWnd); }); return true; } @@ -476,7 +597,7 @@ break; } - if (!isValidWindow(windowId, false, true)) { + if (!isValidWindow(hWnd, false, true)) { return false; } @@ -647,9 +768,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 @@ -772,8 +894,7 @@ } bool Win32WindowContext::customWindowHandler(HWND hWnd, UINT message, WPARAM wParam, - LPARAM lParam, LRESULT *result) - { + LPARAM lParam, LRESULT *result) { switch (message) { case WM_NCCALCSIZE: { // Windows鏄牴鎹繖涓秷鎭殑杩斿洖鍊兼潵璁剧疆绐楀彛鐨勫鎴峰尯锛堢獥鍙d腑鐪熸鏄剧ず鐨勫唴瀹癸級 @@ -824,7 +945,7 @@ // bar, the four window borders, the frame shadow, the menu bar, the // status bar, the scroll bar, etc. But for Qt, it draws most of the // window area (client + non-client) itself. We now know that the - // title bar and the window frame is in the non-client area and we + // title bar and the window frame is in the non-client area, and we // can set the scope of the client area in this message, so we can // remove the title bar and the window frame by let the non-client // area be covered by the client area (because we can't really get @@ -861,30 +982,35 @@ // implement an elaborate client-area preservation technique, and // 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]); + const auto clientRect = + ((wParam == FALSE) ? reinterpret_cast<LPRECT>(lParam) + : &(reinterpret_cast<LPNCCALCSIZE_PARAMS>(lParam))->rgrc[0]); if (isWin10OrGreater()) { - // Store the original top margin before the default window procedure applies the default frame. + // Store the original top margin before the default window procedure applies the + // default frame. const LONG originalTop = clientRect->top; - // Apply the default frame because we don't want to remove the whole window frame, - // we still need the standard window frame (the resizable frame border and the frame - // shadow) for the left, bottom and right edges. - // If we return 0 here directly, the whole window frame will be removed (which means - // there will be no resizable frame border and the frame shadow will also disappear), - // and that's also how most applications customize their title bars on Windows. It's - // totally OK but since we want to preserve as much original frame as possible, we - // can't use that solution. - const LRESULT hitTestResult = ::DefWindowProcW(hWnd, WM_NCCALCSIZE, wParam, lParam); + // Apply the default frame because we don't want to remove the whole window + // frame, we still need the standard window frame (the resizable frame border + // and the frame shadow) for the left, bottom and right edges. If we return 0 + // here directly, the whole window frame will be removed (which means there will + // be no resizable frame border and the frame shadow will also disappear), and + // that's also how most applications customize their title bars on Windows. It's + // totally OK but since we want to preserve as much original frame as possible, + // we can't use that solution. + const LRESULT hitTestResult = + ::DefWindowProcW(hWnd, WM_NCCALCSIZE, wParam, lParam); if ((hitTestResult != HTERROR) && (hitTestResult != HTNOWHERE)) { *result = hitTestResult; return true; } - // Re-apply the original top from before the size of the default frame was applied, - // and the whole top frame (the title bar and the top border) is gone now. - // For the top frame, we only has 2 choices: (1) remove the top frame entirely, or - // (2) don't touch it at all. We can't preserve the top border by adjusting the top - // margin here. If we try to modify the top margin, the original title bar will - // always be painted by DWM regardless what margin we set, so here we can only remove - // the top frame entirely and use some special technique to bring the top border back. + // Re-apply the original top from before the size of the default frame was + // applied, and the whole top frame (the title bar and the top border) is gone + // now. For the top frame, we only has 2 choices: (1) remove the top frame + // entirely, or (2) don't touch it at all. We can't preserve the top border by + // adjusting the top margin here. If we try to modify the top margin, the + // original title bar will always be painted by DWM regardless what margin we + // set, so here we can only remove the top frame entirely and use some special + // technique to bring the top border back. clientRect->top = originalTop; } const bool max = IsMaximized(hWnd); @@ -914,8 +1040,7 @@ // still find the right monitor even when we're restoring from // minimized. if (max || full) { - APPBARDATA abd; - SecureZeroMemory(&abd, sizeof(abd)); + APPBARDATA abd{}; abd.cbSize = sizeof(abd); const UINT taskbarState = ::SHAppBarMessage(ABM_GETSTATE, &abd); // First, check if we have an auto-hide taskbar at all: @@ -925,7 +1050,8 @@ // 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 std::optional<MONITORINFOEXW> monitorInfo = + getMonitorForWindow(hWnd); const RECT monitorRect = monitorInfo.value().rcMonitor; // This helper can be used to determine if there's an // auto-hide taskbar on the given edge of the monitor @@ -935,7 +1061,8 @@ abd2.cbSize = sizeof(abd2); abd2.uEdge = edge; abd2.rc = monitorRect; - const auto hTaskbar = reinterpret_cast<HWND>(::SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &abd2)); + const auto hTaskbar = reinterpret_cast<HWND>( + ::SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &abd2)); return (hTaskbar != nullptr); }; top = hasAutohideTaskbar(ABE_TOP); @@ -947,11 +1074,13 @@ APPBARDATA abd2{}; abd2.cbSize = sizeof(abd2); abd2.hWnd = ::FindWindowW(L"Shell_TrayWnd", nullptr); - const HMONITOR windowMonitor = ::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); - const HMONITOR taskbarMonitor = ::MonitorFromWindow(abd2.hWnd, MONITOR_DEFAULTTOPRIMARY); + HMONITOR windowMonitor = + ::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); + HMONITOR taskbarMonitor = + ::MonitorFromWindow(abd2.hWnd, MONITOR_DEFAULTTOPRIMARY); if (taskbarMonitor == windowMonitor) { ::SHAppBarMessage(ABM_GETTASKBARPOS, &abd2); - edge = abd2.uEdge; + edge = int(abd2.uEdge); } top = (edge == ABE_TOP); bottom = (edge == ABE_BOTTOM); @@ -980,9 +1109,10 @@ } } } - // ### TODO: std::ignore = Utils::syncWmPaintWithDwm(); // This should be executed at the very last. - // By returning WVR_REDRAW we can make the window resizing look less broken. - // But we must return 0 if wParam is FALSE, according to Microsoft Docs. + // ### TODO: std::ignore = Utils::syncWmPaintWithDwm(); // This should be executed + // at the very last. By returning WVR_REDRAW we can make the window resizing look + // less broken. But we must return 0 if wParam is FALSE, according to Microsoft + // Docs. // **IMPORTANT NOTE**: // If you are drawing something manually through D3D in your window, don't // try to return WVR_REDRAW here, otherwise Windows exhibits bugs where @@ -993,10 +1123,350 @@ *result = wParam == FALSE ? FALSE : WVR_REDRAW; return true; } + case WM_NCHITTEST: { + // 鍘熺敓Win32绐楀彛鍙湁椤惰竟鏄湪绐楀彛鍐呴儴resize鐨勶紝鍏朵綑涓夎竟閮芥槸鍦ㄧ獥鍙� + // 澶栭儴杩涜resize鐨勶紝鍏跺師鐞嗘槸锛學S_THICKFRAME杩欎釜绐楀彛鏍峰紡浼氬湪绐� + // 鍙g殑宸︺�佸彸鍜屽簳杈规坊鍔犱笁涓�忔槑鐨剅esize鍖哄煙锛岃繖涓変釜鍖哄煙鍦ㄦ甯哥姸鎬� + // 涓嬫槸瀹屽叏涓嶅彲瑙佺殑锛屽畠浠敱DWM璐熻矗缁樺埗鍜屾帶鍒躲�傝繖浜涘尯鍩熺殑瀹藉害绛変簬 + // (SM_CXSIZEFRAME + SM_CXPADDEDBORDER)锛岄珮搴︾瓑浜� + // (SM_CYSIZEFRAME + SM_CXPADDEDBORDER)锛屽湪100%缂╂斁鏃讹紝鍧囩瓑 + // 浜�8鍍忕礌銆傚畠浠睘浜庣獥鍙e尯鍩熺殑涓�閮ㄥ垎锛屼絾涓嶅睘浜庡鎴峰尯锛岃�屾槸灞炰簬闈炲 + // 鎴峰尯锛屽洜姝etWindowRect鑾峰彇鐨勫尯鍩熶腑鏄寘鍚繖涓変釜resize鍖哄煙鐨勶紝 + // 鑰孏etClientRect鑾峰彇鐨勫尯鍩熸槸涓嶅寘鍚畠浠殑銆傚綋鎶� + // DWMWA_EXTENDED_FRAME_BOUNDS浣滀负鍙傛暟璋冪敤 + // DwmGetWindowAttribute鏃讹紝涔熻兘鑾峰彇鍒颁竴涓獥鍙eぇ灏忥紝杩欎釜澶у皬浠� + // 浜庡墠闈袱鑰呬箣闂达紝鏆傛椂涓嶇煡閬撹繖涓暟鎹殑鎰忎箟鍙婂叾浣滅敤銆傛垜浠湪 + // WM_NCCALCSIZE娑堟伅鐨勫鐞嗕腑锛屽凡缁忔妸鏁翠釜绐楀彛閮借缃负瀹㈡埛鍖轰簡锛屼篃 + // 灏辨槸璇达紝鎴戜滑鐨勭獥鍙e凡缁忔病鏈夐潪瀹㈡埛鍖轰簡锛屽洜姝ら偅涓変釜閫忔槑鐨剅esize鍖� + // 鍩燂紝姝ゅ埢涔熷凡缁忔垚涓虹獥鍙e鎴峰尯鐨勪竴閮ㄥ垎浜嗭紝浠庤�屽彉寰椾笉閫忔槑浜嗐�傛墍浠� + // 鐜板湪鐨剅esize锛岀湅璧锋潵鍍忔槸鍦ㄧ獥鍙e唴閮╮esize锛屾槸鍥犱负鍘熸湰閫忔槑鐨勫湴鏂� + // 鐜板湪鍙樺緱涓嶉�忔槑浜嗭紝瀹為檯涓婏紝鍗曠函浠庤寖鍥翠笂鏉ョ湅锛岀幇鍦ㄦ垜浠瑀esize鐨勫湴鏂癸紝 + // 灏辨槸鏅�氱獥鍙g殑杈规澶栭儴锛岄偅涓変釜閫忔槑鍖哄煙鐨勮寖鍥淬�� + // 鍥犳锛屽鏋滄垜浠妸杈规瀹屽叏鍘绘帀锛堝氨鏄垜浠鍦ㄥ仛鐨勪簨鎯咃級锛宺esize灏� + // 浼氱湅璧锋潵鏄湪鍐呴儴杩涜锛岃繖涓棶棰橀�氳繃甯歌鏂规硶闈炲父闅句互瑙e喅銆傛垜娴嬭瘯杩� + // QQ鍜岄拤閽夌殑绐楀彛锛屽畠浠殑绐楀彛灏辨槸鍦ㄥ閮╮esize锛屼絾瀹為檯涓婂畠浠槸閫氳繃 + // 鎶婄獥鍙e疄闄呯殑鍐呭锛屽祵鍏ュ埌涓�涓畬鍏ㄩ�忔槑鐨勪絾灏哄瑕佸ぇ涓�鍦堢殑绐楀彛涓疄鐜� + // 鐨勶紝铏界劧鐪嬭捣鏉ユ晥鏋滆繕涓嶉敊锛屼絾瀵逛簬姝ら」鐩�岃█锛屼唬鐮佸拰绐楀彛缁撴瀯杩囦簬澶� + // 鏉傦紝鍥犳鎴戞病鏈夐噰鐢ㄦ鏂规銆傜劧鑰岋紝瀵逛簬鍏蜂綋鐨勮蒋浠堕」鐩�岃█锛屽叾鍋氭硶涔� + // 涓嶅け涓轰竴涓紭绉�鐨勮В鍐虫柟妗堬紝姣曠珶鍏跺湪澶у鏁版潯浠朵笅鐨勮〃鐜伴兘杩樺彲浠ャ�� + // + // 鍜�1.x鐨勫仛娉曚笉鍚岋紝鐜板湪鐨�2.x閫夋嫨浜嗕繚鐣欑獥鍙d笁杈癸紝鍘婚櫎鏁翠釜绐楀彛椤堕儴锛� + // 濂藉鏄繚鐣欎簡绯荤粺鐨勫師鐢熻竟妗嗭紝澶栬杈冨ソ锛屼笖涓庣郴缁熺粨鍚堢揣瀵嗭紝鑰屼笖resize + // 鐨勮〃鐜颁篃鏈夊緢澶ф敼鍠勶紝缂虹偣鏄渶瑕佽嚜琛岀粯鍒堕《閮ㄨ竟妗嗙嚎銆傚師鏈互涓哄彧鑳藉儚 + // Windows Terminal閭f牱鍦╓M_PAINT閲屾悶榛戦瓟娉曪紝浣嗗悗鏉ュ彂鐜帮紝鍏跺疄鍙� + // 瑕侀鑹茬浉杩戯紝鎴戜滑鑷缁樺埗涓�鏍瑰疄绾夸篃鍑犱箮鑳戒互鍋囦贡鐪燂紝鑰屼笖杩欐牱涔熶笉浼� + // 鐮村潖Qt鑷繁鐨勭粯鍒剁郴缁燂紝鑳藉仛鍒颁笉渚濊禆榛戦瓟娉曞氨鑳藉疄鐜板儚Windows Terminal + // 閭f牱澶栬鍜屽姛鑳介兘姣旇緝瀹岀編鐨勮嚜瀹氫箟杈规銆� + + // A normal Win32 window can be resized outside of it. Here is the + // reason: the WS_THICKFRAME window style will cause a window has three + // transparent areas beside the window's left, right and bottom + // edge. Their width or height is eight pixels if the window is not + // scaled. In most cases, they are totally invisible. It's DWM's + // responsibility to draw and control them. They exist to let the + // user resize the window, visually outside of it. They are in the + // window area, but not the client area, so they are in the + // non-client area actually. But we have turned the whole window + // area into client area in WM_NCCALCSIZE, so the three transparent + // resize areas also become a part of the client area and thus they + // become visible. When we resize the window, it looks like we are + // resizing inside of it, however, that's because the transparent + // resize areas are visible now, we ARE resizing outside of the + // window actually. But I don't know how to make them become + // transparent again without breaking the frame shadow drawn by DWM. + // If you really want to solve it, you can try to embed your window + // into a larger transparent window and draw the frame shadow + // yourself. As what we have said in WM_NCCALCSIZE, you can only + // remove the top area of the window, this will let us be able to + // resize outside of the window and don't need much process in this + // message, it looks like a perfect plan, however, the top border is + // missing due to the whole top area is removed, and it's very hard + // to bring it back because we have to use a trick in WM_PAINT + // (learned from Windows Terminal), but no matter what we do in + // WM_PAINT, it will always break the backing store mechanism of Qt, + // so actually we can't do it. And it's very difficult to do such + // things in NativeEventFilters as well. What's worse, if we really + // do this, the four window borders will become white and they look + // horrible in dark mode. This solution only supports Windows 10 + // because the border width on Win10 is only one pixel, however it's + // eight pixels on Windows 7 so preserving the three window borders + // looks terrible on old systems. + // + // Unlike the 1.x code, we choose to preserve the three edges of the + // window in 2.x, and get rid of the whole top part of the window. + // There are quite some advantages such as the appearance looks much + // better and due to we have the original system window frame, our + // 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 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. + + [[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); + + RECT clientRect{0, 0, 0, 0}; + ::GetClientRect(hWnd, &clientRect); + auto clientWidth = RECT_WIDTH(clientRect); + auto clientHeight = RECT_HEIGHT(clientRect); + + QPoint qtScenePos = fromNativeLocalPosition( + m_windowHandle, QPoint(nativeLocalPos.x, nativeLocalPos.y)); + + 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. + *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/fullscreen/minimized, of course). + if (isWindowNoState(hWnd)) { + static constexpr const int kBorderSize = 2; + 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. + *result = (isTitleBar ? HTCAPTION : HTCLIENT); + } else { + if (isTop && isRight) { + *result = HTTOPRIGHT; + } else if (isTop) { + *result = HTTOP; + } else { + *result = HTRIGHT; + } + } + } + } + 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. + switch (sysButtonType) { + case CoreWindowAgent::WindowIcon: + *result = HTSYSMENU; + break; + case CoreWindowAgent::Help: + *result = HTHELP; + break; + case CoreWindowAgent::Minimize: + *result = HTREDUCE; + break; + case CoreWindowAgent::Maximize: + *result = HTZOOM; + break; + 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. + *result = HTCLIENT; + } + return true; + } + // OK, we are not inside any chrome buttons, try to find out which part of the + // window are we hitting. + + 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. + 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); + return true; + } + if (full) { + *result = HTCLIENT; + return true; + } + if (max) { + *result = (isTitleBar ? HTCAPTION : HTCLIENT); + return true; + } + // At this point, we know that the cursor is inside the client area + // so it has to be either the little border at the top of our custom + // title bar or the drag bar. Apparently, it must be the drag bar or + // the little border at the top which the user can use to move or + // resize the window. + if (isTop) { + // 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); + return true; + } + if (isTitleBar) { + *result = HTCAPTION; + return true; + } + *result = HTCLIENT; + return true; + } else { + if (full) { + *result = HTCLIENT; + return true; + } + if (max) { + *result = (isTitleBar ? HTCAPTION : HTCLIENT); + return true; + } + if (!isFixedSize) { + const bool isBottom = (nativeLocalPos.y >= (clientHeight - frameSize)); + // Make the border a little wider to let the user easy to resize on corners. + const auto scaleFactor = ((isTop || isBottom) ? qreal(2) : qreal(1)); + const int scaledFrameSizeX = std::round(qreal(frameSize) * scaleFactor); + const bool isLeft = (nativeLocalPos.x < scaledFrameSizeX); + const bool isRight = (nativeLocalPos.x >= (clientWidth - scaledFrameSizeX)); + if (dontOverrideCursor && (isTop || isBottom || isLeft || isRight)) { + // Return HTCLIENT instead of HTBORDER here, because the mouse is + // inside the window now, return HTCLIENT to let the controls + // inside our window can still capture mouse events. + *result = (isTitleBar ? HTCAPTION : HTCLIENT); + return true; + } + if (isTop) { + if (isLeft) { + *result = HTTOPLEFT; + return true; + } + if (isRight) { + *result = HTTOPRIGHT; + return true; + } + *result = HTTOP; + return true; + } + if (isBottom) { + if (isLeft) { + *result = HTBOTTOMLEFT; + return true; + } + if (isRight) { + *result = HTBOTTOMRIGHT; + return true; + } + *result = HTBOTTOM; + return true; + } + if (isLeft) { + *result = HTLEFT; + return true; + } + if (isRight) { + *result = HTRIGHT; + return true; + } + } + if (isTitleBar) { + *result = HTCAPTION; + return true; + } + *result = HTCLIENT; + 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 == FALSE) { + *result = TRUE; + } else { + *result = FALSE; + } + } + 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