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 件核心工作:
- 隐藏系统标题栏:通过
Qt::FramelessWindowHint
标志去除默认标题栏 - 启用鼠标跟踪:实时捕获鼠标移动,用于更新光标形状(如边缘 hover 时切换光标)
- 安装事件过滤器:确保子部件(如
QLabel
、QPushButton
)的鼠标事件能被主窗口捕获,避免光标更新失效
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
窗口的拖拽移动和边缘调整大小是无标题栏窗口的核心交互,依赖 mousePressEvent
、mouseMoveEvent
、mouseReleaseEvent
三个事件的协同处理。
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);
}