[Qt] 手把手教你制作一个无边框窗口

参考文章:Qt实战6.万能的无边框窗口(FramelessWindow)

准备工作

首先创建了一个简单的Qt工程,这里我使用QWidget作为窗体基类。

#include "QApplication"
#include "frameless/frameless.hxx"

int main(int argc, char *argv[]) {
    QApplication a(argc, argv);

    frameless framelessWindow;
    framelessWindow.show();

    return QApplication::exec();
}


main.cxx

//
// Created by Marcus on 2023/8/14.
//

#include "QWidget"

#ifndef FRAMELESSWINDOWTEST_FRAMELESS_HXX
#define FRAMELESSWINDOWTEST_FRAMELESS_HXX

class frameless : public QWidget {
    Q_OBJECT

public:
    explicit frameless(QWidget *parent = nullptr);

    ~frameless() override;

};

#endif //FRAMELESSWINDOWTEST_FRAMELESS_HXX


./frameless/frameless.hxx

//
// Created by Marcus on 2023/8/14.
//

#include "frameless.hxx"
frameless::frameless(QWidget *parent) {

}

frameless::~frameless() {

}


./frameless/frameless.cxx

开启无边框窗口

注意:以下代码包含三个window flag,请选择自己需要的window flag使用。

frameless::frameless(QWidget *parent) {
    setWindowFlags(Qt::FramelessWindowHint | Qt::SubWindow | Qt::WindowStaysOnTopHint);
}

下面是关于这些window flag的说明:

  • Qt::FramelessWindowHint:这个是必须要设置的,此window flag会开启无边框窗体
  • Qt::SubWindow:如果需要运行程序时在任务栏上不显示程序图标(举一个例子,假设需要开发一款桌宠,在软件运行时最好不要在任务栏上显示程序的图标)时需要此window flag
  • Qt::WindowStaysOnTopHint:需要程序始终在屏幕顶部时,请设置此window flag

Window Flag效果

注意:演示各个window flag时仅设置单个window flag,实际使用时可以设置多个window flag,使用|隔开

Qt::FramelessWindowHint

setWindowFlags(Qt::FramelessWindowHint);

设置前
设置后

Qt::SubWindow

setWindowFlags(Qt::SubWindow);

设置前
设置后
需要注意的是,设置以后关闭/最大化/最小化按钮也不见了,此时窗口也无法拖拽缩放

Qt::WindowStaysOnTopHint

setWindowFlags(Qt::WindowStaysOnTopHint);

设置前
设置后

自定义标题栏

下面我将加入一个Material Design 2风格的标题栏,由于创建工程时没有创建.ui文件,所以我修改了frameless.cxx,若你有一个.ui文件,可以在QDesginer下修改窗体中的控件。

下面是会用到的头文件:

#include "QWidget"
#include "QVBoxLayout"
#include "QHBoxLayout"
#include "QSpacerItem"
#include "QPushButton"
#include "QLabel"


./frameless/frameless.hxx

frameless::frameless(QWidget *parent) {

    //设置这个窗体标题不会在标题栏上显示,但是设置的标题会在其他地方显示,拿Windows系统举例,你可以在启用Aero Peek以后在任务栏找到程序图标,鼠标停留几秒就能看到设置的标题
    setWindowTitle("无边框窗体测试");                   //设置窗体标题

    setWindowFlags(Qt::FramelessWindowHint);    //设置无边框

    this->resize(800, 600);                    //将窗口大小缩放为800像素×600像素

    auto * appLayout = new QVBoxLayout;              //新增一个局部appLayout并且设置其边距为0
    appLayout->setContentsMargins(0, 0, 0, 0);
    appLayout->setSpacing(0);

    auto * titleBar = new QWidget;                   //这个QWidget将作为标题栏的容器
    titleBar->setObjectName("titleBar");       //设置object name,方便后期使用QSS进行美化

    auto * titleBarContainer = new QHBoxLayout;      //新建一个局部titleBarContainer,它将成为标题栏的局部,这里我们设置其左右边距为16像素,上下边距为0,局部中每个控件间距离为5像素
    titleBarContainer->setContentsMargins(16, 0, 16, 0);
    titleBarContainer->setSpacing(5);

    //(下面那两行太长了旁边我觉得写不下就写这了)下面这个QSpacerItem是标题栏左侧的空白区域
    auto * titleBarSpacer = new QSpacerItem(24, 24, QSizePolicy::Expanding, QSizePolicy::Expanding);
    titleBarContainer->addSpacerItem(titleBarSpacer);

    auto * minimizeApp = new QPushButton;            //最小化按钮
    minimizeApp->setObjectName("minimizeApp");
    titleBarContainer->addWidget(minimizeApp);

    auto * maximizeApp = new QPushButton;            //最大化按钮
    maximizeApp->setObjectName("maximizeApp");
    titleBarContainer->addWidget(maximizeApp);

    auto * closeApp = new QPushButton;               //退出按钮
    closeApp->setObjectName("closeApp");
    titleBarContainer->addWidget(closeApp);

    titleBar->setLayout(titleBarContainer);          //将之前新建的局部titleBarContainer设置为标题栏的局部

    appLayout->addWidget(titleBar);                  //将标题栏加入局部appLayout

    //下面不再赘述新建控件的过程
    auto * appBar = new QWidget;
    appBar->setObjectName("appBar");

    auto * appBarLayout = new QHBoxLayout;

    auto * appTitle = new QLabel;
    appTitle->setObjectName("appTitle");
    appTitle->setText("无边框窗体测试");
    appBarLayout->addWidget(appTitle);

    appBar->setLayout(appBarLayout);
    appLayout->addWidget(appBar);

    auto * appContent = new QWidget;
    appContent->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

    auto * appContentLayout = new QVBoxLayout;

    auto * helloWorld = new QLabel;
    helloWorld->setText("Hello World!");
    helloWorld->setObjectName("helloWorld");
    appContentLayout->addWidget(helloWorld);

    auto * appContentSpacer = new QSpacerItem(16, 16, QSizePolicy::Expanding, QSizePolicy::Expanding);
    appContentLayout->addSpacerItem(appContentSpacer);

    appContent->setLayout(appContentLayout);
    appLayout->addWidget(appContent);


    this->setLayout(appLayout);                      //设置QWidget的局部为appLayout

    //设置窗体样式
    this->setStyleSheet("#titleBar{ background: #1976D2; min-height: 32px; max-height: 32px; }"
                        "#minimizeApp{ background: transparent; border-image: url(:/icon/icon/trigMinimize.svg); height: 24px; width: 24px; }"
                        "#maximizeApp{ background: transparent; border-image: url(:/icon/icon/trigMaximize.svg); height: 24px; width: 24px; }"
                        "#closeApp{ background: transparent; border-image: url(:/icon/icon/closeApp.svg); height: 24px; width: 24px; }"
                        "#appBar{ background: #2196F3; min-height: 64px; max-height: 64px; }"
                        "#appTitle{ background: transparent; color: #fff; font-size: 24px; font-weight: bold; font-family: 思源黑体 CN; }"
                        "#helloWorld{ background: transparent; color: #000000; font-size: 16px; font-weight: normal; font-family: 思源黑体 CN; }"
                        );
}


./frameless/frameless.cxx

setWindowTitle的好处


使用
setWindowTitle的好处

最后我们得到了一个自定义的窗体:
无边框窗体

最小化、最大化和退出

现在自定义的标题栏已经好了,我们需要实现标题所述功能。
首先新建一些槽备用:

class frameless : public QWidget {
    Q_OBJECT

public:
    explicit frameless(QWidget *parent = nullptr);

    ~frameless() override;
    
public slots:
    void trigMinimize();

    void trigMaximize();
    
    void trigExit();
    

};


./frameless/frameless.hxx

最小化

void frameless::trigMinimize() {
    this->showMinimized();
}

连接槽:

auto * minimizeApp = new QPushButton;            //最小化按钮
    minimizeApp->setObjectName("minimizeApp");
    connect(minimizeApp, SIGNAL(clicked(bool)), this, SLOT(trigMinimize()));
    titleBarContainer->addWidget(minimizeApp);

最大化

首先先新建一个bool变量,方便后续判断程序是否已经最大化,并将maximizeApp按钮的声明移至头文件中(如果使用Qt Desginer不用移动声明):

class frameless : public QWidget {
    Q_OBJECT

public:
    explicit frameless(QWidget *parent = nullptr);

    ~frameless() override;

public slots:
    void trigMinimize();

    void trigMaximize();

    void trigExit();
    
private:
    bool isAppMaximized = false;

	QPushButton * maximizeApp = new QPushButton;
};
void frameless::trigMaximize() {
    if(isAppMaximized == false){
        this->showMaximized();
        //最大化以后给最大化按钮换个图标
        maximizeApp->setStyleSheet("#maximizeApp{ background: transparent; border-image: url(:/icon/icon/trigNormal.svg); height: 24px; width: 24px; }");
        isAppMaximized = true;
    } //窗口不是最大化时,最大化窗口
    else{
        this->showNormal();
        maximizeApp->setStyleSheet("#maximizeApp{ background: transparent; border-image: url(:/icon/icon/trigMaximize.svg); height: 24px; width: 24px; }");
        isAppMaximized = false;
    } //窗口最大化时,将窗口恢复正常大小
}
maximizeApp->setObjectName("maximizeApp");
    connect(maximizeApp, SIGNAL(clicked(bool)), this, SLOT(trigMaximize()));
    titleBarContainer->addWidget(maximizeApp);

退出

需要在头文件中包含QApplication

void frameless::trigExit() {
    QApplication::quit();
}
auto * closeApp = new QPushButton;               //退出按钮
    closeApp->setObjectName("closeApp");
    connect(closeApp, SIGNAL(clicked(bool)), this, SLOT(trigExit()));
    titleBarContainer->addWidget(closeApp);

双击最大化

在头文件中加入MouseEvent

#include "QMouseEvent"
...
public slots:
    void trigMinimize();

    void trigMaximize();

    void trigExit();

    void mouseDoubleClickEvent(QMouseEvent * e);
void frameless::mouseDoubleClickEvent(QMouseEvent * e) {
    if(e->y() <= 32){
        trigMaximize();
    } //e的y值就是鼠标点击相对于窗体的y坐标,32为标题栏高度,这个值小于等于32像素即为在标题栏上双击,在其他地方双击鼠标不起作用
}

拖拽事件

在头文件中加入:

public slots:
    void trigMinimize();

    void trigMaximize();

    void trigExit();

    void mouseDoubleClickEvent(QMouseEvent * e);

    void mousePressEvent(QMouseEvent * e);

    void mouseMoveEvent(QMouseEvent * e);

    void mouseReleaseEvent(QMouseEvent * e);

private:
    bool isAppMaximized = false;

    bool isMoveAllowed = false;

    QPoint originalPos;

    QPushButton * maximizeApp = new QPushButton;
void frameless::mousePressEvent(QMouseEvent * e) {
    if(e->y() <= 32){
        isMoveAllowed = true;
        originalPos = e->pos();
    } //在标题栏上按下鼠标,准备拖拽时执行;isMoveAllowed为真时允许移动窗体,originalPos记录了此时鼠标相对桌面的坐标
}

void frameless::mouseMoveEvent(QMouseEvent * e) {
    if(isMoveAllowed == true){
        if(isAppMaximized == true){
            trigMaximize();
            this->move(e->globalPos());
        } //如果程序被最大化,先恢复正常大小的窗口,然后再继续

        this->move(e->globalPos() - originalPos); //e->globalPos()为鼠标相对于窗口的位置
    }
}

void frameless::mouseReleaseEvent(QMouseEvent * e) {
    isMoveAllowed = false; //禁止窗体移动
}