当前位置: 首页 > news >正文

Qt 自定义无标题栏窗口:FramelessWidget 实现与解析

文章目录

    • 一、核心功能概览
    • 二、代码核心模块解析
      • 2.1 类结构与成员变量
      • 2.2 构造函数:无标题栏初始化
      • 2.3 鼠标事件处理:拖拽与 Resize
        • 2.3.1 鼠标按下事件(mousePressEvent)
        • 2.3.2 鼠标移动事件(mouseMoveEvent)
        • 2.3.3 鼠标释放事件(mouseReleaseEvent)
      • 2.4 光标形状更新(updateCursorShape)
      • 2.5 窗口大小调整(handleResize)
      • 2.6 全屏切换与状态记忆
        • 2.6.1 双击切换全屏(mouseDoubleClickEvent)
        • 2.6.2 状态记忆(changeEvent)
      • 2.7 事件过滤器(eventFilter)
    • 三、使用示例
    • 四、源码

在 Qt 开发中,默认窗口的标题栏样式往往难以满足个性化 UI 需求。无论是桌面应用的品牌化设计,还是特定场景下的交互优化,自定义无标题栏窗口都是常见需求。本文将基于一份完整的 FramelessWidget 实现代码,详细解析无标题栏窗口的核心技术点,包括窗口拖拽、边缘调整大小、全屏切换等功能,帮助开发者快速掌握自定义窗口的实现思路。

在这里插入图片描述

一、核心功能概览

本文实现的 FramelessWidget 继承自 QWidget,去除了系统默认标题栏,同时保留并增强了窗口的核心交互能力,主要功能包括:

  • 无标题栏基础:通过 Qt 窗口标志隐藏系统标题栏
  • 窗口拖拽:鼠标点击内容区可拖拽移动窗口
  • 边缘调整大小:窗口边缘/角落 hover 时切换光标,支持拖拽调整大小
  • 全屏交互:双击窗口切换全屏/正常状态,鼠标靠近顶部也可触发全屏
  • 状态记忆:最小化恢复时,自动还原之前的全屏/正常状态
  • 子部件兼容:通过事件过滤器确保子部件不影响窗口的光标更新与交互

二、代码核心模块解析

2.1 类结构与成员变量

首先看 FramelessWidget 的类定义,核心成员变量用于存储窗口状态、鼠标位置和交互标记,先明确各变量的作用:

class FramelessWidget : public QWidget
{Q_OBJECT
public:explicit FramelessWidget(QWidget *parent = nullptr);~FramelessWidget();void setOldWindowState(Qt::WindowStates state); // 设置历史窗口状态protected:// 重写 Qt 事件处理函数void mousePressEvent(QMouseEvent *event) override;void mouseMoveEvent(QMouseEvent *event) override;void mouseReleaseEvent(QMouseEvent *event) override;void mouseDoubleClickEvent(QMouseEvent *event) override;bool eventFilter(QObject *obj, QEvent *event) override;void changeEvent(QEvent *event) override;// 自定义辅助函数void handleResize(QMouseEvent *event); // 处理窗口调整大小void updateCursorShape(const QPoint &globalPos); // 更新光标形状private:Qt::WindowStates m_OldWindowState; // 最小化前的窗口状态(用于恢复)Qt::WindowStates m_WindowState;    // 当前窗口状态bool m_readyMove;                  // 是否准备拖拽移动QPoint m_currentPos;               // 窗口初始位置(拖拽时用)QPoint m_mouseStartPoint;          // 鼠标按下时的全局位置(拖拽时用)bool m_resizing;                   // 是否正在调整窗口大小int m_resizeEdge;                  // 当前调整的窗口边缘(左/右/上/下/角落)QPoint m_resizeStartPos;           // 调整大小开始时的鼠标全局位置QRect m_resizeStartGeometry;       // 调整大小开始时的窗口几何信息
};

2.2 构造函数:无标题栏初始化

构造函数是无标题栏窗口的基础,主要完成 3 件核心工作:

  1. 隐藏系统标题栏:通过 Qt::FramelessWindowHint 标志去除默认标题栏
  2. 启用鼠标跟踪:实时捕获鼠标移动,用于更新光标形状(如边缘 hover 时切换光标)
  3. 安装事件过滤器:确保子部件(如 QLabelQPushButton)的鼠标事件能被主窗口捕获,避免光标更新失效
const int RESIZE_MARGIN = 10; // 窗口边缘可调整大小的区域宽度(10px)FramelessWidget::FramelessWidget(QWidget *parent) : QWidget(parent)
{// 1. 隐藏系统标题栏setWindowFlag(Qt::FramelessWindowHint);// 2. 启用鼠标跟踪(不点击也能捕获鼠标移动)setMouseTracking(true);// 3. 为所有子部件安装事件过滤器installEventFilter(this);
}

2.3 鼠标事件处理:拖拽与 Resize

窗口的拖拽移动和边缘调整大小是无标题栏窗口的核心交互,依赖 mousePressEventmouseMoveEventmouseReleaseEvent 三个事件的协同处理。

2.3.1 鼠标按下事件(mousePressEvent)

按下鼠标时,需要判断当前操作是“准备拖拽”还是“准备调整大小”:

  • 若鼠标在边缘区域(m_resizeEdge != 0):标记为“准备调整大小”,记录初始光标位置和窗口几何信息
  • 若鼠标在内容区:标记为“准备拖拽”,记录窗口初始位置和鼠标按下位置
  • 全屏状态下不响应任何按下事件
void FramelessWidget::mousePressEvent(QMouseEvent *event)
{if (this->windowState() == Qt::WindowFullScreen)return; // 全屏状态不响应if (event->button() == Qt::LeftButton){if (m_resizeEdge != 0){// 边缘按下:开始调整大小m_resizing = true;m_resizeStartPos = event->globalPos(); // 记录初始鼠标位置m_resizeStartGeometry = geometry();    // 记录初始窗口大小}else{// 内容区按下:准备拖拽m_readyMove = true;m_currentPos = frameGeometry().topLeft(); // 窗口初始位置(屏幕坐标)m_mouseStartPoint = event->globalPos();   // 鼠标按下位置(屏幕坐标)}}
}
2.3.2 鼠标移动事件(mouseMoveEvent)

移动鼠标时,根据当前状态(拖拽/Resize/无操作)执行不同逻辑:

  • 若正在调整大小(m_resizing):调用 handleResize 处理窗口大小变化
  • 若正在拖拽(m_readyMove):计算鼠标移动距离,更新窗口位置
  • 若无操作:调用 updateCursorShape 更新光标形状(如边缘 hover 显示 resize 光标)
void FramelessWidget::mouseMoveEvent(QMouseEvent *event)
{if (this->windowState() == Qt::WindowFullScreen)return; // 全屏状态不响应if (m_resizing){handleResize(event); // 处理调整大小}else if (m_readyMove){// 计算鼠标移动距离 = 当前鼠标位置 - 按下时的位置QPoint moveDistance = event->globalPos() - m_mouseStartPoint;// 窗口新位置 = 初始位置 + 移动距离move(m_currentPos + moveDistance);}else{updateCursorShape(event->globalPos()); // 更新光标形状}
}
2.3.3 鼠标释放事件(mouseReleaseEvent)

释放鼠标时,重置交互状态(拖拽/Resize 标记),并添加一个小彩蛋:鼠标靠近屏幕顶部(y ≤ 10px)释放时,触发全屏

void FramelessWidget::mouseReleaseEvent(QMouseEvent *event)
{if (event->button() == Qt::LeftButton){// 重置交互状态m_resizing = false;m_readyMove = false;m_resizeEdge = 0;setCursor(Qt::ArrowCursor); // 恢复默认光标// 彩蛋:靠近顶部释放触发全屏if (event->globalPos().y() <= 10 && !m_resizing){this->setWindowState(Qt::WindowFullScreen);}}
}

2.4 光标形状更新(updateCursorShape)

根据鼠标在窗口的位置,动态切换光标形状,提升用户体验:

  • 角落(左上/右下):对角 resize 光标(Qt::SizeFDiagCursor
  • 角落(右上/左下):对角 resize 光标(Qt::SizeBDiagCursor
  • 左右边缘:水平 resize 光标(Qt::SizeHorCursor
  • 上下边缘:垂直 resize 光标(Qt::SizeVerCursor
  • 内容区:默认箭头光标(Qt::ArrowCursor

同时处理特殊状态:全屏或最大化时,强制显示默认光标

void FramelessWidget::updateCursorShape(const QPoint &globalPos)
{// 全屏/最大化时不改变光标if (this->windowState() == Qt::WindowFullScreen || this->isMaximized()){setCursor(Qt::ArrowCursor);return;}// 将鼠标全局坐标(屏幕)转换为窗口局部坐标QPoint localPos = mapFromGlobal(globalPos);int x = localPos.x();int y = localPos.y();int width = this->width();int height = this->height();// 判断鼠标是否在边缘区域(RESIZE_MARGIN = 10px)bool left = x < RESIZE_MARGIN;bool right = x > width - RESIZE_MARGIN;bool top = y < RESIZE_MARGIN;bool bottom = y > height - RESIZE_MARGIN;// 根据边缘组合设置光标和 resizeEdgeif (left && top) { setCursor(Qt::SizeFDiagCursor); m_resizeEdge = Qt::TopEdge | Qt::LeftEdge; }else if (left && bottom) { setCursor(Qt::SizeBDiagCursor); m_resizeEdge = Qt::BottomEdge | Qt::LeftEdge; }else if (right && top) { setCursor(Qt::SizeBDiagCursor); m_resizeEdge = Qt::TopEdge | Qt::RightEdge; }else if (right && bottom) { setCursor(Qt::SizeFDiagCursor); m_resizeEdge = Qt::BottomEdge | Qt::RightEdge; }else if (left) { setCursor(Qt::SizeHorCursor); m_resizeEdge = Qt::LeftEdge; }else if (right) { setCursor(Qt::SizeHorCursor); m_resizeEdge = Qt::RightEdge; }else if (top) { setCursor(Qt::SizeVerCursor); m_resizeEdge = Qt::TopEdge; }else if (bottom) { setCursor(Qt::SizeVerCursor); m_resizeEdge = Qt::BottomEdge; }else { setCursor(Qt::ArrowCursor); m_resizeEdge = 0; }
}

2.5 窗口大小调整(handleResize)

handleResize 是调整窗口大小的核心逻辑,根据 m_resizeEdge 标记的边缘,计算窗口新的几何形状,并确保窗口不小于设置的最小大小(minimumWidth/minimumHeight)。

例如:

  • 调整左边缘:修改窗口的 left 坐标,若宽度小于最小值,则强制 left = 右边缘 - 最小宽度
  • 调整右边缘:修改窗口的 right 坐标,若宽度小于最小值,则强制 right = 左边缘 + 最小宽度
void FramelessWidget::handleResize(QMouseEvent *event)
{QRect newGeometry = m_resizeStartGeometry; // 初始窗口形状QPoint delta = event->globalPos() - m_resizeStartPos; // 鼠标移动距离// 左边缘调整:修改 leftif (m_resizeEdge & Qt::LeftEdge){newGeometry.setLeft(m_resizeStartGeometry.left() + delta.x());// 防止宽度小于最小值if (newGeometry.width() < minimumWidth()){newGeometry.setLeft(m_resizeStartGeometry.right() - minimumWidth());}}// 右边缘调整:修改 rightif (m_resizeEdge & Qt::RightEdge){newGeometry.setRight(m_resizeStartGeometry.right() + delta.x());if (newGeometry.width() < minimumWidth()){newGeometry.setRight(m_resizeStartGeometry.left() + minimumWidth());}}// 上边缘调整:修改 topif (m_resizeEdge & Qt::TopEdge){newGeometry.setTop(m_resizeStartGeometry.top() + delta.y());if (newGeometry.height() < minimumHeight()){newGeometry.setTop(m_resizeStartGeometry.bottom() - minimumHeight());}}// 下边缘调整:修改 bottomif (m_resizeEdge & Qt::BottomEdge){newGeometry.setBottom(m_resizeStartGeometry.bottom() + delta.y());if (newGeometry.height() < minimumHeight()){newGeometry.setBottom(m_resizeStartGeometry.top() + minimumHeight());}}// 应用新的窗口形状setGeometry(newGeometry);
}

2.6 全屏切换与状态记忆

2.6.1 双击切换全屏(mouseDoubleClickEvent)

双击窗口左键时,根据当前状态切换全屏/正常状态:

  • 若当前是全屏:切换为正常状态(Qt::WindowNoState
  • 若当前是正常状态:切换为全屏(Qt::WindowFullScreen
void FramelessWidget::mouseDoubleClickEvent(QMouseEvent *event)
{if (event->button() == Qt::LeftButton){if (this->windowState() == Qt::WindowFullScreen)this->setWindowState(Qt::WindowNoState); // 全屏 → 正常elsethis->setWindowState(Qt::WindowFullScreen); // 正常 → 全屏}
}
2.6.2 状态记忆(changeEvent)

当窗口状态变化时(如最小化、恢复),通过 changeEvent 记忆历史状态,避免最小化后恢复时丢失全屏状态:

  • 若之前是最小化(m_WindowState == Qt::WindowMinimized),且恢复时不是全屏,则检查 m_OldWindowState
  • m_OldWindowState 是全屏,则恢复为全屏
void FramelessWidget::changeEvent(QEvent *event)
{if (event->type() == QEvent::WindowStateChange){// 最小化恢复时,还原之前的全屏状态if (m_WindowState == Qt::WindowMinimized && this->windowState() != Qt::WindowFullScreen){if (m_OldWindowState == Qt::WindowFullScreen){this->setWindowState(Qt::WindowFullScreen);}}// 更新当前窗口状态m_WindowState = this->windowState();}QWidget::changeEvent(event);
}

通过 setOldWindowState 函数,外部可手动设置历史状态,例如在自定义标题栏的“最小化”按钮中调用:

void MyTitleBar::onMinimizeClicked()
{// 保存当前状态,用于恢复时判断是否全屏m_framelessWidget->setOldWindowState(m_framelessWidget->windowState());m_framelessWidget->showMinimized();
}

2.7 事件过滤器(eventFilter)

Qt 中,子部件(如 QPushButton)会优先捕获鼠标事件,导致主窗口无法收到鼠标移动事件,进而光标形状无法更新。通过事件过滤器,可将子部件的 MouseMove 事件传递给主窗口,确保光标更新正常。

bool FramelessWidget::eventFilter(QObject *obj, QEvent *event)
{// 子部件的鼠标移动事件,传递给主窗口处理(更新光标)if (event->type() == QEvent::MouseMove && !m_resizing && !m_readyMove){QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);updateCursorShape(mouseEvent->globalPos());}// 其他事件按默认逻辑处理return QWidget::eventFilter(obj, event);
}

三、使用示例

FramelessWidget 是一个基础类,可直接继承使用,或作为主窗口的基类。以下是一个简单示例:

// 1. 自定义窗口类,继承 FramelessWidget
#include "framelesswidget.h"
#include <QLabel>
#include <QVBoxLayout>class MyMainWindow : public FramelessWidget
{Q_OBJECT
public:MyMainWindow(QWidget *parent = nullptr) : FramelessWidget(parent){// 设置窗口最小大小(避免 resize 过小)setMinimumSize(800, 600);// 设置窗口背景色(区分内容区)setStyleSheet("background-color: #f5f5f5;");// 添加内容(示例:一个标签和按钮)QVBoxLayout *layout = new QVBoxLayout(this);layout->setContentsMargins(20, 20, 20, 20); // 内边距QLabel *titleLabel = new QLabel("Qt 无标题栏窗口示例", this);titleLabel->setStyleSheet("font-size: 28px; color: #333; font-weight: bold;");titleLabel->setAlignment(Qt::AlignCenter);QPushButton *testBtn = new QPushButton("测试按钮", this);testBtn->setStyleSheet("padding: 10px 20px; font-size: 16px;");layout->addWidget(titleLabel);layout->addWidget(testBtn);layout->addStretch(); // 拉伸填充}
};// 2. main 函数中使用
#include <QApplication>int main(int argc, char *argv[])
{QApplication a(argc, argv);MyMainWindow w;w.show(); // 显示窗口return a.exec();
}

四、源码

#include "framelesswidget.h"
#include <QMouseEvent>
#include <QCoreApplication>const int RESIZE_MARGIN = 10;FramelessWidget::FramelessWidget(QWidget *parent) : QWidget(parent)
{setWindowFlag(Qt::FramelessWindowHint);// 启用鼠标跟踪setMouseTracking(true);// 为所有子部件启用鼠标跟踪installEventFilter(this);
}FramelessWidget::~FramelessWidget()
{
}void FramelessWidget::setOldWindowState(Qt::WindowStates state)
{m_OldWindowState = state;
}void FramelessWidget::mousePressEvent(QMouseEvent *event)
{if (this->windowState() == Qt::WindowFullScreen){// 全屏状态下,不响应鼠标事件return;}if (event->button() == Qt::LeftButton){if (m_resizeEdge != 0){// 如果在边缘区域按下,开始调整大小m_resizing = true;m_resizeStartPos = event->globalPos();m_resizeStartGeometry = geometry();}else{// 鼠标在窗口内容区域按下了左键,准备开始移动m_readyMove = true;// 记录当前窗口和鼠标的位置m_currentPos = frameGeometry().topLeft();m_mouseStartPoint = event->globalPos();}}
}void FramelessWidget::mouseMoveEvent(QMouseEvent *event)
{if (this->windowState() == Qt::WindowFullScreen){// 全屏状态下,不响应鼠标事件return;}if (m_resizing){// 正在调整窗口大小handleResize(event);}else if (m_readyMove){// 正在移动窗口QPoint moveDistance = event->globalPos() - m_mouseStartPoint;move(m_currentPos + moveDistance);}else{// 更新鼠标光标形状updateCursorShape(event->globalPos());}
}void FramelessWidget::mouseReleaseEvent(QMouseEvent *event)
{if (event->button() == Qt::LeftButton){m_resizing = false;m_readyMove = false;m_resizeEdge = 0;// 恢复默认光标setCursor(Qt::ArrowCursor);// 靠近顶部全屏if (event->globalPos().y() <= 10 && !m_resizing){this->setWindowState(Qt::WindowFullScreen);}}
}void FramelessWidget::updateCursorShape(const QPoint &globalPos)
{if (this->windowState() == Qt::WindowFullScreen || this->isMaximized()){setCursor(Qt::ArrowCursor);return;}// 将全局坐标转换为窗口内的局部坐标QPoint localPos = mapFromGlobal(globalPos);int x = localPos.x();int y = localPos.y();int width = this->width();int height = this->height();// 检测鼠标在哪个边缘区域bool left = x < RESIZE_MARGIN;bool right = x > width - RESIZE_MARGIN;bool top = y < RESIZE_MARGIN;bool bottom = y > height - RESIZE_MARGIN;if (left && top){setCursor(Qt::SizeFDiagCursor);m_resizeEdge = Qt::TopEdge | Qt::LeftEdge;}else if (left && bottom){setCursor(Qt::SizeBDiagCursor);m_resizeEdge = Qt::BottomEdge | Qt::LeftEdge;}else if (right && top){setCursor(Qt::SizeBDiagCursor);m_resizeEdge = Qt::TopEdge | Qt::RightEdge;}else if (right && bottom){setCursor(Qt::SizeFDiagCursor);m_resizeEdge = Qt::BottomEdge | Qt::RightEdge;}else if (left){setCursor(Qt::SizeHorCursor);m_resizeEdge = Qt::LeftEdge;}else if (right){setCursor(Qt::SizeHorCursor);m_resizeEdge = Qt::RightEdge;}else if (top){setCursor(Qt::SizeVerCursor);m_resizeEdge = Qt::TopEdge;}else if (bottom){setCursor(Qt::SizeVerCursor);m_resizeEdge = Qt::BottomEdge;}else{setCursor(Qt::ArrowCursor);m_resizeEdge = 0;}
}void FramelessWidget::handleResize(QMouseEvent *event)
{QRect newGeometry = m_resizeStartGeometry;QPoint delta = event->globalPos() - m_resizeStartPos;if (m_resizeEdge & Qt::LeftEdge){newGeometry.setLeft(m_resizeStartGeometry.left() + delta.x());if (newGeometry.width() < minimumWidth()){newGeometry.setLeft(m_resizeStartGeometry.right() - minimumWidth());}}if (m_resizeEdge & Qt::RightEdge){newGeometry.setRight(m_resizeStartGeometry.right() + delta.x());if (newGeometry.width() < minimumWidth()){newGeometry.setRight(m_resizeStartGeometry.left() + minimumWidth());}}if (m_resizeEdge & Qt::TopEdge){newGeometry.setTop(m_resizeStartGeometry.top() + delta.y());if (newGeometry.height() < minimumHeight()){newGeometry.setTop(m_resizeStartGeometry.bottom() - minimumHeight());}}if (m_resizeEdge & Qt::BottomEdge){newGeometry.setBottom(m_resizeStartGeometry.bottom() + delta.y());if (newGeometry.height() < minimumHeight()){newGeometry.setBottom(m_resizeStartGeometry.top() + minimumHeight());}}setGeometry(newGeometry);
}bool FramelessWidget::eventFilter(QObject *obj, QEvent *event)
{// 将鼠标移动事件传递给主窗口,用于更新光标形状if (event->type() == QEvent::MouseMove && !m_resizing && !m_readyMove){QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event);updateCursorShape(mouseEvent->globalPos());}return QWidget::eventFilter(obj, event);
}void FramelessWidget::mouseDoubleClickEvent(QMouseEvent *event)
{if (event->button() == Qt::LeftButton){if (this->windowState() == Qt::WindowFullScreen){this->setWindowState(Qt::WindowNoState);}else{this->setWindowState(Qt::WindowFullScreen);}}
}void FramelessWidget::changeEvent(QEvent *event)
{if (event->type() == QEvent::WindowStateChange){if (m_WindowState == Qt::WindowMinimized && this->windowState() != Qt::WindowFullScreen){if (m_OldWindowState == Qt::WindowFullScreen){this->setWindowState(Qt::WindowFullScreen);}}m_WindowState = this->windowState();}QWidget::changeEvent(event);
}
http://www.dtcms.com/a/462534.html

相关文章:

  • 海林建设局网站济南百度推广排名优化
  • 扩充ec2硬盘对应的lvm
  • 哪些公司的网站做的漂亮上海优化排名推广
  • 做素材类的网站赚钱吗改进网站建设
  • 中山好的网站建设西安公关公司
  • 网页网站的制作过程dedecms建设慕课网站
  • K8S(二)—— K8S 1.28 集群部署指南(kubeadm 方式)
  • Eclipse Mosquitto 在小内存下怎么修改配置文件
  • 猫眼浏览器(Chrome内核增强版浏览器)官方便携版
  • 福建省住房和建设厅网站合同 制作 网站
  • 构建可信数据体系——解读数据治理指南-构建可信数据路线图【附全文阅读】
  • qemu调试edk2
  • 正规网站做菠菜广告焦作网站建设哪家正规
  • 有什么网站可以发布个人信息网站优化计划书
  • 小程序停车场名称动态化实现方案
  • 张家港建设工程质量监督站网站中国廉政文化建设网站
  • sunshine :Moonlight 的自托管游戏串流服务端
  • 防爆手机与普通手机的区别!
  • 铁路建设标准网站专业网站建
  • 免费建网站可信吗wordpress 仪表盘界面
  • SQL Server 限制IP访问数据库的设置方法及注意事项
  • 测试中的 AAA 模式与 Given–When–Then 模式详解
  • Android Studio 虚拟机启动失败/没反应,排查原因。提供一种排查方式。
  • 网站设计需要考虑的基本原则建房子找哪个网站设计
  • C#基础11-常用类
  • 流媒体 网站开发网站制作排版
  • 那个网站适合学生做兼职广州网站车管所
  • 公司内部网站系统微信小程序直播开通条件
  • 个人备案做门户网站wordpress 视频
  • 网站开发公司地址免费引流软件下载