Compare commits

...

2 Commits

Author SHA1 Message Date
Yuhang Zhao 9279500c18 minor tweaks 2023-08-28 11:18:45 +08:00
SineStriker 0ce47469a9
Use better workaround to emulate mouse events (#273) 2023-08-28 11:01:24 +08:00
2 changed files with 74 additions and 96 deletions

View File

@ -416,14 +416,15 @@ Short answer: it's impossible. Full explaination: of course we can use the same
## Special Thanks ## Special Thanks
*Ordered by first contribution time* *Ordered by first contribution time (it may not be very accurate, sorry)*
- [Yuhang Zhao](https://github.com/wangwenx190): Help me create this project. This project is mainly based on his code. - [Yuhang Zhao](https://github.com/wangwenx190): Help me create this project. This project is mainly based on his code.
- [Julien](https://github.com/JulienMaille): Help me test this library on many various environments and help me fix the bugs we found. Contributed many code to improve this library. The MainWindow example is mostly based on his code. - [Julien](https://github.com/JulienMaille): Help me test this library on many various environments and help me fix the bugs we found. Contributed many code to improve this library. The MainWindow example is mostly based on his code.
- [Altair Wei](https://github.com/altairwei): Help me fix quite some small bugs and give me many important suggestions, the 2.x version is also inspired by his idea during our discussions. - [Altair Wei](https://github.com/altairwei): Help me fix quite some small bugs and give me many important suggestions, the 2.x version is also inspired by his idea during our discussions.
- [Kenji Mouri](https://github.com/MouriNaruto): Give me a lot of help on Win32 native developing. - [Kenji Mouri](https://github.com/MouriNaruto): Give me a lot of help on Win32 native developing.
- [Dylan Liu](https://github.com/mentalfl0w): Help me improve the build process on macOS. - [Dylan Liu](https://github.com/mentalfl0w): Help me improve the build process on macOS.
- [SineStriker](https://github.com/SineStriker): He spent almost a whole week helping me improve the Snap Layout implementation, fix potential bugs and give me a lot of useful suggestions. Without his great effort, the new implementation may never come. - [SineStriker](https://github.com/SineStriker): Spent over a whole week helping me improve the Snap Layout implementation, fixing potential bugs and also give me a lot of professional and useful suggestions. Without his great effort, the new implementation may never come.
- And also thanks to other contributors not listed here! Without their valuable help, this library wouldn't have such good quality and user experience!
## License ## License

View File

@ -103,7 +103,11 @@ enum class WindowPart : quint8
struct FramelessWin32HelperData struct FramelessWin32HelperData
{ {
SystemParameters params = {}; SystemParameters params = {};
std::pair<std::optional<int>, std::optional<int>> hitTestResult = {}; // Store the last hit test result, it's helpful to handle WM_MOUSEMOVE and WM_NCMOUSELEAVE.
WindowPart lastHitTestResult = WindowPart::Outside;
// True if we blocked a WM_MOUSELEAVE when mouse moves on chrome button, false when a
// WM_MOUSELEAVE comes or we manually call TrackMouseEvent().
bool mouseLeaveBlocked = false;
Dpi dpi = {}; Dpi dpi = {};
#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1)) #if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1))
QRect restoreGeometry = {}; QRect restoreGeometry = {};
@ -445,33 +449,24 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
} }
}; };
const auto clearWindowPartCache = [&data, &muData, &emulateClientAreaMessage]() -> void { if (uMsg == WM_MOUSELEAVE) {
if (getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)) == WindowPart::ChromeButton) { if (!isTaggedMessage(wParam)) {
emulateClientAreaMessage(WM_NCMOUSELEAVE); // Qt will call TrackMouseEvent() to get the WM_MOUSELEAVE message when it receives
muData.hitTestResult = {}; // 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
if ((uMsg == WM_MOUSELEAVE) && !isTaggedMessage(wParam)) { // mouse has left the control, and thus we actually lost the hover state.
// Qt will call TrackMouseEvent() to get the WM_MOUSELEAVE message when it receives // So we filter out these superfluous mouse leave events here to avoid this issue.
// WM_MOUSEMOVE messages, and since we are converting every WM_NCMOUSEMOVE message const QPoint qtScenePos = Utils::fromNativeLocalPosition(window, QPoint{ msg->pt.x, msg->pt.y });
// to WM_MOUSEMOVE message and send it back to the window to be able to hover our SystemButtonType dummy = SystemButtonType::Unknown;
// controls, we also get lots of WM_MOUSELEAVE messages at the same time because of if (data.params.isInsideSystemButtons(qtScenePos, &dummy)) {
// the reason above, and these superfluous mouse leave events cause Qt to think the muData.mouseLeaveBlocked = true;
// mouse has left the control, and thus we actually lost the hover state. *result = FALSE;
// So we filter out these superfluous mouse leave events here to avoid this issue. return true;
const QPoint qtScenePos = Utils::fromNativeLocalPosition(window, QPoint{ msg->pt.x, msg->pt.y }); }
SystemButtonType dummy = SystemButtonType::Unknown;
if (data.params.isInsideSystemButtons(qtScenePos, &dummy)) {
*result = FALSE;
return true;
}
}
if (uMsg == WM_SIZE) {
if ((wParam == SIZE_MAXIMIZED) || (wParam == SIZE_MINIMIZED)) {
clearWindowPartCache();
} }
muData.mouseLeaveBlocked = false;
} }
switch (uMsg) { switch (uMsg) {
@ -810,14 +805,7 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
// appearance with the system's one. // appearance with the system's one.
const auto hitTestRecorder = qScopeGuard([&muData, &result](){ const auto hitTestRecorder = qScopeGuard([&muData, &result](){
auto &first = std::get<0>(muData.hitTestResult); muData.lastHitTestResult = getHittedWindowPart(*result);
auto &second = std::get<1>(muData.hitTestResult);
if (second.has_value()) {
first = second;
} else if (!first.has_value()){
first = HTNOWHERE;
}
second = *result;
}); });
const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; const auto nativeGlobalPos = POINT{ GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
@ -999,13 +987,12 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
} }
return true; return true;
} }
case WM_MOUSEMOVE: { case WM_MOUSEMOVE:
const WindowPart previousWindowPart = getHittedWindowPart(data.hitTestResult.first.value_or(HTNOWHERE)); if ((data.lastHitTestResult != WindowPart::ChromeButton) && data.mouseLeaveBlocked) {
const WindowPart currentWindowPart = getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)); muData.mouseLeaveBlocked = false;
if ((previousWindowPart == WindowPart::ChromeButton) && (currentWindowPart == WindowPart::ClientArea)) {
std::ignore = requestForMouseLeaveMessage(hWnd, false); std::ignore = requestForMouseLeaveMessage(hWnd, false);
} }
} break; break;
case WM_NCMOUSEMOVE: case WM_NCMOUSEMOVE:
case WM_NCLBUTTONDOWN: case WM_NCLBUTTONDOWN:
case WM_NCLBUTTONUP: case WM_NCLBUTTONUP:
@ -1024,22 +1011,54 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
case WM_NCPOINTERDOWN: case WM_NCPOINTERDOWN:
case WM_NCPOINTERUP: case WM_NCPOINTERUP:
#endif #endif
case WM_NCMOUSEHOVER: case WM_NCMOUSEHOVER: {
const WindowPart currentWindowPart = data.lastHitTestResult;
if (uMsg == WM_NCMOUSEMOVE) {
if (currentWindowPart != WindowPart::ChromeButton) {
std::ignore = data.params.resetQtGrabbedControl();
if (muData.mouseLeaveBlocked) {
emulateClientAreaMessage(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`.
muData.lastHitTestResult = WindowPart::Outside;
}
if (currentWindowPart == WindowPart::ChromeButton) {
emulateClientAreaMessage();
if (uMsg == WM_NCMOUSEMOVE) {
*result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam);
} else {
*result = (((uMsg >= WM_NCXBUTTONDOWN) && (uMsg <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE);
}
return true;
}
} break;
case WM_NCMOUSELEAVE: { case WM_NCMOUSELEAVE: {
const WindowPart previousWindowPart = getHittedWindowPart(data.hitTestResult.first.value_or(HTNOWHERE)); const WindowPart currentWindowPart = data.lastHitTestResult;
const WindowPart currentWindowPart = getHittedWindowPart(data.hitTestResult.second.value_or(HTNOWHERE)); if (currentWindowPart == WindowPart::ChromeButton) {
if (uMsg == WM_NCMOUSELEAVE) { // If we press on the chrome button and move mouse, Windows will take the pressing area
if ((previousWindowPart == WindowPart::ChromeButton) && (currentWindowPart == WindowPart::Outside)) { // as HTCLIENT which maybe because of our former retransmission of WM_NCLBUTTONDOWN, as
// If current window part is chrome button, it indicates that we must have clicked // a result, a WM_NCMOUSELEAVE will come immediately and a lot of WM_MOUSEMOVE will come
// the minimize button or maximize button, we also should send the client leave // if we move the mouse, we should track the mouse in advance.
// message to Qt. if (muData.mouseLeaveBlocked) {
muData.mouseLeaveBlocked = false;
std::ignore = requestForMouseLeaveMessage(hWnd, false);
}
} else {
if (data.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(WM_NCMOUSELEAVE); emulateClientAreaMessage(WM_NCMOUSELEAVE);
} }
if (currentWindowPart == WindowPart::Outside) { if (currentWindowPart == WindowPart::Outside) {
// The mouse is leaving the window from the non-client area, clear window part cache.
muData.hitTestResult = {};
// Notice: we're not going to clear window part cache when the mouse leaves window // 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 // 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, // the mouse leaves window from client area and enters window from non-client area,
@ -1047,40 +1066,6 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
std::ignore = data.params.resetQtGrabbedControl(); std::ignore = data.params.resetQtGrabbedControl();
} }
} else {
if (uMsg == WM_NCMOUSEMOVE) {
if (currentWindowPart != WindowPart::ChromeButton) {
std::ignore = data.params.resetQtGrabbedControl();
}
if ((previousWindowPart == WindowPart::ChromeButton)
&& ((currentWindowPart == WindowPart::TitleBar)
|| (currentWindowPart == WindowPart::ResizeBorder)
|| (currentWindowPart == WindowPart::FixedBorder))) {
emulateClientAreaMessage(WM_NCMOUSELEAVE);
}
// We need to make sure we get the correct window part when a WM_NCMOUSELEAVE come,
// so we reset current window part to null when we receive a WM_NCMOUSEMOVE.
// If the mouse is entering the client area, there must be a WM_NCHITTEST setting current
// window part to NCCLIENT before the WM_NCMOUSELEAVE comes;
// If the mouse is leaving the window, current window part remains as null.
auto &hitTestResult = muData.hitTestResult;
if (hitTestResult.second.has_value()) {
hitTestResult.first = hitTestResult.second;
hitTestResult.second.reset();
}
}
if (currentWindowPart == WindowPart::ChromeButton) {
emulateClientAreaMessage();
if (uMsg == WM_NCMOUSEMOVE) {
*result = ::DefWindowProcW(hWnd, WM_NCMOUSEMOVE, wParam, lParam);
} else {
*result = (((uMsg >= WM_NCXBUTTONDOWN) && (uMsg <= WM_NCXBUTTONDBLCLK)) ? TRUE : FALSE);
}
return true;
}
} }
} break; } break;
#if (QT_VERSION < QT_VERSION_CHECK(6, 2, 2)) // I contributed this small technique to upstream Qt since 6.2.2 #if (QT_VERSION < QT_VERSION_CHECK(6, 2, 2)) // I contributed this small technique to upstream Qt since 6.2.2
@ -1152,17 +1137,9 @@ bool FramelessHelperWin::nativeEventFilter(const QByteArray &eventType, void *me
#endif // (QT_VERSION < QT_VERSION_CHECK(6, 5, 1)) #endif // (QT_VERSION < QT_VERSION_CHECK(6, 5, 1))
data.params.forceChildrenRepaint(500); data.params.forceChildrenRepaint(500);
} break; } break;
case WM_DWMCOMPOSITIONCHANGED: { case WM_DWMCOMPOSITIONCHANGED:
// Re-apply the custom window frame if recovered from the basic theme. // Re-apply the custom window frame if recovered from the basic theme.
std::ignore = Utils::updateWindowFrameMargins(windowId, false); std::ignore = Utils::updateWindowFrameMargins(windowId, false);
} break;
case WM_ACTIVATE:
if (LOWORD(wParam) == WA_INACTIVE) {
clearWindowPartCache();
}
break;
case WM_INITMENU:
clearWindowPartCache();
break; break;
#if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1)) #if (QT_VERSION < QT_VERSION_CHECK(6, 5, 1))
case WM_ENTERSIZEMOVE: // Sent to a window when the user drags the title bar or the resize border. case WM_ENTERSIZEMOVE: // Sent to a window when the user drags the title bar or the resize border.