From 0fe297b5e48d51fabae1f8e9256bb66b0adfb5b0 Mon Sep 17 00:00:00 2001 From: Yuhang Zhao <2546789017@qq.com> Date: Fri, 8 May 2020 21:07:44 +0800 Subject: [PATCH] Add UNIX helper back. With some improvements. Signed-off-by: Yuhang Zhao <2546789017@qq.com> --- framelesshelper.cpp | 502 +++++++++++++++++++++++++++++++++++ framelesshelper.h | 82 ++++++ framelesshelper_unix.pro | 11 + framelesshelper_windows.pro | 2 +- main_unix.cpp | 41 +++ main.cpp => main_windows.cpp | 0 6 files changed, 637 insertions(+), 1 deletion(-) create mode 100644 framelesshelper.cpp create mode 100644 framelesshelper.h create mode 100644 framelesshelper_unix.pro create mode 100644 main_unix.cpp rename main.cpp => main_windows.cpp (100%) diff --git a/framelesshelper.cpp b/framelesshelper.cpp new file mode 100644 index 0000000..b3939bf --- /dev/null +++ b/framelesshelper.cpp @@ -0,0 +1,502 @@ +/* + * 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 +#ifdef QT_WIDGETS_LIB +#include +#endif +#ifdef QT_QUICK_LIB +#include +#endif +#include +#include +#include +#include +#include + +Q_DECLARE_METATYPE(QMargins) + +namespace { + +QWindow *getWindowHandle(QObject *const val) { + if (val) { + const auto validWindow = [](QWindow *const window) -> QWindow * { + return (window && window->handle()) ? window : nullptr; + }; + if (val->isWindowType()) { + return validWindow(qobject_cast(val)); + } +#ifdef QT_WIDGETS_LIB + else if (val->isWidgetType()) { + const auto widget = qobject_cast(val); + if (widget) { + return validWindow(widget->windowHandle()); + } + } +#endif + else { + qWarning().noquote() << "Can't acquire the window handle: only " + "QWidget and QWindow are accepted."; + } + } + return nullptr; +} + +} // namespace + +FramelessHelper::FramelessHelper(QObject *parent) : QObject(parent) { + // ### TODO: The default border width and height on Windows is 8 pixels if + // the scale factor is 1.0. Don't know how to acquire these values on UNIX + // platforms through native API. + m_borderWidth = 8; + m_borderHeight = 8; + m_titleBarHeight = 30; +} + +void FramelessHelper::updateQtFrame(QWindow *const window, + const 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::getBorderWidth() const { return m_borderWidth; } + +void FramelessHelper::setBorderWidth(const int val) { m_borderWidth = val; } + +int FramelessHelper::getBorderHeight() const { return m_borderHeight; } + +void FramelessHelper::setBorderHeight(const int val) { m_borderHeight = val; } + +int FramelessHelper::getTitleBarHeight() const { return m_titleBarHeight; } + +void FramelessHelper::setTitleBarHeight(const int val) { + m_titleBarHeight = val; +} + +QVector FramelessHelper::getIgnoreAreas(QObject *const obj) const { + if (!obj) { + return {}; + } + return m_ignoreAreas.value(obj); +} + +void FramelessHelper::setIgnoreAreas(QObject *const obj, + const QVector &val) { + if (obj) { + m_ignoreAreas[obj] = val; + } +} + +QVector FramelessHelper::getDraggableAreas(QObject *const obj) const { + if (!obj) { + return {}; + } + return m_draggableAreas.value(obj); +} + +void FramelessHelper::setDraggableAreas(QObject *const obj, + const QVector &val) { + if (obj) { + m_draggableAreas[obj] = val; + } +} + +QVector> +FramelessHelper::getIgnoreObjects(QObject *const obj) const { + if (!obj) { + return {}; + } + return m_ignoreObjects.value(obj); +} + +void FramelessHelper::setIgnoreObjects(QObject *const obj, + const QVector> &val) { + if (obj) { + m_ignoreObjects[obj] = val; + } +} + +QVector> +FramelessHelper::getDraggableObjects(QObject *const obj) const { + if (!obj) { + return {}; + } + return m_draggableObjects.value(obj); +} + +void FramelessHelper::setDraggableObjects( + QObject *const obj, const QVector> &val) { + if (obj) { + m_draggableObjects[obj] = val; + } +} + +void FramelessHelper::removeWindowFrame(QObject *const obj) { + if (obj) { + // Don't miss the Qt::Window flag. + const Qt::WindowFlags flags = Qt::Window | Qt::FramelessWindowHint; + QWindow *window = getWindowHandle(obj); + if (window) { + window->setFlags(flags); + // MouseTracking is always enabled for QWindow. + window->installEventFilter(this); + updateQtFrame(window, m_titleBarHeight); + } +#ifdef QT_WIDGETS_LIB + else { + const auto widget = qobject_cast(obj); + if (widget) { + widget->setWindowFlags(flags); + // We can't get MouseMove events if MouseTracking is + // disabled. + widget->setMouseTracking(true); + widget->installEventFilter(this); + } + } +#endif + } +} + +bool FramelessHelper::eventFilter(QObject *object, QEvent *event) { + const auto isWindowTopLevel = [](QObject *const window) -> bool { + if (window) { + if (window->isWindowType()) { + return qobject_cast(window)->isTopLevel(); + } +#ifdef QT_WIDGETS_LIB + else if (window->isWidgetType()) { + return qobject_cast(window)->isTopLevel(); + } +#endif + } + return false; + }; + if (!object || !isWindowTopLevel(object)) { + event->ignore(); + return false; + } + const auto getWindowEdges = [this](const QPointF &point, const int ww, + const 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 = [](const 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 = [](const int x, const int y, + const QVector &areas) -> bool { + if (!areas.isEmpty()) { + for (auto &&area : qAsConst(areas)) { + if (area.contains(x, y)) { + return true; + } + } + } + return false; + }; + const auto isInSpecificObjects = + [](const int x, const int y, + const QVector> &objects) -> bool { + if (!objects.isEmpty()) { + for (auto &&obj : qAsConst(objects)) { + if (!obj) { + continue; + } +#ifdef QT_WIDGETS_LIB + const auto widget = qobject_cast(obj); + if (widget) { + if (QRect(widget->x(), widget->y(), widget->width(), + widget->height()) + .contains(x, y)) { + return true; + } + } +#endif +#ifdef QT_QUICK_LIB + const auto quickItem = qobject_cast(obj); + if (quickItem) { + if (QRect(quickItem->x(), quickItem->y(), + quickItem->width(), quickItem->height()) + .contains(x, y)) { + return true; + } + } +#endif + } + } + return false; + }; + const auto isInIgnoreAreas = + [this, &isInSpecificAreas](const QPointF &point, + QObject *const window) -> bool { + if (!window) { + return false; + } + return isInSpecificAreas(point.x(), point.y(), getIgnoreAreas(window)); + }; + const auto isInIgnoreObjects = + [this, &isInSpecificObjects](const QPointF &point, + QObject *const window) -> bool { +#if defined(QT_WIDGETS_LIB) || defined(QT_QUICK_LIB) + if (!window) { + return false; + } + return isInSpecificObjects(point.x(), point.y(), + getIgnoreObjects(window)); +#else + Q_UNUSED(point) + Q_UNUSED(window) + return false; +#endif + }; + const auto isInDraggableAreas = + [this, &isInSpecificAreas](const QPointF &point, + QObject *const window) -> bool { + if (!window) { + return false; + } + const auto areas = getDraggableAreas(window); + return (areas.isEmpty() + ? true + : isInSpecificAreas(point.x(), point.y(), areas)); + }; + const auto isInDraggableObjects = + [this, &isInSpecificObjects](const QPointF &point, + QObject *const window) -> bool { +#if defined(QT_WIDGETS_LIB) || defined(QT_QUICK_LIB) + if (!window) { + return false; + } + const auto objs = getDraggableObjects(window); + return (objs.isEmpty() + ? true + : isInSpecificObjects(point.x(), point.y(), objs)); +#else + Q_UNUSED(point) + Q_UNUSED(window) + return true; +#endif + }; + const auto isResizePermitted = + [&isInIgnoreAreas, &isInIgnoreObjects](const QPointF &point, + QObject *const window) -> bool { + if (!window) { + return false; + } + return (!isInIgnoreAreas(point, window) && + !isInIgnoreObjects(point, window)); + }; + const auto isInTitlebarArea = + [this, &isInDraggableAreas, &isInDraggableObjects, &isResizePermitted]( + const QPointF &point, QObject *const window) -> bool { + if (window) { + return ((point.y() <= m_titleBarHeight) && + isInDraggableAreas(point, window) && + isInDraggableObjects(point, window) && + isResizePermitted(point, window)); + } + return false; + }; + const auto moveOrResize = [&getWindowEdges, &isResizePermitted, + &isInTitlebarArea](const QPointF &point, + QObject *const object) { + 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 { + if (window->windowStates().testFlag( + Qt::WindowState::WindowNoState) && + isResizePermitted(point, object)) { + 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)) { + // ### FIXME: If the current object is a QWidget, we can use + // getWindowHandle(object) to get the window handle, but if we + // call showMaximized() of that window, it will not be + // maximized, it will be moved to the top-left edge of the + // screen without changing it's size instead. Why? Convert the + // object to QWidget and call showMaximized() doesn't have this + // issue. + if (object->isWindowType()) { + const auto window = qobject_cast(object); + if (window) { + if (window->windowStates().testFlag( + Qt::WindowState::WindowFullScreen)) { + break; + } + if (window->windowStates().testFlag( + Qt::WindowState::WindowMaximized)) { + window->showNormal(); + } else { + window->showMaximized(); + } + window->setCursor(Qt::CursorShape::ArrowCursor); + } + } +#ifdef QT_WIDGETS_LIB + else if (object->isWidgetType()) { + 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); + } + } +#endif + } + } + } break; + case QEvent::MouseButtonPress: { + const auto mouseEvent = static_cast(event); + if (mouseEvent) { + if (mouseEvent->button() != Qt::MouseButton::LeftButton) { + break; + } + moveOrResize(mouseEvent->localPos(), object); + } + } break; + case QEvent::MouseMove: { + const auto mouseEvent = static_cast(event); + if (mouseEvent) { + QWindow *window = getWindowHandle(object); + if (window) { + if (window->windowStates().testFlag( + Qt::WindowState::WindowNoState)) { + window->setCursor(getCursorShape( + getWindowEdges(mouseEvent->localPos(), window->width(), + window->height()))); + } + } +#ifdef QT_WIDGETS_LIB + else { + const auto widget = qobject_cast(object); + if (widget) { + if (!widget->isMinimized() && !widget->isMaximized() && + !widget->isFullScreen()) { + widget->setCursor(getCursorShape( + getWindowEdges(mouseEvent->localPos(), + widget->width(), widget->height()))); + } + } + } +#endif + } + } break; + case QEvent::TouchBegin: + case QEvent::TouchUpdate: + moveOrResize( + static_cast(event)->touchPoints().first().pos(), + object); + break; + default: + break; + } + event->ignore(); + return false; +} diff --git a/framelesshelper.h b/framelesshelper.h new file mode 100644 index 0000000..17c8f3b --- /dev/null +++ b/framelesshelper.h @@ -0,0 +1,82 @@ +/* + * 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 +#include + +QT_BEGIN_NAMESPACE +QT_FORWARD_DECLARE_CLASS(QWindow) +QT_END_NAMESPACE + +class FramelessHelper : public QObject { + Q_OBJECT + Q_DISABLE_COPY_MOVE(FramelessHelper) + +public: + explicit FramelessHelper(QObject *parent = nullptr); + ~FramelessHelper() override = default; + + static void updateQtFrame(QWindow *const window, const int titleBarHeight); + + int getBorderWidth() const; + void setBorderWidth(const int val); + + int getBorderHeight() const; + void setBorderHeight(const int val); + + int getTitleBarHeight() const; + void setTitleBarHeight(const int val); + + QVector getIgnoreAreas(QObject *const obj) const; + void setIgnoreAreas(QObject *const obj, const QVector &val); + + QVector getDraggableAreas(QObject *const obj) const; + void setDraggableAreas(QObject *const obj, const QVector &val); + + QVector> getIgnoreObjects(QObject *const obj) const; + void setIgnoreObjects(QObject *const obj, + const QVector> &val); + + QVector> getDraggableObjects(QObject *const obj) const; + void setDraggableObjects(QObject *const obj, + const QVector> &val); + + void removeWindowFrame(QObject *const obj); + +protected: + bool eventFilter(QObject *object, QEvent *event) override; + +private: + using Areas = QMap, QVector>; + using Objects = QMap, QVector>>; + + int m_borderWidth = -1, m_borderHeight = -1, m_titleBarHeight = -1; + Areas m_ignoreAreas = {}, m_draggableAreas = {}; + Objects m_ignoreObjects = {}, m_draggableObjects = {}; +}; diff --git a/framelesshelper_unix.pro b/framelesshelper_unix.pro new file mode 100644 index 0000000..1dd7d84 --- /dev/null +++ b/framelesshelper_unix.pro @@ -0,0 +1,11 @@ +TARGET = framelessapplication +debug: TARGET = $$join(TARGET,,,_debug) +TEMPLATE = app +QT += gui-private +qtHaveModule(widgets): QT += widgets +qtHaveModule(quick): QT += quick +CONFIG += c++17 strict_c++ warn_on utf8_source +DEFINES += QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII +VERSION = 1.0.0 +HEADERS += framelesshelper.h +SOURCES += framelesshelper.cpp main_unix.cpp diff --git a/framelesshelper_windows.pro b/framelesshelper_windows.pro index 29bc6e0..6e0670b 100644 --- a/framelesshelper_windows.pro +++ b/framelesshelper_windows.pro @@ -9,6 +9,6 @@ DEFINES += WIN32_LEAN_AND_MEAN QT_NO_CAST_FROM_ASCII QT_NO_CAST_TO_ASCII CONFIG -= embed_manifest_exe RC_FILE = resources.rc HEADERS += winnativeeventfilter.h -SOURCES += winnativeeventfilter.cpp main.cpp +SOURCES += winnativeeventfilter.cpp main_windows.cpp RESOURCES += resources.qrc OTHER_FILES += manifest.xml diff --git a/main_unix.cpp b/main_unix.cpp new file mode 100644 index 0000000..21b2382 --- /dev/null +++ b/main_unix.cpp @@ -0,0 +1,41 @@ +#include "framelesshelper.h" +#include +#include + +int main(int argc, char *argv[]) { + // High DPI scaling is enabled by default from Qt 6 +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + // Windows: we are using the manifest file to get maximum compatibility + // because some APIs are not supprted on old systems such as Windows 7 + // and Windows 8. And once we have set the DPI awareness level in the + // manifest file, any attemptation to try to change it through API will + // fail. In other words, Qt won't be able to enable or disable high DPI + // scaling or change the DPI awareness level once we have set it in the + // manifest file. So the following two lines are uesless actually (However, + // they are still useful on other platforms). + QGuiApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QGuiApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); +#endif +#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)) +#if 0 + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::Round); +#else + // Don't round the scale factor. + // This will break QWidget applications because they can't render correctly. + // Qt Quick applications won't have this issue. + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); +#endif +#endif + + QApplication application(argc, argv); + + FramelessHelper helper; + + QWidget widget; + helper.removeWindowFrame(&widget); + widget.show(); + + return QApplication::exec(); +} diff --git a/main.cpp b/main_windows.cpp similarity index 100% rename from main.cpp rename to main_windows.cpp