From cdd8ca2e84115ecc29ba1b0f24cd9edacbae9cc4 Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Sat, 4 Apr 2020 22:20:02 +0800 Subject: [PATCH] Update. Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- framelesshelper.cpp | 453 +++++++++++++++++++++ framelesshelper.h | 101 +++++ framelesswidget.pro => framelesshelper.pro | 3 +- main.cpp | 8 +- winnativeeventfilter.cpp | 74 ---- winnativeeventfilter.h | 10 - 6 files changed, 562 insertions(+), 87 deletions(-) create mode 100644 framelesshelper.cpp create mode 100644 framelesshelper.h rename framelesswidget.pro => framelesshelper.pro (85%) diff --git a/framelesshelper.cpp b/framelesshelper.cpp new file mode 100644 index 0000000..b7dadb6 --- /dev/null +++ b/framelesshelper.cpp @@ -0,0 +1,453 @@ +/* + * MIT License + * + * Copyright (C) 2020 by wangwenx190 (Yuhang Zhao) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "framelesshelper.h" + +#include +#include +#include +#include +#include +#include +#include + +#ifdef Q_OS_WINDOWS +#include "winnativeeventfilter.h" +#else +#include +#include +#include +#include +#include +#endif + +Q_DECLARE_METATYPE(QMargins) + +FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) { + connect(this, &FramelessHelper::titlebarHeightChanged, this, + &FramelessHelper::updateQtFrame); +#ifdef Q_OS_WINDOWS + m_borderWidth = WinNativeEventFilter::borderWidth(nullptr); + m_borderHeight = WinNativeEventFilter::borderHeight(nullptr); + m_titlebarHeight = WinNativeEventFilter::titlebarHeight(nullptr); +#else + m_borderWidth = 8; + m_borderHeight = 8; + QWidget widget; + QStyleOption styleOption; + styleOption.initFrom(&widget); + m_titlebarHeight = widget.style()->pixelMetric( + QStyle::PixelMetric::PM_TitleBarHeight, &styleOption); + qDebug().noquote() << "Window device pixel ratio:" + << widget.devicePixelRatioF(); + qDebug().noquote() << "Window border width:" << m_borderWidth + << "Window border height:" << m_borderHeight + << "Window titlebar height:" << m_titlebarHeight; +#endif + updateQtFrame(m_titlebarHeight); +} + +int FramelessHelper::borderWidth() const { return m_borderWidth; } + +void FramelessHelper::setBorderWidth(int val) { + if (m_borderWidth != val) { + m_borderWidth = val; +#ifdef Q_OS_WINDOWS + WinNativeEventFilter::setBorderWidth(val); +#endif + Q_EMIT borderWidthChanged(val); + } +} + +int FramelessHelper::borderHeight() const { return m_borderHeight; } + +void FramelessHelper::setBorderHeight(int val) { + if (m_borderHeight != val) { + m_borderHeight = val; +#ifdef Q_OS_WINDOWS + WinNativeEventFilter::setBorderHeight(val); +#endif + Q_EMIT borderHeightChanged(val); + } +} + +int FramelessHelper::titlebarHeight() const { return m_titlebarHeight; } + +void FramelessHelper::setTitlebarHeight(int val) { + if (m_titlebarHeight != val) { + m_titlebarHeight = val; +#ifdef Q_OS_WINDOWS + WinNativeEventFilter::setTitlebarHeight(val); +#endif + Q_EMIT titlebarHeightChanged(val); + } +} + +FramelessHelper::Areas FramelessHelper::ignoreAreas() const { + return m_ignoreAreas; +} + +void FramelessHelper::setIgnoreAreas(const Areas &val) { + if (m_ignoreAreas != val) { + m_ignoreAreas = val; +#ifdef Q_OS_WINDOWS + auto iter = val.cbegin(); + while (iter != val.cend()) { + if (iter.key()) { + const auto hwnd = + static_cast(getWindowRawHandle(iter.key())); + if (hwnd) { + const auto data = WinNativeEventFilter::windowData(hwnd); + data->ignoreAreas = iter.value(); + } + } + ++iter; + } +#endif + Q_EMIT ignoreAreasChanged(val); + } +} + +FramelessHelper::Areas FramelessHelper::draggableAreas() const { + return m_draggableAreas; +} + +void FramelessHelper::setDraggableAreas(const Areas &val) { + if (m_draggableAreas != val) { + m_draggableAreas = val; +#ifdef Q_OS_WINDOWS + auto iter = val.cbegin(); + while (iter != val.cend()) { + if (iter.key()) { + const auto hwnd = + static_cast(getWindowRawHandle(iter.key())); + if (hwnd) { + const auto data = WinNativeEventFilter::windowData(hwnd); + data->draggableAreas = iter.value(); + } + } + ++iter; + } +#endif + Q_EMIT draggableAreasChanged(val); + } +} + +QVector FramelessHelper::framelessWindows() const { + return m_framelessWindows; +} + +void FramelessHelper::setFramelessWindows(const QVector &val) { + if (m_framelessWindows != val) { + m_framelessWindows = val; + if (!val.isEmpty()) { + for (auto &&object : qAsConst(val)) { + if (object) { +#ifdef Q_OS_WINDOWS + const auto hwnd = + static_cast(getWindowRawHandle(object)); + if (hwnd) { + WinNativeEventFilter::addFramelessWindow(hwnd); + } else { + qWarning().noquote() + << "Can't make the window frameless: failed to " + "acquire the window handle."; + } +#else + // Don't miss the Qt::Window flag. + const Qt::WindowFlags flags = + Qt::Window | Qt::FramelessWindowHint; + QWindow *window = getWindowHandle(object); + if (window) { + window->setFlags(flags); + // MouseTracking is always enabled for QWindow. + window->installEventFilter(this); + } else { + const auto widget = qobject_cast(object); + if (widget) { + widget->setWindowFlags(flags); + // We can't get MouseMove events if MouseTracking is + // disabled. + widget->setMouseTracking(true); + widget->installEventFilter(this); + } + } +#endif + } + } + } + Q_EMIT framelessWindowsChanged(val); + } +} + +#ifndef Q_OS_WINDOWS +bool FramelessHelper::eventFilter(QObject *object, QEvent *event) { + // TODO: Judge whether it's a top level window and whether we should filter + // it. + if (!object) { + event->ignore(); + return false; + } + const auto getWindowEdges = [this](const QPointF &point, int ww, + int wh) -> Qt::Edges { + if (point.y() < m_borderHeight) { + if (point.x() < m_borderWidth) { + return Qt::Edge::TopEdge | Qt::Edge::LeftEdge; + } + if (point.x() > (ww - m_borderWidth)) { + return Qt::Edge::TopEdge | Qt::Edge::RightEdge; + } + return Qt::Edge::TopEdge; + } + if (point.y() > (wh - m_borderHeight)) { + if (point.x() < m_borderWidth) { + return Qt::Edge::BottomEdge | Qt::Edge::LeftEdge; + } + if (point.x() > (ww - m_borderWidth)) { + return Qt::Edge::BottomEdge | Qt::Edge::RightEdge; + } + return Qt::Edge::BottomEdge; + } + if (point.x() < m_borderWidth) { + return Qt::Edge::LeftEdge; + } + if (point.x() > (ww - m_borderWidth)) { + return Qt::Edge::RightEdge; + } + return {}; + }; + const auto getCursorShape = [](Qt::Edges edges) -> Qt::CursorShape { + if ((edges.testFlag(Qt::Edge::TopEdge) && + edges.testFlag(Qt::Edge::LeftEdge)) || + (edges.testFlag(Qt::Edge::BottomEdge) && + edges.testFlag(Qt::Edge::RightEdge))) { + return Qt::CursorShape::SizeFDiagCursor; + } + if ((edges.testFlag(Qt::Edge::TopEdge) && + edges.testFlag(Qt::Edge::RightEdge)) || + (edges.testFlag(Qt::Edge::BottomEdge) && + edges.testFlag(Qt::Edge::LeftEdge))) { + return Qt::CursorShape::SizeBDiagCursor; + } + if (edges.testFlag(Qt::Edge::TopEdge) || + edges.testFlag(Qt::Edge::BottomEdge)) { + return Qt::CursorShape::SizeVerCursor; + } + if (edges.testFlag(Qt::Edge::LeftEdge) || + edges.testFlag(Qt::Edge::RightEdge)) { + return Qt::CursorShape::SizeHorCursor; + } + return Qt::CursorShape::ArrowCursor; + }; + const auto isInSpecificAreas = [](int x, int y, + const QVector &areas) -> bool { + for (auto &&area : qAsConst(areas)) { + if (area.contains(x, y, true)) { + return true; + } + } + return false; + }; + const auto isInTitlebarArea = + [this, &isInSpecificAreas](const QPointF &point, + QObject *window) -> bool { + if (window) { + return (point.y() < m_titlebarHeight) && + !isInSpecificAreas(point.x(), point.y(), + m_ignoreAreas.value(window)) && + (m_draggableAreas.isEmpty() + ? true + : isInSpecificAreas(point.x(), point.y(), + m_draggableAreas.value(window))); + } + return false; + }; + const auto moveOrResize = [this, object, &getWindowEdges, + &isInTitlebarArea](const QPointF &point) { + QWindow *window = getWindowHandle(object); + if (window) { + const Qt::Edges edges = + getWindowEdges(point, window->width(), window->height()); + if (edges == Qt::Edges{}) { + if (isInTitlebarArea(point, object)) { + window->startSystemMove(); + } + } else { + window->startSystemResize(edges); + } + } else { + qWarning().noquote() << "Can't move or resize the window: failed " + "to acquire the window handle."; + } + }; + switch (event->type()) { + case QEvent::MouseButtonDblClick: { + const auto mouseEvent = static_cast(event); + if (mouseEvent) { + if (mouseEvent->button() != Qt::MouseButton::LeftButton) { + break; + } + if (isInTitlebarArea(mouseEvent->localPos(), object)) { + QWindow *window = getWindowHandle(object); + if (window) { + if (window->windowStates().testFlag(Qt::WindowFullScreen)) { + break; + } + if (window->windowStates().testFlag(Qt::WindowMaximized)) { + window->showNormal(); + } else { + window->showMaximized(); + } + window->setCursor(Qt::CursorShape::ArrowCursor); + } else { + const auto widget = qobject_cast(object); + if (widget) { + if (widget->isFullScreen()) { + break; + } + if (widget->isMaximized()) { + widget->showNormal(); + } else { + widget->showMaximized(); + } + widget->setCursor(Qt::CursorShape::ArrowCursor); + } + } + } + } + break; + } + case QEvent::MouseButtonPress: { + const auto mouseEvent = static_cast(event); + if (mouseEvent) { + if (mouseEvent->button() != Qt::MouseButton::LeftButton) { + break; + } + moveOrResize(mouseEvent->localPos()); + } + break; + } + case QEvent::MouseMove: { + const auto mouseEvent = static_cast(event); + if (mouseEvent) { + QWindow *window = getWindowHandle(object); + if (window) { + window->setCursor(getCursorShape( + getWindowEdges(mouseEvent->localPos(), window->width(), + window->height()))); + } else { + const auto widget = qobject_cast(object); + if (widget) { + widget->setCursor(getCursorShape( + getWindowEdges(mouseEvent->localPos(), widget->width(), + widget->height()))); + } + } + } + break; + } + case QEvent::TouchBegin: + case QEvent::TouchUpdate: { + moveOrResize( + static_cast(event)->touchPoints().first().pos()); + break; + } + default: { + break; + } + } + event->ignore(); + return false; +} +#endif + +QWindow *FramelessHelper::getWindowHandle(QObject *val) { + if (val) { + const auto validWindow = [](QWindow *window) -> QWindow * { + return (window && window->handle()) ? window : nullptr; + }; + if (val->isWindowType()) { + return validWindow(qobject_cast(val)); + } else if (val->isWidgetType()) { + const auto widget = qobject_cast(val); + if (widget) { + return validWindow(widget->windowHandle()); + } + } else { + qWarning().noquote() << "Can't acquire the window handle: only " + "QWidget and QWindow are accepted."; + } + } + return nullptr; +} + +void FramelessHelper::updateQtFrame(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); + } + } else { + qWarning().noquote() << "Can't modify the window frame: failed " + "to acquire the window handle."; + } + } + } +} + +#ifdef Q_OS_WINDOWS +void *FramelessHelper::getWindowRawHandle(QObject *object) { + if (object) { + QWindow *window = getWindowHandle(object); + if (window) { + const auto handle = QGuiApplication::platformNativeInterface() + ->nativeResourceForWindow("handle", window); + if (handle) { + return handle; + } + } else { + const auto widget = qobject_cast(object); + if (widget) { + return reinterpret_cast(widget->winId()); + } + } + } + return nullptr; +} +#endif diff --git a/framelesshelper.h b/framelesshelper.h new file mode 100644 index 0000000..c80807b --- /dev/null +++ b/framelesshelper.h @@ -0,0 +1,101 @@ +/* + * MIT License + * + * Copyright (C) 2020 by wangwenx190 (Yuhang Zhao) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#pragma once + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE +QT_FORWARD_DECLARE_CLASS(QWindow) +QT_END_NAMESPACE + +class FramelessHelper : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(FramelessHelper) + + Q_PROPERTY(int borderWidth READ borderWidth WRITE setBorderWidth NOTIFY + borderWidthChanged) + Q_PROPERTY(int borderHeight READ borderHeight WRITE setBorderHeight NOTIFY + borderHeightChanged) + Q_PROPERTY(int titlebarHeight READ titlebarHeight WRITE setTitlebarHeight + NOTIFY titlebarHeightChanged) + Q_PROPERTY(Areas ignoreAreas READ ignoreAreas WRITE setIgnoreAreas NOTIFY + ignoreAreasChanged) + Q_PROPERTY(Areas draggableAreas READ draggableAreas WRITE setDraggableAreas + NOTIFY draggableAreasChanged) + Q_PROPERTY(QVector framelessWindows READ framelessWindows WRITE + setFramelessWindows NOTIFY framelessWindowsChanged) + +public: + using Areas = QHash>; + + explicit FramelessHelper(QObject *parent = nullptr); + ~FramelessHelper() override = default; + + int borderWidth() const; + void setBorderWidth(int val); + + int borderHeight() const; + void setBorderHeight(int val); + + int titlebarHeight() const; + void setTitlebarHeight(int val); + + Areas ignoreAreas() const; + void setIgnoreAreas(const Areas &val); + + Areas draggableAreas() const; + void setDraggableAreas(const Areas &val); + + QVector framelessWindows() const; + void setFramelessWindows(const QVector &val); + +protected: +#ifndef Q_OS_WINDOWS + bool eventFilter(QObject *object, QEvent *event) override; +#endif + +private: + QWindow *getWindowHandle(QObject *val); +#ifdef Q_OS_WINDOWS + void *getWindowRawHandle(QObject *object); +#endif + void updateQtFrame(int val); + +Q_SIGNALS: + void borderWidthChanged(int); + void borderHeightChanged(int); + void titlebarHeightChanged(int); + void ignoreAreasChanged(const Areas &); + void draggableAreasChanged(const Areas &); + void framelessWindowsChanged(const QVector &); + +private: + int m_borderWidth = -1, m_borderHeight = -1, m_titlebarHeight = -1; + Areas m_ignoreAreas, m_draggableAreas; + QVector m_framelessWindows; +}; diff --git a/framelesswidget.pro b/framelesshelper.pro similarity index 85% rename from framelesswidget.pro rename to framelesshelper.pro index ffc9aad..4c2b569 100644 --- a/framelesswidget.pro +++ b/framelesshelper.pro @@ -11,4 +11,5 @@ win32 { SOURCES += winnativeeventfilter.cpp } DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII -SOURCES += main.cpp +HEADERS += framelesshelper.h +SOURCES += framelesshelper.cpp main.cpp diff --git a/main.cpp b/main.cpp index ba7db5a..354007d 100644 --- a/main.cpp +++ b/main.cpp @@ -1,11 +1,13 @@ -#include "winnativeeventfilter.h" +#include "framelesshelper.h" #include #include int main(int argc, char *argv[]) { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) #if 0 QGuiApplication::setHighDpiScaleFactorRoundingPolicy( @@ -18,8 +20,10 @@ int main(int argc, char *argv[]) { QApplication application(argc, argv); + FramelessHelper helper; + QWidget widget; - WinNativeEventFilter::addFramelessWindow(&widget); + helper.setFramelessWindows({&widget}); widget.show(); return QApplication::exec(); diff --git a/winnativeeventfilter.cpp b/winnativeeventfilter.cpp index fdfd937..9dfe593 100644 --- a/winnativeeventfilter.cpp +++ b/winnativeeventfilter.cpp @@ -27,21 +27,15 @@ #include #include #include -#include #include -#include -#include #include #include #include #include -#include #include #include #include -Q_DECLARE_METATYPE(QMargins) - // The following constants are necessary for our code but they are not defined // in MinGW's header files, so we have to define them ourself. The values are // taken from MSDN and Windows SDK header files. @@ -844,25 +838,6 @@ void WinNativeEventFilter::updateWindow(HWND handle) { } } -void WinNativeEventFilter::updateQtFrame(QWindow *window) { - if (window) { - // Reduce top frame to zero since we paint it ourselves. Use - // device pixel to avoid rounding errors. - const QMargins margins = { - 0, -titlebarHeight(reinterpret_cast(window->winId())), 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); - } - } -} - void WinNativeEventFilter::initDLLs() { QLibrary user32Lib(QString::fromUtf8("User32")), shcoreLib(QString::fromUtf8("SHCore")); @@ -928,52 +903,3 @@ void WinNativeEventFilter::setTitlebarHeight(int tbh) { m_titlebarHeight = tbh; } } - -void WinNativeEventFilter::setFramelessWindows(QVector windows) { - if (!windows.isEmpty()) { - for (auto &&window : qAsConst(windows)) { - addFramelessWindow(window); - } - } -} - -void WinNativeEventFilter::addFramelessWindow(QObject *window, - WINDOWDATA *data) { - if (window) { - const auto getWindowHandle = [](QWindow *win) -> HWND { - if (win && win->handle()) { - const auto handle = - QGuiApplication::platformNativeInterface() - ->nativeResourceForWindow("handle", win); - if (handle) { - return static_cast(handle); - } - } - return reinterpret_cast(win->winId()); - }; - HWND hwnd = nullptr; - if (window->isWidgetType()) { - const auto widget = qobject_cast(window); - if (widget) { - QWindow *const _window = widget->windowHandle(); - if (_window) { - hwnd = getWindowHandle(_window); - } else { - hwnd = reinterpret_cast(widget->winId()); - } - } - } else if (window->isWindowType()) { - const auto _window = qobject_cast(window); - if (_window) { - hwnd = getWindowHandle(_window); - } - } else { - qWarning().noquote() << "Only QWidget and QWindow are accepted."; - } - if (hwnd) { - addFramelessWindow(hwnd, data); - } else { - qWarning().noquote() << "Failed to acquire window handle."; - } - } -} diff --git a/winnativeeventfilter.h b/winnativeeventfilter.h index a0f3ee5..d28ec3f 100644 --- a/winnativeeventfilter.h +++ b/winnativeeventfilter.h @@ -29,11 +29,6 @@ #include #include -QT_BEGIN_NAMESPACE -QT_FORWARD_DECLARE_CLASS(QObject) -QT_FORWARD_DECLARE_CLASS(QWindow) -QT_END_NAMESPACE - class WinNativeEventFilter : public QAbstractNativeEventFilter { Q_DISABLE_COPY_MOVE(WinNativeEventFilter) @@ -54,8 +49,6 @@ public: explicit WinNativeEventFilter(); ~WinNativeEventFilter() override; - static void updateQtFrame(QWindow *window); - // Make all top level windows become frameless, unconditionally. static void install(); // Make all top level windows back to normal. @@ -64,12 +57,9 @@ public: // Frameless windows handle list static QVector framelessWindows(); static void setFramelessWindows(QVector windows); - static void setFramelessWindows(QVector windows); // Make the given window become frameless. static void addFramelessWindow(HWND window, WINDOWDATA *data = nullptr); - static void addFramelessWindow(QObject *window, WINDOWDATA *data = nullptr); static void removeFramelessWindow(HWND window); - static void removeFramelessWindow(QObject *window); static void clearFramelessWindows(); // Set borderWidth, borderHeight or titlebarHeight to a negative value to