Qt 高级编程 035:无边框窗口+自定义标题栏 完整实现详解
- [Bilibili 同步视频](#Bilibili 同步视频)
- [一、前期准备:创建 Qt 基础工程📂](#一、前期准备:创建 Qt 基础工程📂)
- 二、核心进阶一:纯无边框窗口实现鼠标拖拽移动🚀
-
- [2.1 实现原理剖析📐](#2.1 实现原理剖析📐)
- [2.2 完整代码实现](#2.2 完整代码实现)
- [2.3 效果与问题总结](#2.3 效果与问题总结)
- 三、核心进阶二:自定义标题栏开发🎛️
-
- [3.1 标题栏类整体设计](#3.1 标题栏类整体设计)
- [3.2 标题栏完整代码](#3.2 标题栏完整代码)
-
- [标题栏头文件 CTitleBar.h](#标题栏头文件 CTitleBar.h)
- [标题栏源文件 CTitleBar.cpp](#标题栏源文件 CTitleBar.cpp)
- [3.3 主窗口整合标题栏](#3.3 主窗口整合标题栏)
-
- [修改后 MainWindow.cpp](#修改后 MainWindow.cpp)
- 布局踩坑说明💡
- [四、核心进阶三:标题栏专属拖拽方案(Windows 消息版)📨](#四、核心进阶三:标题栏专属拖拽方案(Windows 消息版)📨)
-
- [4.1 实现思路](#4.1 实现思路)
- [4.2 代码改造(Windows 平台专用)](#4.2 代码改造(Windows 平台专用))
- 五、核心进阶四:窗口边缘拉伸功能实现📏
- 六、整体总结与拓展延伸📝
-
- [6.1 功能整体回顾](#6.1 功能整体回顾)
- [6.2 拓展开发方向](#6.2 拓展开发方向)
- [6.3 开发建议](#6.3 开发建议)
在桌面客户端开发领域🎨,原生窗口自带的边框、标题栏样式千篇一律,难以契合个性化 UI 设计需求。基于 Qt 框架打造无边框窗口,并搭配自研标题栏、实现窗口拖拽与边缘拉伸功能,已是现代桌面软件的主流开发方案。本文将由浅入深,从项目搭建、无边框配置、鼠标拖拽逻辑,到自定义标题栏、窗口拉伸优化,完整拆解整套实现流程,附核心代码与踩坑解决方案💻。
Bilibili 同步视频
一、前期准备:创建 Qt 基础工程📂
1.1 项目新建流程
打开 Qt 开发工具,在解决方案目录上右键,选择新建项目 ,项目模板选用Widget Application。
-
项目命名:示例命名为
3-2,主窗体基类设置为Widget; -
主类名称:统一修改为
MainWindow,保持代码规范; -
冗余清理:工程创建完成后,删除默认 UI 文件,本次全程纯代码布局,无需可视化 UI 编辑文件;
-
启动配置:将当前工程设置为启动项目,保证编译运行正常。
1.2 窗口无边框属性配置
Qt 默认窗口带有系统原生边框、标题栏与控制按钮,想要去除边框,核心依靠窗口标志位(Window Flags) 配置。
核心代码
cpp
// MainWindow.h
#include <QWidget>
class MainWindow : public QWidget
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
};
cpp
// MainWindow.cpp
#include "MainWindow.h"
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
// 配置无边框核心属性
this->setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint);
}
代码解析
-
Qt::FramelessWindowHint:核心标志位,彻底移除系统原生窗口边框; -
Qt::WindowMinimizeButtonHint:保留窗口最小化功能标识,适配基础窗口操作; -
编译运行后可观察效果:窗口边框完全消失,但此时衍生出两大问题:窗口无法拖拽移动、无法边缘拉伸,且原生关闭、最大化按钮一并失效,这也是后续代码需要重点解决的痛点⚠️。
二、核心进阶一:纯无边框窗口实现鼠标拖拽移动🚀
去除边框后,系统默认的窗口拖拽逻辑失效,我们需要重写 Qt 鼠标事件,通过计算鼠标与窗口的相对位移,手动实现拖拽效果。
2.1 实现原理剖析📐
直接将窗口坐标绑定鼠标绝对位置,会出现窗口 "瞬移、闪烁" 的问题,核心原因是丢失了相对偏移量:
-
鼠标按下瞬间,记录鼠标桌面绝对坐标 与窗口左上角桌面绝对坐标,计算二者偏移差值;
-
鼠标拖动过程中,实时获取鼠标新的绝对坐标,用新坐标减去初始偏移量,作为窗口新坐标;
-
遵循矢量平移逻辑,保证鼠标拖拽时,窗口跟随鼠标平稳移动,视觉体验流畅无闪烁。
2.2 完整代码实现
头文件声明(重写鼠标事件)
cpp
// MainWindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QWidget>
#include <QMouseEvent>
#include <QPoint>
class MainWindow : public QWidget
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
// 重写鼠标按下、鼠标移动事件,protected权限为Qt事件重写规范
protected:
// 鼠标按下事件
void mousePressEvent(QMouseEvent *event) override;
// 鼠标移动事件
void mouseMoveEvent(QMouseEvent *event) override;
private:
QPoint m_offset; // 存储鼠标与窗口的相对偏移量
};
#endif // MAINWINDOW_H
源文件逻辑实现
cpp
// MainWindow.cpp
#include "MainWindow.h"
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
// 设置无边框
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint);
// 设置窗口初始大小
this->resize(800, 600);
}
// 鼠标按下:记录初始偏移量
void MainWindow::mousePressEvent(QMouseEvent *event)
{
// 仅响应鼠标左键按下
if (event->button() == Qt::LeftButton)
{
// globalPos():鼠标相对于桌面的绝对坐标
// pos():鼠标相对于当前窗口的相对坐标
// this->pos():窗口左上角相对于桌面的绝对坐标
m_offset = event->globalPos() - this->pos();
}
// 保留父类原有事件逻辑
QWidget::mousePressEvent(event);
}
// 鼠标移动:根据偏移量移动窗口
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
// 左键按住时执行拖拽
if (event->buttons() & Qt::LeftButton)
{
// 新窗口坐标 = 鼠标新绝对坐标 - 初始偏移量
this->move(event->globalPos() - m_offset);
}
QWidget::mouseMoveEvent(event);
}
2.3 效果与问题总结
编译运行后,按住窗口任意位置均可平稳拖拽,无闪烁、瞬移现象。但该方案仅适用于纯无边框空白窗口 ,若后续添加自定义标题栏、功能按钮,此拖拽逻辑会与子控件冲突,因此下文中会提供适配标题栏的全新拖拽方案。
三、核心进阶二:自定义标题栏开发🎛️
专业桌面软件均会自研标题栏,集成程序图标、窗口标题、最小化 / 最大化 / 关闭按钮,替代系统原生标题栏。本节从零封装独立标题栏类,并完成布局适配。
3.1 标题栏类整体设计
新建普通 C++ 类CTitleBar,继承自QWidget,内部封装:图标标签、标题标签、三大功能按钮,采用水平布局管理控件,同时封装 UI 初始化逻辑,解耦代码。
3.2 标题栏完整代码
标题栏头文件 CTitleBar.h
cpp
#ifndef CTITLEBAR_H
#define CTITLEBAR_H
#include <QWidget>
#include <QLabel>
#include <QPushButton>
#include <QHBoxLayout>
class CTitleBar : public QWidget
{
Q_OBJECT
public:
explicit CTitleBar(QWidget *parent = nullptr);
~CTitleBar() override = default;
private:
// 私有方法:初始化所有UI控件
void initUI();
private:
QLabel* m_pLogoLab; // 程序图标
QLabel* m_pTitleLab; // 窗口标题
QPushButton* m_pMinBtn; // 最小化按钮
QPushButton* m_pMaxBtn; // 最大化按钮
QPushButton* m_pCloseBtn; // 关闭按钮
QHBoxLayout* m_pHLayout; // 水平布局
};
#endif // CTITLEBAR_H
标题栏源文件 CTitleBar.cpp
cpp
#include "CTitleBar.h"
CTitleBar::CTitleBar(QWidget *parent)
: QWidget(parent)
{
initUI(); // 初始化UI
// 设置标题栏固定高度,适配整体界面
this->setFixedHeight(42);
// 设置标题栏背景色,区分主体窗口
this->setStyleSheet("background-color: #383838;");
}
void CTitleBar::initUI()
{
// 1. 实例化所有控件
m_pLogoLab = new QLabel(this);
m_pTitleLab = new QLabel("Qt无边框窗口演示", this);
m_pMinBtn = new QPushButton("-", this);
m_pMaxBtn = new QPushButton("□", this);
m_pCloseBtn = new QPushButton("×", this);
// 2. 设置控件固定大小,防止布局错乱
m_pLogoLab->setFixedSize(32, 32);
m_pMinBtn->setFixedSize(32, 32);
m_pMaxBtn->setFixedSize(32, 32);
m_pCloseBtn->setFixedSize(32, 32);
// 3. 初始化水平布局,设置边距
m_pHLayout = new QHBoxLayout(this);
m_pHLayout->setContentsMargins(5, 5, 5, 5); // 上下左右内边距5px
m_pHLayout->setSpacing(10); // 控件之间间距10px
// 4. 向布局添加控件,添加弹簧实现左右分布
m_pHLayout->addWidget(m_pLogoLab);
m_pHLayout->addWidget(m_pTitleLab);
m_pHLayout->addStretch(); // 弹性弹簧,将按钮挤至右侧
m_pHLayout->addWidget(m_pMinBtn);
m_pHLayout->addWidget(m_pMaxBtn);
m_pHLayout->addWidget(m_pCloseBtn);
}
3.3 主窗口整合标题栏
在MainWindow中创建标题栏对象,使用垂直布局将标题栏置于顶部,下方放置窗口内容区域,同时清空布局默认边距,消除白色留白。
修改后 MainWindow.cpp
cpp
#include "MainWindow.h"
#include "CTitleBar.h"
#include <QVBoxLayout>
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
// 1. 无边框配置
setWindowFlags(Qt::FramelessWindowHint | Qt::WindowMinimizeButtonHint);
this->resize(800, 600);
// 2. 创建标题栏
CTitleBar* pTitleBar = new CTitleBar(this);
// 3. 垂直布局:标题栏 + 内容区域
QVBoxLayout* pVLayout = new QVBoxLayout(this);
pVLayout->setContentsMargins(0, 0, 0, 0); // 清空布局边距,消除留白
pVLayout->setSpacing(0);
pVLayout->addWidget(pTitleBar);
// 下方内容占位区域
QWidget* pContent = new QWidget(this);
pContent->setStyleSheet("background-color: #FFFFFF;");
pVLayout->addWidget(pContent);
}
// 鼠标事件代码此处暂时保留,下文替换为标题栏专属拖拽方案
void MainWindow::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
{
m_offset = event->globalPos() - this->pos();
}
QWidget::mousePressEvent(event);
}
void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton)
{
this->move(event->globalPos() - m_offset);
}
QWidget::mouseMoveEvent(event);
}
布局踩坑说明💡
-
布局默认存在内边距,会导致窗口四周出现白色留白,必须调用
setContentsMargins(0,0,0,0)清空边距; -
控件未设置固定大小,会出现挤压、重叠问题,图标、按钮统一设置
32*32标准尺寸; -
使用
addStretch()弹性弹簧,实现 "左侧图标 + 标题、右侧功能按钮" 的经典标题栏布局。
四、核心进阶三:标题栏专属拖拽方案(Windows 消息版)📨
上文全局鼠标拖拽会导致点击按钮、图标也能拖拽窗口 ,不符合软件交互逻辑。本节改用Windows 原生消息机制,仅在标题栏区域实现拖拽,兼容性更强、逻辑更规范。
4.1 实现思路
重写mousePressEvent,在鼠标按下时,向 Windows 系统发送窗口拖拽消息WM_NCLBUTTONDOWN,配合HTCAPTION标识模拟点击系统标题栏行为,无需手动计算偏移量,代码更简洁。
4.2 代码改造(Windows 平台专用)
头文件引入依赖
cpp
// MainWindow.h 顶部添加Windows头文件(仅Windows平台)
#include <QtWinExtras>
#include <Windows.h>
重写鼠标按下事件(替换原有拖拽逻辑)
cpp
void MainWindow::mousePressEvent(QMouseEvent *event)
{
// 仅左键按下时触发标题栏拖拽
if (event->button() == Qt::LeftButton)
{
// 发送Windows消息:模拟点击系统标题栏拖拽
SendMessage((HWND)this->winId(), WM_NCLBUTTONDOWN, HTCAPTION, 0);
}
QWidget::mousePressEvent(event);
}
代码解析
-
winId():获取 Qt 窗口对应的 Windows 原生句柄HWND; -
WM_NCLBUTTONDOWN:Windows 非客户区鼠标左键按下消息; -
HTCAPTION:标识点击区域为窗口标题栏,系统自动接管拖拽逻辑; -
优势:完美区分标题栏与功能按钮,点击按钮不会误触发拖拽,是商用项目首选方案。
五、核心进阶四:窗口边缘拉伸功能实现📏
无边框窗口失去系统原生拉伸功能,我们通过重写nativeWindowEvent原生窗口事件,检测鼠标位置,判断鼠标是否悬浮在窗口左、右、上、下及四角边缘,触发对应拉伸逻辑。
5.1 拉伸核心原理
-
自定义边缘判定宽度(示例设为 5px):鼠标距离窗口边缘≤5px 时,判定为拉伸区域;
-
在
nativeWindowEvent中拦截 Windows 鼠标消息,根据鼠标坐标匹配边缘位置; -
区分上下左右单边拉伸 与四角斜向拉伸,调用系统原生拉伸消息;
-
兼容布局边距为 0 的场景,添加
WA_Hover属性,解决控件遮挡导致的鼠标判定失效问题。
5.2 完整拉伸代码实现
头文件声明原生事件
cpp
// MainWindow.h 类内添加声明
protected:
// 重写Qt原生窗口事件,拦截Windows消息
bool nativeWindowEvent(QEvent *event) override;
private:
const int m_borderWidth = 5; // 窗口拉伸边缘宽度 5px
源文件实现拉伸逻辑
cpp
// MainWindow.cpp
bool MainWindow::nativeWindowEvent(QEvent *event)
{
// 拦截Windows原生消息
if (event->type() == QEvent::WinEventAct)
{
MSG* pMsg = reinterpret_cast<MSG*>(event->nativeEvent());
if (pMsg == nullptr)
return false;
// 判定鼠标位置,执行拉伸
POINT mousePos = pMsg->pt;
QRect winRect = this->frameGeometry();
int x = mousePos.x;
int y = mousePos.y;
int w = winRect.width();
int h = winRect.height();
int left = winRect.left();
int top = winRect.top();
// 判定左边缘
if (x >= left && x <= left + m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTLEFT, 0);
}
// 判定右边缘
else if (x >= left + w - m_borderWidth && x <= left + w)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTRIGHT, 0);
}
// 判定上边缘
else if (y >= top && y <= top + m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTTOP, 0);
}
// 判定下边缘
else if (y >= top + h - m_borderWidth && y <= top + h)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTBOTTOM, 0);
}
// 左上角
else if (x <= left + m_borderWidth && y <= top + m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTTOPLEFT, 0);
}
// 右上角
else if (x >= left + w - m_borderWidth && y <= top + m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTTOPRIGHT, 0);
}
// 左下角
else if (x <= left + m_borderWidth && y >= top + h - m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTBOTTOMLEFT, 0);
}
// 右下角
else if (x >= left + w - m_borderWidth && y >= top + h - m_borderWidth)
{
::ReleaseCapture();
::SendMessage(pMsg->hwnd, WM_NCLBUTTONDOWN, HTBOTTOMRIGHT, 0);
}
}
return QWidget::nativeWindowEvent(event);
}
5.3 关键问题优化 & 踩坑修复🔧
-
布局边距为 0 导致拉伸失效
当窗口布局
setContentsMargins(0,0,0,0)时,子控件会完全覆盖窗口边缘,鼠标判定失效。解决方案:给主窗口添加悬浮属性:cppthis->setAttribute(Qt::WA_Hover, true); -
标题栏区域冲突
鼠标落在标题栏控件上时,优先执行拖拽逻辑,屏蔽拉伸判定。在坐标判断中增加区域拦截,检测到鼠标命中子控件时直接返回,不再执行拉伸代码。
-
**
ReleaseCapture()**作用释放鼠标捕获状态,保证 Windows 消息可以正常接收鼠标操作,是拉伸功能必不可少的 API。
六、整体总结与拓展延伸📝
6.1 功能整体回顾
本文基于 Qt 框架,循序渐进完成了三大核心功能:
-
无边框窗口 :通过
FramelessWindowHint标志位移除系统原生边框; -
窗口拖拽:两种方案 ------ 纯鼠标偏移计算(空白窗口)、Windows 消息模拟(标题栏窗口,商用推荐);
-
自定义标题栏:独立类封装 + 水平布局,实现图标、标题、三大功能按钮;
-
边缘拉伸:拦截原生 Windows 消息,检测鼠标位置实现八方向拉伸,配套解决布局遮挡问题。
6.2 拓展开发方向
-
为标题栏按钮绑定最小化、最大化、关闭槽函数,完善窗口基础操作;
-
适配多平台:本文 Windows 消息方案仅适用于 Windows 系统,Linux/macOS 需改用 Qt 跨平台鼠标逻辑;
-
美化升级:替换按钮图标、添加 hover 悬浮样式、实现窗口阴影、磨砂玻璃特效;
-
交互优化:限制窗口最小尺寸,防止拉伸至过小导致界面错乱。
6.3 开发建议
无边框窗口是 Qt 桌面美化的基础知识点,Windows 消息方案 在兼容性、交互体验上优于纯 Qt 鼠标事件,建议在正式项目中优先使用。同时开发过程中重点关注布局边距、控件遮挡、消息拦截三大高频问题,可大幅减少调试耗时。

整套代码可直接编译运行,基于 Qt5/Qt6 均兼容,是桌面客户端、播放器、工具类软件的通用基础模板✨。