Qt 高级编程 035:无边框窗口+自定义标题栏 完整实现详解

Qt 高级编程 035:无边框窗口+自定义标题栏 完整实现详解

在桌面客户端开发领域🎨,原生窗口自带的边框、标题栏样式千篇一律,难以契合个性化 UI 设计需求。基于 Qt 框架打造无边框窗口,并搭配自研标题栏、实现窗口拖拽与边缘拉伸功能,已是现代桌面软件的主流开发方案。本文将由浅入深,从项目搭建、无边框配置、鼠标拖拽逻辑,到自定义标题栏、窗口拉伸优化,完整拆解整套实现流程,附核心代码与踩坑解决方案💻。

Bilibili 同步视频

Qt 高级编程 035:无边框窗口+自定义标题栏 完整实现详解

一、前期准备:创建 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);
}

代码解析

  1. Qt::FramelessWindowHint:核心标志位,彻底移除系统原生窗口边框

  2. Qt::WindowMinimizeButtonHint:保留窗口最小化功能标识,适配基础窗口操作;

  3. 编译运行后可观察效果:窗口边框完全消失,但此时衍生出两大问题:窗口无法拖拽移动、无法边缘拉伸,且原生关闭、最大化按钮一并失效,这也是后续代码需要重点解决的痛点⚠️。

二、核心进阶一:纯无边框窗口实现鼠标拖拽移动🚀

去除边框后,系统默认的窗口拖拽逻辑失效,我们需要重写 Qt 鼠标事件,通过计算鼠标与窗口的相对位移,手动实现拖拽效果。

2.1 实现原理剖析📐

直接将窗口坐标绑定鼠标绝对位置,会出现窗口 "瞬移、闪烁" 的问题,核心原因是丢失了相对偏移量

  1. 鼠标按下瞬间,记录鼠标桌面绝对坐标窗口左上角桌面绝对坐标,计算二者偏移差值;

  2. 鼠标拖动过程中,实时获取鼠标新的绝对坐标,用新坐标减去初始偏移量,作为窗口新坐标;

  3. 遵循矢量平移逻辑,保证鼠标拖拽时,窗口跟随鼠标平稳移动,视觉体验流畅无闪烁。

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);
}

布局踩坑说明💡

  1. 布局默认存在内边距,会导致窗口四周出现白色留白,必须调用setContentsMargins(0,0,0,0)清空边距;

  2. 控件未设置固定大小,会出现挤压、重叠问题,图标、按钮统一设置32*32标准尺寸;

  3. 使用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);
}

代码解析

  1. winId():获取 Qt 窗口对应的 Windows 原生句柄HWND

  2. WM_NCLBUTTONDOWN:Windows 非客户区鼠标左键按下消息;

  3. HTCAPTION:标识点击区域为窗口标题栏,系统自动接管拖拽逻辑;

  4. 优势:完美区分标题栏与功能按钮,点击按钮不会误触发拖拽,是商用项目首选方案。

五、核心进阶四:窗口边缘拉伸功能实现📏

无边框窗口失去系统原生拉伸功能,我们通过重写nativeWindowEvent原生窗口事件,检测鼠标位置,判断鼠标是否悬浮在窗口左、右、上、下及四角边缘,触发对应拉伸逻辑。

5.1 拉伸核心原理

  1. 自定义边缘判定宽度(示例设为 5px):鼠标距离窗口边缘≤5px 时,判定为拉伸区域;

  2. nativeWindowEvent中拦截 Windows 鼠标消息,根据鼠标坐标匹配边缘位置;

  3. 区分上下左右单边拉伸四角斜向拉伸,调用系统原生拉伸消息;

  4. 兼容布局边距为 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 关键问题优化 & 踩坑修复🔧

  1. 布局边距为 0 导致拉伸失效

    当窗口布局setContentsMargins(0,0,0,0)时,子控件会完全覆盖窗口边缘,鼠标判定失效。解决方案:给主窗口添加悬浮属性:

    cpp 复制代码
    this->setAttribute(Qt::WA_Hover, true);
  2. 标题栏区域冲突

    鼠标落在标题栏控件上时,优先执行拖拽逻辑,屏蔽拉伸判定。在坐标判断中增加区域拦截,检测到鼠标命中子控件时直接返回,不再执行拉伸代码。

  3. **ReleaseCapture()**作用

    释放鼠标捕获状态,保证 Windows 消息可以正常接收鼠标操作,是拉伸功能必不可少的 API。

六、整体总结与拓展延伸📝

6.1 功能整体回顾

本文基于 Qt 框架,循序渐进完成了三大核心功能:

  1. 无边框窗口 :通过FramelessWindowHint标志位移除系统原生边框;

  2. 窗口拖拽:两种方案 ------ 纯鼠标偏移计算(空白窗口)、Windows 消息模拟(标题栏窗口,商用推荐);

  3. 自定义标题栏:独立类封装 + 水平布局,实现图标、标题、三大功能按钮;

  4. 边缘拉伸:拦截原生 Windows 消息,检测鼠标位置实现八方向拉伸,配套解决布局遮挡问题。

6.2 拓展开发方向

  1. 为标题栏按钮绑定最小化、最大化、关闭槽函数,完善窗口基础操作;

  2. 适配多平台:本文 Windows 消息方案仅适用于 Windows 系统,Linux/macOS 需改用 Qt 跨平台鼠标逻辑;

  3. 美化升级:替换按钮图标、添加 hover 悬浮样式、实现窗口阴影、磨砂玻璃特效;

  4. 交互优化:限制窗口最小尺寸,防止拉伸至过小导致界面错乱。

6.3 开发建议

无边框窗口是 Qt 桌面美化的基础知识点,Windows 消息方案 在兼容性、交互体验上优于纯 Qt 鼠标事件,建议在正式项目中优先使用。同时开发过程中重点关注布局边距、控件遮挡、消息拦截三大高频问题,可大幅减少调试耗时。

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

相关推荐
随性而行3601 小时前
微信API接口与AI自动化:开发者的实现思路
运维·服务器·开发语言·人工智能·微信·自动化
lingran__1 小时前
C++_STL简介
开发语言·c++
南国韭菜2 小时前
【无标题】
c++·qt
旋律翼22 小时前
Qt Bridges for C# 深度技术解析
开发语言·qt·c#
弹简特2 小时前
【Java项目-企悦抽】07-用户与认证模块-实现用户注册01
java·开发语言
L歪歪君2 小时前
Apache Doris全链路性能优化实战指南:从架构设计到生产落地
java·开发语言·算法
你怎么知道我是队长2 小时前
JavaScript 的控制语句介绍
开发语言·javascript·ecmascript
没用的阿_吉2 小时前
vs2022offline 生成
c++
RSTJ_16253 小时前
PYTHON+AI LLM DAY ONE HUNDRED AND THREE
开发语言·人工智能·python
qq_416409133 小时前
专业的openclaw推荐
c++