Qt开发 | 无边框窗口 | 自定义标题栏 | 拖拽拉伸 | 窗口阴影 | 圆角窗口

发布于:2024-07-01 ⋅ 阅读:(13) ⋅ 点赞:(0)

一、QWidget类介绍

  QWidget 是 Qt 框架中的一个核心类,它是所有控件和窗口的基类。QWidget 提供了创建和管理 GUI 组件的基础功能。Qt所有的窗口、控件以及布局都是派生于QWidget。

关键特性与功能

  • 窗口和控件的基类:几乎所有的 GUI 组件都是 QWidget 的子类,包括按钮、文本框、标签、窗口等。
  • 布局管理QWidget 支持多种布局管理方式,如水平布局、垂直布局和网格布局,这些布局可以帮助开发者轻松地组织控件的位置和大小。
  • 事件处理QWidget 可以接收和处理各种事件,如鼠标点击、键盘输入、窗口大小变化等。
  • 绘图功能QWidget 提供了基本的绘图功能,允许开发者在控件上绘制图形和文本。
  • 控件属性QWidget 允许设置控件的各种属性,如背景颜色、边框样式、是否可编辑等。
  • 焦点管理QWidget 可以控制控件的焦点,决定哪些控件可以接收键盘输入。
  • 尺寸和位置QWidget 可以设置控件的大小、位置以及最小和最大尺寸。
  • 子控件管理QWidget 可以添加、移除和查询子控件。
  • 窗口修饰:对于窗口类型的 QWidget,可以设置窗口的标题、图标、状态(如是否可最小化、最大化)等。
  • 资源管理QWidget 管理着与之关联的资源,如字体、颜色、画笔等。
  • 信号和槽QWidget 支持 Qt 的信号和槽机制,允许控件之间的事件通信。
  • 国际化QWidget 支持国际化,可以根据不同地区的设置显示不同的文本。

详细内容可参考https://doc.qt.io/qt-5/qwidget.html

Qt很多问题都可以从QWidget类找到答案,学习Qt大部分就是在与QWidget打交道。

二、无边框窗口的基本实现

  无边框窗口就是在QWidget的基础上去掉窗口边框实现窗口移动(窗口随鼠标做矢量的平移)。

  • 如何去掉窗口边框

    this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
    
  • 如何实现窗口移动

    去掉窗口边框后,窗口无法移动。需要重写鼠标按下、移动事件函数。如果你在子类中重写了 mousePressEvent 函数,就可以自定义控件对鼠标按下事件的响应。在按下鼠标时,让窗口移动。

    事件处理:鼠标按下事件函数用于接收鼠标按下事件。当用户在控件上按下鼠标按钮时,Qt 框架会调用这个函数。

    //鼠标按下事件函数
    void QWidget::mousePressEvent(QMouseEvent *event) override;
    //鼠标移动事件函数
    void QWidget::mouseMoveEvent(QMouseEvent *event) override;
    
    • QMouseEvent

      该对象包含鼠标事件的详细信息,如鼠标的坐标位置、按下的是哪个鼠标按钮、鼠标事件的类型等。

    移动位置函数:用来移动窗口、控件

    void move(const QPoint&);
    

    要实现窗口随鼠标做矢量平移的效果,需要利用Qt中的一些位置

    • 窗口位置:窗口左上角相对于桌面左上角的位置

      this->pos();
      
    • 鼠标位置

      event->pos();		//鼠标相对于应用程序的位置
      event->globalPos();	//鼠标相对于桌面左上角的位置,绝对位置
      
    • 利用鼠标与窗口左上角的相对位置,实现窗口随坐标的矢量平移

      void MainWidget::mousePressEvent(QMouseEvent* event)
      {
          mouse_pos = event->globalPos();
          window_pos = this->pos();
          diff_pos = mouse_pos - window_pos; //鼠标和窗口的相对位移
      }
      
      void MainWidget::mouseMoveEvent(QMouseEvent* event)
      {
          QPoint pos = event->globalPos();
          //this->move(pos);              //这样移动后窗口的左上角瞬间移动到鼠标处
          this->move(pos - diff_pos);     //这样移动窗口随坐标矢量平移
      }
      

示例:

MainWidget.h

#pragma once

#include <QtWidgets/QWidget>

class MainWidget : public QWidget
{
    Q_OBJECT

public:
    MainWidget(QWidget *parent = Q_NULLPTR);

private:
    void mousePressEvent(QMouseEvent* event) override;  //重写鼠标按下事件处理函数
    void mouseMoveEvent(QMouseEvent* event) override;   //重写鼠标移动事件处理函数

private:
    QPoint diff_pos;    //鼠标和窗口的相对位移
    QPoint window_pos;  //窗口的位置
    QPoint mouse_pos;   //鼠标的位置
};

MainWidget.cpp

#include "MainWidget.h"
#include <QMouseEvent>

MainWidget::MainWidget(QWidget *parent)
    : QWidget(parent)
{
    //去掉窗口边框
    this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
}

void MainWidget::mousePressEvent(QMouseEvent* event)
{
    mouse_pos = event->globalPos();
    window_pos = this->pos();
    diff_pos = mouse_pos - window_pos; //鼠标和窗口的相对位移
}

void MainWidget::mouseMoveEvent(QMouseEvent* event)
{
    QPoint pos = event->globalPos();
    //this->move(pos);              //这样移动后窗口的左上角瞬间移动到鼠标处
    this->move(pos - diff_pos);     //这样移动窗口随坐标矢量平移
}

main.cpp

#include "MainWidget.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWidget w;
    w.show();
    return a.exec();
}

三、自定义标题栏并实现拖拽拉伸

  在去掉窗口边框与实现窗口移动后,没有标题栏类,需要自定义标题栏类。创建标题栏界面类,添加到界面上,并添加最小化、最大化,关闭按钮。

  • 无边框窗口拖拽移动更好的解决办法

    #include <qt_windows.h>
    #pragma comment(lib, "user32.lib")
    
    void CTitleBar::mousePressEvent(QMouseEvent* event)	
    {
    	if (ReleaseCapture())
    	{
    		QWidget* pWindow = this->window();
    		if (pWindow->isTopLevel())
    		{
    			SendMessage(HWND(pWindow->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
    		}
    	}
    	event->ignore();
    }
    
  • 无边窗口的拉伸、改变大小

    #include <qt_windows.h>
    #pragma comment(lib, "user32.lib")
    
    
    this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);    //无边框窗口
    this->setAttribute(Qt::WA_Hover);	//启用控件悬停效果,配合判断鼠标状态
    
    //通过判断鼠标的状态进行拉伸
    bool MainWidget::nativeEvent(const QByteArray& eventType, void* message, long* result)
    {
    	MSG* param = static_cast<MSG*>(message);
    
    	switch (param->message) //根据 param->message 的值来处理不同类型的消息
    	{
    	case WM_NCHITTEST:	//WM_NCHITTEST 是一个 Windows 消息,用于确定鼠标点击发生在窗口的哪个部分
    	{	
    		//将鼠标的屏幕坐标转换为相对于控件左上角的坐标
    		int nX = GET_X_LPARAM(param->lParam) - this->geometry().x();
    		int nY = GET_Y_LPARAM(param->lParam) - this->geometry().y();
    
    		/*if (childAt(nX, nY) != nullptr)
    			return QWidget::nativeEvent(eventType, message, result);*/
    
    		//检查鼠标坐标是否在控件的内部边界内,排除边框
    		if (nX > m_nBorderWidth && nX < this->width() - m_nBorderWidth &&
    			nY > m_nBorderWidth && nY < this->height() - m_nBorderWidth)
    		{
    			//如果鼠标位置下存在子控件,则调用基类的 nativeEvent 处理事件;后续会调用该控件的鼠标点击函数用于拖拽移动
    			if (childAt(nX, nY) != nullptr)
    				return QWidget::nativeEvent(eventType, message, result);
    		}
    
    		//判断鼠标点击是否在控件的边框上,并设置 *result 为相应的值。例如 HTLEFT、HTRIGHT、HTTOP 等,这些值定义了鼠标点击的边框部分。
    		if ((nX > 0) && (nX < m_nBorderWidth))
    			*result = HTLEFT;
    
    		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width()))
    			*result = HTRIGHT;
    
    		if ((nY > 0) && (nY < m_nBorderWidth))
    			*result = HTTOP;
    
    		if ((nY > this->height() - m_nBorderWidth) && (nY < this->height()))
    			*result = HTBOTTOM;
    
    		if ((nX > 0) && (nX < m_nBorderWidth) && (nY > 0)
    			&& (nY < m_nBorderWidth))
    			*result = HTTOPLEFT;
    
    		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
    			&& (nY > 0) && (nY < m_nBorderWidth))
    			*result = HTTOPRIGHT;
    
    		if ((nX > 0) && (nX < m_nBorderWidth)
    			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
    			*result = HTBOTTOMLEFT;
    
    		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
    			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
    			*result = HTBOTTOMRIGHT;
    		//如果处理了 WM_NCHITTEST 消息,函数返回 true,表示事件已被处理。
    		return true;
    	}
    	}
    	return false; //如果没有处理任何消息,函数返回 false,表示事件将被传递给 Qt 进行默认处理。
    }
    

示例:

CTitleBar.h

#pragma once
#include <QtWidgets/QWidget>
#include <QLabel>
#include <QPushButton>

/**
	自定义标题栏界面类,添加设置、最小化、最大化,关闭按钮
*/
class CTitleBar : public QWidget
{
public:
	CTitleBar(QWidget* parent = nullptr);
	~CTitleBar();

private:
	void initUI();	//初始化UI
	void mousePressEvent(QMouseEvent* event)override;	//无边框窗口拖拽移动

private:
	QLabel* m_pLogo;
	QLabel* m_pTitleTextLabel;

	QPushButton* m_pSetBtn;
	QPushButton* m_pMinBtn;
	QPushButton* m_pMaxBtn;
	QPushButton* m_pCloseBtn;
};

CTitleBar.cpp

#pragma comment(lib, "user32.lib")
#include "CTitleBar.h"
#include <qt_windows.h>
#include <QHBoxLayout>
#include <QMouseEvent>w

CTitleBar::CTitleBar(QWidget* parent)
{
	initUI();
}

CTitleBar::~CTitleBar()
{
}

void CTitleBar::initUI()
{
	this->setAttribute(Qt::WA_StyledBackground);    //禁止父窗口影响子窗口样式
	this->setFixedHeight(32 + 10);
	this->setStyleSheet("background-color:rgb(54, 54, 54)");

	m_pLogo = new QLabel(this);
	m_pLogo->setFixedWidth(32);

	m_pTitleTextLabel = new QLabel(this);
	m_pTitleTextLabel->setText(u8"我是标题");
	m_pTitleTextLabel->setFixedWidth(120);

	m_pSetBtn = new QPushButton(this);
	m_pSetBtn->setFixedSize(QSize(32, 32));

	m_pMinBtn = new QPushButton(this);
	m_pMinBtn->setFixedSize(QSize(32, 32));

	m_pMaxBtn = new QPushButton(this);
	m_pMaxBtn->setFixedSize(QSize(32, 32));

	m_pCloseBtn = new QPushButton(this);
	m_pCloseBtn->setFixedSize(QSize(32, 32));

	//布局
	QHBoxLayout* m_pHLay = new QHBoxLayout(this);
	m_pHLay->addWidget(m_pLogo);
	m_pHLay->addWidget(m_pTitleTextLabel);
	m_pHLay->addStretch();	//弹簧
	m_pHLay->addWidget(m_pSetBtn);
	m_pHLay->addWidget(m_pMinBtn);
	m_pHLay->addWidget(m_pMaxBtn);
	m_pHLay->addWidget(m_pCloseBtn);

	//边界间隔
	m_pHLay->setContentsMargins(5, 5, 5, 5);
}

//处理鼠标按下事件--无边框窗口拖拽移动的解决办法
void CTitleBar::mousePressEvent(QMouseEvent* event)	
{
	//释放当前捕获的鼠标,若成功释放返回true
	if (ReleaseCapture())
	{	
		QWidget* pWindow = this->window();	//获取窗口指针
		if (pWindow->isTopLevel())	//检查获取到的窗口是否是一个顶级窗口(即不是其他窗口的子控件)
		{
			//如果窗口是顶级窗口,使用 SendMessage 函数向父窗口句柄发送一个 WM_SYSCOMMAND 消息,该消息指示窗口开始移动操作。
			SendMessage(HWND(pWindow->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
		}
	}
	event->ignore();
}

MainWdiget.h

#pragma once

#include <QtWidgets/QWidget>
#include "CTitleBar.h"

class MainWidget : public QWidget
{
    Q_OBJECT

public:
    MainWidget(QWidget *parent = nullptr);
    ~MainWidget();

private:
    void initUI();  //初始化UI
    bool nativeEvent(const QByteArray& eventType, void* message, long* result)override;


private:
    CTitleBar* m_pTitleBar = nullptr;
    int m_nBorderWidth = 5;
};

MainWdiget.cpp

#include "MainWidget.h"
#include <QVBoxLayout>
#include <qt_windows.h>
#include <windows.h>
#include <windowsx.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib,"dwmapi.lib")

MainWidget::MainWidget(QWidget *parent)
    : QWidget(parent)
{
    //this->resize()
    this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);    //无边框窗口
    this->setAttribute(Qt::WA_Hover);	//启用控件悬停效果,配合判断鼠标状态

    initUI();
}

MainWidget::~MainWidget()
{}

void MainWidget::initUI()
{
    m_pTitleBar = new CTitleBar(this);  //标题栏
    QWidget* w = new QWidget(this);
    w->setMinimumSize(QSize(600, 400));

    QVBoxLayout* pVlay = new QVBoxLayout(this);
    pVlay->addWidget(m_pTitleBar);
    pVlay->addWidget(w);

    pVlay->setContentsMargins(0, 0, 0, 0);  //边界间隙

}

//通过判断鼠标的状态进行拉伸
bool MainWidget::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
	MSG* param = static_cast<MSG*>(message);

	switch (param->message) //根据 param->message 的值来处理不同类型的消息
	{
	case WM_NCHITTEST:	//WM_NCHITTEST 是一个 Windows 消息,用于确定鼠标点击发生在窗口的哪个部分
	{	
		//将鼠标的屏幕坐标转换为相对于控件左上角的坐标
		int nX = GET_X_LPARAM(param->lParam) - this->geometry().x();
		int nY = GET_Y_LPARAM(param->lParam) - this->geometry().y();

		/*if (childAt(nX, nY) != nullptr)
			return QWidget::nativeEvent(eventType, message, result);*/

		//检查鼠标坐标是否在控件的内部边界内,排除边框
		if (nX > m_nBorderWidth && nX < this->width() - m_nBorderWidth &&
			nY > m_nBorderWidth && nY < this->height() - m_nBorderWidth)
		{
			//如果鼠标位置下存在子控件,则调用基类的 nativeEvent 处理事件;后续会调用该控件的鼠标点击函数用于拖拽移动
			if (childAt(nX, nY) != nullptr)
				return QWidget::nativeEvent(eventType, message, result);
		}

		//判断鼠标点击是否在控件的边框上,并设置 *result 为相应的值。例如 HTLEFT、HTRIGHT、HTTOP 等,这些值定义了鼠标点击的边框部分。
		if ((nX > 0) && (nX < m_nBorderWidth))
			*result = HTLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width()))
			*result = HTRIGHT;

		if ((nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOP;

		if ((nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOM;

		if ((nX > 0) && (nX < m_nBorderWidth) && (nY > 0)
			&& (nY < m_nBorderWidth))
			*result = HTTOPLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOPRIGHT;

		if ((nX > 0) && (nX < m_nBorderWidth)
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMRIGHT;
		//如果处理了 WM_NCHITTEST 消息,函数返回 true,表示事件已被处理。
		return true;
	}
	}
	return false; //如果没有处理任何消息,函数返回 false,表示事件将被传递给 Qt 进行默认处理。
}

main.cpp

#include "MainWidget.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWidget w;
    w.show();
    return a.exec();
}

运行结果:无边框、可移动、可伸缩

image-20240629203728563

四、设计一个无边框窗口公共类

  抽取代码,新建无边框窗口类CFrameLessWidgetBase,这个类具有可拉伸的特性,然后主窗口继承于这个类。

CFrameLessWidgetBase.h

#pragma once
#include <QtWidgets/QWidget>

class CFrameLessWidgetBase : public QWidget
{
public:
	CFrameLessWidgetBase(QWidget* parent = nullptr);
	~CFrameLessWidgetBase();

protected:
	bool nativeEvent(const QByteArray& eventType, void* message, long* result)override;
private:
	int m_nBorderWidth = 5;
};

CFrameLessWidgetBase.cpp

#include "CFrameLessWidgetBase.h"
#include <qt_windows.h>
#include <windows.h>
#include <windowsx.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib,"dwmapi.lib")

CFrameLessWidgetBase::CFrameLessWidgetBase(QWidget* parent)
{
	this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);    //无边框窗口
	this->setAttribute(Qt::WA_Hover);	//启用控件悬停效果,配合判断鼠标状态
}

CFrameLessWidgetBase::~CFrameLessWidgetBase()
{
}
//通过判断鼠标的状态进行拉伸
bool CFrameLessWidgetBase::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
	MSG* param = static_cast<MSG*>(message);

	switch (param->message) //根据 param->message 的值来处理不同类型的消息
	{
	case WM_NCHITTEST:	//WM_NCHITTEST 是一个 Windows 消息,用于确定鼠标点击发生在窗口的哪个部分
	{
		//将鼠标的屏幕坐标转换为相对于控件左上角的坐标
		int nX = GET_X_LPARAM(param->lParam) - this->geometry().x();
		int nY = GET_Y_LPARAM(param->lParam) - this->geometry().y();

		/*if (childAt(nX, nY) != nullptr)
			return QWidget::nativeEvent(eventType, message, result);*/

			//检查鼠标坐标是否在控件的内部边界内,排除边框
		if (nX > m_nBorderWidth && nX < this->width() - m_nBorderWidth &&
			nY > m_nBorderWidth && nY < this->height() - m_nBorderWidth)
		{
			//如果鼠标位置下存在子控件,则调用基类的 nativeEvent 处理事件;后续会调用该控件的鼠标点击函数用于拖拽移动
			if (childAt(nX, nY) != nullptr)
				return QWidget::nativeEvent(eventType, message, result);
		}

		//判断鼠标点击是否在控件的边框上,并设置 *result 为相应的值。例如 HTLEFT、HTRIGHT、HTTOP 等,这些值定义了鼠标点击的边框部分。
		if ((nX > 0) && (nX < m_nBorderWidth))
			*result = HTLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width()))
			*result = HTRIGHT;

		if ((nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOP;

		if ((nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOM;

		if ((nX > 0) && (nX < m_nBorderWidth) && (nY > 0)
			&& (nY < m_nBorderWidth))
			*result = HTTOPLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOPRIGHT;

		if ((nX > 0) && (nX < m_nBorderWidth)
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMRIGHT;
		//如果处理了 WM_NCHITTEST 消息,函数返回 true,表示事件已被处理。
		return true;
	}
	}
	return false; //如果没有处理任何消息,函数返回 false,表示事件将被传递给 Qt 进行默认处理。
}

CTitleBar.h

#pragma once
#include <QtWidgets/QWidget>
#include <QLabel>
#include <QPushButton>

/**
	自定义标题栏界面类,添加设置、最小化、最大化,关闭按钮
*/
class CTitleBar : public QWidget
{
public:
	CTitleBar(QWidget* parent = nullptr);
	~CTitleBar();

private:
	void initUI();	//初始化UI
	void mousePressEvent(QMouseEvent* event)override;	//无边框窗口拖拽移动

private:
	QLabel* m_pLogo;
	QLabel* m_pTitleTextLabel;

	QPushButton* m_pSetBtn;
	QPushButton* m_pMinBtn;
	QPushButton* m_pMaxBtn;
	QPushButton* m_pCloseBtn;
};

CTitleBar.cpp

#pragma comment(lib, "user32.lib")
#include "CTitleBar.h"
#include <qt_windows.h>
#include <QHBoxLayout>
#include <QMouseEvent>w

CTitleBar::CTitleBar(QWidget* parent)
{
	initUI();
}

CTitleBar::~CTitleBar()
{
}

void CTitleBar::initUI()
{
	this->setAttribute(Qt::WA_StyledBackground);    //禁止父窗口影响子窗口样式
	this->setFixedHeight(32 + 10);
	this->setStyleSheet("background-color:rgb(54, 54, 54)");

	m_pLogo = new QLabel(this);
	m_pLogo->setFixedWidth(32);

	m_pTitleTextLabel = new QLabel(this);
	m_pTitleTextLabel->setText(u8"我是标题");
	m_pTitleTextLabel->setFixedWidth(120);

	m_pSetBtn = new QPushButton(this);
	m_pSetBtn->setFixedSize(QSize(32, 32));

	m_pMinBtn = new QPushButton(this);
	m_pMinBtn->setFixedSize(QSize(32, 32));

	m_pMaxBtn = new QPushButton(this);
	m_pMaxBtn->setFixedSize(QSize(32, 32));

	m_pCloseBtn = new QPushButton(this);
	m_pCloseBtn->setFixedSize(QSize(32, 32));

	//布局
	QHBoxLayout* m_pHLay = new QHBoxLayout(this);
	m_pHLay->addWidget(m_pLogo);
	m_pHLay->addWidget(m_pTitleTextLabel);
	m_pHLay->addStretch();	//弹簧
	m_pHLay->addWidget(m_pSetBtn);
	m_pHLay->addWidget(m_pMinBtn);
	m_pHLay->addWidget(m_pMaxBtn);
	m_pHLay->addWidget(m_pCloseBtn);

	//边界间隔
	m_pHLay->setContentsMargins(5, 5, 5, 5);
}

//处理鼠标按下事件--无边框窗口拖拽移动的解决办法
void CTitleBar::mousePressEvent(QMouseEvent* event)	
{
	//释放当前捕获的鼠标,若成功释放返回true
	if (ReleaseCapture())
	{	
		QWidget* pWindow = this->window();	//获取窗口指针
		if (pWindow->isTopLevel())	//检查获取到的窗口是否是一个顶级窗口(即不是其他窗口的子控件)
		{
			//如果窗口是顶级窗口,使用 SendMessage 函数向父窗口句柄发送一个 WM_SYSCOMMAND 消息,该消息指示窗口开始移动操作。
			SendMessage(HWND(pWindow->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
		}
	}
	event->ignore();
}

MainWidget.h

#pragma once

#include <QtWidgets/QWidget>
#include "CFrameLessWidgetBase.h"
#include "CTitleBar.h"

class MainWidget : public CFrameLessWidgetBase
{
    Q_OBJECT

public:
    MainWidget(QWidget *parent = nullptr);
    ~MainWidget();

private:
    void initUI();  //初始化UI
    
private:
    CTitleBar* m_pTitleBar = nullptr;

};

MainWidgwet.cpp

#include "MainWidget.h"
#include <QVBoxLayout>


MainWidget::MainWidget(QWidget *parent)
    : CFrameLessWidgetBase(parent)
{
    //this->resize()
    initUI();
}

MainWidget::~MainWidget()
{}

void MainWidget::initUI()
{
    m_pTitleBar = new CTitleBar(this);  //标题栏
    QWidget* w = new QWidget(this);
    w->setMinimumSize(QSize(600, 400));

    QVBoxLayout* pVlay = new QVBoxLayout(this);
    pVlay->addWidget(m_pTitleBar);
    pVlay->addWidget(w);

    pVlay->setContentsMargins(0, 0, 0, 0);  //边界间隙

}

五、标题栏qss美化、关闭、最小化、最大化

  • qt按钮样式表设置

    //以设置按钮为例
    m_pSetBtn = new QPushButton(this);
    m_pSetBtn->setFixedSize(QSize(32, 32));
    //hover指鼠标悬停在按钮时的样式
    m_pSetBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/set.svg);border:none}" \
    		"QPushButton:hover{" \
    		"background-color:rgb(99, 99, 99);" \
    		"background-image:url(:/MainWidget/resources/titlebar/set_hover.svg);border:none;}");
    
  • 关闭、最小化、最大化功能实现

    • 方案一:发信号到父窗口,父窗口收到信号后进行相应操作

    • 方案二:在子窗口获取父窗口指针,进行相应操作

      QWidget* pWindow = this->window();
      pWindow->showMinimized();	//最小化
      

      this->window()介绍

      QWidget* QWidget::window() const;
      

      返回此小部件的窗口,即具有(或可能具有)窗口系统框架的下一个祖先小部件。如果小部件是一个窗口,则返回小部件本身。

      槽函数代码为:

      void CTitleBar::onClicked()
      {
      	QPushButton* pBtn = qobject_cast<QPushButton*>(sender());	//获得发送者
      
      	QWidget* pWindow = this->window();	//获取父窗口指针
      	if (pBtn == m_pMinBtn)
      	{
      		pWindow->showMinimized();	//最小化显示
      	}
      	else if (pBtn == m_pMaxBtn)
      	{
      		if (pWindow->isMaximized())	//若已经是最大化,则常规显示
      		{
      			pWindow->showNormal();
      			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/normal.svg);border:none}" \
      				"QPushButton:hover{" \
      				"background-color:rgb(99, 99, 99);" \
      				"background-image:url(:/MainWidget/resources/titlebar/normal_hover.svg);border:none;}");
      		}
      		else//否则,最大化显示
      		{
      			pWindow->showMaximized();
      			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/max.svg);border:none}" \
      				"QPushButton:hover{" \
      				"background-color:rgb(99, 99, 99);" \
      				"background-image:url(:/MainWidget/resources/titlebar/max_hover.svg);border:none;}");
      		}
      	}
      	else if (pBtn == m_pCloseBtn)
      	{
      		emit signal_closeBtn();	//向主窗口发送信号来关闭
      	}
      }
      

示例:

CFrameLessWidgetBase.h

#pragma once
#include <QtWidgets/QWidget>

class CFrameLessWidgetBase : public QWidget
{
public:
	CFrameLessWidgetBase(QWidget* parent = nullptr);
	~CFrameLessWidgetBase();

protected:
	bool nativeEvent(const QByteArray& eventType, void* message, long* result)override;
private:
	int m_nBorderWidth = 5;
};

CFrameLessWidgetBase.cpp

#include "CFrameLessWidgetBase.h"
#include <qt_windows.h>
#include <windows.h>
#include <windowsx.h>
#pragma comment(lib, "user32.lib")
#pragma comment(lib,"dwmapi.lib")

CFrameLessWidgetBase::CFrameLessWidgetBase(QWidget* parent)
{
	this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);    //无边框窗口
	this->setAttribute(Qt::WA_Hover);	//启用控件悬停效果,配合判断鼠标状态
}

CFrameLessWidgetBase::~CFrameLessWidgetBase()
{
}
//通过判断鼠标的状态进行拉伸
bool CFrameLessWidgetBase::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
	MSG* param = static_cast<MSG*>(message);

	switch (param->message) //根据 param->message 的值来处理不同类型的消息
	{
	case WM_NCHITTEST:	//WM_NCHITTEST 是一个 Windows 消息,用于确定鼠标点击发生在窗口的哪个部分
	{
		//将鼠标的屏幕坐标转换为相对于控件左上角的坐标
		int nX = GET_X_LPARAM(param->lParam) - this->geometry().x();
		int nY = GET_Y_LPARAM(param->lParam) - this->geometry().y();

		//这种逻辑是不合理的,因为边框也在控件之上,点击边框会一直调用基类的 nativeEvent 处理事件,而不能伸缩
		/*if (childAt(nX, nY) != nullptr)
			return QWidget::nativeEvent(eventType, message, result);*/

			//检查鼠标坐标是否在控件的内部边界内,排除边框
		if (nX > m_nBorderWidth && nX < this->width() - m_nBorderWidth &&
			nY > m_nBorderWidth && nY < this->height() - m_nBorderWidth)
		{
			//如果鼠标位置下存在子控件,则调用基类的 nativeEvent 处理事件;后续会调用该控件的鼠标点击函数用于拖拽移动
			if (childAt(nX, nY) != nullptr)
				return QWidget::nativeEvent(eventType, message, result);
		}

		//判断鼠标点击是否在控件的边框上,并设置 *result 为相应的值。例如 HTLEFT、HTRIGHT、HTTOP 等,这些值定义了鼠标点击的边框部分。
		if ((nX > 0) && (nX < m_nBorderWidth))
			*result = HTLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width()))
			*result = HTRIGHT;

		if ((nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOP;

		if ((nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOM;

		if ((nX > 0) && (nX < m_nBorderWidth) && (nY > 0)
			&& (nY < m_nBorderWidth))
			*result = HTTOPLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > 0) && (nY < m_nBorderWidth))
			*result = HTTOPRIGHT;

		if ((nX > 0) && (nX < m_nBorderWidth)
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMLEFT;

		if ((nX > this->width() - m_nBorderWidth) && (nX < this->width())
			&& (nY > this->height() - m_nBorderWidth) && (nY < this->height()))
			*result = HTBOTTOMRIGHT;
		//如果处理了 WM_NCHITTEST 消息,函数返回 true,表示事件已被处理。
		return true;
	}
	}
	return false; //如果没有处理任何消息,函数返回 false,表示事件将被传递给 Qt 进行默认处理。
}

CTitleBar.h

#pragma once
#include <QtWidgets/QWidget>
#include <QLabel>
#include <QPushButton>

/**
	自定义标题栏界面类,添加设置、最小化、最大化,关闭按钮
*/
class CTitleBar : public QWidget
{
	Q_OBJECT
public:
	CTitleBar(QWidget* parent = nullptr);
	~CTitleBar();

private:
	void initUI();	//初始化UI
	void mousePressEvent(QMouseEvent* event)override;	//无边框窗口拖拽移动
	void mouseDoubleClickEvent(QMouseEvent* event)override;	//鼠标双击事件

private slots:
	void onClicked();	//最大最小关闭按钮统一的槽函数

signals:
	void signal_closeBtn();	//点击关闭按钮时向主窗口发送信号

private:
	QLabel* m_pLogo;
	QLabel* m_pTitleTextLabel;

	QPushButton* m_pSetBtn;
	QPushButton* m_pMinBtn;
	QPushButton* m_pMaxBtn;
	QPushButton* m_pCloseBtn;
};

CTitleBar.cpp

#pragma comment(lib, "user32.lib")
#include "CTitleBar.h"
#include <qt_windows.h>
#include <QHBoxLayout>
#include <QMouseEvent>w

CTitleBar::CTitleBar(QWidget* parent)
{
	initUI();
}

CTitleBar::~CTitleBar()
{
}

void CTitleBar::initUI()
{
	this->setAttribute(Qt::WA_StyledBackground);    //禁止父窗口影响子窗口样式
	this->setFixedHeight(32 + 10);
	this->setStyleSheet("background-color:rgb(54, 54, 54)");

	m_pLogo = new QLabel(this);
	m_pLogo->setFixedWidth(32);
	m_pLogo->setStyleSheet("background-image:url(:/MainWidget/resources/titlebar/title_icon.png);border:none");

	m_pTitleTextLabel = new QLabel(this);
	m_pTitleTextLabel->setText(u8"我是标题");
	m_pTitleTextLabel->setFixedWidth(120);
	m_pTitleTextLabel->setStyleSheet("QLabel{font-family: Microsoft YaHei; \
		font-size:18px; \
		color:#BDC8E2;background-color:rgb(54,54,54);}");

	m_pSetBtn = new QPushButton(this);
	m_pSetBtn->setFixedSize(QSize(32, 32));

	m_pSetBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/set.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/set_hover.svg);border:none;}");

	m_pMinBtn = new QPushButton(this);
	m_pMinBtn->setFixedSize(QSize(32, 32));
	m_pMinBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/min.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/min_hover.svg);border:none;}");

	m_pMaxBtn = new QPushButton(this);
	m_pMaxBtn->setFixedSize(QSize(32, 32));
	m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/normal.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/normal_hover.svg);border:none;}");

	m_pCloseBtn = new QPushButton(this);
	m_pCloseBtn->setFixedSize(QSize(32, 32));
	m_pCloseBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/close.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/close_hover.svg);border:none;}");

	//布局
	QHBoxLayout* m_pHLay = new QHBoxLayout(this);
	m_pHLay->addWidget(m_pLogo);
	m_pHLay->addWidget(m_pTitleTextLabel);
	m_pHLay->addStretch();	//弹簧

	m_pHLay->addWidget(m_pSetBtn);
	QSpacerItem* pItem1 = new QSpacerItem(20, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem1);

	m_pHLay->addWidget(m_pMinBtn);
	QSpacerItem* pItem2 = new QSpacerItem(18, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem2);

	m_pHLay->addWidget(m_pMaxBtn);
	QSpacerItem* pItem3 = new QSpacerItem(18, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem3);   // 弹簧每次使用时得new出来,不能重复使用,否则会报析构错误

	m_pHLay->addWidget(m_pCloseBtn);

	//边界间隔
	m_pHLay->setContentsMargins(5, 5, 5, 5);

	//三个发送者(按钮)链接到同一个槽
	connect(m_pMaxBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
	connect(m_pMinBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
	connect(m_pCloseBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
}

//处理鼠标按下事件--无边框窗口拖拽移动的解决办法
void CTitleBar::mousePressEvent(QMouseEvent* event)	
{
	//释放当前捕获的鼠标,若成功释放返回true
	if (ReleaseCapture())
	{	
		QWidget* pWindow = this->window();	//获取窗口指针
		if (pWindow->isTopLevel())	//检查获取到的窗口是否是一个顶级窗口(即不是其他窗口的子控件)
		{
			//如果窗口是顶级窗口,使用 SendMessage 函数向父窗口句柄发送一个 WM_SYSCOMMAND 消息,该消息指示窗口开始移动操作。
			SendMessage(HWND(pWindow->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
		}
	}
	event->ignore();
}

//鼠标双击事件
void CTitleBar::mouseDoubleClickEvent(QMouseEvent* event)
{
	m_pMaxBtn->clicked();
}

void CTitleBar::onClicked()
{
	QPushButton* pBtn = qobject_cast<QPushButton*>(sender());	//获得发送者

	QWidget* pWindow = this->window();	//获取父窗口指针
	if (pBtn == m_pMinBtn)
	{
		pWindow->showMinimized();	//最小化显示
	}
	else if (pBtn == m_pMaxBtn)
	{
		if (pWindow->isMaximized())	//若已经是最大化,则常规显示
		{
			pWindow->showNormal();
			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/normal.svg);border:none}" \
				"QPushButton:hover{" \
				"background-color:rgb(99, 99, 99);" \
				"background-image:url(:/MainWidget/resources/titlebar/normal_hover.svg);border:none;}");
		}
		else//否则,最大化显示
		{
			pWindow->showMaximized();
			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/max.svg);border:none}" \
				"QPushButton:hover{" \
				"background-color:rgb(99, 99, 99);" \
				"background-image:url(:/MainWidget/resources/titlebar/max_hover.svg);border:none;}");
		}
	}
	else if (pBtn == m_pCloseBtn)
	{
		emit signal_closeBtn();	//向主窗口发送信号来关闭
	}
}

MainWidget.h

#pragma once

#include <QtWidgets/QWidget>
#include "CFrameLessWidgetBase.h"
#include "CTitleBar.h"

class MainWidget : public CFrameLessWidgetBase
{
    Q_OBJECT

public:
    MainWidget(QWidget *parent = nullptr);
    ~MainWidget();

private:
    void initUI();  //初始化UI

private slots:
    void on_closeSlot();
    
private:
    CTitleBar* m_pTitleBar = nullptr;
};

MainWidget.cpp

#include "MainWidget.h"
#include <QVBoxLayout>
#include <QMessageBox>


MainWidget::MainWidget(QWidget *parent)
    : CFrameLessWidgetBase(parent)
{
    //this->resize()
    initUI();

    //标题栏中的关闭按钮链接槽函数
    connect(m_pTitleBar, &CTitleBar::signal_closeBtn, this, &MainWidget::on_closeSlot);
}

MainWidget::~MainWidget()
{}

void MainWidget::initUI()
{
    m_pTitleBar = new CTitleBar(this);  //标题栏
    QWidget* w = new QWidget(this);
    w->setMinimumSize(QSize(600, 400));

    QVBoxLayout* pVlay = new QVBoxLayout(this);
    pVlay->addWidget(m_pTitleBar);
    pVlay->addWidget(w);

    pVlay->setContentsMargins(0, 0, 0, 0);  //边界间隙

}

void MainWidget::on_closeSlot()
{
    //窗口关闭处理
    QMessageBox::StandardButton _exit = QMessageBox::warning(this, u8"提示", u8"确定要退出吗", QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes);
    if (_exit == QMessageBox::Yes)
    {
        close();
    }
}

main.cpp

#include "MainWidget.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWidget w;
    w.show();
    return a.exec();
}

运行结果

  无边框窗口、可移动、可伸缩、标题栏样式表、最大化、最小化、关闭、双击标题栏可最大化与正常显示。

image-20240629224659047

六、实现窗口阴影

  要实现窗口阴影,需要设置两层窗口

  • 将底层窗口设置为窗体半透明、无边框

    //设置底层窗体背景为半透明
    this->setAttribute(Qt::WA_TranslucentBackground, true);
    
    //设置无边框
    this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
    
  • 将顶层窗口设置背景颜色

    //给顶层widget设置背景颜色,不然看不见,因为底层widget已经透明了
    pRealWidget->setStyleSheet("background-color:rgb(255, 254, 253)");
    
  • 新建Qt窗口阴影类,给顶层QWidget设置阴影

    QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);
    
    //设置阴影距离
    shadow->setOffset(0, 0);
    //设置阴影颜色  686868
    shadow->setColor(QColor("#252a2b"));	//深灰色
    //设置阴影区域
    shadow->setBlurRadius(30);
    
    //给顶层QWidget设置阴影
    pRealWidget->setGraphicsEffect(shadow);
    

示例:

CLoginDlg.h

#pragma once

#include <QDialog>


class CLoginDlg : public QDialog
{
	Q_OBJECT

public:
	CLoginDlg(QWidget* parent = Q_NULLPTR);
	~CLoginDlg();

private:
	void mousePressEvent(QMouseEvent* event) override;
	void mouseMoveEvent(QMouseEvent* event) override;

private:
	//记录鼠标,窗口位置
	QPoint windowPos;
	QPoint mousePos;
	QPoint dPos;
};

CLoginDlg.cpp

#include "CLoginDlg.h"
#include "CLoginRealWidget.h"
#include <QGraphicsDropShadowEffect>
#include <QVboxLayout>
#include <QMouseEvent>


CLoginDlg::CLoginDlg(QWidget* parent)
    : QDialog(parent)
{
    //设置窗体的背景为半透明
    this->setAttribute(Qt::WA_TranslucentBackground, true);

    //设置无边框
    this->setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);

    QVBoxLayout* pMainLay = new QVBoxLayout(this);
    CLoginRealWidget* pRealWidget = new CLoginRealWidget(this);
    pMainLay->addWidget(pRealWidget);
    pMainLay->setContentsMargins(30, 30, 30, 30);
    setLayout(pMainLay);

    //给顶层widget设置背景颜色,不然看不见,因为底层widget已经透明了
    pRealWidget->setStyleSheet("background-color:rgb(255, 254, 253)");

    QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);

    //设置阴影距离
    shadow->setOffset(0, 0);
    //设置阴影颜色  686868
    shadow->setColor(QColor("#686868")); //灰色
    //设置阴影区域
    shadow->setBlurRadius(30);

    //给顶层QWidget设置阴影
    pRealWidget->setGraphicsEffect(shadow);
}

CLoginDlg::~CLoginDlg()
{
}

void CLoginDlg::mousePressEvent(QMouseEvent* event)
{
    this->windowPos = this->pos();       // 获得部件当前位置
    this->mousePos = event->globalPos(); // 获得鼠标位置
    this->dPos = mousePos - windowPos;   // 移动后部件所在的位置
}

void CLoginDlg::mouseMoveEvent(QMouseEvent* event)
{
    this->move(event->globalPos() - this->dPos);
}

CLoginRealWidget.h

#pragma once
#include <QWidget>


class CLoginRealWidget : public QWidget
{
	Q_OBJECT

public:
	CLoginRealWidget(QWidget* p = nullptr);
	~CLoginRealWidget();

};

CLoginRealWidget.cpp

#include "CLoginRealWidget.h"
#include <QLabel>
#include <QLineEdit>
#include <QCheckBox>
#include <QPushButton>
#include <QHBoxLayout>
#include <QGridLayout>

CLoginRealWidget::CLoginRealWidget(QWidget* p)
	:QWidget(p)
{
	//禁止父窗口影响子窗口样式
	setAttribute(Qt::WA_StyledBackground);
	setWindowFlags(Qt::FramelessWindowHint);

	//头像
	QLabel* pImageLabel = new QLabel(this);
	QPixmap pixmap(":/CLoginDlg/resource/user_image.png");
	pImageLabel->setFixedSize(150, 150);
	pImageLabel->setPixmap(pixmap);
	pImageLabel->setScaledContents(true);

	//用户名
	QLineEdit* pUserNameLineEdit = new QLineEdit(this);
	pUserNameLineEdit->setFixedSize(300, 50);
	pUserNameLineEdit->setPlaceholderText(QStringLiteral("QQ号码/手机/邮箱"));

	//密码
	QLineEdit* pPasswordLineEdit = new QLineEdit(this);
	pPasswordLineEdit->setFixedSize(300, 50);
	pPasswordLineEdit->setPlaceholderText(QStringLiteral("密码"));
	pPasswordLineEdit->setEchoMode(QLineEdit::Password);

	QPushButton* pForgotButton = new QPushButton(this);
	pForgotButton->setText(QStringLiteral("找回密码"));
	pForgotButton->setFixedWidth(80);

	QCheckBox* pRememberCheckBox = new QCheckBox(this);
	pRememberCheckBox->setText(QStringLiteral("记住密码"));

	QCheckBox* pAutoLoginCheckBox = new QCheckBox(this);
	pAutoLoginCheckBox->setText(QStringLiteral("自动登录"));

	QPushButton* pLoginButton = new QPushButton(this);
	pLoginButton->setFixedHeight(48);
	pLoginButton->setText(QStringLiteral("登录"));

	QPushButton* pRegisterButton = new QPushButton(this);
	pRegisterButton->setFixedHeight(48);
	pRegisterButton->setText(QStringLiteral("注册账号"));

	QHBoxLayout* pMainLay = new QHBoxLayout(this);

	QSpacerItem* pHSpacer1 = new QSpacerItem(25, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	pMainLay->addSpacerItem(pHSpacer1);

	QGridLayout* pGridLayout = new QGridLayout(this);

	// 头像 第0行,第0列开始,占3行1列
	pGridLayout->addWidget(pImageLabel, 0, 0, 3, 1);

	// 用户名输入框 第0行,第1列开始,占1行2列
	pGridLayout->addWidget(pUserNameLineEdit, 0, 1, 1, 2);

	// 密码输入框 第1行,第1列开始,占1行2列
	pGridLayout->addWidget(pPasswordLineEdit, 1, 1, 1, 2);

	// 忘记密码 第2行,第1列开始,占1行1列
	pGridLayout->addWidget(pForgotButton, 2, 1, 1, 1);

	// 记住密码 第2行,第2列开始,占1行1列 水平居中 垂直居中
	pGridLayout->addWidget(pRememberCheckBox, 2, 2, 1, 1, Qt::AlignLeft | Qt::AlignVCenter);

	// 自动登录 第2行,第2列开始,占1行1列 水平居右 垂直居中
	pGridLayout->addWidget(pAutoLoginCheckBox, 2, 2, 1, 1, Qt::AlignRight | Qt::AlignVCenter);

	// 登录按钮 第3行,第1列开始,占1行2列
	pGridLayout->addWidget(pLoginButton, 3, 1, 1, 2);

	// 注册按钮 第4行,第1列开始,占1行2列
	pGridLayout->addWidget(pRegisterButton, 4, 1, 1, 2);

	// 设置水平间距
	pGridLayout->setHorizontalSpacing(10);

	// 设置垂直间距
	pGridLayout->setVerticalSpacing(10);

	pMainLay->addLayout(pGridLayout);
	QSpacerItem* pHSpacer2 = new QSpacerItem(25, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	pMainLay->addSpacerItem(pHSpacer2);

	pMainLay->setContentsMargins(5, 5, 5, 5);
	setLayout(pMainLay);
}

CLoginRealWidget::~CLoginRealWidget()
{
}

main.cpp

#include "CLoginDlg.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    CLoginDlg w;
    w.show();
    return a.exec();
}

运行结果

image-20240630001329337

七、圆角窗口

  圆角实现有两种方案,前提是:

this->setAttribute(Qt::WA_TranslucentBackground);   //设置窗口背景透明
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);    //设置无边框窗口
  • 方案一:重写paintEvent方法

    void MainWidget::paintEvent(QPaintEvent * event)
    {
        QPainter painter(this);
        painter.setRenderHint(QPainter::Antialiasing);  //反锯齿
        painter.setBrush(QBrush(QColor(155, 54, 54)));
        painter.setPen(Qt::transparent);
        QRect rect = this->rect();
        painter.drawRoundedRect(rect, 15, 15); //设置窗口圆角15px
    }
    
  • 方案二:样式表qss

    this->setStyleSheet("QWidget{background-color:gray;border-radius:30px;}");
    
    //窗口透明后,需要重写该方法,不然窗口无法显示
    void MainWidget::paintEvent(QPaintEvent* event)
    {
        QStyleOption opt; //QStyleOption 用于传递绘制项的配置选项给样式函数
        opt.init(this);  //表示样式选项与当前的 MainWidget 相关联
    
        QPainter p(this);
        style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); //调用当前样式的 drawPrimitive 方法来绘制一个基本的绘制项
        QWidget::paintEvent(event); //调用基类 QWidget 的 paintEvent 方法
    }
    

    样式表qss可以设置更多圆角样式

    • 左上角:border-top-left-radius:15px
    • 右上角:border-top-right-radius:15px
    • 左下角:border-bottom-left-radius:15px
    • 右下角:border-bottom-right-radius:15px

    border-radius的参数设置

    • 可以只传一个参数,这样x,y方向的圆度是一样的,例如:border-radius:30px
    • 传两个参数:border-radius:15px 20px
      • 第一个参数设置X轴方向的半径
      • 第二个参数设置Y轴方向的半径

八、一个自定义标题栏带圆角阴影的窗口

  需要设计双层窗口,底层为透明窗口,顶层是放控件的窗口。

示例:

CFrameLessWidgetBase.h

/*
无边框窗口基类--可拉伸
其它窗口派生于该类即可
*/
#pragma once
#include <QWidget>

class CFrameLessWidgetBase : public QWidget
{
public:
	CFrameLessWidgetBase(QWidget* parent = nullptr);
	~CFrameLessWidgetBase();

protected:
	bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;

private:
	int mouse_margin = 5;
};

CFrameLessWidgetBase.cpp

#include "CFrameLessWidgetBase.h"

#ifdef Q_OS_WIN
#include <qt_windows.h>
#include <Windows.h>
#include <windowsx.h>
#endif

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

CFrameLessWidgetBase::CFrameLessWidgetBase(QWidget* parent)
{
	setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint);
	setAttribute(Qt::WA_Hover);
}

CFrameLessWidgetBase::~CFrameLessWidgetBase()
{
}

bool CFrameLessWidgetBase::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
	MSG* param = static_cast<MSG*>(message);

	switch (param->message)
	{
	case WM_NCHITTEST:
	{
		int nX = GET_X_LPARAM(param->lParam) - this->geometry().x();
		int nY = GET_Y_LPARAM(param->lParam) - this->geometry().y();

		// 如果鼠标位于内部子控件上,则不进行处理
		if (nX > mouse_margin && nX < width() - mouse_margin &&
			nY > mouse_margin && nY < this->height() - mouse_margin)
		{
			if (childAt(nX, nY) != nullptr)
				return QWidget::nativeEvent(eventType, message, result);
		}

		// 鼠标区域位于窗体边框,进行缩放
		if ((nX > 0) && (nX < mouse_margin))
			*result = HTLEFT;

		if ((nX > this->width() - mouse_margin) && (nX < this->width()))
			*result = HTRIGHT;

		if ((nY > 0) && (nY < mouse_margin))
			*result = HTTOP;

		if ((nY > this->height() - mouse_margin) && (nY < this->height()))
			*result = HTBOTTOM;

		if ((nX > 0) && (nX < mouse_margin) && (nY > 0)
			&& (nY < mouse_margin))
			*result = HTTOPLEFT;

		if ((nX > this->width() - mouse_margin) && (nX < this->width())
			&& (nY > 0) && (nY < mouse_margin))
			*result = HTTOPRIGHT;

		if ((nX > 0) && (nX < mouse_margin)
			&& (nY > this->height() - mouse_margin) && (nY < this->height()))
			*result = HTBOTTOMLEFT;

		if ((nX > this->width() - mouse_margin) && (nX < this->width())
			&& (nY > this->height() - mouse_margin) && (nY < this->height()))
			*result = HTBOTTOMRIGHT;

		return true;
	}
	}

	return QWidget::nativeEvent(eventType, message, result);
}

CTitleBar.h

#pragma once
#include <QtWidgets/QWidget>
#include <QLabel>
#include <QPushButton>

/**
	自定义标题栏界面类,添加设置、最小化、最大化,关闭按钮
*/
class CTitleBar : public QWidget
{
	Q_OBJECT
public:
	CTitleBar(QWidget* parent = nullptr);
	~CTitleBar();

private:
	void initUI();	//初始化UI
	void paintEvent(QPaintEvent* event) override;
	void mousePressEvent(QMouseEvent* event)override;	//无边框窗口拖拽移动
	void mouseDoubleClickEvent(QMouseEvent* event)override;	//鼠标双击事件

private slots:
	void onClicked();	//最大最小关闭按钮统一的槽函数

signals:
	void sig_close();
	void sig_max(bool isMax);

private:
	QLabel* m_pLogo;
	QLabel* m_pTitleTextLabel;

	QPushButton* m_pSetBtn;
	QPushButton* m_pMinBtn;
	QPushButton* m_pMaxBtn;
	QPushButton* m_pCloseBtn;
};

CTitleBar.cpp

#pragma comment(lib, "user32.lib")
#include "CTitleBar.h"
#include <qt_windows.h>
#include <QHBoxLayout>
#include <QMouseEvent>
#include <QStyleOption>
#include <QPainter>

CTitleBar::CTitleBar(QWidget* parent)
{
	initUI();
}

CTitleBar::~CTitleBar()
{
}

void CTitleBar::initUI()
{
	this->setAttribute(Qt::WA_StyledBackground);    //禁止父窗口影响子窗口样式
	this->setFixedHeight(32 + 10);
	//左上,右上圆角10;左下,右下圆角0
	this->setStyleSheet("QWidget{background-color:rgb(54,54,54); \
        border-top-left-radius:10px; \
        border-top-right-radius:10px; \
		border-bottom-left-radius:0px; \
		border-bottom-right-radius:0px;}");

	m_pLogo = new QLabel(this);
	m_pLogo->setFixedWidth(32);
	m_pLogo->setStyleSheet("background-image:url(:/MainWidget/resources/titlebar/title_icon.png);border:none");

	m_pTitleTextLabel = new QLabel(this);
	m_pTitleTextLabel->setText(u8"我是标题");
	m_pTitleTextLabel->setFixedWidth(120);
	m_pTitleTextLabel->setStyleSheet("QLabel{font-family: Microsoft YaHei; \
		font-size:18px; \
		color:#BDC8E2;background-color:rgb(54,54,54);}");

	m_pSetBtn = new QPushButton(this);
	m_pSetBtn->setFixedSize(QSize(32, 32));
	m_pSetBtn->setText("");
	m_pSetBtn->setFlat(true);
	m_pSetBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/set.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/set_hover.svg);border:none;}");

	m_pMinBtn = new QPushButton(this);
	m_pMinBtn->setFixedSize(QSize(32, 32));
	m_pMinBtn->setText("");
	m_pMinBtn->setFlat(true);
	m_pMinBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/min.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/min_hover.svg);border:none;}");

	m_pMaxBtn = new QPushButton(this);
	m_pMaxBtn->setFixedSize(QSize(32, 32));
	m_pMaxBtn->setText("");
	m_pMaxBtn->setFlat(true);
	m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/normal.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/normal_hover.svg);border:none;}");

	m_pCloseBtn = new QPushButton(this);
	m_pCloseBtn->setFixedSize(QSize(32, 32));
	m_pCloseBtn->setText("");
	m_pCloseBtn->setFlat(true);
	m_pCloseBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/close.svg);border:none}" \
		"QPushButton:hover{" \
		"background-color:rgb(99, 99, 99);" \
		"background-image:url(:/MainWidget/resources/titlebar/close_hover.svg);border:none;}");

	//布局
	QHBoxLayout* m_pHLay = new QHBoxLayout(this);
	m_pHLay->addWidget(m_pLogo);
	m_pHLay->addWidget(m_pTitleTextLabel);
	m_pHLay->addStretch();	//弹簧

	m_pHLay->addWidget(m_pSetBtn);
	QSpacerItem* pItem1 = new QSpacerItem(20, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem1);

	m_pHLay->addWidget(m_pMinBtn);
	QSpacerItem* pItem2 = new QSpacerItem(18, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem2);

	m_pHLay->addWidget(m_pMaxBtn);
	QSpacerItem* pItem3 = new QSpacerItem(18, 20, QSizePolicy::Fixed, QSizePolicy::Fixed);
	m_pHLay->addSpacerItem(pItem3);   // 弹簧每次使用时得new出来,不能重复使用,否则会报析构错误

	m_pHLay->addWidget(m_pCloseBtn);

	//边界间隔
	m_pHLay->setContentsMargins(5, 5, 5, 5);

	//三个发送者(按钮)链接到同一个槽
	connect(m_pMaxBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
	connect(m_pMinBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
	connect(m_pCloseBtn, &QPushButton::clicked, this, &CTitleBar::onClicked);
}

void CTitleBar::paintEvent(QPaintEvent* event)
{
	//决定样式表是否起作用
	//QStyleOption opt;
	//opt.init(this);
	//QPainter p(this);
	//style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
	//QWidget::paintEvent(event);
}

//处理鼠标按下事件--无边框窗口拖拽移动的解决办法
void CTitleBar::mousePressEvent(QMouseEvent* event)	
{
	//释放当前捕获的鼠标,若成功释放返回true
	if (ReleaseCapture())
	{	
		QWidget* pWindow = this->window();	//获取窗口指针
		if (pWindow->isTopLevel())	//检查获取到的窗口是否是一个顶级窗口(即不是其他窗口的子控件)
		{
			//如果窗口是顶级窗口,使用 SendMessage 函数向父窗口句柄发送一个 WM_SYSCOMMAND 消息,该消息指示窗口开始移动操作。
			SendMessage(HWND(pWindow->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
		}
	}
	event->ignore();
}

//鼠标双击事件
void CTitleBar::mouseDoubleClickEvent(QMouseEvent* event)
{
	m_pMaxBtn->clicked();
}

void CTitleBar::onClicked()
{
	QPushButton* pBtn = qobject_cast<QPushButton*>(sender());	//获得发送者

	QWidget* pWindow = this->window();	//获取父窗口指针
	if (pBtn == m_pMinBtn)
	{
		pWindow->showMinimized();	//最小化显示
	}
	else if (pBtn == m_pMaxBtn)
	{
		if (pWindow->isMaximized())	//若已经是最大化,则常规显示
		{
			pWindow->showNormal();
			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/normal.svg);border:none; \
					background-position:center; \
					background-repeat:no-repeat;}  \
					QPushButton:hover{ \
					background-color:rgb(99, 99, 99); \
					background-image:url(:/MainWidget/resource/titlebar/normal_hover.svg);border:none;}");
			emit sig_max(false);
		}
		else//否则,最大化显示
		{
			pWindow->showMaximized();
			m_pMaxBtn->setStyleSheet("QPushButton{background-image:url(:/MainWidget/resources/titlebar/max.svg);border:none; \
					background-position:center; \
					background-repeat:no-repeat;}  \
					QPushButton:hover{ \
					background-color:rgb(99, 99, 99);  \
					background-image:url(:/MainWidget/resource/titlebar/max_hover.svg);border:none;}");
			emit sig_max(true);
		}
	}
	else if (pBtn == m_pCloseBtn)
	{
		emit sig_close();
	}
}

CTopWidget.h

#pragma once
#include <QWidget>

class CTopWidget : public QWidget
{
	Q_OBJECT

public:
	CTopWidget(QWidget* p = nullptr);
	~CTopWidget();

signals:
	void sig_close();
	void sig_max(bool isMax);

private:
	void paintEvent(QPaintEvent* event) override;
};

CTopWidget.cpp

#include "CTopWidget.h"
#include "CTitleBar.h"
#include <QVBoxLayout>
#include <QStyleOption>
#include <QPainter>

CTopWidget::CTopWidget(QWidget* p)
    :QWidget(p)
{
    setAttribute(Qt::WA_StyledBackground); //禁止父窗口影响子窗口样式
    this->setAttribute(Qt::WA_TranslucentBackground);   //设置窗口背景透明
    //this->setStyleSheet("QWidget{background-color:rgb(255, 254, 253);border-radius:10px;}");
    /*this->setStyleSheet("QWidget{background-color:rgb(255, 254, 253); \
        border-bottom-left-radius:15px; \
        border-bottom-right-radius:15px;}");*/

    QVBoxLayout* pVLay = new QVBoxLayout(this);
    CTitleBar* pTitle = new CTitleBar(this);
    QWidget* pWidget = new QWidget(this);
    pWidget->setStyleSheet("QWidget{background-color:rgb(255, 254, 253); \
    border-bottom-left-radius:15px; \
    border-bottom-right-radius:15px;}");

    //需要指定pWidget最小尺寸,或最大尺寸
    pWidget->setMinimumSize(1200, 800);

    pVLay->addWidget(pTitle);
    pVLay->addWidget(pWidget);
    pVLay->setContentsMargins(0, 0, 0, 0);
    setLayout(pVLay);

    connect(pTitle, &CTitleBar::sig_close, this, &CTopWidget::sig_close);
    connect(pTitle, &CTitleBar::sig_max, this, &CTopWidget::sig_max);
}

CTopWidget::~CTopWidget()
{
}

void CTopWidget::paintEvent(QPaintEvent* event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);  //反锯齿
    painter.setBrush(QBrush(QColor(255, 254, 253)));
    painter.setPen(Qt::transparent);
    QRect rect = this->rect();
    painter.drawRoundedRect(rect, 10, 10); //设置窗口圆角15px
}

MainWidget.h

#pragma once

#include <QtWidgets/QWidget>
#include "CFrameLessWidgetBase.h"
#include "CTopWidget.h"
#include <QVBoxLayout>
#include <QGraphicsDropShadowEffect>

//派生于CFrameLessWidgetBase
class MainWidget : public CFrameLessWidgetBase
{
    Q_OBJECT

public:
    MainWidget(QWidget* parent = Q_NULLPTR);

private slots:
    void onClose();
    void onDoMax(bool isMax);

private:
    QVBoxLayout* m_pMainVLay = nullptr;
    CTopWidget* m_pTopWidget = nullptr;
    QGraphicsDropShadowEffect* m_pShadow = nullptr;
};

MainWidget.cpp

#include "MainWidget.h"


MainWidget::MainWidget(QWidget* parent)
    : CFrameLessWidgetBase(parent)
{
    //设置窗体透明
    this->setAttribute(Qt::WA_TranslucentBackground, true);

    m_pMainVLay = new QVBoxLayout(this);
    m_pTopWidget = new CTopWidget();
    m_pMainVLay->addWidget(m_pTopWidget);

    int shadow_width = 30;
    m_pMainVLay->setContentsMargins(shadow_width, shadow_width, shadow_width, shadow_width);
    setLayout(m_pMainVLay);

    //给顶层widget设置背景颜色,不然看不见,因为底层widget已经透明了
    m_pTopWidget->setStyleSheet("background-color:rgb(255, 254, 253)");
    m_pShadow = new QGraphicsDropShadowEffect(this);

    //设置阴影距离
    m_pShadow->setOffset(0, 0);

    //设置阴影颜色  686868
    m_pShadow->setColor(QColor("#686868"));

    //设置阴影区域
    m_pShadow->setBlurRadius(shadow_width - 5);

    //给顶层QWidget设置阴影
    m_pTopWidget->setGraphicsEffect(m_pShadow);

    connect(m_pTopWidget, &CTopWidget::sig_close, this, &MainWidget::onClose);
    connect(m_pTopWidget, &CTopWidget::sig_max, this, &MainWidget::onDoMax);
}

void MainWidget::onClose()
{
    close();
}

void MainWidget::onDoMax(bool isMax)
{
    int shadow_width = 25;
    if (isMax)
    {
        shadow_width = 0;
    }
    else
    {

    }

    m_pMainVLay->setContentsMargins(shadow_width, shadow_width, shadow_width, shadow_width);

    //设置阴影区域
    m_pShadow->setBlurRadius(shadow_width);

    //给顶层QWidget设置阴影
    m_pTopWidget->setGraphicsEffect(m_pShadow);
}

main.cpp

#include "MainWidget.h"
#include <QtWidgets/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWidget w;
    w.show();
    return a.exec();
}

运行结果

image-20240630164412822


网站公告

今日签到

点亮在社区的每一天
去签到