QT Windows完美无边框解决方案

本文介绍了如何使用C++在Windows平台上创建一个无边框窗口,并自定义标题栏功能,包括最大化、最小化、关闭按钮以及响应鼠标事件,如最大化按钮触发SnapLayout布局。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

                                (有python C++ 源码 可直接运行)

Windows 平台的系统特性如放大缩小动画、贴靠动画等都保留了下来,无边框去掉标题栏,所以需要自己实现最小化、最大化、关闭 三个按钮。win11上鼠标进入最大化按钮时触发snap layout布局。

C++

        创建或继承 BorderWindow 类。 顺便学习Q_D Q_Q d指针 q指针 

#ifndef SOFTPETAL_WINDOWS_H
#define SOFTPETAL_WINDOWS_H

#define S_D(Class) \
        private:  \
            Class(const Class &) = delete;\
            Class &operator=(const Class &) = delete;\
        friend class Class##Private ; \
        Class##Private * const d;

#define S_Q(Class) \
        private:     \
            Class##Private(const Class##Private &) = delete; \
            Class##Private(Class##Private &&) = delete;\
            Class##Private &operator=(const Class##Private &) = delete; \
            Class##Private& operator=(Class##Private&&)= delete;\
        friend Class;\
        Class * const q;

#include <QWidget>


class QAbstractButton;

class AbstractTitleBar : public QWidget {
Q_OBJECT

public:
    explicit AbstractTitleBar(QWidget *parent = nullptr);

    // 标题栏最大化按钮
    [[nodiscard]] virtual QAbstractButton *maximizeButton() const = 0;

    // 双击标题栏 最大化按钮点击时调用
    inline void mouseDoubleClick() { mouseDoubleClickEvent(nullptr); };
protected:
    void mousePressEvent(QMouseEvent *event) override;

    void mouseDoubleClickEvent(QMouseEvent *event) override;

    void paintEvent(QPaintEvent *event) override;

    // BorderWindow 中setWindowTitle 时调用此函数,更改自定义窗口标题
    virtual void windowTitleEvent(const QString &title);

    // BorderWindow 中setWindowIcon 时调用此函数,更改自定义窗口图标
    virtual void windowIconEvent(const QIcon &icon);

    // BorderWindow 窗口最大化或者恢复时调用此函数,可以更改最大化按钮图标
    virtual void windowStateEvent(bool state);

    // BorderWindow 中调用insertWidget会触发此函数,可用在自定义标题栏添加控件
    virtual void insertWidgetEvent(int index, QWidget *widget, int stretch, Qt::Alignment alignment);

    // BorderWindow 中调用removeWidget会触发此函数,可用移除标题栏某个控件
    virtual void removeWidgetEvent(QWidget *widget);

    // 一些自定义事件
    virtual void flagsEvent(unsigned int flags);

    // 用于控制标题栏布局边距
    virtual void marginsEvent(int left, int top, int right, int bottom);

    // 用于控制标题栏高度
    virtual void heightEvent(int h);

private:
    friend class BorderWindow;
};

class QPushButton;

class QLabel;

class TitleBar : public AbstractTitleBar {
Q_OBJECT

S_D(TitleBar)
//S_D(TitleBar) 相当于:删除赋值函数,声明TitleBarPrivate为友元类

//private:
//    TitleBar(const TitleBar &) = delete;
//    TitleBar &operator=(const TitleBar &) = delete;
//    friend class TitleBarPrivate;
//    TitleBarPrivate *const d;

public:
    enum TitleFlag {
        IconHint = 0x1,// 窗口图标是否显示
        TitleHint = 0x2,// 窗口标题
        MinimizeHint = 0x4,// 最小化按钮
        MaximizeHint = 0x8,// 最大化按钮
        CloseHint = 0x10// 关闭按钮
    };

    friend inline TitleFlag operator|(TitleFlag lhs, TitleFlag rhs) {
        return static_cast<TitleBar::TitleFlag>(static_cast<int>(lhs) | static_cast<int>(rhs));
    }

    explicit TitleBar(QWidget *parent = nullptr);

    ~TitleBar() override;

    // 返回自带的标题栏控件,用于设置属性
    [[nodiscard]] QAbstractButton *maximizeButton() const override;

    [[nodiscard]] QPushButton *titleBarIcon() const;

    [[nodiscard]] QLabel *titleBarTitle() const;

    [[nodiscard]] QPushButton *minimizeButton() const;

    [[nodiscard]] QPushButton *closeButton() const;

protected:
    void windowTitleEvent(const QString &title) override;

    void windowIconEvent(const QIcon &icon) override;

    void windowStateEvent(bool state) override;

    void insertWidgetEvent(int index, QWidget *widget, int stretch, Qt::Alignment alignment) override;

    void removeWidgetEvent(QWidget *widget) override;

    void flagsEvent(unsigned int flags) override;

    void marginsEvent(int left, int top, int right, int bottom) override;

    void heightEvent(int h) override;

};

class BorderWindow : public QWidget {
Q_OBJECT

S_D(BorderWindow)
public:
    explicit BorderWindow(QWidget *parent = nullptr);

    ~BorderWindow() override;

    // 设置自定义标题栏 需要继承AbstractTitleBar
    void setTitleBarWidget(AbstractTitleBar *title);

    // 设置窗口中心小部件 默认是空的QWidget
    void setCentralWidget(QWidget *central);

    void setWindowIcon(const QIcon &icon);

    void setWindowTitle(const QString &title);

    // 设置标题栏自定义事件
    void setTitleBarFlags(unsigned int flags);

    // 设置标题栏单个事件
    void setTitleBarFlag(unsigned int flag, bool on = true);

    // 设置标题栏边距
    void setTitleBarMargins(int left, int top, int right, int bottom);

    // 设置标题栏高度
    void setTitleBarHeight(int h);

    // 向标题栏插入控件
    void insertWidget(int index, QWidget *widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment());

    // 移除标题栏某个控件
    void removeWidget(QWidget *widget);

    // 返回标题栏
    [[nodiscard]] AbstractTitleBar *titleBarWidget() const;

    // 返回中心部件
    [[nodiscard]] QWidget *centralWidget() const;

    // 返回标题栏自定义事件
    [[nodiscard]] unsigned int titleBarFlag() const;

protected:
    void changeEvent(QEvent *event) override;

    bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override;

};


#endif //SOFTPETAL_WINDOWS_H

//#include "Sources/rcc.h"

#include "Windows.h"
#include <QMouseEvent>
#include <QApplication>
#include <QStyleOption>
#include <QStyle>
#include <QPainter>
#include <QWindow>
#include <QVBoxLayout>

#ifdef Q_OS_WIN

#pragma comment(lib, "dwmapi")
#pragma comment(lib, "user32.lib")

#include <windows.h>
#include <windowsx.h>
#include <dwmapi.h>
#include <QPushButton>
#include <QLabel>

#endif


AbstractTitleBar::AbstractTitleBar(QWidget *parent) : QWidget(parent) {}

void AbstractTitleBar::mousePressEvent(QMouseEvent *event) {
    QWidget::mousePressEvent(event);
    if (event->button() == Qt::LeftButton) window()->windowHandle()->startSystemMove();
}

void AbstractTitleBar::mouseDoubleClickEvent(QMouseEvent *event) {
    window()->isMaximized() ? window()->showNormal() : window()->showMaximized();
}

//继承QWidget需要重写paintEvent 否则样式QSS 无效
void AbstractTitleBar::paintEvent(QPaintEvent *event) {
    QStyleOption opt;
    opt.initFrom(this);
    QPainter p(this);
    style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}

// 抽象空函数,派生类自己实现
void AbstractTitleBar::windowTitleEvent(const QString &title) {}

void AbstractTitleBar::windowIconEvent(const QIcon &icon) {}

void AbstractTitleBar::windowStateEvent(bool state) {}

void AbstractTitleBar::insertWidgetEvent(int index, QWidget *widget, int stretch, Qt::Alignment alignment) {}

void AbstractTitleBar::removeWidgetEvent(QWidget *widget) {}

void AbstractTitleBar::flagsEvent(unsigned int flags) {}

void AbstractTitleBar::marginsEvent(int left, int top, int right, int bottom) {}

void AbstractTitleBar::heightEvent(int h) {}

class TitleBarPrivate {
S_Q(TitleBar)

    QHBoxLayout *m_TitleLayout; //标题栏水平布局
    QPushButton *m_IconLabel; // 标题栏图标
    QLabel *m_TitleLabel; // 窗口标题

    QPushButton *m_MinimizeButton;//最小化按钮
    QPushButton *m_MaximizeButton;// 最大化按钮
    QPushButton *m_CloseButton; //关闭按钮

    explicit TitleBarPrivate(TitleBar *ptr) : q(ptr) {
        m_TitleLayout = new QHBoxLayout(q);
        m_TitleLayout->setContentsMargins(5, 5, 10, 2);

        m_IconLabel = new QPushButton(q);
        m_IconLabel->setObjectName("TitleBarIcon");
        m_IconLabel->setFixedWidth(25);
        m_IconLabel->setIcon(QApplication::style()->standardIcon(QStyle::SP_ComputerIcon));
        m_IconLabel->setIconSize(QSize(20, 20));
        m_IconLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

        m_TitleLabel = new QLabel(QApplication::applicationName(), q);
        m_TitleLabel->setObjectName("TitleBarTitle");

        m_MinimizeButton = new QPushButton(q);
        m_MinimizeButton->setObjectName("TitleBarMinimize");
        m_MinimizeButton->setFixedWidth(25);
        m_MinimizeButton->setIcon(QIcon(":/Images/minimize.png"));
        m_MinimizeButton->setIconSize(QSize(20, 20));
        m_MinimizeButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

        m_MaximizeButton = new QPushButton(q);
        m_MaximizeButton->setObjectName("TitleBarMaximize");
        m_MaximizeButton->setFixedWidth(25);
        m_MaximizeButton->setIcon(QIcon(":/Images/maximize.png"));
        m_MaximizeButton->setIconSize(QSize(15, 15));
        m_MaximizeButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

        m_CloseButton = new QPushButton(q);
        m_CloseButton->setObjectName("TitleBarClose");
        m_CloseButton->setFixedWidth(25);
        m_CloseButton->setIcon(QIcon(":/Images/close.png"));
        m_CloseButton->setIconSize(QSize(15, 15));
        m_CloseButton->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);


        m_TitleLayout->addWidget(m_IconLabel);
        m_TitleLayout->addWidget(m_TitleLabel);
        m_TitleLayout->addWidget(m_MinimizeButton);
        m_TitleLayout->addWidget(m_MaximizeButton);
        m_TitleLayout->addWidget(m_CloseButton);

        QObject::connect(m_MinimizeButton, &QPushButton::clicked, q->window(), &QWidget::showMinimized);
        QObject::connect(m_CloseButton, &QPushButton::clicked, q->window(), &QWidget::close);
        QObject::connect(m_MaximizeButton, &QPushButton::clicked, q, &TitleBar::mouseDoubleClick);

    }
};

TitleBar::TitleBar(QWidget *parent) : AbstractTitleBar(parent), d(new TitleBarPrivate(this)) {}

TitleBar::~TitleBar() { delete d; }

QAbstractButton *TitleBar::maximizeButton() const { return d->m_MaximizeButton; }

QPushButton *TitleBar::titleBarIcon() const { return d->m_IconLabel; }

QLabel *TitleBar::titleBarTitle() const { return d->m_TitleLabel; }

QPushButton *TitleBar::minimizeButton() const { return d->m_MinimizeButton; }

QPushButton *TitleBar::closeButton() const { return d->m_CloseButton; }

void TitleBar::windowTitleEvent(const QString &title) { d->m_TitleLabel->setText(title); }

void TitleBar::windowIconEvent(const QIcon &icon) { d->m_IconLabel->setIcon(icon); }

// 最大化 时更好最大化按钮图标
void TitleBar::windowStateEvent(bool state) {
    d->m_MaximizeButton->setIcon(QIcon(state ? ":/Images/normal.png" : ":/Images/maximize.png"));
}

// 插入小部件
void TitleBar::insertWidgetEvent(int index, QWidget *widget, int stretch, Qt::Alignment alignment) {
    d->m_TitleLayout->insertWidget(index, widget, stretch, alignment);
}

// 移除小部件
void TitleBar::removeWidgetEvent(QWidget *widget) { d->m_TitleLayout->removeWidget(widget); }

// 自定义事件 控制默认标题栏某些控件是否显示
void TitleBar::flagsEvent(unsigned int flags) {
    d->m_IconLabel->setVisible(flags & IconHint);
    d->m_TitleLabel->setVisible(flags & TitleHint);
    d->m_MinimizeButton->setVisible(flags & MinimizeHint);
    d->m_MaximizeButton->setVisible(flags & MaximizeHint);
    d->m_CloseButton->setVisible(flags & CloseHint);
}

// 设置标题栏边距
void TitleBar::marginsEvent(int left, int top, int right, int bottom) {
    d->m_TitleLayout->setContentsMargins(left, top, right, bottom);
}

// 设置标题栏高度
void TitleBar::heightEvent(int h) { setFixedHeight(h); }


class BorderWindowPrivate {
S_Q(BorderWindow)
    unsigned int m_Flags{0x00};

    QVBoxLayout *m_CentralLayout;

    AbstractTitleBar *m_TitleBar;
    QWidget *m_CentralWidget;

    explicit BorderWindowPrivate(BorderWindow *ptr) : q(ptr) {
        m_CentralLayout = new QVBoxLayout(q);
        m_CentralLayout->setContentsMargins(0, 0, 0, 0);
        m_CentralLayout->setSpacing(0);

        m_TitleBar = new TitleBar(q);
        m_TitleBar->setFixedHeight(30);

        m_CentralWidget = new QWidget(q);

        m_CentralLayout->addWidget(m_TitleBar);
        m_CentralLayout->addWidget(m_CentralWidget);
    }
};


BorderWindow::BorderWindow(QWidget *parent) : QWidget(parent),
                                              d(new BorderWindowPrivate(this)) {
    setWindowFlags(Qt::Window
                   | Qt::WindowMaximizeButtonHint
                   | Qt::WindowMinimizeButtonHint
                   | Qt::FramelessWindowHint);
    // 不然在最小化 关闭 时没有特性
    HWND hWnd = (HWND) winId();
    LONG style = GetWindowLongW(hWnd, GWL_STYLE);
    SetWindowLong(
            hWnd,
            GWL_STYLE,
            style
            | WS_MINIMIZEBOX
            | WS_MAXIMIZEBOX
            | WS_CAPTION
            | CS_DBLCLKS
            | WS_THICKFRAME
    );
    // 设置标题栏 按钮全部显示 重写了|运算
    setTitleBarFlags(TitleBar::IconHint |
                     TitleBar::TitleHint |
                     TitleBar::MinimizeHint |
                     TitleBar::MaximizeHint |
                     TitleBar::CloseHint);
    setStyleSheet(R"(
        TitleBar{border-bottom:1px solid #CFCFCF;}
        #TitleBarMinimize,#TitleBarMaximize,#TitleBarClose{border-radius:3px;}
        #TitleBarMinimize:hover,#TitleBarMaximize:hover{background:#c0c4cd;}
        #TitleBarClose:hover{background-color: rgba(255, 0, 0,200);}
        #TitleBarTitle{color:#FFB90F;font-size: 14px;font-family: Microsoft YaHei;}
        #TitleBarIcon{ border:none;padding: 0px;}
    )");

}

BorderWindow::~BorderWindow() { delete d; }

// 设置自定义标题栏
void BorderWindow::setTitleBarWidget(AbstractTitleBar *title) {
    d->m_CentralLayout->replaceWidget(d->m_TitleBar, title);
    d->m_TitleBar->deleteLater();
    d->m_TitleBar = title;
}

void BorderWindow::setCentralWidget(QWidget *central) {
    d->m_CentralLayout->replaceWidget(d->m_CentralWidget, central);
    d->m_CentralWidget->deleteLater();
    d->m_CentralWidget = central;
}

void BorderWindow::setWindowIcon(const QIcon &icon) {
    QWidget::setWindowIcon(icon);
    d->m_TitleBar->windowIconEvent(icon);

}

void BorderWindow::setWindowTitle(const QString &title) {
    QWidget::setWindowTitle(title);
    d->m_TitleBar->windowTitleEvent(title);
}

void BorderWindow::setTitleBarFlags(unsigned int flags) {
    d->m_Flags = flags;
    d->m_TitleBar->flagsEvent(flags);
}

void BorderWindow::setTitleBarFlag(unsigned int flag, bool on) {
    setTitleBarFlags(on ? (d->m_Flags | flag) : (d->m_Flags & ~flag));
}

void BorderWindow::setTitleBarMargins(int left, int top, int right, int bottom) {
    d->m_TitleBar->marginsEvent(left, top, right, bottom);
}

void BorderWindow::setTitleBarHeight(int h) {
    d->m_TitleBar->heightEvent(h);
}

void BorderWindow::insertWidget(int index, QWidget *widget, int stretch, Qt::Alignment alignment) {
    d->m_TitleBar->insertWidgetEvent(index, widget, stretch, alignment);
}

void BorderWindow::removeWidget(QWidget *widget) {
    d->m_TitleBar->removeWidgetEvent(widget);
}

//窗口状态发生变化时触发
void BorderWindow::changeEvent(QEvent *event) {
    if (event->type() == QEvent::WindowStateChange) {
        switch (windowState()) {
            case Qt::WindowMaximized: {
                // 如果当前窗口是最大化,告诉一下标题栏 更换最大化按钮图标
                d->m_TitleBar->windowStateEvent(true);

                // 最大化后增加一些边距
                int border = GetSystemMetrics(SM_CXSIZEFRAME);
                setContentsMargins(border, border, border, border);
                break;
            }
            case Qt::WindowNoState:
                // 恢复时 消除边距
                d->m_TitleBar->windowStateEvent(false);
                setContentsMargins(0, 0, 0, 0);
                break;
            default:
                break;
        }
    }
    QWidget::changeEvent(event);
}

bool BorderWindow::nativeEvent(const QByteArray &eventType, void *message, qintptr *result) {
    // 鼠标靠近窗口边缘5个像素触发可拖拽窗口
    const static int boundary = 5;
    MSG *msg = static_cast<MSG *>(message);
    switch (msg->message) {
        // 无边框
        case WM_NCCALCSIZE: {
            *result = HTNOWHERE;
            return true;
        }
            // win11 圆角
        case WM_ACTIVATE: {
            MARGINS margins = {1, 1, 0, 1};
            HRESULT hr = DwmExtendFrameIntoClientArea(msg->hwnd, &margins);
            *result = hr;
            return true;
        }
        case WM_NCHITTEST: {
            POINT mouse = {GET_X_LPARAM(msg->lParam), GET_Y_LPARAM(msg->lParam)};
            RECT rc;
            GetWindowRect(msg->hwnd, &rc);
            RECT rcFrame = {0};
            AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);

            bool left = (mouse.x >= rc.left && mouse.x < rc.left + boundary);
            bool right = (mouse.x < rc.right && mouse.x >= rc.right - boundary);
            bool top = (mouse.y >= rc.top && mouse.y < rc.top + boundary);
            bool bottom = (mouse.y < rc.bottom && mouse.y >= rc.bottom - boundary);

            if (top && left) {
                *result = HTTOPLEFT;
                return true;
            } else if (top && right) {
                *result = HTTOPRIGHT;
                return true;
            } else if (bottom && left) {
                *result = HTBOTTOMLEFT;
                return true;
            } else if (bottom && right) {
                *result = HTBOTTOMRIGHT;
                return true;
            } else if (top) {
                *result = HTTOP;
                return true;
            } else if (left) {
                *result = HTLEFT;
                return true;
            } else if (right) {
                *result = HTRIGHT;
                return true;
            } else if (bottom) {
                *result = HTBOTTOM;
                return true;
            }
            // 判断鼠标是否悬停在最大化按钮上, win11 触发snap layout
            // 以下代码确保不同分辨率下正确触发
            QAbstractButton *maximize = d->m_TitleBar->maximizeButton();
            assert(d->m_TitleBar != nullptr && maximize != nullptr &&
                   "titleBar or maximizeButton() returned a null pointer!");
            QPoint globalPos(mouse.x, mouse.y);
            QWindow *handle = window()->windowHandle();
            if (handle && handle->screen()) {
                QScreen *screen = handle->screen();
                QPoint offset = screen->geometry().topLeft();
                globalPos = (globalPos - offset) / screen->devicePixelRatio() + offset;
            }
            const QPoint &localPos = maximize->mapFromGlobal(globalPos);
            // 判断鼠标是否在最大化按钮上,如果在创建一个鼠标进入按钮的事件,发送给QT事件,否则QT无法收到这个事件 QSS失效
            if (maximize->rect().contains(localPos)) {
                // 鼠标进入按钮事件和在按钮上移动事件
                QMouseEvent mouseEvent(maximize->underMouse() ? QEvent::MouseMove : QEvent::Enter,
                                       localPos, globalPos,
                                       Qt::NoButton, Qt::NoButton, Qt::NoModifier);
                QCoreApplication::sendEvent(maximize, &mouseEvent);
                maximize->update();
                *result = HTMAXBUTTON;
                return true;
            } else {
                // 如果没有在最大化按钮上面查看鼠标是否移出了按钮,发送鼠标移出按钮事件
                if (maximize->underMouse()) {
                    QMouseEvent mouseEvent(QEvent::Leave, localPos, globalPos,
                                           Qt::NoButton, Qt::NoButton, Qt::NoModifier);
                    QCoreApplication::sendEvent(maximize, &mouseEvent);
                    maximize->update();
                    return true;
                }
            }

            *result = HTCLIENT;
            return false;
        }
            // 按下最大化按钮事件,发送一个鼠标按下事件
        case WM_NCLBUTTONDOWN: {
            if (msg->wParam == HTMAXBUTTON) {
                QAbstractButton *maximize = d->m_TitleBar->maximizeButton();
                QMouseEvent mouseEvent(QEvent::MouseButtonPress,
                                       QPoint(), QPoint(),
                                       Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
                QCoreApplication::sendEvent(maximize, &mouseEvent);
                *result = HTNOWHERE;
                return true;
            }
        }
            // 鼠标释放事件,确保是一次完整的点击事件
        case WM_NCLBUTTONUP: {
            if (msg->wParam == HTMAXBUTTON) {
                QAbstractButton *maximize = d->m_TitleBar->maximizeButton();
                QMouseEvent mouseEvent(QEvent::MouseButtonRelease,
                                       QPoint(), QPoint(),
                                       Qt::LeftButton, Qt::LeftButton, Qt::NoModifier);
                QCoreApplication::sendEvent(maximize, &mouseEvent);
                *result = HTNOWHERE;
                return true;
            }
        }
            // 这里是鼠标移入移出移动事件,上面已经处理过所以直接返回true 这个事件已经处理过 QT不必在处理
        case WM_NCMOUSEHOVER:
        case WM_NCMOUSELEAVE:
        case WM_NCMOUSEMOVE:
            if (msg->wParam == HTMAXBUTTON) {
                *result = HTNOWHERE;
                return true;
            }
    }

    return false;
}

AbstractTitleBar *BorderWindow::titleBarWidget() const {
    return d->m_TitleBar;
}

QWidget *BorderWindow::centralWidget() const {
    return d->m_CentralWidget;
}

unsigned int BorderWindow::titleBarFlag() const {
    return d->m_Flags;
}

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值