From c1fddf0028893b4cfd250d4dc0c78327212bc1dd Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Mon, 6 Apr 2020 12:12:32 +0800 Subject: [PATCH] Update. Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- README.md | 1 + framelesshelper.cpp | 44 ++++++----- framelesshelper.h | 4 +- winnativeeventfilter.cpp | 163 ++++++++++++++++++++++++--------------- winnativeeventfilter.h | 14 ++-- 5 files changed, 139 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 45afbb9..47aec71 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ 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. - Don't change the window flags (for example, enable the Qt::FramelessWindowHint flag) because it will break the functionality of this code. I'll get rid of the window frame (including the titlebar of course) in Win32 native events. - All traditional Win32 APIs are replaced by their DPI-aware ones, if there is one. - Start from Windows 10, normal windows usually have a one pixel width border line, I don't add it because not everyone like it. You can draw one manually if you really need it. diff --git a/framelesshelper.cpp b/framelesshelper.cpp index 396882a..3a0f121 100644 --- a/framelesshelper.cpp +++ b/framelesshelper.cpp @@ -46,7 +46,9 @@ Q_DECLARE_METATYPE(QMargins) FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) { connect(this, &FramelessHelper::titlebarHeightChanged, this, - &FramelessHelper::updateQtFrame); + &FramelessHelper::updateQtFrame_internal); + connect(this, &FramelessHelper::framelessWindowsChanged, + [this]() { updateQtFrame_internal(m_titlebarHeight); }); #ifdef Q_OS_WINDOWS m_borderWidth = WinNativeEventFilter::borderWidth(nullptr); m_borderHeight = WinNativeEventFilter::borderHeight(nullptr); @@ -67,7 +69,26 @@ FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) { << "Window border height:" << m_borderHeight << "Window titlebar height:" << m_titlebarHeight; #endif - updateQtFrame(m_titlebarHeight); + updateQtFrame_internal(m_titlebarHeight); +} + +void FramelessHelper::updateQtFrame(QWindow *window, int titlebarHeight) { + if (window && (titlebarHeight > 0)) { + // Reduce top frame to zero since we paint it ourselves. Use + // device pixel to avoid rounding errors. + const QMargins margins = {0, -titlebarHeight, 0, 0}; + const QVariant marginsVar = QVariant::fromValue(margins); + // The dynamic property takes effect when creating the platform + // window. + window->setProperty("_q_windowsCustomMargins", marginsVar); + // If a platform window exists, change via native interface. + QPlatformWindow *platformWindow = window->handle(); + if (platformWindow) { + QGuiApplication::platformNativeInterface()->setWindowProperty( + platformWindow, QString::fromUtf8("WindowsCustomMargins"), + marginsVar); + } + } } int FramelessHelper::borderWidth() const { return m_borderWidth; } @@ -433,27 +454,12 @@ QWindow *FramelessHelper::getWindowHandle(QObject *val) { return nullptr; } -void FramelessHelper::updateQtFrame(int val) { +void FramelessHelper::updateQtFrame_internal(int val) { if (!m_framelessWindows.isEmpty()) { for (auto &&object : qAsConst(m_framelessWindows)) { QWindow *window = getWindowHandle(object); if (window) { - // Reduce top frame to zero since we paint it ourselves. Use - // device pixel to avoid rounding errors. - const QMargins margins = {0, -val, 0, 0}; - const QVariant marginsVar = QVariant::fromValue(margins); - // The dynamic property takes effect when creating the platform - // window. - window->setProperty("_q_windowsCustomMargins", marginsVar); - // If a platform window exists, change via native interface. - QPlatformWindow *platformWindow = window->handle(); - if (platformWindow) { - QGuiApplication::platformNativeInterface() - ->setWindowProperty( - platformWindow, - QString::fromUtf8("WindowsCustomMargins"), - marginsVar); - } + updateQtFrame(window, val); } else { qWarning().noquote() << "Can't modify the window frame: failed " "to acquire the window handle."; diff --git a/framelesshelper.h b/framelesshelper.h index c80807b..b995482 100644 --- a/framelesshelper.h +++ b/framelesshelper.h @@ -56,6 +56,8 @@ public: explicit FramelessHelper(QObject *parent = nullptr); ~FramelessHelper() override = default; + static void updateQtFrame(QWindow *window, int titlebarHeight); + int borderWidth() const; void setBorderWidth(int val); @@ -84,7 +86,7 @@ private: #ifdef Q_OS_WINDOWS void *getWindowRawHandle(QObject *object); #endif - void updateQtFrame(int val); + void updateQtFrame_internal(int val); Q_SIGNALS: void borderWidthChanged(int); diff --git a/winnativeeventfilter.cpp b/winnativeeventfilter.cpp index 491565d..a1d1e95 100644 --- a/winnativeeventfilter.cpp +++ b/winnativeeventfilter.cpp @@ -155,7 +155,7 @@ void WinNativeEventFilter::uninstall() { } if (!m_framelessWindows.isEmpty()) { for (auto &&window : qAsConst(m_framelessWindows)) { - updateWindow(window); + refreshWindow(window); } m_framelessWindows.clear(); } @@ -185,7 +185,7 @@ void WinNativeEventFilter::addFramelessWindow(HWND window, WINDOWDATA *data) { void WinNativeEventFilter::removeFramelessWindow(HWND window) { if (window && m_framelessWindows.contains(window)) { m_framelessWindows.removeAll(window); - updateWindow(window); + refreshWindow(window); } } @@ -295,10 +295,15 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, reinterpret_cast(msg->lParam)->lpCreateParams; SetWindowLongPtrW(msg->hwnd, GWLP_USERDATA, reinterpret_cast(userData)); + refreshWindow(msg->hwnd); break; } case WM_NCCALCSIZE: { - // MSDN: No special handling is needed when wParam is FALSE. + // If wParam is TRUE, it specifies that the application should indicate + // which part of the client area contains valid information. The system + // copies the valid information to the specified area within the new + // client area. If wParam is FALSE, the application does not need to + // indicate the valid part of the client area. if (static_cast(msg->wParam)) { if (IsMaximized(msg->hwnd)) { const HMONITOR monitor = @@ -352,9 +357,16 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, } } } - // This line removes the window frame (including the titlebar). + // "*result = 0" removes the window frame (including the titlebar). // But the frame shadow is lost at the same time. We'll bring it // back later. + // Don't use "*result = WVR_REDRAW", although it can also remove + // the window frame, it will cause child widgets have strange behaviors. + // "*result = 0" means we have processed this message and let Windows + // ignore it to avoid Windows process this message again. + // "return true" means we have filtered this message and let Qt ignore + // it, in other words, it'll block Qt's own handling of this message, + // so if you don't know what Qt does internally, don't block it. *result = 0; return true; } @@ -487,7 +499,7 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, auto &mmi = *reinterpret_cast(msg->lParam); if (QOperatingSystemVersion::current() < QOperatingSystemVersion::Windows8) { - // Buggy on Windows 7: + // FIXME: Buggy on Windows 7: // The origin of coordinates is the top left edge of the // monitor's work area. Why? It should be the top left edge of // the monitor's area. @@ -499,8 +511,15 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, qAbs(rcWorkArea.left - rcMonitorArea.left); mmi.ptMaxPosition.y = qAbs(rcWorkArea.top - rcMonitorArea.top); } - mmi.ptMaxSize.x = qAbs(rcWorkArea.right - rcWorkArea.left); - mmi.ptMaxSize.y = qAbs(rcWorkArea.bottom - rcWorkArea.top); + if (data->windowData.maximumSize.isEmpty()) { + mmi.ptMaxSize.x = qAbs(rcWorkArea.right - rcWorkArea.left); + mmi.ptMaxSize.y = qAbs(rcWorkArea.bottom - rcWorkArea.top); + } else { + mmi.ptMaxSize.x = getDevicePixelRatioForWindow(msg->hwnd) * + data->windowData.maximumSize.width(); + mmi.ptMaxSize.y = getDevicePixelRatioForWindow(msg->hwnd) * + data->windowData.maximumSize.height(); + } mmi.ptMaxTrackSize.x = mmi.ptMaxSize.x; mmi.ptMaxTrackSize.y = mmi.ptMaxSize.y; if (!data->windowData.minimumSize.isEmpty()) { @@ -526,9 +545,11 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, // Prevent Windows from drawing the default title bar by temporarily // toggling the WS_VISIBLE style. SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle & ~WS_VISIBLE); + refreshWindow(msg->hwnd); const LRESULT ret = DefWindowProcW(msg->hwnd, msg->message, msg->wParam, msg->lParam); SetWindowLongPtrW(msg->hwnd, GWL_STYLE, oldStyle); + refreshWindow(msg->hwnd); *result = ret; return true; } @@ -555,8 +576,9 @@ bool WinNativeEventFilter::nativeEventFilter(const QByteArray &eventType, const auto dpi = dpiX == dpiY ? dpiY : dpiX; qDebug().noquote() << "Window DPI changed: new DPI -->" << dpi << ", new DPR -->" - << qreal(dpi) / qreal(m_defaultDotsPerInch); - updateWindow(msg->hwnd); + << getPreferedNumber(qreal(dpi) / + qreal(m_defaultDotsPerInch)); + refreshWindow(msg->hwnd); break; } default: { @@ -618,8 +640,14 @@ void WinNativeEventFilter::handleDwmCompositionChanged(LPWINDOW data) { const MARGINS margins = {-1, -1, -1, -1}; DwmExtendFrameIntoClientArea(data->hWnd, &margins); } + WTA_OPTIONS options; + options.dwFlags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON; + options.dwMask = options.dwFlags; + // This is the official way to hide the window caption text and window icon. + SetWindowThemeAttribute(data->hWnd, WTA_NONCLIENT, &options, + sizeof(options)); handleBlurForWindow(data); - updateWindow(data->hWnd); + refreshWindow(data->hWnd); } void WinNativeEventFilter::handleThemeChanged(LPWINDOW data) { @@ -724,62 +752,17 @@ UINT WinNativeEventFilter::getDotsPerInchForWindow(HWND handle) { } qreal WinNativeEventFilter::getDevicePixelRatioForWindow(HWND handle) { - qreal dpr = handle + const qreal dpr = handle ? (qreal(getDotsPerInchForWindow(handle)) / qreal(m_defaultDotsPerInch)) : m_defaultDevicePixelRatio; -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - switch (QGuiApplication::highDpiScaleFactorRoundingPolicy()) { - case Qt::HighDpiScaleFactorRoundingPolicy::PassThrough: - // Default behavior for Qt 6. - break; - case Qt::HighDpiScaleFactorRoundingPolicy::Floor: - dpr = qFloor(dpr); - break; - case Qt::HighDpiScaleFactorRoundingPolicy::Ceil: - dpr = qCeil(dpr); - break; - default: - // Default behavior for Qt 5.6 to 5.15 - dpr = qRound(dpr); - break; - } -#else - // Default behavior for Qt 5.6 to 5.15 - dpr = qRound(dpr); -#endif - return dpr; + return getPreferedNumber(dpr); } int WinNativeEventFilter::getSystemMetricsForWindow(HWND handle, int index) { if (m_GetSystemMetricsForDpi) { - UINT dpi = getDotsPerInchForWindow(handle); -#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) - const bool shouldRound = - QGuiApplication::highDpiScaleFactorRoundingPolicy() != - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough; -#else - const bool shouldRound = true; -#endif - if (shouldRound) { - if (dpi < (m_defaultDotsPerInch * 1.5)) { - dpi = m_defaultDotsPerInch; - } else if (dpi == (m_defaultDotsPerInch * 1.5)) { - dpi = m_defaultDotsPerInch * 1.5; - } else if (dpi < (m_defaultDotsPerInch * 2.5)) { - dpi = m_defaultDotsPerInch * 2; - } else if (dpi == (m_defaultDotsPerInch * 2.5)) { - dpi = m_defaultDotsPerInch * 2.5; - } else if (dpi < (m_defaultDotsPerInch * 3.5)) { - dpi = m_defaultDotsPerInch * 3; - } else if (dpi == (m_defaultDotsPerInch * 3.5)) { - dpi = m_defaultDotsPerInch * 3.5; - } else if (dpi < (m_defaultDotsPerInch * 4.5)) { - dpi = m_defaultDotsPerInch * 4; - } else { - qWarning().noquote() << "DPI too large:" << dpi; - } - } - return m_GetSystemMetricsForDpi(index, dpi); + return m_GetSystemMetricsForDpi(index, + static_cast(getPreferedNumber( + getDotsPerInchForWindow(handle)))); } else { return GetSystemMetrics(index) * getDevicePixelRatioForWindow(handle); } @@ -788,7 +771,7 @@ int WinNativeEventFilter::getSystemMetricsForWindow(HWND handle, int index) { void WinNativeEventFilter::setWindowData(HWND window, WINDOWDATA *data) { if (window && data) { createUserData(window, data); - updateWindow(window); + refreshWindow(window); } } @@ -824,11 +807,12 @@ void WinNativeEventFilter::createUserData(HWND handle, WINDOWDATA *data) { } SetWindowLongPtrW(handle, GWLP_USERDATA, reinterpret_cast(_data)); + refreshWindow(handle); } } } -void WinNativeEventFilter::updateWindow(HWND handle) { +void WinNativeEventFilter::refreshWindow(HWND handle) { if (handle) { // SWP_FRAMECHANGED: Applies new frame styles set using the // SetWindowLong function. Sends a WM_NCCALCSIZE message to the window, @@ -851,6 +835,8 @@ void WinNativeEventFilter::updateWindow(HWND handle) { SetWindowPos(handle, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER); + // Inform the window to adjust it's size to let it's contents fit the + // window. SendMessageW(handle, WM_SIZE, 0, 0); // The UpdateWindow function updates the client area of the specified // window by sending a WM_PAINT message to the window if the window's @@ -927,3 +913,56 @@ void WinNativeEventFilter::setTitlebarHeight(int tbh) { m_titlebarHeight = tbh; } } + +qreal WinNativeEventFilter::getPreferedNumber(qreal num) { + qreal result = -1.0; + const auto getRoundedNumber = [](qreal in) -> qreal { + // If the given number is not very large, we assume it's a + // device pixel ratio (DPR), otherwise we assume it's a DPI. + if (in < m_defaultDotsPerInch) { + return qRound(in); + } else { + if (in < (m_defaultDotsPerInch * 1.5)) { + return m_defaultDotsPerInch; + } else if (in == (m_defaultDotsPerInch * 1.5)) { + return m_defaultDotsPerInch * 1.5; + } else if (in < (m_defaultDotsPerInch * 2.5)) { + return m_defaultDotsPerInch * 2; + } else if (in == (m_defaultDotsPerInch * 2.5)) { + return m_defaultDotsPerInch * 2.5; + } else if (in < (m_defaultDotsPerInch * 3.5)) { + return m_defaultDotsPerInch * 3; + } else if (in == (m_defaultDotsPerInch * 3.5)) { + return m_defaultDotsPerInch * 3.5; + } else if (in < (m_defaultDotsPerInch * 4.5)) { + return m_defaultDotsPerInch * 4; + } else { + qWarning().noquote() + << "DPI too large:" << static_cast(in); + } + } + return -1.0; + }; +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) + switch (QGuiApplication::highDpiScaleFactorRoundingPolicy()) { + case Qt::HighDpiScaleFactorRoundingPolicy::PassThrough: + // Default behavior for Qt 6. + result = num; + break; + case Qt::HighDpiScaleFactorRoundingPolicy::Floor: + result = qFloor(num); + break; + case Qt::HighDpiScaleFactorRoundingPolicy::Ceil: + result = qCeil(num); + break; + default: + // Default behavior for Qt 5.6 to 5.15 + result = getRoundedNumber(num); + break; + } +#else + // Default behavior for Qt 5.6 to 5.15 + result = getRoundedNumber(num); +#endif + return result; +} diff --git a/winnativeeventfilter.h b/winnativeeventfilter.h index d28ec3f..869426c 100644 --- a/winnativeeventfilter.h +++ b/winnativeeventfilter.h @@ -37,7 +37,7 @@ public: BOOL blurEnabled = FALSE; int borderWidth = -1, borderHeight = -1, titlebarHeight = -1; QVector ignoreAreas, draggableAreas; - QSize minimumSize = {-1, -1}; + QSize maximumSize = {-1, -1}, minimumSize = {-1, -1}; }; typedef struct tagWINDOW { HWND hWnd = nullptr; @@ -76,11 +76,14 @@ public: static void setBorderHeight(int bh); static void setTitlebarHeight(int tbh); - // DPI-aware border width of the given window. + // DPI-aware border width of the given window (if the pointer is null, + // return the system's standard value). static int borderWidth(HWND handle); - // DPI-aware border height of the given window. + // DPI-aware border height of the given window (if the pointer is null, + // return the system's standard value). static int borderHeight(HWND handle); - // DPI-aware titlebar height of the given window. + // DPI-aware titlebar height of the given window (if the pointer is null, + // return the system's standard value). static int titlebarHeight(HWND handle); #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)) @@ -98,7 +101,8 @@ private: void handleDwmCompositionChanged(LPWINDOW data); void handleThemeChanged(LPWINDOW data); void handleBlurForWindow(LPWINDOW data); - static void updateWindow(HWND handle); + static void refreshWindow(HWND handle); + static qreal getPreferedNumber(qreal num); static UINT getDotsPerInchForWindow(HWND handle); static qreal getDevicePixelRatioForWindow(HWND handle); static int getSystemMetricsForWindow(HWND handle, int index);