在桌面应用开发中,默认的窗口标题栏样式往往无法满足个性化 UI 需求。Qt 允许我们移除系统原生标题栏,然后自定义一个完全属于自己的标题栏,同时保留窗口的拖拽移动、缩放、最大化/还原、最小化和关闭等基本功能。
本文将详细介绍如何实现:
-
无边框窗口 (
Qt::FramelessWindowHint) -
自定义标题栏(包含图标、标题、三个控制按钮)
-
标题栏拖拽移动窗口 (利用 Windows API 的
SendMessage) -
标题栏双击最大化/还原
-
窗口边缘缩放 (通过拦截
WM_NCHITTEST消息)
⚠️ 注意:文中缩放和标题栏拖拽的实现用到了 Windows 原生 API(
user32.lib),因此仅支持 Windows 平台。若需要跨平台方案,可以参考文末的扩展思路。
一、整体设计思路
-
设置主窗口为无边框。
-
创建一个自定义标题栏控件
TitleBar,包含:-
图标
QLabel -
标题
QLabel -
最小化、最大化/还原、关闭按钮(
QPushButton)
-
-
将
TitleBar添加到主窗口的布局顶部。 -
实现标题栏的鼠标事件:
-
鼠标双击 → 窗口最大化/还原
-
鼠标按下(在标题栏区域) → 通过 Windows API 触发窗口拖动
-
-
为主窗口安装事件过滤器到
TitleBar,使标题栏能响应窗口标题/图标的变化。 -
重写主窗口的
nativeEvent,处理WM_NCHITTEST消息,实现窗口边缘缩放。

二、创建自定义标题栏(TitleBar)
2.1 头文件 titlebar.h
cpp
#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_H
2.2 实现文件 titlebar.cpp
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_cast<QWidget*>(obj);
if (w) {
m_titleLabel->setText(w->windowTitle());
return true;
}
break;
}
case QEvent::WindowIconChange: {
QWidget *w = qobject_cast<QWidget*>(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_cast<QPushButton*>(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
cpp
#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_H
3.2 实现文件 widget.cpp
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_cast<MSG*>(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 示例)
为了让标题栏更漂亮,可以在全局样式表中加入如下样式:
cpp
/* 标题栏背景 */
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 API SendMessage |
| 双击最大化/还原 | mouseDoubleClickEvent 模拟按钮点击 |
| 窗口边缘缩放 | 拦截 WM_NCHITTEST 消息,返回相应区域标识 |
你可以基于此代码继续扩展,比如增加窗口阴影、皮肤切换、最小化到托盘等功能。如果是商业项目且需要跨平台,建议使用纯 Qt 事件模拟,或者考虑 Qt 官方提供的 Qt::CustomizeWindowHint 配合 QGraphicsDropShadowEffect 来实现更美观的效果。