本节课,来实现一个非常精简的无边框窗口,支持拖动4个边和4个角来缩放窗口
整体效果如下:
MnXzbhs8moEls3xsqCTcDkN9n6d.gif

整体效果说明:

  • 当鼠标位于 12346789 区域时,鼠标形状如上,可以进行窗口缩放
  • 当鼠标位于 5 区域时,鼠标恢复正常形状如上,可以进行窗口的移动

image-20260820125954436

1. 新建项目、去除边框

1.1 新建工程

使用纯代码布局,新建FramelessDemo项目时,取消勾选 “Generate form” ,如下:
image-20260820130012074

项目创建完毕的目录结构,如下:
image-20260820130026094

1.2 添加按钮,设置样式

在空白界面上,添加两个水平布局的按钮,并设置窗体的最小大小,设置窗体的背景色,如下:

#include <QDebug>
#include <QHBoxLayout>
#include <QPushButton>

Widget::Widget(QWidget* parent) : QWidget(parent)
{
    this->setWindowTitle("明王讲QT | 第五章 事件、无边框窗口 | 简单无边框窗口");
    this->resize(800, 500);
    this->setMinimumSize(400, 300);
    this->setStyleSheet("background:#303030");  // 设置背景色

    QHBoxLayout* layout = new QHBoxLayout(this);
    layout->setSpacing(10);
    layout->setContentsMargins(10, 10, 10, 10);

    QPushButton* btn1 = new QPushButton("确定");
    QPushButton* btn2 = new QPushButton("取消");

    layout->addWidget(btn1);
    layout->addWidget(btn2);

    QString style = R"(
        QPushButton {
            background-color: rgb(64, 64, 64);
            font:20px "Microsoft YaHei";
            color:rgb(200,200,200);
            border: 1px solid #707070;
            border-radius: 5px;
            padding: 5px;
        }
        QPushButton:hover {
            background-color: rgb(40, 40, 40);
        }
        QPushButton:pressed {
            background-color: rgb(64, 64, 64);
        }
     )";

    btn1->setStyleSheet(style);
    btn2->setStyleSheet(style);

    connect(btn1, &QPushButton::clicked, this, [=]() { qDebug() << btn1->text(); });
    connect(btn2, &QPushButton::clicked, this, [=]() { qDebug() << btn2->text(); });
}

此时,运行程序:
image-20260820130052666

1.3 去除标题栏

自带的标题栏很丑,在构造中,添加一条语句即可去除,如下:

Widget::Widget(QWidget* parent) : QWidget(parent)
{
    this->setWindowTitle("明王讲QT | 第五章 事件、无边框窗口 | 简单无边框窗口");
    this->resize(800, 500);
    this->setStyleSheet("background:#303030");  // 设置背景色

    // 去除标题栏
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
}

1.4 右键退出

没有了标题栏,就无法点击右上角的 “关闭” 按钮来退出程序了。
接下来增加一个右键退出的功能

首先,来到widget.h中,声明几个鼠标事件函数并空实现,如下:

class Widget : public QWidget {

protected:
    void mousePressEvent(QMouseEvent* event);
    void mouseMoveEvent(QMouseEvent* event);
    void mouseReleaseEvent(QMouseEvent* event);
};

然后,来到widget.cpp的构造中,创建菜单,如下:

Widget::Widget(QWidget* parent) : QWidget(parent)
{
    // ...
    
    // 创建右键菜单
    menu = new QMenu(this);
    QAction* action = new QAction("退出程序", this);
    connect(action, &QAction::triggered, this, [=]() { this->close(); });
    menu->addAction(action);

    // 设置菜单的样式表
    menu->setStyleSheet(R"(
        QMenu {
            background-color: rgb(120, 120, 120);
        }
        QMenu::item:selected {
            background-color: rgb(40, 40, 40);
        }
    )");
}

然后,来到widget.cpp 中,实现mousePressEvent()函数,如下:

#include <QMouseEvent>

void Widget::mousePressEvent(QMouseEvent* event)
{
    switch (event->button()) {
        case Qt::RightButton:
            menu->exec(event->globalPos());  // 在鼠标右键点击的位置显示菜单
            break;
    }
    QWidget::mousePressEvent(event);
}

此时,点击右键就可以直接退出程序

2. 拖动窗口

由于没有了标题栏,无法移动窗口。接下来就实现窗口的移动

首先,在widget.h中,声明需要的成员变量和成员函数:

// 针对9个区域,定义了一个枚举类型
enum Location { TOP, BOTTOM, LEFT, RIGHT, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT, CENTER };

class Widget : public QWidget {

protected:
    void mousePressEvent(QMouseEvent* event);
    void mouseMoveEvent(QMouseEvent* event);
    void mouseReleaseEvent(QMouseEvent* event);

private:
    bool isLeftPressed = false;  // 鼠标左键是否按下
    QPoint offset;               // 记录鼠标按下的位置
    Location location;
};

然后,来到widget.cpp中,修改鼠标按下、移动和释放的3个事件处理函数,如下:

void Widget::mousePressEvent(QMouseEvent* event)
{
    switch (event->button()) {
        case Qt::LeftButton:
            isLeftPressed = true;
            offset = event->globalPos() - this->frameGeometry().topLeft();
            break;
        case Qt::RightButton:
            menu->exec(event->globalPos());  // 在鼠标右键点击的位置显示菜单
            break;
    }

    QWidget::mousePressEvent(event);
}

void Widget::mouseMoveEvent(QMouseEvent* event)
{
    QPoint globalPos = event->globalPos();

    if (isLeftPressed) {
        this->move(globalPos - offset);
    }
}

void Widget::mouseReleaseEvent(QMouseEvent* event)
{
    if (event->button() == Qt::LeftButton) {
        isLeftPressed = false;
    }
}

3. 边界处的光标形状

接下来实现鼠标在边界处按下时,可以缩放窗口的功能。
不过本节先来实现当鼠标移动到边界时,改变鼠标的形状的功能。

首先,声明并实现setCursorShape()函数,如下:

// widget.h
// 定义一个间距
#define PADDING 5

class Widget : public QWidget {
private:
    void setCursorShape(const QPoint& cursorPoint);
};

// widget.cpp
void Widget::setCursorShape(const QPoint& cursorPoint)
{
    QRect rect = this->rect();
    QPoint topLeft = mapToGlobal(rect.topLeft());
    QPoint bottomRight = mapToGlobal(rect.bottomRight());

    int x = cursorPoint.x();
    int y = cursorPoint.y();

    if (x >= topLeft.x() && x <= topLeft.x() + PADDING && y >= topLeft.y() && y <= topLeft.y() + PADDING) {
        // 左上角
        location = TOP_LEFT;
        this->setCursor(QCursor(Qt::SizeFDiagCursor));
    } else if (x <= bottomRight.x() && x >= bottomRight.x() - PADDING && y <= bottomRight.y() && y >= bottomRight.y() - PADDING) {
        // 右下角
        location = BOTTOM_RIGHT;
        this->setCursor(QCursor(Qt::SizeFDiagCursor));
    } else if (x >= topLeft.x() && x <= topLeft.x() + PADDING && y <= bottomRight.y() && y >= bottomRight.y() - PADDING) {
        //左下角
        location = BOTTOM_LEFT;
        this->setCursor(QCursor(Qt::SizeBDiagCursor));
    } else if (x <= bottomRight.x() && x >= bottomRight.x() - PADDING && y >= topLeft.y() && y <= topLeft.y() + PADDING) {
        // 右上角
        location = TOP_RIGHT;
        this->setCursor(QCursor(Qt::SizeBDiagCursor));
    } else if (x >= topLeft.x() && x <= topLeft.x() + PADDING) {
        // 左边
        location = LEFT;
        this->setCursor(QCursor(Qt::SizeHorCursor));
    } else if (x <= bottomRight.x() && x >= bottomRight.x() - PADDING) {
        // 右边
        location = RIGHT;
        this->setCursor(QCursor(Qt::SizeHorCursor));
    } else if (y >= topLeft.y() && y <= topLeft.y() + PADDING) {
        // 上边
        location = TOP;
        this->setCursor(QCursor(Qt::SizeVerCursor));
    } else if (y <= bottomRight.y() && y >= bottomRight.y() - PADDING) {
        // 下边
        location = BOTTOM;
        this->setCursor(QCursor(Qt::SizeVerCursor));
    } else {
        // 默认
        location = CENTER;
        this->setCursor(QCursor(Qt::ArrowCursor));
    }
}

然后,来到widget.cpp中修改mouseMoveEvent(),如下:

void Widget::mouseMoveEvent(QMouseEvent* event)
{
    QPoint globalPos = event->globalPos();

    // 1. 左键未按下:则设置光标形状并返回
    if (!isLeftPressed) {
        this->setCursorShape(globalPos);
        return;
    }

    // 2. 左键按下:在CENTER位置按下,移动窗口并返回
    if (location == CENTER) {
        move(globalPos - offset);
        return;
    }
}

最后,不要忘记来到widget.h的构造中,开启鼠标跟踪,如下:

Widget::Widget(QWidget* parent) : QWidget(parent)
{
    // 去除标题栏
    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint);
    // 开启鼠标跟踪
    this->setMouseTracking(true);
}

3. 实现窗口缩放

只需接着修改mouseMoveEvent(),增加以下代码:

void Widget::mouseMoveEvent(QMouseEvent* event)
{
    // ...

    // 3. 左键按下:鼠标在 边缘 位置按下
    QRect rect = this->rect();
    QPoint topLeft = mapToGlobal(rect.topLeft());
    QPoint bottomRight = mapToGlobal(rect.bottomRight());
    QRect rMove(topLeft, bottomRight);

    switch (location) {
        case TOP:
            // 如果不加if判断,则窗口高度达到最小高度后,会被鼠标 "向下推走"
            if (bottomRight.y() - globalPos.y() > this->minimumHeight()) {
                rMove.setY(globalPos.y());
            }
            break;
        case BOTTOM:
            rMove.setHeight(globalPos.y() - topLeft.y());
            break;
        case LEFT:
            // 如果不加if判断,则窗口高度达到最小宽度后,会被鼠标 "向右推走"
            if (bottomRight.x() - globalPos.x() > this->minimumWidth()) {
                rMove.setX(globalPos.x());
            }
            break;
        case RIGHT:
            rMove.setWidth(globalPos.x() - topLeft.x());
            break;
        case TOP_LEFT:
            // 如果不加if判断,则窗口达到最小宽高后,会被鼠标 "向右下推走"
            if (bottomRight.x() - globalPos.x() > this->minimumWidth()) {
                rMove.setX(globalPos.x());
            }
            if (bottomRight.y() - globalPos.y() > this->minimumHeight()) {
                rMove.setY(globalPos.y());
            }
            break;
        case TOP_RIGHT:
            rMove.setWidth(globalPos.x() - topLeft.x());
            // 如果不加if判断,则窗口达到最小宽高后,会被鼠标 "向下推走"
            if (bottomRight.y() - globalPos.y() > this->minimumHeight()) {
                rMove.setY(globalPos.y());
            }
            break;
        case BOTTOM_LEFT:
            // 如果不加if判断,则窗口达到最小宽高后,会被鼠标 "向右推走"
            if (bottomRight.x() - globalPos.x() > this->minimumWidth()) {
                rMove.setX(globalPos.x());
            }
            rMove.setHeight(globalPos.y() - topLeft.y());
            break;
        case BOTTOM_RIGHT:
            rMove.setWidth(globalPos.x() - topLeft.x());
            rMove.setHeight(globalPos.y() - topLeft.y());
            break;
        default:
            break;
    }
    this->setGeometry(rMove);
}