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


在 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;
// 根据边缘组合设置光标和 resizeEdge
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; }
}

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; // 鼠标移动距离
// 左边缘调整:修改 left
if (m_resizeEdge & Qt::LeftEdge)
{
newGeometry.setLeft(m_resizeStartGeometry.left() + delta.x());
// 防止宽度小于最小值
if (newGeometry.width() < minimumWidth())
{
newGeometry.setLeft(m_resizeStartGeometry.right() - minimumWidth());
}
}
// 右边缘调整:修改 right
if (m_resizeEdge & Qt::RightEdge)
{
newGeometry.setRight(m_resizeStartGeometry.right() + delta.x());
if (newGeometry.width() < minimumWidth())
{
newGeometry.setRight(m_resizeStartGeometry.left() + minimumWidth());
}
}
// 上边缘调整:修改 top
if (m_resizeEdge & Qt::TopEdge)
{
newGeometry.setTop(m_resizeStartGeometry.top() + delta.y());
if (newGeometry.height() < minimumHeight())
{
newGeometry.setTop(m_resizeStartGeometry.bottom() - minimumHeight());
}
}
// 下边缘调整:修改 bottom
if (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); // 全屏 → 正常
else
this->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);
      }
posted @ 2025-11-08 12:41  clnchanpin  阅读(24)  评论(0)    收藏  举报