
在桌面应用开发中默认的窗口标题栏样式往往无法满足个性化 UI 需求。Qt 允许我们移除系统原生标题栏然后自定义一个完全属于自己的标题栏同时保留窗口的拖拽移动、缩放、最大化/还原、最小化和关闭等基本功能。本文将详细介绍如何实现无边框窗口Qt::FramelessWindowHint自定义标题栏包含图标、标题、三个控制按钮标题栏拖拽移动窗口利用 Windows API 的SendMessage标题栏双击最大化/还原窗口边缘缩放通过拦截WM_NCHITTEST消息⚠️ 注意文中缩放和标题栏拖拽的实现用到了 Windows 原生 APIuser32.lib因此仅支持 Windows 平台。若需要跨平台方案可以参考文末的扩展思路。一、整体设计思路设置主窗口为无边框。创建一个自定义标题栏控件TitleBar包含图标QLabel标题QLabel最小化、最大化/还原、关闭按钮QPushButton将TitleBar添加到主窗口的布局顶部。实现标题栏的鼠标事件鼠标双击 → 窗口最大化/还原鼠标按下在标题栏区域 → 通过 Windows API 触发窗口拖动为主窗口安装事件过滤器到TitleBar使标题栏能响应窗口标题/图标的变化。重写主窗口的nativeEvent处理WM_NCHITTEST消息实现窗口边缘缩放。二、创建自定义标题栏TitleBar2.1 头文件titlebar.h#ifndef TITLEBAR_H #define TITLEBAR_H #include QWidget #include QLabel #include QPushButton class TitleBar : public QWidget { Q_OBJECT public: explicit TitleBar(QWidget *parent nullptr); ~TitleBar(); protected: // 双击标题栏最大化/还原 void mouseDoubleClickEvent(QMouseEvent *event) override; // 鼠标按下时开始拖动窗口仅 Windows void mousePressEvent(QMouseEvent *event) override; // 事件过滤器监听主窗口的标题/图标/大小变化 bool eventFilter(QObject *obj, QEvent *event) override; private slots: void onButtonClicked(); // 处理三个按钮的点击 private: void updateMaximizeState(); // 更新最大化按钮的状态图标/tooltip private: QLabel *m_iconLabel; QLabel *m_titleLabel; QPushButton *m_minimizeBtn; QPushButton *m_maximizeBtn; QPushButton *m_closeBtn; }; #endif // TITLEBAR_H2.2 实现文件 titlebar.cpp#include titlebar.h #include QHBoxLayout #include QMouseEvent #include QApplication #ifdef Q_OS_WIN #include qt_windows.h #pragma comment(lib, user32.lib) #endif TitleBar::TitleBar(QWidget *parent) : QWidget(parent) { setFixedHeight(32); // 固定标题栏高度 setObjectName(TitleBar); // 方便样式表定位 // 创建控件 m_iconLabel new QLabel(this); m_titleLabel new QLabel(this); m_minimizeBtn new QPushButton(this); m_maximizeBtn new QPushButton(this); m_closeBtn new QPushButton(this); // 图标样式 m_iconLabel-setFixedSize(20, 20); m_iconLabel-setScaledContents(true); // 标题文字自动拉伸 m_titleLabel-setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); // 按钮样式可替换为自己的图标 m_minimizeBtn-setIconSize(QSize(27, 22)); m_minimizeBtn-setIcon(QIcon(:/images/min.png)); m_minimizeBtn-setFlat(true); m_minimizeBtn-setToolTip(tr(最小化)); m_maximizeBtn-setIconSize(QSize(27, 22)); m_maximizeBtn-setIcon(QIcon(:/images/max.png)); m_maximizeBtn-setFlat(true); m_maximizeBtn-setToolTip(tr(最大化)); m_closeBtn-setIconSize(QSize(27, 22)); m_closeBtn-setIcon(QIcon(:/images/close.png)); m_closeBtn-setFlat(true); m_closeBtn-setToolTip(tr(关闭)); // 布局 QHBoxLayout *layout new QHBoxLayout(this); layout-addWidget(m_iconLabel); layout-addSpacing(5); layout-addWidget(m_titleLabel); layout-addStretch(); layout-addWidget(m_minimizeBtn); layout-addWidget(m_maximizeBtn); layout-addWidget(m_closeBtn); layout-setSpacing(0); layout-setContentsMargins(5, 0, 5, 0); setLayout(layout); // 信号连接 connect(m_minimizeBtn, QPushButton::clicked, this, TitleBar::onButtonClicked); connect(m_maximizeBtn, QPushButton::clicked, this, TitleBar::onButtonClicked); connect(m_closeBtn, QPushButton::clicked, this, TitleBar::onButtonClicked); } TitleBar::~TitleBar() {} // 双击标题栏 → 模拟最大化按钮点击 void TitleBar::mouseDoubleClickEvent(QMouseEvent *event) { Q_UNUSED(event); emit m_maximizeBtn-clicked(); } // 鼠标按下标题栏 → 向系统发送拖动窗口的消息仅 Windows void TitleBar::mousePressEvent(QMouseEvent *event) { #ifdef Q_OS_WIN // 释放当前鼠标捕获如果有 ReleaseCapture(); QWidget *pWindow this-window(); if (pWindow-isTopLevel()) { // 发送系统命令移动窗口SC_MOVE HTCAPTION SendMessage(HWND(pWindow-winId()), WM_SYSCOMMAND, SC_MOVE HTCAPTION, 0); } event-ignore(); // 让事件继续传递 #else Q_UNUSED(event); // 跨平台方案需要自己计算偏移量实现拖拽 #endif } // 事件过滤器监听主窗口的标题、图标、大小变化 bool TitleBar::eventFilter(QObject *obj, QEvent *event) { switch (event-type()) { case QEvent::WindowTitleChange: { QWidget *w qobject_castQWidget*(obj); if (w) { m_titleLabel-setText(w-windowTitle()); return true; } break; } case QEvent::WindowIconChange: { QWidget *w qobject_castQWidget*(obj); if (w) { QIcon icon w-windowIcon(); if (!icon.isNull()) m_iconLabel-setPixmap(icon.pixmap(m_iconLabel-size())); return true; } break; } case QEvent::Resize: { updateMaximizeState(); return true; } default: break; } return QWidget::eventFilter(obj, event); } // 窗口按钮点击处理 void TitleBar::onButtonClicked() { QPushButton *btn qobject_castQPushButton*(sender()); QWidget *pWindow this-window(); if (!pWindow-isTopLevel()) return; if (btn m_minimizeBtn) { pWindow-showMinimized(); } else if (btn m_maximizeBtn) { if (pWindow-isMaximized()) pWindow-showNormal(); else pWindow-showMaximized(); } else if (btn m_closeBtn) { pWindow-close(); } } // 更新最大化按钮的图标和提示根据当前窗口状态 void TitleBar::updateMaximizeState() { QWidget *pWindow this-window(); if (!pWindow-isTopLevel()) return; bool isMax pWindow-isMaximized(); if (isMax) { m_maximizeBtn-setToolTip(tr(还原)); m_maximizeBtn-setIcon(QIcon(:/images/restore.png)); // 可替换还原图标 } else { m_maximizeBtn-setToolTip(tr(最大化)); m_maximizeBtn-setIcon(QIcon(:/images/max.png)); } }三、主窗口实现无边框 缩放3.1 头文件widget.h#ifndef WIDGET_H #define WIDGET_H #include QWidget class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent nullptr); ~Widget(); protected: // 处理 Windows 原生消息实现窗口缩放 bool nativeEvent(const QByteArray eventType, void *message, long *result) override; private: int m_borderWidth; // 边框缩放区域的宽度像素 }; #endif // WIDGET_H3.2 实现文件widget.cpp#include widget.h #include titlebar.h #include QVBoxLayout #include QPalette #ifdef Q_OS_WIN #include qt_windows.h #include Windowsx.h #endif Widget::Widget(QWidget *parent) : QWidget(parent) { // 1. 设置窗口标志无边框 置顶可选 setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint); // 若要支持透明背景可取消下一行注释 // setAttribute(Qt::WA_TranslucentBackground); // 2. 创建自定义标题栏 TitleBar *titleBar new TitleBar(this); installEventFilter(titleBar); // 让标题栏能收到本窗口的标题/图标变化事件 // 3. 设置窗口内容布局将标题栏放在顶部 QVBoxLayout *mainLayout new QVBoxLayout(this); mainLayout-addWidget(titleBar); mainLayout-addStretch(); // 此处可添加实际的内容控件 mainLayout-setSpacing(0); mainLayout-setContentsMargins(0, 0, 0, 0); setLayout(mainLayout); // 4. 窗口初始属性 resize(800, 600); setWindowTitle(自定义标题栏窗口); setWindowIcon(QIcon(:/images/app_icon.png)); // 设置窗口背景色示例 QPalette pal(palette()); pal.setColor(QPalette::Window, QColor(45, 45, 45)); setAutoFillBackground(true); setPalette(pal); // 5. 边框缩放敏感区域的宽度像素 m_borderWidth 5; } Widget::~Widget() {} // 拦截 Windows 消息实现窗口边缘缩放 bool Widget::nativeEvent(const QByteArray eventType, void *message, long *result) { Q_UNUSED(eventType); #ifdef Q_OS_WIN MSG *msg static_castMSG*(message); if (msg-message WM_NCHITTEST) { // 获取鼠标坐标相对于当前窗口客户区 int xPos GET_X_LPARAM(msg-lParam) - this-geometry().x(); int yPos GET_Y_LPARAM(msg-lParam) - this-geometry().y(); // 如果鼠标位于子控件上如按钮、文本框则交给系统默认处理不做缩放 if (childAt(xPos, yPos) ! nullptr) return QWidget::nativeEvent(eventType, message, result); // 默认返回 HTCAPTION使整个客户区可拖动如果不希望整个窗口可拖动可改成 HTCLIENT *result HTCAPTION; // 依次判断鼠标是否在边缘区域左、右、上、下、四个角 bool left (xPos 0 xPos m_borderWidth); bool right (xPos width() - m_borderWidth xPos width()); bool top (yPos 0 yPos m_borderWidth); bool bottom (yPos height() - m_borderWidth yPos height()); if (left top) *result HTTOPLEFT; else if (right top) *result HTTOPRIGHT; else if (left bottom) *result HTBOTTOMLEFT; else if (right bottom) *result HTBOTTOMRIGHT; else if (left) *result HTLEFT; else if (right) *result HTRIGHT; else if (top) *result HTTOP; else if (bottom) *result HTBOTTOM; return true; // 消息已处理 } #endif return QWidget::nativeEvent(eventType, message, result); }四、关键点详解4.1 为什么要在mousePressEvent中使用 Windows API默认情况下Qt 的无边框窗口无法直接通过鼠标拖拽标题栏移动。我们需要向系统发送WM_SYSCOMMAND消息参数SC_MOVE HTCAPTION告诉 Windows 进入“窗口移动”模式。ReleaseCapture()用于释放之前的鼠标捕获确保消息发送成功。4.2 事件过滤器的作用通过installEventFilter(titleBar)主窗口的所有事件标题变化、图标变化、大小变化都会被传递给TitleBar的eventFilter函数。这样标题栏可以自动同步主窗体的标题和图标而不需要手动调用更新函数。4.3 窗口缩放原理无边框窗口的缩放需要处理 Windows 的WM_NCHITTEST命中测试消息。系统通过此消息询问鼠标当前位置属于窗口的哪个区域客户区、标题栏、边框等。我们根据鼠标坐标判断是否在窗口边缘并返回对应的HT*宏如HTLEFT、HTTOP等系统就会自动启用相应的缩放光标和行为。五、样式美化QSS 示例为了让标题栏更漂亮可以在全局样式表中加入如下样式/* 标题栏背景 */ TitleBar { background-color: #2c3e50; border-bottom: 1px solid #1a2632; } /* 标题文字 */ TitleBar QLabel#titleLabel { color: white; font-size: 12px; font-weight: normal; } /* 按钮悬停效果 */ TitleBar QPushButton:hover { background-color: #34495e; } TitleBar QPushButton:pressed { background-color: #1abc9c; }注意要为m_titleLabel设置objectName(titleLabel)才能用 ID 选择器。六、跨平台扩展思路上述方案依赖 Windows API如果希望程序能在 Linux/macOS 上运行并实现相同的拖拽移动和缩放效果可以采用纯 Qt 事件模拟方式6.1 拖拽移动纯 Qt 实现在TitleBar::mousePressEvent中记录按下位置在mouseMoveEvent中计算偏移并调用window()-move()。需要同时处理鼠标释放。6.2 窗口缩放纯 Qt 实现在主窗口的mousePressEvent中判断鼠标是否位于边缘区域通过rect()和mousePos计算然后进入缩放模式。在mouseMoveEvent中动态修改窗口的resize()和move()。需要处理光标形状变化setCursor。这种方法虽然工作量稍大但可以做到真正的跨平台。不过边缘缩放的平滑度略逊于原生实现。七、总结本文介绍了在 Windows 平台上使用 Qt 实现自定义无边框窗口的完整方案包括功能实现方式无边框setWindowFlags(Qt::FramelessWindowHint)自定义标题栏独立 Widget 布局 事件过滤拖拽移动Windows APISendMessage双击最大化/还原mouseDoubleClickEvent模拟按钮点击窗口边缘缩放拦截WM_NCHITTEST消息返回相应区域标识你可以基于此代码继续扩展比如增加窗口阴影、皮肤切换、最小化到托盘等功能。如果是商业项目且需要跨平台建议使用纯 Qt 事件模拟或者考虑 Qt 官方提供的Qt::CustomizeWindowHint配合QGraphicsDropShadowEffect来实现更美观的效果。