diff --git a/README.md b/README.md index 47aec71..1fb1baa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # FramelessHelper +## Features + +- Frameless but have frame shadow. +- Drag and resize. +- High DPI scaling. +- Multi-monitor support (different resolution and DPI). +- Windows platform: act like a normal window, such as have animations when minimizing and maximizing, support tile windows, etc ... + ## Usage ```cpp @@ -28,20 +36,12 @@ Notes - Only top level windows can be frameless. Applying this code to child windows or widgets will result in unexpected behavior. - If you want to use your own border width, border height, titlebar height or minimum window size, just use the original numbers, no need to scale them according to DPI, this code will do the scaling automatically. -## Features - -- Frameless but have frame shadow. -- Drag and resize. -- High DPI scaling. -- Multi-monitor support (different resolution and DPI). -- Windows: act like a normal window, such as have animations when minimizing and maximizing, support tile windows, etc ... - ## Tested Platforms - Windows 7 ~ 10 - Should work on X11, Wayland and macOS, but not tested. -## For Windows developers +## Notes for Windows developers - The `FramelessHelper` class is just a simple wrapper of the `WinNativeEventFilter` class, you can use the latter directly instead if you encounter with some strange problems. - If you are using `WinNativeEventFilter` directly, don't forget to call `FramelessHelper::updateQtFrame` everytime after you make a widget or window become frameless, it will make the new frame margins work correctly if `setGeometry` or `frameGeometry` is called. @@ -53,13 +53,33 @@ Notes - The border width (8 if not scaled), border height (8 if not scaled) and titlebar height (30 if not scaled) are acquired by Win32 APIs and are the same with other standard windows, and thus you should not modify them. - You can also copy all the code to `[virtual protected] bool QWidget::nativeEvent(const QByteArray &eventType, void *message, long *result)` or `[virtual protected] bool QWindow::nativeEvent(const QByteArray &eventType, void *message, long *result)`, it's the same with install a native event filter to the application. -## References +## References for Windows developers + +### Microsoft Docs + +- +- +- +- +- +- + +### Chromium + +- +- +- + +### Mozilla Firefox + +- + +### GitHub - - - - -- ## Special Thanks diff --git a/winnativeeventfilter.cpp b/winnativeeventfilter.cpp index 3e596d5..04c66c5 100644 --- a/winnativeeventfilter.cpp +++ b/winnativeeventfilter.cpp @@ -205,6 +205,10 @@ BOOL isCompositionEnabled() { return SUCCEEDED(m_lpDwmIsCompositionEnabled(&enabled)) && enabled; } +// The thickness of an auto-hide taskbar in pixels. +const int kAutoHideTaskbarThicknessPx = 2; +const int kAutoHideTaskbarThicknessPy = kAutoHideTaskbarThicknessPx; + const UINT m_defaultDotsPerInch = USER_DEFAULT_SCREEN_DPI; const qreal m_defaultDevicePixelRatio = 1.0; @@ -219,7 +223,7 @@ QVector m_framelessWindows; WinNativeEventFilter::WinNativeEventFilter() { initWin32Api(); } -WinNativeEventFilter::~WinNativeEventFilter() { removeUserData(); }; +WinNativeEventFilter::~WinNativeEventFilter() = default; void WinNativeEventFilter::install() { if (m_instance.isNull()) { @@ -399,59 +403,78 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, // 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. + const auto getClientAreaInsets = [](HWND _hWnd) -> RECT { + if (IsMaximized(_hWnd)) { + // Windows automatically adds a standard width border to all + // sides when a window is maximized. + int frameThickness_x = + getSystemMetricsForWindow(_hWnd, SM_CXSIZEFRAME) + + getSystemMetricsForWindow(_hWnd, SM_CXPADDEDBORDER); + int frameThickness_y = + getSystemMetricsForWindow(_hWnd, SM_CYSIZEFRAME) + + getSystemMetricsForWindow(_hWnd, SM_CXPADDEDBORDER); + // TODO: Chromium: HWNDMessageHandlerDelegate::HasFrame() + const bool hasFrame = false; + if (!hasFrame) { + frameThickness_x -= 1; + frameThickness_y -= 1; + } + RECT rect; + rect.top = frameThickness_y; + rect.bottom = frameThickness_y; + rect.left = frameThickness_x; + rect.right = frameThickness_x; + return rect; + } + return {0, 0, 0, 0}; + }; + const RECT insets = getClientAreaInsets(msg->hwnd); const auto mode = static_cast(msg->wParam); // If the window bounds change, we're going to relayout and repaint // anyway. Returning WVR_REDRAW avoids an extra paint before that of the // old client pixels in the (now wrong) location, and thus makes actions // like resizing a window from the left edge look slightly less broken. *result = mode ? WVR_REDRAW : 0; + // We special case when left or top insets are 0, since these conditions + // actually require another repaint to correct the layout after glass + // gets turned on and off. + if ((insets.left == 0) || (insets.top == 0)) { + *result = 0; + } + const auto clientRect = mode + ? &(reinterpret_cast(msg->lParam)->rgrc[0]) + : reinterpret_cast(msg->lParam); + clientRect->top += insets.top; + clientRect->bottom -= insets.bottom; + clientRect->left += insets.left; + clientRect->right -= insets.right; if (IsMaximized(msg->hwnd)) { - const HMONITOR windowMonitor = - m_lpMonitorFromWindow(msg->hwnd, MONITOR_DEFAULTTONEAREST); - MONITORINFO monitorInfo; - SecureZeroMemory(&monitorInfo, sizeof(monitorInfo)); - monitorInfo.cbSize = sizeof(monitorInfo); - m_lpGetMonitorInfoW(windowMonitor, &monitorInfo); - const auto rect = mode - ? &(reinterpret_cast(msg->lParam)->rgrc[0]) - : reinterpret_cast(msg->lParam); - *rect = monitorInfo.rcWork; - // If the client rectangle is the same as the monitor's - // rectangle, the shell assumes that the window has gone - // fullscreen, so it removes the topmost attribute from any - // auto-hide appbars, making them inaccessible. To avoid - // this, reduce the size of the client area by one pixel on - // a certain edge. The edge is chosen based on which side of - // the monitor is likely to contain an auto-hide appbar, so - // the missing client area is covered by it. - if (m_lpEqualRect(&monitorInfo.rcWork, &monitorInfo.rcMonitor)) { - APPBARDATA abd; - SecureZeroMemory(&abd, sizeof(abd)); - abd.cbSize = sizeof(abd); - const UINT taskbarState = - m_lpSHAppBarMessage(ABM_GETSTATE, &abd); - if (taskbarState & ABS_AUTOHIDE) { - int edge = -1; - abd.hWnd = m_lpFindWindowW(L"Shell_TrayWnd", nullptr); - if (abd.hWnd) { - const HMONITOR taskbarMonitor = m_lpMonitorFromWindow( - abd.hWnd, MONITOR_DEFAULTTOPRIMARY); - if (taskbarMonitor && - (taskbarMonitor == windowMonitor)) { - m_lpSHAppBarMessage(ABM_GETTASKBARPOS, &abd); - edge = abd.uEdge; - } - } - if (edge == ABE_BOTTOM) { - rect->bottom--; - } else if (edge == ABE_LEFT) { - rect->left++; - } else if (edge == ABE_TOP) { - rect->top++; - } else if (edge == ABE_RIGHT) { - rect->right--; + APPBARDATA abd; + SecureZeroMemory(&abd, sizeof(abd)); + abd.cbSize = sizeof(abd); + const UINT taskbarState = m_lpSHAppBarMessage(ABM_GETSTATE, &abd); + if (taskbarState & ABS_AUTOHIDE) { + int edge = -1; + abd.hWnd = m_lpFindWindowW(L"Shell_TrayWnd", nullptr); + if (abd.hWnd) { + const HMONITOR windowMonitor = m_lpMonitorFromWindow( + msg->hwnd, MONITOR_DEFAULTTONEAREST); + const HMONITOR taskbarMonitor = m_lpMonitorFromWindow( + abd.hWnd, MONITOR_DEFAULTTOPRIMARY); + if (taskbarMonitor == windowMonitor) { + m_lpSHAppBarMessage(ABM_GETTASKBARPOS, &abd); + edge = abd.uEdge; } } + if (edge == ABE_TOP) { + clientRect->top += kAutoHideTaskbarThicknessPy; + } else if (edge == ABE_BOTTOM) { + clientRect->bottom -= kAutoHideTaskbarThicknessPy; + } else if (edge == ABE_LEFT) { + clientRect->left += kAutoHideTaskbarThicknessPx; + } else if (edge == ABE_RIGHT) { + clientRect->right -= kAutoHideTaskbarThicknessPx; + } } // We cannot return WVR_REDRAW when there is nonclient area, or // Windows exhibits bugs where client pixels and child HWNDs are @@ -516,8 +539,9 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, const LONG ww = clientRect.right; const LONG wh = clientRect.bottom; POINT mouse; - // Don't use HIWORD or LOWORD because their results are unsigned numbers - // however the cursor position may be negative due to in a different + // Don't use HIWORD(lParam) and LOWORD(lParam) to get cursor + // coordinates because their results are unsigned numbers, however + // the cursor position may be negative due to in a different // monitor. mouse.x = GET_X_LPARAM(_lParam); mouse.y = GET_Y_LPARAM(_lParam); @@ -637,8 +661,7 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, case WM_SETTEXT: { // Disable painting while these messages are handled to prevent them // from drawing a window caption over the client area. - const LONG_PTR oldStyle = - m_lpGetWindowLongPtrW(msg->hwnd, GWL_STYLE); + const LONG_PTR oldStyle = m_lpGetWindowLongPtrW(msg->hwnd, GWL_STYLE); // Prevent Windows from drawing the default title bar by temporarily // toggling the WS_VISIBLE style. m_lpSetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle & ~WS_VISIBLE); @@ -655,7 +678,7 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, case WM_ERASEBKGND: { // Prevent the system from erasing the background of our window // to avoid weired flashing problems. - *result = 1; // Any non-zero content is OK. + *result = 1; // Any non-zero value is OK. return true; } default: { @@ -682,6 +705,8 @@ void WinNativeEventFilter::updateGlass(HWND handle) { margins = {-1, -1, -1, -1}; } m_lpDwmExtendFrameIntoClientArea(handle, &margins); + m_lpRedrawWindow(handle, nullptr, nullptr, + RDW_INVALIDATE | RDW_ERASE | RDW_FRAME | RDW_ALLCHILDREN); } UINT WinNativeEventFilter::getDotsPerInchForWindow(HWND handle) { @@ -914,15 +939,3 @@ qreal WinNativeEventFilter::getPreferedNumber(qreal num) { #endif return result; } - -void WinNativeEventFilter::removeUserData() { - // TODO: all top level windows of QGuiApplication. - if (!m_framelessWindows.isEmpty()) { - for (auto &&window : std::as_const(m_framelessWindows)) { - const auto userData = reinterpret_cast(m_lpGetWindowLongPtrW(window, GWLP_USERDATA)); - if (userData) { - delete userData; - } - } - } -} diff --git a/winnativeeventfilter.h b/winnativeeventfilter.h index 52306e6..6a9aecf 100644 --- a/winnativeeventfilter.h +++ b/winnativeeventfilter.h @@ -110,5 +110,4 @@ private: static UINT getDotsPerInchForWindow(HWND handle); static qreal getDevicePixelRatioForWindow(HWND handle); static int getSystemMetricsForWindow(HWND handle, int index); - void removeUserData(); };