From 6fc9534184515a156e52e60e64f2961f263ef203 Mon Sep 17 00:00:00 2001 From: Tad Young Date: Sat, 24 Jun 2023 14:37:54 +0800 Subject: [PATCH] refactor(FramelessWindow): use Qt API for window resizing (#81) No longer use Win32API for window resizing. This should work under all platforms that support window resizing. --- app/framelesswindow.cpp | 180 ++++++++++++++++++++++------------------ app/framelesswindow.h | 17 ++-- app/mainwindow.cpp | 7 ++ 3 files changed, 114 insertions(+), 90 deletions(-) diff --git a/app/framelesswindow.cpp b/app/framelesswindow.cpp index 6149670..510afe9 100644 --- a/app/framelesswindow.cpp +++ b/app/framelesswindow.cpp @@ -1,33 +1,33 @@ // SPDX-FileCopyrightText: 2022 Gary Wang +// SPDX-FileCopyrightText: 2023 Tad Young // // SPDX-License-Identifier: MIT #include "framelesswindow.h" +#include +#include #include #include - -#ifdef _WIN32 -#include -#endif // _WIN32 +#include FramelessWindow::FramelessWindow(QWidget *parent) : QWidget(parent) , m_centralLayout(new QVBoxLayout(this)) + , m_oldCursorShape(Qt::ArrowCursor) + , m_oldEdges() { #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - // The Qt::WindowMinMaxButtonsHint or Qt::WindowMinimizeButtonHint here is to - // provide the ability to use Winkey + Up/Down to toggle minimize/maximize. - // But a bug introduced in Qt6 that this flag will break the WM_NCHITTEST event. - // See: QTBUG-112356 and discussion in https://github.com/BLumia/pineapple-pictures/pull/81 - // Thanks @yyc12345 for finding out the source of the issue. - this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint); + this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint); #else // There is a bug in Qt 5 that will make pressing Meta+Up cause the app // fullscreen under Windows, see QTBUG-91226 to learn more. // The bug seems no longer exists in Qt 6 (I only tested it under Qt 6.3.0). this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint); #endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + this->setMouseTracking(true); + this->setAttribute(Qt::WA_Hover, true); + this->installEventFilter(this); m_centralLayout->setContentsMargins(QMargins()); } @@ -43,89 +43,103 @@ void FramelessWindow::setCentralWidget(QWidget *widget) m_centralWidget = widget; } -bool FramelessWindow::nativeEvent(const QByteArray &eventType, void *message, NATIVE_RESULT *result) +void FramelessWindow::installResizeCapture(QObject* widget) { -#ifdef _WIN32 - // https://stackoverflow.com/questions/43505580/qt-windows-resizable-frameless-window - // Too lazy to do this now.. just stackoverflow it and did a copy and paste.. - Q_UNUSED(eventType) - MSG* msg = static_cast(message); + widget->installEventFilter(this); +} - if (msg->message == WM_NCHITTEST) { - if (isMaximized()) { - return false; - } +bool FramelessWindow::eventFilter(QObject* o, QEvent* e) +{ + switch (e->type()) { + case QEvent::HoverMove: + { + QWidget* wg = qobject_cast(o); + if (wg != nullptr) + return mouseHover(static_cast(e), wg); - *result = 0; - const LONG borderWidth = 8; - RECT winrect; - GetWindowRect(reinterpret_cast(winId()), &winrect); + break; + } + case QEvent::MouseButtonPress: + return mousePress(static_cast(e)); + } - // must be short to correctly work with multiple monitors (negative coordinates) - short x = msg->lParam & 0x0000FFFF; - short y = (msg->lParam & 0xFFFF0000) >> 16; + return QWidget::eventFilter(o, e); +} - bool resizeWidth = minimumWidth() != maximumWidth(); - bool resizeHeight = minimumHeight() != maximumHeight(); - if (resizeWidth) { - //left border - if (x >= winrect.left && x < winrect.left + borderWidth) { - *result = HTLEFT; - } - //right border - if (x < winrect.right && x >= winrect.right - borderWidth) { - *result = HTRIGHT; - } - } - if (resizeHeight) { - //bottom border - if (y < winrect.bottom && y >= winrect.bottom - borderWidth) { - *result = HTBOTTOM; - } - //top border - if (y >= winrect.top && y < winrect.top + borderWidth) { - *result = HTTOP; - } - } - if (resizeWidth && resizeHeight) { - //bottom left corner - if (x >= winrect.left && x < winrect.left + borderWidth && - y < winrect.bottom && y >= winrect.bottom - borderWidth) - { - *result = HTBOTTOMLEFT; - } - //bottom right corner - if (x < winrect.right && x >= winrect.right - borderWidth && - y < winrect.bottom && y >= winrect.bottom - borderWidth) - { - *result = HTBOTTOMRIGHT; - } - //top left corner - if (x >= winrect.left && x < winrect.left + borderWidth && - y >= winrect.top && y < winrect.top + borderWidth) - { - *result = HTTOPLEFT; - } - //top right corner - if (x < winrect.right && x >= winrect.right - borderWidth && - y >= winrect.top && y < winrect.top + borderWidth) - { - *result = HTTOPRIGHT; - } - } +bool FramelessWindow::mouseHover(QHoverEvent* event, QWidget* wg) +{ + QWindow* win = window()->windowHandle(); + Qt::Edges edges = this->getEdgesByPos(wg->mapToGlobal(event->oldPos()), win->frameGeometry()); - if (*result != 0) - return true; + // backup & restore cursor shape + if (edges && !m_oldEdges) + // entering the edge. backup cursor shape + m_oldCursorShape = win->cursor().shape(); + if (!edges && m_oldEdges) + // leaving the edge. restore cursor shape + win->setCursor(m_oldCursorShape); - QWidget *action = QApplication::widgetAt(QCursor::pos()); - if (action == this) { - *result = HTCAPTION; + // save the latest edges status + m_oldEdges = edges; + + // show resize cursor shape if cursor is within border + if (edges) { + win->setCursor(this->getCursorByEdge(edges, Qt::ArrowCursor)); + return true; + } + + return false; +} + +bool FramelessWindow::mousePress(QMouseEvent* event) +{ + if (event->buttons() & Qt::LeftButton && !isMaximized()) { + QWindow* win = window()->windowHandle(); +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + Qt::Edges edges = this->getEdgesByPos(event->globalPosition().toPoint(), win->frameGeometry()); +#else + Qt::Edges edges = this->getEdgesByPos(event->globalPos(), win->frameGeometry()); +#endif // QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + if (edges) { + win->startSystemResize(edges); return true; } } return false; -#else - return QWidget::nativeEvent(eventType, message, result); -#endif // _WIN32 } + +Qt::CursorShape FramelessWindow::getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor) +{ + if ((edges == (Qt::TopEdge | Qt::LeftEdge)) || (edges == (Qt::RightEdge | Qt::BottomEdge))) + return Qt::SizeFDiagCursor; + else if ((edges == (Qt::TopEdge | Qt::RightEdge)) || (edges == (Qt::LeftEdge | Qt::BottomEdge))) + return Qt::SizeBDiagCursor; + else if (edges & (Qt::TopEdge | Qt::BottomEdge)) + return Qt::SizeVerCursor; + else if (edges & (Qt::LeftEdge | Qt::RightEdge)) + return Qt::SizeHorCursor; + else + return default_cursor; +} + +Qt::Edges FramelessWindow::getEdgesByPos(const QPoint gpos, const QRect& winrect) +{ + const int borderWidth = 8; + Qt::Edges edges; + + int x = gpos.x() - winrect.x(); + int y = gpos.y() - winrect.y(); + + if (x < borderWidth) + edges |= Qt::LeftEdge; + if (x > (winrect.width() - borderWidth)) + edges |= Qt::RightEdge; + if (y < borderWidth) + edges |= Qt::TopEdge; + if (y > (winrect.height() - borderWidth)) + edges |= Qt::BottomEdge; + + return edges; +} + diff --git a/app/framelesswindow.h b/app/framelesswindow.h index 209f70e..95d9c2e 100644 --- a/app/framelesswindow.h +++ b/app/framelesswindow.h @@ -7,12 +7,6 @@ #include -#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) - typedef qintptr NATIVE_RESULT; -#else - typedef long NATIVE_RESULT; -#endif // QT_VERSION_CHECK(6, 0, 0) - QT_BEGIN_NAMESPACE class QVBoxLayout; QT_END_NAMESPACE @@ -24,11 +18,20 @@ public: explicit FramelessWindow(QWidget *parent = nullptr); void setCentralWidget(QWidget * widget); + void installResizeCapture(QObject* widget); protected: - bool nativeEvent(const QByteArray& eventType, void* message, NATIVE_RESULT* result) override; + bool eventFilter(QObject *o, QEvent *e) override; + bool mouseHover(QHoverEvent* event, QWidget* wg); + bool mousePress(QMouseEvent* event); private: + Qt::Edges m_oldEdges; + Qt::CursorShape m_oldCursorShape; + + Qt::CursorShape getCursorByEdge(const Qt::Edges& edges, Qt::CursorShape default_cursor); + Qt::Edges getEdgesByPos(const QPoint pos, const QRect& winrect); + QVBoxLayout * m_centralLayout = nullptr; QWidget * m_centralWidget = nullptr; // just a pointer, doesn't take the ownership. }; diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index b68bf32..eb2c08e 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -162,6 +162,13 @@ MainWindow::MainWindow(QWidget *parent) QTimer::singleShot(0, this, [this](){ m_am->setupShortcuts(); }); + + // allow some mouse events can go through these widgets for resizing window. + installResizeCapture(m_closeButton); + installResizeCapture(m_graphicsView); + installResizeCapture(m_graphicsView->viewport()); + installResizeCapture(m_gv); + installResizeCapture(m_gv->viewport()); } MainWindow::~MainWindow()