Qt 自定义无边框窗口:标题栏、拖拽移动与缩放

在桌面应用开发中,默认的窗口标题栏样式往往无法满足个性化 UI 需求。Qt 允许我们移除系统原生标题栏,然后自定义一个完全属于自己的标题栏,同时保留窗口的拖拽移动、缩放、最大化/还原、最小化和关闭等基本功能。

本文将详细介绍如何实现:

  • 无边框窗口Qt::FramelessWindowHint

  • 自定义标题栏(包含图标、标题、三个控制按钮)

  • 标题栏拖拽移动窗口 (利用 Windows API 的 SendMessage

  • 标题栏双击最大化/还原

  • 窗口边缘缩放 (通过拦截 WM_NCHITTEST 消息)

⚠️ 注意:文中缩放和标题栏拖拽的实现用到了 Windows 原生 API(user32.lib),因此仅支持 Windows 平台。若需要跨平台方案,可以参考文末的扩展思路。

一、整体设计思路

  1. 设置主窗口为无边框。

  2. 创建一个自定义标题栏控件 TitleBar,包含:

    • 图标 QLabel

    • 标题 QLabel

    • 最小化、最大化/还原、关闭按钮(QPushButton

  3. TitleBar 添加到主窗口的布局顶部。

  4. 实现标题栏的鼠标事件:

    • 鼠标双击 → 窗口最大化/还原

    • 鼠标按下(在标题栏区域) → 通过 Windows API 触发窗口拖动

  5. 为主窗口安装事件过滤器到 TitleBar,使标题栏能响应窗口标题/图标的变化。

  6. 重写主窗口的 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),主窗口的所有事件(标题变化、图标变化、大小变化)都会被传递给 TitleBareventFilter 函数。这样标题栏可以自动同步主窗体的标题和图标,而不需要手动调用更新函数。

4.3 窗口缩放原理

无边框窗口的缩放需要处理 Windows 的 WM_NCHITTEST(命中测试)消息。系统通过此消息询问鼠标当前位置属于窗口的哪个区域(客户区、标题栏、边框等)。我们根据鼠标坐标判断是否在窗口边缘,并返回对应的 HT* 宏(如 HTLEFTHTTOP 等),系统就会自动启用相应的缩放光标和行为。

五、样式美化(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 来实现更美观的效果。

相关推荐
fish_xk5 小时前
c++11的初见
开发语言·c++·算法
Amctwd6 小时前
【JavaScript】JS 异步 Promise 解析
开发语言·前端·javascript
JAVA面经实录9176 小时前
JVM高频面试总结(背诵完整版)
java·开发语言·jvm
沪漂阿龙6 小时前
Java JVM 面试题详解:JVM运行原理、内存模型、堆栈方法区、GC垃圾回收、JIT编译、类加载机制与线上调优全攻略
java·开发语言·jvm
小碗羊肉6 小时前
Maven高级
java·开发语言·maven
不知名的老吴6 小时前
C++ 中函数对象的形式概述
开发语言·c++
Shan12056 小时前
C++中函数对象之重载 operator()
开发语言·c++·算法
HelloWorld1024!6 小时前
c++核心之万字详解 * 和 & 所有用法(指针、引用、取地址、解引用、常量修饰)
开发语言
Legendary_0086 小时前
解析 PD Sink 与 LDR6500U:Type-C 取电的核心密码
c语言·开发语言