qt的無邊框視窗支援拖拽、Aero Snap、視窗陰影等特性

Yzi321發表於2024-10-29

環境:Desktop Qt 5.4.1 MSVC2013 32bit

需要的庫:dwmapi.libuser32.lib

需要標頭檔案:<dwmapi.h><windowsx.h>

只顯示重要程式碼

1、去除原邊框、加上陰影、Aero Snap以及其他動畫特效

(1)標頭檔案

#include "Windows.h"
#include "uxtheme.h"
#include "dwmapi.h"
#include "titlebar.h"//自定義類

(2)去除標題、原邊框

初始化,去除邊框

void MainWindow::init()
{
    setWindowFlags(Qt::Window | Qt::FramelessWindowHint);

#ifdef Q_OS_WIN
    HWND hwnd = reinterpret_cast<HWND>(this->winId());

    const LONG style = ( WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CLIPCHILDREN );
    SetWindowLongPtr(hwnd, GWL_STYLE, style);

    const MARGINS shadow = {1, 1, 1, 1};
    DwmExtendFrameIntoClientArea(hwnd, &shadow);

    SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE);
#endif
    // 標題拖動、雙擊事件
    MyTitleBar *title = new MyTitleBar(this);
    qobject_cast<QBoxLayout *>(ui->centralwidget->layout())->insertWidget(0, title);
}

同時在最大化時,增加了邊界,可以自行刪除

bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
{
    MSG* msg = (MSG*)message;
    switch (msg->message)
    {
    case WM_NCCALCSIZE:
    {
        // this kills the window frame and title bar we added with WS_THICKFRAME and WS_CAPTION
        *result = 0;
        return true;
    }
    // 若full時邊框不合適,可以去除此case
    case WM_GETMINMAXINFO:
    {
        if (::IsZoomed(msg->hwnd)) {
            // 最大化時會超出螢幕,所以填充邊框間距
            RECT frame = { 0, 0, 0, 0 };
            AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0);
            frame.left = abs(frame.left);
            frame.top = abs(frame.bottom);
            this->setContentsMargins(frame.left, frame.top, frame.right, frame.bottom);
        }
        else {
            this->setContentsMargins(0, 0, 0, 0);
        }

        *result = ::DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
        return true;
    }
    break;
    default:
        return QMainWindow::nativeEvent(eventType, message, result);
    }
}

(3)支援手動修改視窗

需要實現一個QAbstractNativeEventFilter 類,內容如下:

標頭檔案
#include <QAbstractNativeEventFilter>
#include <QWidget>
#include "Windows.h"
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))

class NativeEventFilter : public QAbstractNativeEventFilter
{
public:
    virtual bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)Q_DECL_OVERRIDE;
};
cpp檔案
bool NativeEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)

{
#ifdef Q_OS_WIN
    if (eventType != "windows_generic_MSG")
        return false;

    MSG* msg = static_cast<MSG*>(message);
    QWidget* widget = QWidget::find(reinterpret_cast<WId>(msg->hwnd));
    if (!widget)
        return false;

    switch (msg->message) {
    case WM_NCHITTEST: {
        const LONG borderWidth = 9;
        RECT winrect;
        GetWindowRect(msg->hwnd, &winrect);
        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);

        // bottom left
        if (x >= winrect.left && x < winrect.left + borderWidth &&
            y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOMLEFT;
            return true;
        }

        // bottom right
        if (x < winrect.right && x >= winrect.right - borderWidth &&
            y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOMRIGHT;
            return true;
        }

        // top left
        if (x >= winrect.left && x < winrect.left + borderWidth &&
            y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOPLEFT;
            return true;
        }

        // top right
        if (x < winrect.right && x >= winrect.right - borderWidth &&
            y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOPRIGHT;
            return true;
        }

        // left
        if (x >= winrect.left && x < winrect.left + borderWidth)
        {
            *result = HTLEFT;
            return true;
        }

        // right
        if (x < winrect.right && x >= winrect.right - borderWidth)
        {
            *result = HTRIGHT;
            return true;
        }

        // bottom
        if (y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOM;
            return true;
        }

        // top
        if (y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOP;
            return true;
        }

        return false;
    }
    default:
        break;
    }

    return false;
#else
    return false;
#endif
};

應用

然後在視窗建立之前,使用QApplication::installNativeEventFilter 方法把監聽器註冊給主程式。

int main(int argc, char *argv[])
{
    NativeEventFilter f;
    QApplication a(argc, argv);
    a.installNativeEventFilter(&f);//支援手動修改視窗大小
    MainWindow w;
    w.show();
    return a.exec();
}

2、自定義標題欄

  • 需要過載QWidget::mousePressEvent 方法
  • 儲存 Window 控制代碼操作原視窗
class MyTitleBar : public QFrame  {
    Q_OBJECT
public:
    explicit MyTitleBar(QWidget *parent = nullptr);

protected:
    void mousePressEvent(QMouseEvent* ev);


private:
    QWidget *Window = nullptr; // 儲存主視窗的指標

    QVBoxLayout *verticalLayout;
    QHBoxLayout *horizontalLayout;
    QSpacerItem *horizontalSpacer;
    QPushButton *pushButton_min;
    QPushButton *pushButton_normal;
    QPushButton *pushButton_max;
    QPushButton *pushButton_full;
    QSpacerItem *horizontalSpacer_6;
    QPushButton *pushButton_close;
};
建立ui

MyTitleBar::MyTitleBar(QWidget *parent): QFrame (parent), Window(parent)
{
    // 邊緣貼合
    parent->setContentsMargins(0, 0, 0, 0);

    setObjectName("title");
    setMaximumHeight(50);
    this->setStyleSheet("#title{background-color: rgb(255, 255, 255);}");
    verticalLayout = new QVBoxLayout(this);
    verticalLayout->setObjectName("verticalLayout");
    verticalLayout->setContentsMargins(0, 0, 0, 0);
    horizontalLayout = new QHBoxLayout();
    horizontalLayout->setObjectName("horizontalLayout");
    horizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum);

    horizontalLayout->addItem(horizontalSpacer);

    pushButton_min = new QPushButton(this);
    pushButton_min->setObjectName("pushButton_min");

    horizontalLayout->addWidget(pushButton_min);

    pushButton_normal = new QPushButton(this);
    pushButton_normal->setObjectName("pushButton_normal");

    horizontalLayout->addWidget(pushButton_normal);

    pushButton_max = new QPushButton(this);
    pushButton_max->setObjectName("pushButton_max");

    horizontalLayout->addWidget(pushButton_max);

    pushButton_full = new QPushButton(this);
    pushButton_full->setObjectName("pushButton_full");

    horizontalLayout->addWidget(pushButton_full);

    horizontalSpacer_6 = new QSpacerItem(40, 20, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum);

    horizontalLayout->addItem(horizontalSpacer_6);

    pushButton_close = new QPushButton(this);
    pushButton_close->setObjectName("pushButton_close");

    horizontalLayout->addWidget(pushButton_close);

    horizontalLayout->setStretch(0, 1);

    verticalLayout->addLayout(horizontalLayout);

    pushButton_min->setText(QCoreApplication::translate("MainWindow", "MIN", nullptr));
    pushButton_normal->setText(QCoreApplication::translate("MainWindow", "NORMAL", nullptr));
    pushButton_max->setText(QCoreApplication::translate("MainWindow", "MAX", nullptr));
    pushButton_full->setText(QCoreApplication::translate("MainWindow", "FULL", nullptr));
    pushButton_close->setText(QCoreApplication::translate("MainWindow", "CLOSE", nullptr));
}
連線主視窗的變化

最大化和關閉按扭,正常呼叫QWidget::showMaximized()QWidget::close() 等Qt自帶方法即可。

    connect(pushButton_min, &QPushButton::clicked, Window, &QWidget::showMinimized);
    connect(pushButton_close, &QPushButton::clicked, Window, &QWidget::close);
    connect(pushButton_normal, &QPushButton::clicked, Window, &QWidget::showNormal);
    connect(pushButton_full, &QPushButton::clicked, Window, &QWidget::showFullScreen);
    connect(pushButton_max, &QPushButton::clicked, Window, &QWidget::showMaximized);
重截QWidget::mousePressEvent 方法
#include "Windows.h"
void MyTitleBar::mousePressEvent(QMouseEvent* ev)
{
    QWidget::mousePressEvent(ev);

    if (Window == nullptr) return;
    if (!ev->isAccepted()) {
        if (ev->type() == QEvent::MouseButtonDblClick) {
            // Toggle maximize/restore on double-click
            if (Window->isMaximized()) {
                Window->showNormal(); // Restore
            }
            else {
                Window->showMaximized(); // Maximize
            }
            return; // Prevent further processing
        }
#ifdef Q_OS_WIN
        ReleaseCapture();
        SendMessage(reinterpret_cast<HWND>(Window->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
#endif
    }
}
新增
    // 標題拖動、雙擊事件
    MyTitleBar *title = new MyTitleBar(this);
    qobject_cast<QBoxLayout *>(ui->centralwidget->layout())->insertWidget(0, title);

3、最終實現效果


參考連結:https://github.com/deimos1877/BorderlessWindow

相關文章