(有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;
}