From 3cfe15a9c3db0993d8b8fef5d148625840e5a75c Mon Sep 17 00:00:00 2001 From: Sine Striker <trueful@163.com> Date: 周二, 05 12月 2023 15:43:39 +0800 Subject: [PATCH] Add host interface --- src/core/contexts/win32windowcontext.cpp | 402 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 396 insertions(+), 6 deletions(-) diff --git a/src/core/contexts/win32windowcontext.cpp b/src/core/contexts/win32windowcontext.cpp index d88418e..dcb6eb8 100644 --- a/src/core/contexts/win32windowcontext.cpp +++ b/src/core/contexts/win32windowcontext.cpp @@ -6,6 +6,7 @@ #include <QtCore/QAbstractNativeEventFilter> #include <QtCore/QCoreApplication> #include <QtCore/QOperatingSystemVersion> +#include <QtCore/QScopeGuard> #include <QtCore/private/qsystemlibrary_p.h> #include <QtGui/private/qhighdpiscaling_p.h> @@ -27,6 +28,7 @@ struct DynamicApis { decltype(&::DwmFlush) pDwmFlush = nullptr; + decltype(&::DwmIsCompositionEnabled) pDwmIsCompositionEnabled = nullptr; decltype(&::GetDpiForWindow) pGetDpiForWindow = nullptr; decltype(&::GetSystemMetricsForDpi) pGetSystemMetricsForDpi = nullptr; decltype(&::GetDpiForMonitor) pGetDpiForMonitor = nullptr; @@ -44,6 +46,8 @@ QSystemLibrary dwmapi(QStringLiteral("dwmapi.dll")); pDwmFlush = reinterpret_cast<decltype(pDwmFlush)>(dwmapi.resolve("DwmFlush")); + pDwmIsCompositionEnabled = reinterpret_cast<decltype(pDwmIsCompositionEnabled)>( + dwmapi.resolve("DwmIsCompositionEnabled")); } ~DynamicApis() = default; @@ -137,6 +141,12 @@ return hwnd2str(reinterpret_cast<WId>(hwnd)); } + static inline bool isWin8OrGreater() { + static const bool result = + QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8; + return result; + } + static inline bool isWin8Point1OrGreater() { static const bool result = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows8_1; @@ -147,6 +157,28 @@ static const bool result = QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10; 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 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) { @@ -213,6 +245,22 @@ 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) { Q_ASSERT(window); if (!window) { @@ -249,7 +297,7 @@ case HTBORDER: return Win32WindowContext::FixedBorder; default: - break; + break; // unreachable } return Win32WindowContext::Outside; } @@ -315,11 +363,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; } @@ -407,8 +455,8 @@ 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() { @@ -424,9 +472,12 @@ } 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); // Store original window proc @@ -994,9 +1045,348 @@ *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 = /*isWindowFixedSize()*/ false; // ### FIXME + 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; + } 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; } -- Gitblit v1.9.1