走进Qt--无边框窗口实现

827 阅读7分钟

前言

为什么去边框?

Qt 默认创建的窗口使用的是操作系统提供的装饰风格(包括边框、标题栏、系统按钮等)。这些边框在外观、交互和布局方面往往受限,无法满足现代 UI 设计需求。因此,我们希望手动实现这些行为,从而获得完全的控制权。

目标

实现一个最基本的无边框窗口,具备如下功能:

  • 去除系统默认边框和标题栏;
  • 实现窗口可移动(鼠标拖动);
  • 提供最小化、最大化、关闭的功能接口(初步结构,按钮稍后实现);

核心技术点

setWindowFlags(Qt::FramelessWindowHint):去除系统边框;

重载 mousePressEvent / mouseMoveEvent 实现窗口拖动;

保留最小化、最大化、关闭功能(后续添加按钮控制);

1 基础无边框窗口构建

目标

  • 实现一个简单的无边框窗口
  • 鼠标拖动窗口移动

示例代码

BasicFramelessWindow.h

#pragma once

#include <QtWidgets/QWidget>
#include<QMouseEvent>


class BasicFramelessWindow : public QWidget
{
    Q_OBJECT

public:
   explicit BasicFramelessWindow(QWidget *parent = nullptr);
    ~BasicFramelessWindow();
protected:
    void mousePressEvent(QMouseEvent* event)override;
    void mouseMoveEvent(QMouseEvent* event)override;
private:
    QPoint dragPosition; //鼠标按下时,记录偏移位置
};

BasicFramelessWindow.cpp

#include "BasicFramelessWindow.h"

BasicFramelessWindow::BasicFramelessWindow(QWidget *parent)
    : QWidget(parent)
{
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); //去除边框
    setAttribute(Qt::WA_TranslucentBackground, false); 
    setFixedSize(600, 400);
    setStyleSheet("background-color:white;border:1px solid grag;");
}

BasicFramelessWindow::~BasicFramelessWindow()
{}

void BasicFramelessWindow::mousePressEvent(QMouseEvent * event)
{
    if (event->button() == Qt::LeftButton) {
        dragPosition = event->globalPosition().toPoint() - this->frameGeometry().topLeft();
        event->accept();
    }
}

void BasicFramelessWindow::mouseMoveEvent(QMouseEvent* event)
{
    if (event->buttons() & Qt::LeftButton) {
        move(event->globalPosition().toPoint() - dragPosition);
        event->accept();
    }
}

效果展示

image.png

没错,一个空白界面。但是由于重写了mousePressEventmouseMoveEvent,可以通过鼠标去拖动窗口移动,现在最简单的无边框窗口雏形就完成了。

2 自定义标题栏与按钮样式

目标

功能描述
自定义标题栏替换系统默认标题栏
三个按钮最小化、最大化/还原、关闭
动态切换样式最大化状态改变按钮图标
支持拖动/双击最大化拖动窗口、双击切换窗口状态

添加自定义标题栏组件

TitleBar.h

#pragma once
#include <QtWidgets/QWidget>
#include<qpushbutton.h>
#include<qlabel.h>
#include<QHBoxLayout>
class TitleBar :
    public QWidget
{
    Q_OBJECT

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

    void setMaximized(bool maximized); //设置图标状态 
signals:
    void signalMinimize();
    void signalMaximizeRestore();
    void signalClose();

protected:
    void mousePressEvent(QMouseEvent* event)override;
    void mouseMoveEvent(QMouseEvent* event)override;
    void mouseDoubleClickEvent(QMouseEvent* event)override;

private:
    QPushButton* btnMin;
    QPushButton* btnMaxRestore;
    QPushButton* btnClose;
    QLabel* titleLabel;
    QPoint dragPosition;
    bool isMaximized;
};

TitleBar.cpp

#include "TitleBar.h"
#include<QMouseEvent>
#include<qstyle.h>
#include<qapplication.h>
#include<qlist.h>

TitleBar::TitleBar(QWidget* parent) : QWidget(parent)
{
	isMaximized = false;
	setFixedHeight(35);
	setAttribute(Qt::WA_StyledBackground, true);//启用样式表主题渲染
	setStyleSheet("TitleBar{background-color:rgb(223,235,250);}"
		"QPushButton {"
		"   border: none;"  // 去掉边框
		"   background-color: transparent;"  // 透明背景
		"}"
		"QPushButton:hover {"
		"   background-color: rgb(211, 226, 237);"  // 鼠标悬停时轻微背景
		"}"
		"QPushButton:pressed {"
		"   background-color: rgba(255, 255, 255, 50);"  // 按下时稍深背景
		"}");

	titleLabel = new QLabel("My App");
	titleLabel->setStyleSheet("border:none;background:transparent;font-weight:bold;padding-left:10px;");

	btnMin = new QPushButton();
	btnMin->setIcon(QIcon(":/new/prefix1/resources/min.png"));
	btnMaxRestore = new QPushButton();
	btnMaxRestore->setIcon(QIcon(":/new/prefix1/resources/max.png"));
	btnClose = new QPushButton();
	btnClose->setIcon(QIcon(":/new/prefix1/resources/close.png"));
	

	auto mainLayout = new QHBoxLayout(this);
	mainLayout->addWidget(titleLabel);
	mainLayout->addStretch();
	mainLayout->addWidget(btnMin);
	mainLayout->addWidget(btnMaxRestore);
	mainLayout->addWidget(btnClose);
	mainLayout->setContentsMargins(0, 0, 0, 0);
	mainLayout->setSpacing(0);

	connect(btnMin, &QPushButton::clicked, this, &TitleBar::signalMinimize);
	connect(btnMaxRestore, &QPushButton::clicked, this, &TitleBar::signalMaximizeRestore);
	connect(btnClose, &QPushButton::clicked, this, &TitleBar::signalClose);
}

void TitleBar::setMaximized(bool maximized)
{
	isMaximized = maximized;
	btnMaxRestore->setIcon(isMaximized ? QIcon(":/new/prefix1/resources/restore.png") :QIcon(":/new/prefix1/resources/max.png"));
}

void TitleBar::mousePressEvent(QMouseEvent* event)
{
	//鼠标左键按下
	if (event->button() == Qt::LeftButton) {
		dragPosition = event->globalPosition().toPoint() - parentWidget()->frameGeometry().topLeft();
	}
}

void TitleBar::mouseMoveEvent(QMouseEvent* event)
{
	if (event->buttons() & Qt::LeftButton && !isMaximized) {
		parentWidget()->move(event->globalPosition().toPoint() - dragPosition);
	}
}

void TitleBar::mouseDoubleClickEvent(QMouseEvent* event)
{
	Q_UNUSED(event);
	emit signalMaximizeRestore(); //双击切换最大化状态
}

整合到主窗口

BasicFramelessWindow.h

#pragma once

#include <QtWidgets/QWidget>
#include<QMouseEvent>
#include"TitleBar.h"


class BasicFramelessWindow : public QWidget
{
    Q_OBJECT

public:
   explicit BasicFramelessWindow(QWidget *parent = nullptr);
    ~BasicFramelessWindow();
protected:
    void mousePressEvent(QMouseEvent* event)override;
    void mouseMoveEvent(QMouseEvent* event)override;

private slots:
    void onMinimize();
    void onMaxRestore();
    void onClose();

private:
    TitleBar* titleBar;
    bool isMaximized;

    QPoint dragPosition; //鼠标按下时,记录偏移位置
};

BasicFramelessWindow.cpp

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

BasicFramelessWindow::BasicFramelessWindow(QWidget *parent)
    : QWidget(parent),isMaximized(false)
{
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); //去除边框
    setAttribute(Qt::WA_TranslucentBackground, false); 
    setFixedSize(600, 400);
    setStyleSheet("background-color:white;border:1px solid grag;");

    titleBar = new TitleBar(this);
    connect(titleBar, &TitleBar::signalMinimize, this, &BasicFramelessWindow::onMinimize);
    connect(titleBar, &TitleBar::signalMaximizeRestore, this, &BasicFramelessWindow::onMaxRestore);
    connect(titleBar, &TitleBar::signalClose, this, &BasicFramelessWindow::onClose);

    auto mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(titleBar);
    mainLayout->addStretch();
    mainLayout->setSpacing(0);
    mainLayout->setContentsMargins(1, 1, 1, 1); //留空

}

BasicFramelessWindow::~BasicFramelessWindow()
{}

void BasicFramelessWindow::mousePressEvent(QMouseEvent * event)
{
    if (event->button() == Qt::LeftButton) {
        dragPosition = event->globalPosition().toPoint() - this->frameGeometry().topLeft();
        event->accept();
    }
}

void BasicFramelessWindow::mouseMoveEvent(QMouseEvent* event)
{
    if (event->buttons() & Qt::LeftButton) {
        move(event->globalPosition().toPoint() - dragPosition);
        event->accept();
    }
}
void BasicFramelessWindow::onMinimize() {
    showMinimized();
}
void BasicFramelessWindow::onMaxRestore() {
    if (isMaximized) {
        showNormal();
    } else{
        showMaximized();
    }
    isMaximized = !isMaximized;
    titleBar->setMaximized(isMaximized);
}
void BasicFramelessWindow::onClose() {
    close();
}

效果展示

image.png

3 窗口边框拖动缩放功能(鼠标拉伸)

目标

实现像普通窗口一样,通过拖动边缘或角落来改变窗口大小,支持:

  • 上、下、左、右边缘拖动
  • 四个角(左上、右上、左下、右下)拖动
功能说明
捕捉鼠标位置判断是否在边缘或角落
改变鼠标形状显示为 ↔、↕、↘ 等缩放光标
响应鼠标拖动动态调整窗口大小
限制最小尺寸避免窗口缩到看不见

代码实现

CustomWindow.h

#pragma once
#include <QtWidgets/QWidget>
#include<QMouseEvent>
#include<qpoint.h>

class CustomWindow :public QWidget
{
    Q_OBJECT
public:
    explicit CustomWindow(QWidget* parent = nullptr);
protected:
    void mousePressEvent(QMouseEvent* event) override;
    void mouseReleaseEvent(QMouseEvent* event)override;
    void mouseMoveEvent(QMouseEvent* event)override;
    void leaveEvent(QEvent* event)override;
private:
    enum ResizeRegion {
        NoEdge = 0,
        Left,
        Right,
        Top,
        Bottom,
        TopLeft,
        TopRight,
        BottomLeft,
        BottomRight
    };

    const int EDGE_MARGIN = 8; //边缘检测范围
    ResizeRegion getResizeRegion(const QPoint& pos);

    bool isResizing = false; //是否正在缩放
    ResizeRegion currentRegion = NoEdge;
    QPoint dragStartGlobalPos; //鼠标拖动起点
    QRect originalGeometry;  //拖动时窗口原始位置
};

CustomWindow.cpp

#include "CustomWindow.h"

CustomWindow::CustomWindow(QWidget* parent):QWidget(parent)
{
	setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint); //无边框
	setMouseTracking(true); //鼠标移动触发mouseMoveEvent
}

//准备拖动
void CustomWindow::mousePressEvent(QMouseEvent* event)
{
	if (event->button() == Qt::LeftButton && currentRegion != NoEdge) {
		isResizing = true;
		dragStartGlobalPos = event->globalPosition().toPoint();
		originalGeometry = geometry();
	}
	QWidget::mousePressEvent(event);
}

//结束拖动
void CustomWindow::mouseReleaseEvent(QMouseEvent* event)
{
	isResizing = false;
	QWidget::mouseReleaseEvent(event);
}

//设置光标或拖动缩放
void CustomWindow::mouseMoveEvent(QMouseEvent* event)
{
	if (isResizing) {
		QPoint delta = event->globalPosition().toPoint() - dragStartGlobalPos;
		QRect newGeom = originalGeometry;

		switch (currentRegion) {
		case Left:
			newGeom.setLeft(originalGeometry.left() + delta.x());
			break;
		case Right:
			newGeom.setRight(originalGeometry.right() + delta.x());
			break;
		case Top:
			newGeom.setTop(originalGeometry.top() + delta.y());
			break;
		case Bottom:
			newGeom.setBottom(originalGeometry.bottom() + delta.y());
			break;
		case TopLeft:
			newGeom.setTopLeft(originalGeometry.topLeft() + delta);
			break;
		case TopRight:
			newGeom.setTopRight(originalGeometry.topRight() + delta);
			break;
		case BottomLeft:
			newGeom.setBottomLeft(originalGeometry.bottomLeft() + delta);
			break;
		case BottomRight:
			newGeom.setBottomRight(originalGeometry.bottomRight() + delta);
			break;
		default:
			break;
		}

		if (newGeom.width() >= minimumWidth() && newGeom.height() >= minimumHeight()) {
			setGeometry(newGeom);
		}
	}
	else {
		//设置鼠标光标形状
		ResizeRegion region = getResizeRegion(event->pos());
		currentRegion = region;

		switch (region) {
		case Left:
		case Right:
			setCursor(Qt::SizeHorCursor);
			break;
		case Top:
		case Bottom:
			setCursor(Qt::SizeVerCursor);
			break;
		case TopLeft:
		case BottomRight:
			setCursor(Qt::SizeFDiagCursor);
			break;
		case TopRight:
		case BottomLeft:
			setCursor(Qt::SizeBDiagCursor);
			break;
		default:
			unsetCursor();
			break;
		}
	}
	QWidget::mouseMoveEvent(event);
}

//鼠标离开窗口,取消高亮
void CustomWindow::leaveEvent(QEvent* event)
{
	if (!isResizing)unsetCursor();
	QWidget::leaveEvent(event);
}

//获取边缘区域
CustomWindow::ResizeRegion CustomWindow::getResizeRegion(const QPoint& pos)
{
	bool onLeft = pos.x() <= EDGE_MARGIN;
	bool onRight = pos.x() >= width() - EDGE_MARGIN;
	bool onTop = pos.y() <= EDGE_MARGIN;
	bool onButtom = pos.y() >= height() - EDGE_MARGIN;

	if (onTop && onLeft)return TopLeft;
	if (onTop && onRight)return TopRight;
	if (onButtom && onLeft)return BottomLeft;
	if (onButtom && onRight)return BottomRight;
	if (onTop)return Top;
	if (onButtom)return Bottom;
	if (onLeft)return Left;
	if (onRight)return Right;
	return NoEdge;
}

整合代码逻辑

由于是单独定义的类,要在BasicFramelessWindow中使用CustomWindow的窗口缩放功能,需要整合逻辑。下面是具体实现:

修改BasicFramelessWindow.h

继承CustomWindow并移除重复的鼠标事件处理逻辑:

#pragma once

#include"TitleBar.h"
#include"CustomWindow.h"


class BasicFramelessWindow : public CustomWindow
{
    Q_OBJECT

public:
   explicit BasicFramelessWindow(QWidget *parent = nullptr);
    ~BasicFramelessWindow();

private slots:
    void onMinimize();
    void onMaxRestore();
    void onClose();

private:
    TitleBar* titleBar;
    bool isMaximized;

};

修改BasicFramelessWindow.cpp

移除原有的鼠标事件处理,调整构造函数:

BasicFramelessWindow::BasicFramelessWindow(QWidget *parent)
    : CustomWindow(parent),isMaximized(false)

BasicFramelessWindow.cppsetFixedSize()函数改为使用resize()

整合完毕,窗口的鼠标拉伸功能就完全实现了。

4 无边框窗口的圆角与阴影效果

由于在现有程序中去设计效果需要更改地方很多,因此,我们创建一个新项目去实现该效果。

代码示例

CustomWindow.h

#pragma once

#include <QtWidgets/QWidget>


class CustomWindow : public QWidget
{
    Q_OBJECT

public:
    CustomWindow(QWidget *parent = nullptr);
    ~CustomWindow();
protected:
    void paintEvent(QPaintEvent* event) override;
private:
    QWidget* contentWidget;  // 主内容区
    void initUI();
   
};

CustomWindow.cpp

#include "CustomWindow.h"
#include <QGraphicsDropShadowEffect>
#include <QPainter>
#include <QPainterPath>
#include <QVBoxLayout>
#include<qlabel.h>

CustomWindow::CustomWindow(QWidget* parent)
    : QWidget(parent) {
    setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
    setAttribute(Qt::WA_TranslucentBackground);  // 支持透明背景
    resize(600, 400);

    initUI();
}

void CustomWindow::initUI() {
    contentWidget = new QWidget(this);
    contentWidget->setObjectName("contentWidget");
    contentWidget->setStyleSheet("#contentWidget {"
        "   background-color: white;"
        "   border-radius: 10px;"
        "   border: 1px solid #E0E0E0;"
        "}");

    // 添加阴影
    QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);
    shadow->setBlurRadius(20);
    shadow->setOffset(0, 0);
    shadow->setColor(QColor(0, 0, 0, 80));
    contentWidget->setGraphicsEffect(shadow);

    auto label = new QLabel("窗口阴影与圆角", contentWidget);
    label->setAlignment(Qt::AlignCenter);
    label->setStyleSheet("font-size: 24px;");
   
    // 布局
    QVBoxLayout* layout = new QVBoxLayout(contentWidget);
    layout->addWidget(label);
    
    QVBoxLayout* mainLayout = new QVBoxLayout(this);
    mainLayout->setContentsMargins(10, 10, 10, 10);  // 阴影边距
    mainLayout->addWidget(contentWidget);
}



void CustomWindow::paintEvent(QPaintEvent* event) {
    // 绘制透明背景
    QPainter painter(this);
    painter.fillRect(rect(), Qt::transparent);
}



CustomWindow::~CustomWindow()
{}

效果展示

image.png

总结

本文从 Qt 默认窗口的局限出发,探讨如何脱离系统窗口样式,构建自定义的无边框窗口界面。我们主要完成了以下几个方面的工作:

  1. 基础无边框窗口实现 使用 Qt::FramelessWindowHint 去除原生边框,并设置透明背景以支持自绘界面。
  2. 添加自定义标题栏及按钮 模拟常见应用布局,自定义最小化、最大化/还原、关闭按钮,支持最大化状态样式切换、标题栏双击行为等。
  3. 实现窗口拖动与拉伸功能 通过重写 mousePressEvent / mouseMoveEvent 或使用 nativeEvent,实现窗口边缘拉伸和任意区域拖动。
  4. 圆角与阴影效果 使用 paintEvent 绘制圆角背景、搭配 QGraphicsDropShadowEffect 实现视觉上的柔和阴影,提升整体美观度。