From a5372ac566469efb59e6cfb29a695021c7c3528d Mon Sep 17 00:00:00 2001 From: Sine Striker <trueful@163.com> Date: 周三, 20 12月 2023 00:53:42 +0800 Subject: [PATCH] Add TODO --- src/core/contexts/win32windowcontext.cpp | 2044 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 2,001 insertions(+), 43 deletions(-) diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index ab32ed0..230bf5d 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -1,16 +1,804 @@ #include "win32windowcontext_p.h" +#include <optional> + #include <QtCore/QHash> +#include <QtCore/QScopeGuard> +#include <QtCore/QTimer> +#include <QtGui/QGuiApplication> +#include <QtGui/QPainter> +#include <QtGui/QPalette> +#include <QtGui/QStyleHints> + +#include <QtCore/private/qwinregistry_p.h> +#include <QtCore/private/qsystemlibrary_p.h> +#include <QtGui/private/qhighdpiscaling_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> +#include <timeapi.h> + +#include "qwkglobal_p.h" + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +Q_DECLARE_METATYPE(QMargins) +#endif namespace QWK { - using WndProcHash = QHash<HWND, Win32WindowContext *>; // hWnd -> context - Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash); + enum _DWMWINDOWATTRIBUTE { + // [set] BOOL, Allows the use of host backdrop brushes for the window. + _DWMWA_USE_HOSTBACKDROPBRUSH = 17, - static WNDPROC g_qtWindowProc = nullptr; // Original Qt window proc function + // Undocumented, the same with DWMWA_USE_IMMERSIVE_DARK_MODE, but available on systems + // before Win10 20H1. + _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19, - extern "C" LRESULT QT_WIN_CALLBACK QWK_WindowsWndProc(HWND hWnd, UINT message, WPARAM wParam, - LPARAM lParam) { + // [set] BOOL, Allows a window to either use the accent color, or dark, according to the + // user Color Mode preferences. + _DWMWA_USE_IMMERSIVE_DARK_MODE = 20, + + // [set] WINDOW_CORNER_PREFERENCE, Controls the policy that rounds top-level window corners + _DWMWA_WINDOW_CORNER_PREFERENCE = 33, + + // [get] UINT, width of the visible border around a thick frame window + _DWMWA_VISIBLE_FRAME_BORDER_THICKNESS = 37, + + // [get, set] SYSTEMBACKDROP_TYPE, Controls the system-drawn backdrop material of a window, + // including behind the non-client area. + _DWMWA_SYSTEMBACKDROP_TYPE = 38, + + // Undocumented, use this value to enable Mica material on Win11 21H2. You should use + // DWMWA_SYSTEMBACKDROP_TYPE instead on Win11 22H2 and newer. + _DWMWA_MICA_EFFECT = 1029 + }; + + // The thickness of an auto-hide taskbar in pixels. + static constexpr const quint8 kAutoHideTaskBarThickness = 2; + + QWK_USED static constexpr const struct { + const uint32_t activeLight = MAKE_RGBA_COLOR(110, 110, 110, 255); // #6E6E6E + const uint32_t activeDark = MAKE_RGBA_COLOR(51, 51, 51, 255); // #333333 + const uint32_t inactiveLight = MAKE_RGBA_COLOR(167, 167, 167, 255); // #A7A7A7 + const uint32_t inactiveDark = MAKE_RGBA_COLOR(61, 61, 62, 255); // #3D3D3E + } kWindowsColorSet; + + // hWnd -> context + using WndProcHash = QHash<HWND, Win32WindowContext *>; + Q_GLOBAL_STATIC(WndProcHash, g_wndProcHash) + + // Original Qt window proc function + static WNDPROC g_qtWindowProc = nullptr; + + struct DynamicApis { + static const DynamicApis &instance() { + static const DynamicApis inst{}; + return inst; + } + + // template <typename T> + // struct DefaultFunc; + // + // template <typename Return, typename... Args> + // struct DefaultFunc<Return(QT_WIN_CALLBACK *)(Args...)> { + // static Return STDAPICALLTYPE func(Args...) { + // return Return{}; + // } + // }; + // + // #define DYNAMIC_API_DECLARE(NAME) decltype(&::NAME) p##NAME = + // DefaultFunc<decltype(&::NAME)>::func + +#define DYNAMIC_API_DECLARE(NAME) decltype(&::NAME) p##NAME = nullptr + + DYNAMIC_API_DECLARE(DwmFlush); + DYNAMIC_API_DECLARE(DwmIsCompositionEnabled); + DYNAMIC_API_DECLARE(DwmGetCompositionTimingInfo); + DYNAMIC_API_DECLARE(DwmGetWindowAttribute); + DYNAMIC_API_DECLARE(DwmSetWindowAttribute); + DYNAMIC_API_DECLARE(GetDpiForWindow); + DYNAMIC_API_DECLARE(GetSystemMetricsForDpi); + DYNAMIC_API_DECLARE(GetDpiForMonitor); + DYNAMIC_API_DECLARE(timeGetDevCaps); + DYNAMIC_API_DECLARE(timeBeginPeriod); + DYNAMIC_API_DECLARE(timeEndPeriod); + +#undef DYNAMIC_API_DECLARE + + private: + DynamicApis() { +#define DYNAMIC_API_RESOLVE(DLL, NAME) \ + p##NAME = reinterpret_cast<decltype(p##NAME)>(DLL.resolve(#NAME)) + + QSystemLibrary user32(QStringLiteral("user32")); + DYNAMIC_API_RESOLVE(user32, GetDpiForWindow); + DYNAMIC_API_RESOLVE(user32, GetSystemMetricsForDpi); + + QSystemLibrary shcore(QStringLiteral("shcore")); + DYNAMIC_API_RESOLVE(shcore, GetDpiForMonitor); + + QSystemLibrary dwmapi(QStringLiteral("dwmapi")); + DYNAMIC_API_RESOLVE(dwmapi, DwmFlush); + DYNAMIC_API_RESOLVE(dwmapi, DwmIsCompositionEnabled); + DYNAMIC_API_RESOLVE(dwmapi, DwmGetCompositionTimingInfo); + DYNAMIC_API_RESOLVE(dwmapi, DwmGetWindowAttribute); + DYNAMIC_API_RESOLVE(dwmapi, DwmSetWindowAttribute); + + QSystemLibrary winmm(QStringLiteral("winmm")); + DYNAMIC_API_RESOLVE(winmm, timeGetDevCaps); + DYNAMIC_API_RESOLVE(winmm, timeBeginPeriod); + DYNAMIC_API_RESOLVE(winmm, timeEndPeriod); + +#undef DYNAMIC_API_RESOLVE + } + + ~DynamicApis() = default; + + Q_DISABLE_COPY_MOVE(DynamicApis) + }; + + 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 { + return !operator==(lhs, rhs); + } + + 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 { + return !operator==(lhs, rhs); + } + + 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 { + return (operator>(lhs, rhs) || operator==(lhs, rhs)); + } + + 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 { + 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 !operator==(lhs, rhs); + } + + 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 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 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*/ QString hwnd2str(const WId windowId) { + // NULL handle is allowed here. + return QLatin1String("0x") + + QString::number(windowId, 16).toUpper().rightJustified(8, u'0'); + } + + 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 = IsWindows8OrGreater_Real(); + return result; + } + + static inline bool isWin8Point1OrGreater() { + static const bool result = IsWindows8Point1OrGreater_Real(); + return result; + } + + static inline bool isWin10OrGreater() { + static const bool result = IsWindows10OrGreater_Real(); + return result; + } + + static inline bool isWin11OrGreater() { + static const bool result = IsWindows11OrGreater_Real(); + return result; + } + + 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 bool isWindowFrameBorderColorized() { + const QWinRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)"); + if (!registry.isValid()) { + return false; + } + const auto value = registry.dwordValue(L"ColorPrevalence"); + if (!value.second) { + return false; + } + return value.first; + } + + static inline bool isDarkThemeActive() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + return QGuiApplication::styleHints()->colorScheme() == Qt::ColorScheme::Dark; +#else + const QWinRegistryKey registry( + HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\CurrentVersion\Themes\Personalize)"); + if (!registry.isValid()) { + return false; + } + const auto value = registry.dwordValue(L"AppsUseLightTheme"); + if (!value.second) { + return false; + } + return !value.first; +#endif + } + + static inline bool isDarkWindowFrameEnabled(HWND hwnd) { + BOOL enabled = FALSE; + const DynamicApis &apis = DynamicApis::instance(); + if (SUCCEEDED(apis.pDwmGetWindowAttribute(hwnd, _DWMWA_USE_IMMERSIVE_DARK_MODE, &enabled, + sizeof(enabled)))) { + return enabled; + } else if (SUCCEEDED(apis.pDwmGetWindowAttribute(hwnd, + _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, + &enabled, sizeof(enabled)))) { + return enabled; + } else { + return false; + } + } + + static inline QColor getAccentColor() { +#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0) + return QGuiApplication::palette().color(QPalette::Accent); +#else + const QWinRegistryKey registry(HKEY_CURRENT_USER, LR"(Software\Microsoft\Windows\DWM)"); + if (!registry.isValid()) { + return {}; + } + const auto value = registry.dwordValue(L"AccentColor"); + if (!value.second) { + return {}; + } + // The retrieved value is in the #AABBGGRR format, we need to + // convert it to the #AARRGGBB format which Qt expects. + const QColor abgr = QColor::fromRgba(value.first); + if (!abgr.isValid()) { + return {}; + } + return QColor::fromRgb(abgr.blue(), abgr.green(), abgr.red(), abgr.alpha()); +#endif + } + + 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); + } else if (apis.pGetDpiForMonitor) { // Win8.1 + HMONITOR monitor = ::MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST); + UINT dpiX{0}; + UINT dpiY{0}; + apis.pGetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY); + return dpiX; + } else { // Win2K + HDC hdc = ::GetDC(nullptr); + const int dpiX = ::GetDeviceCaps(hdc, LOGPIXELSX); + // const int dpiY = ::GetDeviceCaps(hdc, LOGPIXELSY); + ::ReleaseDC(nullptr, hdc); + return quint32(dpiX); + } + } + + static inline quint32 getSystemMetricsForDpi(int index, quint32 dpi) { + const DynamicApis &apis = DynamicApis::instance(); + if (apis.pGetSystemMetricsForDpi) { + return ::GetSystemMetricsForDpi(index, dpi); + } + return ::GetSystemMetrics(index); + } + + static inline quint32 getWindowFrameBorderThickness(HWND hwnd) { + const DynamicApis &apis = DynamicApis::instance(); + if (UINT result = 0; SUCCEEDED(apis.pDwmGetWindowAttribute( + hwnd, _DWMWA_VISIBLE_FRAME_BORDER_THICKNESS, &result, sizeof(result)))) { + return result; + } + return getSystemMetricsForDpi(SM_CXBORDER, getDpiForWindow(hwnd)); + } + + static inline quint32 getResizeBorderThickness(HWND hwnd) { + const quint32 dpi = getDpiForWindow(hwnd); + return getSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) + + getSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); + } + + static inline quint32 getTitleBarHeight(HWND hwnd) { + const quint32 dpi = getDpiForWindow(hwnd); + return getSystemMetricsForDpi(SM_CYCAPTION, dpi) + + getSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) + + getSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi); + } + + static void updateInternalWindowFrameMargins(HWND hwnd, QWindow *window) { + const auto margins = [hwnd]() -> QMargins { + const auto titleBarHeight = int(getTitleBarHeight(hwnd)); + if (isWin10OrGreater()) { + return {0, -titleBarHeight, 0, 0}; + } else { + const auto frameSize = int(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("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); + MONITORINFOEXW monitorInfo{}; + monitorInfo.cbSize = sizeof(monitorInfo); + ::GetMonitorInfoW(monitor, &monitorInfo); + return monitorInfo; + } + + static inline void moveWindowToDesktopCenter(HWND hwnd) { + MONITORINFOEXW monitorInfo = getMonitorForWindow(hwnd); + RECT windowRect{}; + ::GetWindowRect(hwnd, &windowRect); + const auto newX = monitorInfo.rcMonitor.left + + (RECT_WIDTH(monitorInfo.rcMonitor) - RECT_WIDTH(windowRect)) / 2; + const auto newY = monitorInfo.rcMonitor.top + + (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 void moveWindowToMonitor(HWND hwnd, const MONITORINFOEXW &activeMonitor) { + RECT currentMonitorRect = getMonitorForWindow(hwnd).rcMonitor; + RECT activeMonitorRect = activeMonitor.rcMonitor; + // We are in the same monitor, nothing to adjust here. + if (currentMonitorRect == activeMonitorRect) { + return; + } + RECT currentWindowRect{}; + ::GetWindowRect(hwnd, ¤tWindowRect); + auto newWindowX = + activeMonitorRect.left + (currentWindowRect.left - currentMonitorRect.left); + auto newWindowY = activeMonitorRect.top + (currentWindowRect.top - currentMonitorRect.top); + ::SetWindowPos(hwnd, nullptr, newWindowX, newWindowY, RECT_WIDTH(currentWindowRect), + RECT_HEIGHT(currentWindowRect), + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOOWNERZORDER); + } + + static inline void bringWindowToFront(HWND hwnd) { + HWND oldForegroundWindow = ::GetForegroundWindow(); + if (!oldForegroundWindow) { + // The foreground window can be NULL, it's not an API error. + return; + } + MONITORINFOEXW activeMonitor = getMonitorForWindow(oldForegroundWindow); + // We need to show the window first, otherwise we won't be able to bring it to front. + if (!::IsWindowVisible(hwnd)) { + ::ShowWindow(hwnd, SW_SHOW); + } + if (IsMinimized(hwnd)) { + // Restore the window if it is minimized. + ::ShowWindow(hwnd, SW_RESTORE); + // Once we've been restored, throw us on the active monitor. + moveWindowToMonitor(hwnd, activeMonitor); + // When the window is restored, it will always become the foreground window. + // So return early here, we don't need the following code to bring it to front. + return; + } + // OK, our window is not minimized, so now we will try to bring it to front manually. + // First try to send a message to the current foreground window to check whether + // it is currently hanging or not. + if (!::SendMessageTimeoutW(oldForegroundWindow, WM_NULL, 0, 0, + SMTO_BLOCK | SMTO_ABORTIFHUNG | SMTO_NOTIMEOUTIFNOTHUNG, 1000, + nullptr)) { + // The foreground window hangs, can't activate current window. + return; + } + DWORD windowThreadProcessId = ::GetWindowThreadProcessId(oldForegroundWindow, nullptr); + DWORD currentThreadId = ::GetCurrentThreadId(); + // We won't be able to change a window's Z order if it's not our own window, + // so we use this small technique to pretend the foreground window is ours. + ::AttachThreadInput(windowThreadProcessId, currentThreadId, TRUE); + + [[maybe_unused]] const auto &cleaner = + qScopeGuard([windowThreadProcessId, currentThreadId]() { + ::AttachThreadInput(windowThreadProcessId, currentThreadId, FALSE); // + }); + + ::BringWindowToTop(hwnd); + // Activate the window too. This will force us to the virtual desktop this + // window is on, if it's on another virtual desktop. + ::SetActiveWindow(hwnd); + // Throw us on the active monitor. + moveWindowToMonitor(hwnd, activeMonitor); + } + + static inline bool isFullScreen(HWND hwnd) { + RECT windowRect{}; + ::GetWindowRect(hwnd, &windowRect); + // Compare to the full area of the screen, not the work area. + return (windowRect == getMonitorForWindow(hwnd).rcMonitor); + } + + static inline bool isWindowNoState(HWND hwnd) { +#if 0 + WINDOWPLACEMENT wp{}; + wp.length = sizeof(wp); + ::GetWindowPlacement(hwnd, &wp); + return ((wp.showCmd == SW_NORMAL) || (wp.showCmd == SW_RESTORE)); +#else + if (isFullScreen(hwnd)) { + return false; + } + const auto style = static_cast<DWORD>(::GetWindowLongPtrW(hwnd, GWL_STYLE)); + return (!(style & (WS_MINIMIZE | WS_MAXIMIZE))); +#endif + } + + static void syncPaintEventWithDwm() { + // No need to sync with DWM if DWM composition is disabled. + if (!isDwmCompositionEnabled()) { + return; + } + const DynamicApis &apis = DynamicApis::instance(); + // Dirty hack to workaround the resize flicker caused by DWM. + LARGE_INTEGER freq{}; + ::QueryPerformanceFrequency(&freq); + TIMECAPS tc{}; + apis.ptimeGetDevCaps(&tc, sizeof(tc)); + const UINT ms_granularity = tc.wPeriodMin; + apis.ptimeBeginPeriod(ms_granularity); + LARGE_INTEGER now0{}; + ::QueryPerformanceCounter(&now0); + // ask DWM where the vertical blank falls + DWM_TIMING_INFO dti{}; + dti.cbSize = sizeof(dti); + apis.pDwmGetCompositionTimingInfo(nullptr, &dti); + LARGE_INTEGER now1{}; + ::QueryPerformanceCounter(&now1); + // - DWM told us about SOME vertical blank + // - past or future, possibly many frames away + // - convert that into the NEXT vertical blank + const auto period = qreal(dti.qpcRefreshPeriod); + const auto dt = qreal(dti.qpcVBlank - now1.QuadPart); + const qreal ratio = (dt / period); + auto w = qreal(0); + auto m = qreal(0); + if ((dt > qreal(0)) || qFuzzyIsNull(dt)) { + w = ratio; + } else { + // reach back to previous period + // - so m represents consistent position within phase + w = (ratio - qreal(1)); + } + m = (dt - (period * w)); + if ((m < qreal(0)) || qFuzzyCompare(m, period) || (m > period)) { + return; + } + const qreal m_ms = (qreal(1000) * m / qreal(freq.QuadPart)); + ::Sleep(static_cast<DWORD>(std::round(m_ms))); + apis.ptimeEndPeriod(ms_granularity); + } + + static void showSystemMenu2(HWND hWnd, const POINT &pos, const bool selectFirstEntry, + const bool fixedSize) { + HMENU hMenu = ::GetSystemMenu(hWnd, FALSE); + if (!hMenu) { + // The corresponding window doesn't have a system menu, most likely due to the + // lack of the "WS_SYSMENU" window style. This situation should not be treated + // as an error so just ignore it and return early. + return; + } + + const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd); + ::EnableMenuItem(hMenu, SC_CLOSE, (MF_BYCOMMAND | MFS_ENABLED)); + ::EnableMenuItem(hMenu, SC_MAXIMIZE, + (MF_BYCOMMAND | ((maxOrFull || fixedSize) ? MFS_DISABLED : MFS_ENABLED))); + ::EnableMenuItem(hMenu, SC_RESTORE, + (MF_BYCOMMAND | ((maxOrFull && !fixedSize) ? MFS_ENABLED : MFS_DISABLED))); + // The first menu item should be selected by default if the menu is brought + // up by keyboard. I don't know how to pre-select a menu item but it seems + // highlight can do the job. However, there's an annoying issue if we do + // this manually: the highlighted menu item is really only highlighted, + // not selected, so even if the mouse cursor hovers on other menu items + // or the user navigates to other menu items through keyboard, the original + // highlight bar will not move accordingly, the OS will generate another + // highlight bar to indicate the current selected menu item, which will make + // the menu look kind of weird. Currently I don't know how to fix this issue. + ::HiliteMenuItem(hWnd, hMenu, SC_RESTORE, + (MF_BYCOMMAND | (selectFirstEntry ? MFS_HILITE : MFS_UNHILITE))); + ::EnableMenuItem(hMenu, SC_MINIMIZE, (MF_BYCOMMAND | MFS_ENABLED)); + ::EnableMenuItem(hMenu, SC_SIZE, + (MF_BYCOMMAND | ((maxOrFull || fixedSize) ? MFS_DISABLED : MFS_ENABLED))); + ::EnableMenuItem(hMenu, SC_MOVE, (MF_BYCOMMAND | (maxOrFull ? MFS_DISABLED : MFS_ENABLED))); + + // The default menu item will appear in bold font. There can only be one default + // menu item per menu at most. Set the item ID to "UINT_MAX" (or simply "-1") + // can clear the default item for the given menu. + UINT defaultItemId = UINT_MAX; + if (isWin11OrGreater()) { + if (maxOrFull) { + defaultItemId = SC_RESTORE; + } else { + defaultItemId = SC_MAXIMIZE; + } + } + if (defaultItemId == UINT_MAX) { + defaultItemId = SC_CLOSE; + } + ::SetMenuDefaultItem(hMenu, defaultItemId, FALSE); + + // Popup the system menu at the required position. + const auto result = ::TrackPopupMenu( + hMenu, + (TPM_RETURNCMD | (QGuiApplication::isRightToLeft() ? TPM_RIGHTALIGN : TPM_LEFTALIGN)), + pos.x, pos.y, 0, hWnd, nullptr); + + // Unhighlight the first menu item after the popup menu is closed, otherwise it will keep + // highlighting until we unhighlight it manually. + ::HiliteMenuItem(hWnd, hMenu, SC_RESTORE, (MF_BYCOMMAND | MFS_UNHILITE)); + + if (!result) { + // The user canceled the menu, no need to continue. + return; + } + + // Send the command that the user chooses to the corresponding window. + ::PostMessageW(hWnd, WM_SYSCOMMAND, result, 0); + } + + static inline Win32WindowContext::WindowPart getHitWindowPart(int hitTestResult) { + switch (hitTestResult) { + case HTCLIENT: + return Win32WindowContext::ClientArea; + case HTCAPTION: + return Win32WindowContext::TitleBar; + case HTSYSMENU: + case HTHELP: + case HTREDUCE: + case HTZOOM: + case HTCLOSE: + return Win32WindowContext::ChromeButton; + case HTLEFT: + case HTRIGHT: + case HTTOP: + case HTTOPLEFT: + case HTTOPRIGHT: + case HTBOTTOM: + case HTBOTTOMLEFT: + case HTBOTTOMRIGHT: + return Win32WindowContext::ResizeBorder; + case HTBORDER: + return Win32WindowContext::FixedBorder; + default: + // unreachable + break; + } + return Win32WindowContext::Outside; + } + + static bool isValidWindow(HWND hWnd, bool checkVisible, bool checkTopLevel) { + if (!::IsWindow(hWnd)) { + return false; + } + const LONG_PTR styles = ::GetWindowLongPtrW(hWnd, GWL_STYLE); + if (styles & WS_DISABLED) { + return false; + } + const LONG_PTR exStyles = ::GetWindowLongPtrW(hWnd, GWL_EXSTYLE); + if (exStyles & WS_EX_TOOLWINDOW) { + return 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)) { + return false; + } + } + if (checkTopLevel) { + if (::GetAncestor(hWnd, GA_ROOT) != hWnd) { + return false; + } + } + return true; + } + + // 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 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. + class WindowsNativeEventFilter : public AppNativeEventFilter { + 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; + } + + // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1546 + // Qt needs to refer to the WM_NCCALCSIZE message data that hasn't been processed, so we + // have to process it after Qt acquires the initial data. + auto msg = static_cast<const MSG *>(message); + if (msg->message == WM_NCCALCSIZE && lastMessageContext) { + LRESULT res; + if (lastMessageContext->nonClientCalcSizeHandler(msg->hwnd, msg->message, + msg->wParam, msg->lParam, &res)) { + *result = decltype(*result)(res); + return true; + } + } + return false; + } + + static WindowsNativeEventFilter *instance; + static Win32WindowContext *lastMessageContext; + + static inline void install() { + if (instance) { + return; + } + instance = new WindowsNativeEventFilter(); + } + + static inline void uninstall() { + if (!instance) { + return; + } + delete instance; + instance = nullptr; + } + }; + + WindowsNativeEventFilter *WindowsNativeEventFilter::instance = nullptr; + Win32WindowContext *WindowsNativeEventFilter::lastMessageContext = nullptr; + + // https://github.com/qt/qtbase/blob/e26a87f1ecc40bc8c6aa5b889fce67410a57a702/src/plugins/platforms/windows/qwindowscontext.cpp#L1025 + // We can see from the source code that Qt will filter out some messages first and then send the + // unfiltered messages to the event dispatcher. To activate the Snap Layout feature on Windows + // 11, we must process some non-client area messages ourselves, but unfortunately these messages + // have been filtered out already in that line, and thus we'll never have the chance to process + // them ourselves. This is Qt's low level platform specific code, so we don't have any official + // ways to change this behavior. But luckily we can replace the window procedure function of + // Qt's windows, and in this hooked window procedure function, we finally have the chance to + // process window messages before Qt touches them. So we reconstruct the MSG structure and send + // it to our own custom native event filter to do all the magic works. But since the system menu + // feature doesn't necessarily belong to the native implementation, we seperate the handling + // code and always process the system menu part in this function for both implementations. + // + // Original event flow: + // [Entry] Windows Message Queue + // | + // [Qt Window Proc] qwindowscontext.cpp#L1547: qWindowsWndProc() + // ``` + // const bool handled = QWindowsContext::instance()->windowsProc + // (hwnd, message, et, wParam, lParam, &result, + // &platformWindow); + // ``` + // | + // [Non-Input Filter] qwindowscontext.cpp#L1025: QWindowsContext::windowsProc() + // ``` + // if (!isInputMessage(msg.message) && + // filterNativeEvent(&msg, result)) + // return true; + // ``` + // | + // [User Filter] qwindowscontext.cpp#L1588: QWindowsContext::windowsProc() + // ``` + // QAbstractEventDispatcher *dispatcher = + // QAbstractEventDispatcher::instance(); + // qintptr filterResult = 0; + // if (dispatcher && + // dispatcher->filterNativeEvent(nativeEventType(), msg, + // &filterResult)) { + // *result = LRESULT(filterResult); + // return true; + // } + // ``` + // | + // [Extra work] The rest of QWindowsContext::windowsProc() and qWindowsWndProc() + // + // Notice: Only non-input messages will be processed by the user-defined global native event + // filter!!! These events are then passed to the widget class's own overridden + // QWidget::nativeEvent() as a local filter, where all native events can be handled, but we must + // create a new class derived from QWidget which we don't intend to. Therefore, we don't expect + // to process events from the global native event filter, but instead hook Qt's window + // procedure. + + extern "C" LRESULT QT_WIN_CALLBACK QWKHookedWndProc(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam) { Q_ASSERT(hWnd); if (!hWnd) { return FALSE; @@ -22,70 +810,1240 @@ return ::DefWindowProcW(hWnd, message, wParam, lParam); } - // Try hooked procedure - LRESULT result; - bool handled = ctx->windowProc(hWnd, message, wParam, lParam, &result); - if (handled) { + // Since Qt does the necessary processing of the WM_NCCALCSIZE message, we need to + // forward it right away and process it in our native event filter. + if (message == WM_NCCALCSIZE) { + WindowsNativeEventFilter::lastMessageContext = ctx; + LRESULT result = ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); + WindowsNativeEventFilter::lastMessageContext = nullptr; return result; } - // Fallback to Qt's procedure + // Try hooked procedure and save result + LRESULT result; + if (ctx->windowProc(hWnd, message, wParam, lParam, &result)) { + return result; + } + + // Continue dispatching. return ::CallWindowProcW(g_qtWindowProc, hWnd, message, wParam, lParam); } - Win32WindowContext::Win32WindowContext(QWindow *window, WindowItemDelegate *delegate) - : AbstractWindowContext(window, delegate), windowId(0) { + static inline void addManagedWindow(HWND hWnd, Win32WindowContext *ctx) { + // Store original window proc + if (!g_qtWindowProc) { + g_qtWindowProc = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)); + } + + // Hook window proc + ::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, reinterpret_cast<LONG_PTR>(QWKHookedWndProc)); + + // Install global native event filter + WindowsNativeEventFilter::install(); + + // Save window handle mapping + g_wndProcHash->insert(hWnd, ctx); + } + + static inline void removeManagedWindow(HWND hWnd) { + // Remove window handle mapping + if (!g_wndProcHash->remove(hWnd)) + return; + + // Remove event filter if the all windows has been destroyed + if (g_wndProcHash->empty()) { + WindowsNativeEventFilter::uninstall(); + } + } + + Win32WindowContext::Win32WindowContext() : AbstractWindowContext() { } Win32WindowContext::~Win32WindowContext() { - // Remove window handle mapping - auto hWnd = reinterpret_cast<HWND>(windowId); - g_wndProcHash->remove(hWnd); + if (windowId) { + removeManagedWindow(reinterpret_cast<HWND>(windowId)); + } } - bool Win32WindowContext::setup() { - auto winId = m_windowHandle->winId(); - Q_ASSERT(winId); - if (!winId) { - return false; + QString Win32WindowContext::key() const { + return QStringLiteral("win32"); + } + + void Win32WindowContext::virtual_hook(int id, void *data) { + switch (id) { + case CentralizeHook: { + const auto hwnd = reinterpret_cast<HWND>(windowId); + moveWindowToDesktopCenter(hwnd); + return; + } + + case RaiseWindowHook: { + const auto hwnd = reinterpret_cast<HWND>(windowId); + bringWindowToFront(hwnd); + return; + } + + case ShowSystemMenuHook: { + const auto &pos = *static_cast<const QPoint *>(data); + auto hWnd = reinterpret_cast<HWND>(windowId); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + const QPoint nativeGlobalPos = + QHighDpi::toNativeGlobalPosition(pos, m_windowHandle); +#else + const QPoint nativeGlobalPos = QHighDpi::toNativePixels(pos, m_windowHandle); +#endif + showSystemMenu2(hWnd, qpoint2point(nativeGlobalPos), false, + m_delegate->isHostSizeFixed(m_host)); + return; + } + + case WindowAttributeChangedHook: { + auto args = static_cast<void **>(data); + const auto &key = *static_cast<const QString *>(args[0]); + const auto &newVar = *static_cast<const QVariant *>(args[1]); + const auto &oldVar = *static_cast<const QVariant *>(args[2]); + + if (key == QStringLiteral("no-frame-shadow")) { + if (newVar.toBool()) { + // TODO: set off + } else { + // TODO: set on + } + } + + break; + } + + case DefaultColorsHook: { + auto &map = *static_cast<QMap<QString, QColor> *>(data); + map.clear(); + map.insert(QStringLiteral("activeLight"), kWindowsColorSet.activeLight); + map.insert(QStringLiteral("activeDark"), kWindowsColorSet.activeDark); + map.insert(QStringLiteral("inactiveLight"), kWindowsColorSet.inactiveLight); + map.insert(QStringLiteral("inactiveDark"), kWindowsColorSet.inactiveDark); + return; + } + + case DrawWindows10BorderHook: { + auto args = static_cast<void **>(data); + auto &painter = *static_cast<QPainter *>(args[0]); + const auto &rect = *static_cast<const QRect *>(args[1]); + const auto ®ion = *static_cast<const QRegion *>(args[2]); + const auto hwnd = reinterpret_cast<HWND>(windowId); + + QPen pen; + pen.setWidth(getWindowFrameBorderThickness(hwnd) * 2); + + const bool dark = isDarkThemeActive() && isDarkWindowFrameEnabled(hwnd); + if (m_delegate->isWindowActive(m_host)) { + if (isWindowFrameBorderColorized()) { + pen.setColor(getAccentColor()); + } else { + static QColor frameBorderActiveColorLight(kWindowsColorSet.activeLight); + static QColor frameBorderActiveColorDark(kWindowsColorSet.activeDark); + pen.setColor(dark ? frameBorderActiveColorDark + : frameBorderActiveColorLight); + } + } else { + static QColor frameBorderInactiveColorLight(kWindowsColorSet.inactiveLight); + static QColor frameBorderInactiveColorDark(kWindowsColorSet.inactiveDark); + pen.setColor(dark ? frameBorderInactiveColorDark + : frameBorderInactiveColorLight); + } + painter.save(); + + // We needs anti-aliasing to give us better result. + painter.setRenderHint(QPainter::Antialiasing); + + painter.setPen(pen); + painter.drawLine(QLine{ + QPoint{0, 0}, + QPoint{m_windowHandle->width(), 0} + }); + painter.restore(); + return; + } + + default: { + // unreachable + break; + } + } + AbstractWindowContext::virtual_hook(id, data); + } + + bool Win32WindowContext::needBorderPainter() const { + return isWin10OrGreater() && !isWin11OrGreater(); + } + + int Win32WindowContext::borderThickness() const { + return getWindowFrameBorderThickness(reinterpret_cast<HWND>(windowId)); + } + + void Win32WindowContext::winIdChanged(QWindow *oldWindow) { + removeManagedWindow(reinterpret_cast<HWND>(windowId)); + if (!m_windowHandle) { + return; } // Install window hook + auto winId = m_windowHandle->winId(); auto hWnd = reinterpret_cast<HWND>(winId); - auto qtWindowProc = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(hWnd, GWLP_WNDPROC)); - Q_ASSERT(qtWindowProc); - if (!qtWindowProc) { - QWK_WARNING << winLastErrorMessage(); - return false; - } - if (::SetWindowLongPtrW(hWnd, GWLP_WNDPROC, - reinterpret_cast<LONG_PTR>(QWK_WindowsWndProc)) == 0) { - QWK_WARNING << winLastErrorMessage(); - return false; +#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) + for (const auto attr : { + _DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1, + _DWMWA_USE_IMMERSIVE_DARK_MODE, + }) { + const BOOL enable = TRUE; + DynamicApis::instance().pDwmSetWindowAttribute(hWnd, attr, &enable, sizeof(enable)); } +#endif + // Inform Qt we want and have set custom margins + updateInternalWindowFrameMargins(hWnd, m_windowHandle); + + // Add managed window + addManagedWindow(hWnd, this); + + // Cache win id windowId = winId; - - // Store original window proc - if (!g_qtWindowProc) { - g_qtWindowProc = qtWindowProc; - } - - // Save window handle mapping - g_wndProcHash->insert(hWnd, this); - - return true; } bool Win32WindowContext::windowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT *result) { *result = FALSE; - // TODO: Implement - // ... + // We should skip these messages otherwise we will get crashes. + // NOTE: WM_QUIT won't be posted to the WindowProc function. + switch (message) { + case WM_CLOSE: + case WM_DESTROY: + case WM_NCDESTROY: + // Undocumented messages: + case WM_UAHDESTROYWINDOW: + case WM_UNREGISTER_WINDOW_SERVICES: + return false; + default: + break; + } + if (!isValidWindow(hWnd, false, true)) { + return false; + } + + // Test snap layout + if (snapLayoutHandler(hWnd, message, wParam, lParam, result)) { + return true; + } + + // Main implementation + if (customWindowHandler(hWnd, message, wParam, lParam, result)) { + return true; + } + + // Whether to show system menu + if (systemMenuHandler(hWnd, message, wParam, lParam, result)) { + return true; + } + + // Forward to native event filter subscribers + if (!m_nativeEventFilters.isEmpty()) { + MSG msg; + msg.hwnd = hWnd; + msg.message = message; + msg.wParam = wParam; + msg.lParam = lParam; + QT_NATIVE_EVENT_RESULT_TYPE res = 0; + if (dispatch(QByteArrayLiteral("windows_generic_MSG"), &msg, &res)) { + *result = LRESULT(res); + return true; + } + } return false; // Not handled } -} \ No newline at end of file + QWK_USED static constexpr const struct { + const WPARAM wParam = MAKEWPARAM(44500, 61897); + const LPARAM lParam = MAKELPARAM(62662, 44982); // Not used. Reserve for future use. + } kMessageTag; + + static inline quint64 getKeyState() { + quint64 result = 0; + const auto &get = [](const int virtualKey) -> bool { + return (::GetAsyncKeyState(virtualKey) < 0); + }; + const bool buttonSwapped = ::GetSystemMetrics(SM_SWAPBUTTON); + if (get(VK_LBUTTON)) { + result |= (buttonSwapped ? MK_RBUTTON : MK_LBUTTON); + } + if (get(VK_RBUTTON)) { + result |= (buttonSwapped ? MK_LBUTTON : MK_RBUTTON); + } + if (get(VK_SHIFT)) { + result |= MK_SHIFT; + } + if (get(VK_CONTROL)) { + result |= MK_CONTROL; + } + if (get(VK_MBUTTON)) { + result |= MK_MBUTTON; + } + if (get(VK_XBUTTON1)) { + result |= MK_XBUTTON1; + } + if (get(VK_XBUTTON2)) { + result |= MK_XBUTTON2; + } + return result; + } + + static void emulateClientAreaMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, + const std::optional<int> &overrideMessage = std::nullopt) { + const int myMsg = overrideMessage.value_or(message); + const auto wParamNew = [myMsg, wParam]() -> WPARAM { + if (myMsg == WM_NCMOUSELEAVE) { + // wParam is always ignored in mouse leave messages, but here we + // give them a special tag to be able to distinguish which messages + // are sent by ourselves. + return kMessageTag.wParam; + } + const quint64 keyState = getKeyState(); + if ((myMsg >= WM_NCXBUTTONDOWN) && (myMsg <= WM_NCXBUTTONDBLCLK)) { + const auto xButtonMask = GET_XBUTTON_WPARAM(wParam); + return MAKEWPARAM(keyState, xButtonMask); + } + return keyState; + }(); + const auto lParamNew = [myMsg, lParam, hWnd]() -> LPARAM { + if (myMsg == WM_NCMOUSELEAVE) { + // lParam is always ignored in mouse leave messages. + return 0; + } + const auto screenPos = POINT{GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + POINT clientPos = screenPos; + ::ScreenToClient(hWnd, &clientPos); + return MAKELPARAM(clientPos.x, clientPos.y); + }(); +#if 0 +# define SEND_MESSAGE ::SendMessageW +#else +# define SEND_MESSAGE ::PostMessageW +#endif + switch (myMsg) { + case WM_NCHITTEST: // Treat hit test messages as mouse move events. + case WM_NCMOUSEMOVE: + SEND_MESSAGE(hWnd, WM_MOUSEMOVE, wParamNew, lParamNew); + break; + case WM_NCLBUTTONDOWN: + SEND_MESSAGE(hWnd, WM_LBUTTONDOWN, wParamNew, lParamNew); + break; + case WM_NCLBUTTONUP: + SEND_MESSAGE(hWnd, WM_LBUTTONUP, wParamNew, lParamNew); + break; + case WM_NCLBUTTONDBLCLK: + SEND_MESSAGE(hWnd, WM_LBUTTONDBLCLK, wParamNew, lParamNew); + break; + case WM_NCRBUTTONDOWN: + SEND_MESSAGE(hWnd, WM_RBUTTONDOWN, wParamNew, lParamNew); + break; + case WM_NCRBUTTONUP: + SEND_MESSAGE(hWnd, WM_RBUTTONUP, wParamNew, lParamNew); + break; + case WM_NCRBUTTONDBLCLK: + SEND_MESSAGE(hWnd, WM_RBUTTONDBLCLK, wParamNew, lParamNew); + break; + case WM_NCMBUTTONDOWN: + SEND_MESSAGE(hWnd, WM_MBUTTONDOWN, wParamNew, lParamNew); + break; + case WM_NCMBUTTONUP: + SEND_MESSAGE(hWnd, WM_MBUTTONUP, wParamNew, lParamNew); + break; + case WM_NCMBUTTONDBLCLK: + SEND_MESSAGE(hWnd, WM_MBUTTONDBLCLK, wParamNew, lParamNew); + break; + case WM_NCXBUTTONDOWN: + SEND_MESSAGE(hWnd, WM_XBUTTONDOWN, wParamNew, lParamNew); + break; + case WM_NCXBUTTONUP: + SEND_MESSAGE(hWnd, WM_XBUTTONUP, wParamNew, lParamNew); + break; + case WM_NCXBUTTONDBLCLK: + SEND_MESSAGE(hWnd, WM_XBUTTONDBLCLK, wParamNew, lParamNew); + break; +#if 0 // ### TODO: How to handle touch events? + case WM_NCPOINTERUPDATE: + case WM_NCPOINTERDOWN: + case WM_NCPOINTERUP: + break; +#endif + case WM_NCMOUSEHOVER: + SEND_MESSAGE(hWnd, WM_MOUSEHOVER, wParamNew, lParamNew); + break; + case WM_NCMOUSELEAVE: + SEND_MESSAGE(hWnd, WM_MOUSELEAVE, wParamNew, lParamNew); + break; + default: + // unreachable + break; + } + +#undef SEND_MESSAGE + } + + static inline void requestForMouseLeaveMessage(HWND hWnd, bool nonClient) { + TRACKMOUSEEVENT tme{}; + tme.cbSize = sizeof(tme); + tme.dwFlags = TME_LEAVE; + if (nonClient) { + tme.dwFlags |= TME_NONCLIENT; + } + tme.hwndTrack = hWnd; + tme.dwHoverTime = HOVER_DEFAULT; + ::TrackMouseEvent(&tme); + } + + bool Win32WindowContext::snapLayoutHandler(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam, LRESULT *result) { + switch (message) { + case WM_MOUSELEAVE: { + if (wParam != kMessageTag.wParam) { + // Qt will call TrackMouseEvent() to get the WM_MOUSELEAVE message when it + // receives WM_MOUSEMOVE messages, and since we are converting every + // WM_NCMOUSEMOVE message to WM_MOUSEMOVE message and send it back to the window + // to be able to hover our controls, we also get lots of WM_MOUSELEAVE messages + // at the same time because of the reason above, and these superfluous mouse + // leave events cause Qt to think the mouse has left the control, and thus we + // 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 = QHighDpi::fromNativeLocalPosition(point2qpoint(screenPoint), + m_windowHandle); + auto dummy = WindowAgentBase::Unknown; + if (isInSystemButtons(qtScenePos, &dummy)) { + // We must record whether the last WM_MOUSELEAVE was filtered, because if + // Qt does not receive this message it will not call TrackMouseEvent() + // again, resulting in the client area not responding to any mouse event. + mouseLeaveBlocked = true; + *result = FALSE; + return true; + } + } + mouseLeaveBlocked = false; + break; + } + + case WM_MOUSEMOVE: { + // At appropriate time, we will call TrackMouseEvent() for Qt. Simultaneously, + // we unset `mouseLeaveBlocked` mark and pretend as if Qt has received + // WM_MOUSELEAVE. + if (lastHitTestResult != WindowPart::ChromeButton && mouseLeaveBlocked) { + mouseLeaveBlocked = false; + requestForMouseLeaveMessage(hWnd, false); + } + break; + } + + case WM_NCMOUSEMOVE: + case WM_NCLBUTTONDOWN: + case WM_NCLBUTTONUP: + case WM_NCLBUTTONDBLCLK: + case WM_NCRBUTTONDOWN: + case WM_NCRBUTTONUP: + case WM_NCRBUTTONDBLCLK: + case WM_NCMBUTTONDOWN: + case WM_NCMBUTTONUP: + case WM_NCMBUTTONDBLCLK: + case WM_NCXBUTTONDOWN: + case WM_NCXBUTTONUP: + case WM_NCXBUTTONDBLCLK: +#if 0 // ### TODO: How to handle touch events? + case WM_NCPOINTERUPDATE: + case WM_NCPOINTERDOWN: + case WM_NCPOINTERUP: +#endif + case WM_NCMOUSEHOVER: { + const WindowPart currentWindowPart = lastHitTestResult; + if (message == WM_NCMOUSEMOVE) { + if (currentWindowPart != WindowPart::ChromeButton) { + m_delegate->resetQtGrabbedControl(m_host); + if (mouseLeaveBlocked) { + emulateClientAreaMessage(hWnd, message, wParam, lParam, + WM_NCMOUSELEAVE); + } + } + + // We need to make sure we get the right hit-test result when a WM_NCMOUSELEAVE + // comes, so we reset it when we receive a WM_NCMOUSEMOVE. + + // If the mouse is entering the client area, there must be a WM_NCHITTEST + // setting it to `Client` before the WM_NCMOUSELEAVE comes; if the mouse is + // leaving the window, current window part remains as `Outside`. + lastHitTestResult = WindowPart::Outside; + } + + if (currentWindowPart == WindowPart::ChromeButton) { + emulateClientAreaMessage(hWnd, message, wParam, lParam); + if (message == WM_NCMOUSEMOVE) { + // ### FIXME FIXME FIXME + // ### FIXME: Calling DefWindowProc() here is really dangerous, investigate + // how to avoid doing this. + // ### FIXME FIXME FIXME + *result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam); + } else { + // According to MSDN, we should return non-zero for X button messages to + // indicate we have handled these messages (due to historical reasons), for + // all other messages we should return zero instead. + *result = + (((message >= WM_NCXBUTTONDOWN) && (message <= WM_NCXBUTTONDBLCLK)) + ? TRUE + : FALSE); + } + return true; + } + break; + } + + case WM_NCMOUSELEAVE: { + const WindowPart currentWindowPart = lastHitTestResult; + if (currentWindowPart == WindowPart::ChromeButton) { + // If we press on the chrome button and move mouse, Windows will take the + // pressing area as HTCLIENT which maybe because of our former retransmission of + // WM_NCLBUTTONDOWN, as a result, a WM_NCMOUSELEAVE will come immediately and a + // lot of WM_MOUSEMOVE will come if we move the mouse, we should track the mouse + // in advance. + if (mouseLeaveBlocked) { + mouseLeaveBlocked = false; + requestForMouseLeaveMessage(hWnd, false); + } + } else { + if (mouseLeaveBlocked) { + // The mouse is moving from the chrome button to other non-client area, we + // should emulate a WM_MOUSELEAVE message to reset the button state. + emulateClientAreaMessage(hWnd, message, wParam, lParam, WM_NCMOUSELEAVE); + } + + if (currentWindowPart == WindowPart::Outside) { + // Notice: we're not going to clear window part cache when the mouse leaves + // window from client area, which means we will get previous window part as + // HTCLIENT if the mouse leaves window from client area and enters window + // from non-client area, but it has no bad effect. + m_delegate->resetQtGrabbedControl(m_host); + } + } + break; + } + + default: + break; + } + return false; + } + + 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) { + centered = true; + moveWindowToDesktopCenter(hWnd); + } + } + break; + } + + 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 = + QHighDpi::fromNativeLocalPosition(point2qpoint(nativeLocalPos), m_windowHandle); + + bool isFixedSize = m_delegate->isHostSizeFixed(m_host); + bool isTitleBar = isInTitleBarDraggableArea(qtScenePos); + bool dontOverrideCursor = false; // ### TODO + + WindowAgentBase::SystemButton sysButtonType = WindowAgentBase::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 quint8 kBorderSize = 2; + bool isTop = (nativeLocalPos.y <= kBorderSize); + bool isLeft = nativeLocalPos.x <= kBorderSize; + bool isRight = (nativeLocalPos.x >= (clientWidth - kBorderSize)); + if (isTop || isLeft || 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) { + if (isLeft) { + *result = HTTOPLEFT; + } else if (isRight) { + *result = HTTOPRIGHT; + } else { + *result = HTTOP; + } + } else { + if (isLeft) { + *result = HTLEFT; + } 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 WindowAgentBase::WindowIcon: + *result = HTSYSMENU; + break; + case WindowAgentBase::Help: + *result = HTHELP; + break; + case WindowAgentBase::Minimize: + *result = HTREDUCE; + break; + case WindowAgentBase::Maximize: + *result = HTZOOM; + break; + case WindowAgentBase::Close: + *result = HTCLOSE; + break; + default: + // unreachable + break; + } + } + 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 scaledFrameSize = std::round(qreal(frameSize) * scaleFactor); + const bool isLeft = (nativeLocalPos.x < scaledFrameSize); + const bool isRight = (nativeLocalPos.x >= (clientWidth - scaledFrameSize)); + 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; + } + } + + case WM_WINDOWPOSCHANGING: { + // ### FIXME: How does this problem happen and why is it solved? + // When toggling the "Show theme color in title bar and window border" setting in + // Windows Settings, or calling `DrawMenuBar()`, Windows sends a message of + // WM_WINDOWPOSCHANGING with flags 0x37. If we do not process this message, + // the client area as a whole will shift to the left, which looks very abnormal if + // we don't repaint it. This exception disappears if we add SWP_NOCOPYBITS flag. + // But I don't know what caused the problem, or why this would solve it. + static constexpr const auto kBadWindowPosFlag = + SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED; + const auto windowPos = reinterpret_cast<LPWINDOWPOS>(lParam); + if (windowPos->flags == kBadWindowPosFlag) { + windowPos->flags |= SWP_NOCOPYBITS; + } + break; + } + + 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; + } + + bool Win32WindowContext::nonClientCalcSizeHandler(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam, LRESULT *result) { + Q_UNUSED(message) + Q_UNUSED(this) + + // Windows鏄牴鎹繖涓秷鎭殑杩斿洖鍊兼潵璁剧疆绐楀彛鐨勫鎴峰尯锛堢獥鍙d腑鐪熸鏄剧ず鐨勫唴瀹癸級 + // 鍜岄潪瀹㈡埛鍖猴紙鏍囬鏍忋�佺獥鍙h竟妗嗐�佽彍鍗曟爮鍜岀姸鎬佹爮绛塛indows绯荤粺鑷鎻愪緵鐨勯儴鍒� + // 锛屼笉杩囧浜嶲t鏉ヨ锛岄櫎浜嗘爣棰樻爮鍜岀獥鍙h竟妗嗭紝闈炲鎴峰尯鍩烘湰涔熼兘鏄嚜缁樼殑锛夌殑鑼� + // 鍥寸殑锛宭Param閲屽瓨鏀剧殑灏辨槸鏂板鎴峰尯鐨勫嚑浣曞尯鍩燂紝榛樿鏄暣涓獥鍙g殑澶у皬锛屾甯� + // 鐨勭▼搴忛渶瑕佷慨鏀硅繖涓弬鏁帮紝鍛婄煡绯荤粺绐楀彛鐨勫鎴峰尯鍜岄潪瀹㈡埛鍖虹殑鑼冨洿锛堜竴鑸潵璇村彲 + // 浠ュ畬鍏ㄤ氦缁橶indows锛岃鍏惰嚜琛屽鐞嗭紝浣跨敤榛樿鐨勫鎴峰尯鍜岄潪瀹㈡埛鍖猴級锛屽洜姝ゅ鏋� + // 鎴戜滑涓嶄慨鏀筶Param锛屽氨鍙互浣垮鎴峰尯鍏呮弧鏁翠釜绐楀彛锛屼粠鑰屽幓鎺夋爣棰樻爮鍜岀獥鍙h竟妗� + // 锛堝洜涓鸿繖浜涗笢瑗块兘琚鎴峰尯缁欑洊浣忎簡銆備絾杈规闃村奖涔熶細鍥犳鑰屼涪澶憋紝涓嶈繃鎴戜滑浼氫娇 + // 鐢ㄥ叾浠栨柟寮忓皢鍏跺甫鍥烇紝璇峰弬鑰冨叾浠栨秷鎭殑澶勭悊锛屾澶勪笉杩囧鎻愬強锛夈�備絾鏈変釜鎯呭喌瑕� + // 鐗瑰埆娉ㄦ剰锛岄偅灏辨槸绐楀彛鏈�澶у寲鍚庯紝绐楀彛鐨勫疄闄呭昂瀵镐細姣斿睆骞曠殑灏哄澶т竴鐐癸紝浠庤�屼娇 + // 鐢ㄦ埛鐪嬩笉鍒扮獥鍙g殑杈圭晫锛岃繖鏍风敤鎴峰氨涓嶈兘鍦ㄧ獥鍙f渶澶у寲鍚庤皟鏁寸獥鍙g殑澶у皬浜嗭紙铏界劧 + // 杩欎釜鍋氭硶鍚捣鏉ョ壒鍒鎬紝浣哤indows纭疄灏辨槸杩欐牱鍋氱殑锛夛紝鍥犳濡傛灉鎴戜滑瑕佽嚜琛� + // 澶勭悊绐楀彛鐨勯潪瀹㈡埛鍖猴紝灏辫鍦ㄧ獥鍙f渶澶у寲鍚庯紝灏嗙獥鍙h竟妗嗙殑瀹藉害鍜岄珮搴︼紙涓�鑸槸鐩� + // 绛夌殑锛変粠瀹㈡埛鍖鸿鍓帀锛屽惁鍒欐垜浠獥鍙f墍鏄剧ず鐨勫唴瀹瑰氨浼氳秴鍑哄睆骞曡竟鐣岋紝鏄剧ず涓嶅叏銆� + // 濡傛灉鐢ㄦ埛寮�鍚簡浠诲姟鏍忚嚜鍔ㄩ殣钘忥紝鍦ㄧ獥鍙f渶澶у寲鍚庯紝杩樿鑰冭檻浠诲姟鏍忕殑浣嶇疆銆傚洜涓� + // 濡傛灉绐楀彛鏈�澶у寲鍚庯紝鍏跺昂瀵稿拰灞忓箷灏哄鐩哥瓑锛堝洜涓轰换鍔℃爮闅愯棌浜嗭紝鎵�浠ョ獥鍙f渶澶у寲 + // 鍚庡叾瀹炴槸鍏呮弧浜嗘暣涓睆骞曪紝鍙樼浉鐨勫叏灞忎簡锛夛紝Windows浼氳涓虹獥鍙e凡缁忚繘鍏ュ叏灞忕殑 + // 鐘舵�侊紝浠庤�屽鑷磋嚜鍔ㄩ殣钘忕殑浠诲姟鏍忔棤娉曞脊鍑恒�傝閬垮厤杩欎釜鐘跺喌锛屽氨瑕佷娇绐楀彛鐨勫昂瀵� + // 灏忎簬灞忓箷灏哄銆傛垜涓嬮潰鐨勫仛娉曞弬鑰冧簡鐏嫄銆丆hromium鍜學indows Terminal + // 濡傛灉娌℃湁寮�鍚换鍔℃爮鑷姩闅愯棌锛屾槸涓嶅瓨鍦ㄨ繖涓棶棰樼殑锛屾墍浠ヨ鍏堣繘琛屽垽鏂�� + // 涓�鑸儏鍐典笅锛�*result璁剧疆涓�0锛堢浉褰撲簬DefWindowProc鐨勮繑鍥炲�间负0锛夊氨鍙互浜嗭紝 + // 鏍规嵁MSDN鐨勮娉曪紝杩斿洖0鎰忎负姝ゆ秷鎭凡缁忚绋嬪簭鑷澶勭悊浜嗭紝璁¦indows璺宠繃姝ゆ秷 + // 鎭紝鍚﹀垯Windows浼氭坊鍔犲姝ゆ秷鎭殑榛樿澶勭悊锛屽浜庡綋鍓嶈繖涓秷鎭�岃█锛屽氨鎰忓懗鐫� + // 鏍囬鏍忓拰绐楀彛杈规鍙堜細鍥炴潵锛岃繖褰撶劧涓嶆槸鎴戜滑鎯宠鐨勭粨鏋溿�傛牴鎹甅SDN锛屽綋wParam + // 涓篎ALSE鏃讹紝鍙兘杩斿洖0锛屼絾褰撳叾涓篢RUE鏃讹紝鍙互杩斿洖0锛屼篃鍙互杩斿洖涓�涓猈VR_甯� + // 閲忋�傛牴鎹瓹hromium鐨勬敞閲婏紝褰撳瓨鍦ㄩ潪瀹㈡埛鍖烘椂锛屽鏋滆繑鍥濿VR_REDRAW浼氬鑷村瓙 + // 绐楀彛/瀛愭帶浠跺嚭鐜板鎬殑bug锛堣嚜缁樻帶浠堕敊浣嶏級锛屽苟涓擫ucas鍦╓indows 10 + // 涓婃垚鍔熷鐜帮紝璇存槑杩欎釜bug鑷充粖閮芥病鏈夎В鍐炽�傛垜鏌ラ槄浜嗗ぇ閲忚祫鏂欙紝鍙戠幇鍞竴鐨勮В鍐� + // 鏂规灏辨槸杩斿洖0銆備絾濡傛灉涓嶅瓨鍦ㄩ潪瀹㈡埛鍖猴紝涓攚Param涓篢RUE锛屾渶濂借繑鍥� + // WVR_REDRAW锛屽惁鍒欑獥鍙e湪璋冩暣澶у皬鍙兘浼氫骇鐢熶弗閲嶇殑闂儊鐜拌薄銆� + // 铏界劧瀵瑰ぇ澶氭暟娑堟伅鏉ヨ锛岃繑鍥�0閮戒唬琛ㄨWindows蹇界暐姝ゆ秷鎭紝浣嗗疄闄呬笂涓嶅悓娑堟伅 + // 鑳芥帴鍙楃殑杩斿洖鍊兼槸涓嶄竴鏍风殑锛岃娉ㄦ剰鑷鏌ラ槄MSDN銆� + + // Sent when the size and position of a window's client area must be + // calculated. By processing this message, an application can + // control the content of the window's client area when the size or + // position of the window changes. If wParam is TRUE, lParam points + // to an NCCALCSIZE_PARAMS structure that contains information an + // application can use to calculate the new size and position of the + // client rectangle. If wParam is FALSE, lParam points to a RECT + // structure. On entry, the structure contains the proposed window + // rectangle for the window. On exit, the structure should contain + // the screen coordinates of the corresponding window client area. + // The client area is the window's content area, the non-client area + // is the area which is provided by the system, such as the title + // 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 + // 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 + // rid of the non-client area, it will always be there, all we can + // do is to hide it) , which means we should let the client area's + // size the same with the whole window's size. So there is no room + // for the non-client area and then the user won't be able to see it + // again. But how to achieve this? Very easy, just leave lParam (the + // re-calculated client area) untouched. But of course you can + // modify lParam, then the non-client area will be seen and the + // window borders and the window frame will show up. However, things + // are quite different when you try to modify the top margin of the + // client area. DWM will always draw the whole title bar no matter + // what margin value you set for the top, unless you don't modify it + // and remove the whole top area (the title bar + the one pixel + // height window border). This can be confirmed in Windows + // Terminal's source code, you can also try yourself to verify + // it. So things will become quite complicated if you want to + // preserve the four window borders. + + // If `wParam` is `FALSE`, `lParam` points to a `RECT` that contains + // the proposed window rectangle for our window. During our + // processing of the `WM_NCCALCSIZE` message, we are expected to + // modify the `RECT` that `lParam` points to, so that its value upon + // our return is the new client area. We must return 0 if `wParam` + // is `FALSE`. + // If `wParam` is `TRUE`, `lParam` points to a `NCCALCSIZE_PARAMS` + // struct. This struct contains an array of 3 `RECT`s, the first of + // which has the exact same meaning as the `RECT` that is pointed to + // by `lParam` when `wParam` is `FALSE`. The remaining `RECT`s, in + // conjunction with our return value, can + // be used to specify portions of the source and destination window + // rectangles that are valid and should be preserved. We opt not to + // 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 ? &(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. + 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); + 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. + clientRect->top = originalTop; + } + const bool max = IsMaximized(hWnd); + const bool full = isFullScreen(hWnd); + // We don't need this correction when we're fullscreen. We will + // have the WS_POPUP size, so we don't have to worry about + // borders, and the default frame will be fine. + if (max && !full) { + // When a window is maximized, its size is actually a little bit more + // than the monitor's work area. The window is positioned and sized in + // such a way that the resize handles are outside the monitor and + // then the window is clipped to the monitor so that the resize handle + // do not appear because you don't need them (because you can't resize + // a window when it's maximized unless you restore it). + const quint32 frameSize = getResizeBorderThickness(hWnd); + clientRect->top += frameSize; + if (!isWin10OrGreater()) { + clientRect->bottom -= frameSize; + clientRect->left += frameSize; + clientRect->right -= frameSize; + } + } + // Attempt to detect if there's an autohide taskbar, and if + // there is, reduce our size a bit on the side with the taskbar, + // so the user can still mouse-over the taskbar to reveal it. + // Make sure to use MONITOR_DEFAULTTONEAREST, so that this will + // still find the right monitor even when we're restoring from + // minimized. + if (max || full) { + APPBARDATA abd{}; + abd.cbSize = sizeof(abd); + const UINT taskbarState = ::SHAppBarMessage(ABM_GETSTATE, &abd); + // First, check if we have an auto-hide taskbar at all: + if (taskbarState & ABS_AUTOHIDE) { + bool top = false, bottom = false, left = false, right = false; + // Due to ABM_GETAUTOHIDEBAREX was introduced in Windows 8.1, + // we have to use another way to judge this if we are running + // on Windows 7 or Windows 8. + if (isWin8Point1OrGreater()) { + 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. + const auto hasAutohideTaskbar = [monitorRect](const UINT edge) -> bool { + APPBARDATA abd2{}; + abd2.cbSize = sizeof(abd2); + abd2.uEdge = edge; + abd2.rc = monitorRect; + const auto hTaskbar = + reinterpret_cast<HWND>(::SHAppBarMessage(ABM_GETAUTOHIDEBAREX, &abd2)); + return (hTaskbar != nullptr); + }; + top = hasAutohideTaskbar(ABE_TOP); + bottom = hasAutohideTaskbar(ABE_BOTTOM); + left = hasAutohideTaskbar(ABE_LEFT); + right = hasAutohideTaskbar(ABE_RIGHT); + } else { + int edge = -1; + APPBARDATA abd2{}; + abd2.cbSize = sizeof(abd2); + abd2.hWnd = ::FindWindowW(L"Shell_TrayWnd", nullptr); + HMONITOR windowMonitor = ::MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST); + HMONITOR taskbarMonitor = + ::MonitorFromWindow(abd2.hWnd, MONITOR_DEFAULTTOPRIMARY); + if (taskbarMonitor == windowMonitor) { + ::SHAppBarMessage(ABM_GETTASKBARPOS, &abd2); + edge = int(abd2.uEdge); + } + top = (edge == ABE_TOP); + bottom = (edge == ABE_BOTTOM); + left = (edge == ABE_LEFT); + right = (edge == ABE_RIGHT); + } + // If there's a taskbar on any side of the monitor, reduce + // our size a little bit on that edge. + // Note to future code archeologists: + // This doesn't seem to work for fullscreen on the primary + // display. However, testing a bunch of other apps with + // fullscreen modes and an auto-hiding taskbar has + // shown that _none_ of them reveal the taskbar from + // fullscreen mode. This includes Edge, Firefox, Chrome, + // Sublime Text, PowerPoint - none seemed to support this. + // This does however work fine for maximized. + if (top) { + // Peculiarly, when we're fullscreen, + clientRect->top += kAutoHideTaskBarThickness; + } else if (bottom) { + clientRect->bottom -= kAutoHideTaskBarThickness; + } else if (left) { + clientRect->left += kAutoHideTaskBarThickness; + } else if (right) { + clientRect->right -= kAutoHideTaskBarThickness; + } + } + } + // We should call this function only before the function returns. + syncPaintEventWithDwm(); + // 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 + // client pixels and child windows are mispositioned by the width/height + // 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 ? WVR_REDRAW : FALSE; + return true; + } + + bool Win32WindowContext::systemMenuHandler(HWND hWnd, UINT message, WPARAM wParam, + LPARAM lParam, LRESULT *result) { + const auto getNativePosFromMouse = [lParam]() -> POINT { + return {GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam)}; + }; + const auto getNativeGlobalPosFromKeyboard = [hWnd]() -> POINT { + const bool maxOrFull = IsMaximized(hWnd) || isFullScreen(hWnd); + const quint32 frameSize = getResizeBorderThickness(hWnd); + const quint32 horizontalOffset = ((maxOrFull || !isWin10OrGreater()) ? 0 : frameSize); + const auto verticalOffset = [hWnd, maxOrFull, frameSize]() -> quint32 { + const quint32 titleBarHeight = getTitleBarHeight(hWnd); + if (!isWin10OrGreater()) { + return titleBarHeight; + } + if (isWin11OrGreater()) { + if (maxOrFull) { + return (titleBarHeight + frameSize); + } + return titleBarHeight; + } + if (maxOrFull) { + return titleBarHeight; + } + return titleBarHeight - frameSize; + }(); + RECT windowPos{}; + ::GetWindowRect(hWnd, &windowPos); + return {static_cast<LONG>(windowPos.left + horizontalOffset), + static_cast<LONG>(windowPos.top + verticalOffset)}; + }; + bool shouldShowSystemMenu = false; + bool broughtByKeyboard = false; + POINT nativeGlobalPos{}; + switch (message) { + case WM_RBUTTONUP: { + const POINT nativeLocalPos = getNativePosFromMouse(); + const QPoint qtScenePos = + QHighDpi::fromNativeLocalPosition(point2qpoint(nativeLocalPos), m_windowHandle); + if (isInTitleBarDraggableArea(qtScenePos)) { + shouldShowSystemMenu = true; + nativeGlobalPos = nativeLocalPos; + ::ClientToScreen(hWnd, &nativeGlobalPos); + } + break; + } + case WM_NCRBUTTONUP: { + if (wParam == HTCAPTION) { + shouldShowSystemMenu = true; + nativeGlobalPos = getNativePosFromMouse(); + } + break; + } + case WM_SYSCOMMAND: { + const WPARAM filteredWParam = (wParam & 0xFFF0); + if ((filteredWParam == SC_KEYMENU) && (lParam == VK_SPACE)) { + shouldShowSystemMenu = true; + broughtByKeyboard = true; + nativeGlobalPos = getNativeGlobalPosFromKeyboard(); + } + break; + } + case WM_KEYDOWN: + case WM_SYSKEYDOWN: { + const bool altPressed = ((wParam == VK_MENU) || (::GetKeyState(VK_MENU) < 0)); + const bool spacePressed = ((wParam == VK_SPACE) || (::GetKeyState(VK_SPACE) < 0)); + if (altPressed && spacePressed) { + shouldShowSystemMenu = true; + broughtByKeyboard = true; + nativeGlobalPos = getNativeGlobalPosFromKeyboard(); + } + break; + } + default: + break; + } + if (shouldShowSystemMenu) { + showSystemMenu2(hWnd, nativeGlobalPos, broughtByKeyboard, + m_delegate->isHostSizeFixed(m_host)); + // QPA's internal code will handle system menu events separately, and its + // behavior is not what we would want to see because it doesn't know our + // window doesn't have any window frame now, so return early here to avoid + // entering Qt's own handling logic. + *result = FALSE; + return true; + } + return false; + } + +} -- Gitblit v1.9.1