• Qt实战6.万能的无边框窗口(FramelessWindow)


    1 需求描述

    1. 实现一个Qt无边框窗口,自定义最大化、最小化、关闭按钮;
    2. 窗口支持任意拉伸、移动,支持边框阴影;
    3. 窗口能够集成任意其它窗口到内部形成一个整体。

    2 设计思路

    最初实现无边框的目标只有一个,即简单好用。所有实现基于Qt本身,现将窗口分为三层,如图:

    外层和内容层均使用垂直布局,使窗口拉伸时能够自动适应大小。下面对每一层做个简单说明。

    2.1 XWidget

    作为窗口的最外层,设置为透明,为内层ContentWidget边框设置、阴影显示提供支持。同时根据位置设置光标形状(CursorShape),实现窗口的任意拉伸。

    2.2 ContentWidget

    作为内容包含层,可设置边框颜色、宽度、圆角、阴影等效果,同时增加最大化、最小化、关闭按钮,以及logo、软件名称显示部件。

    2.3 CentralWidget

    作为外部嵌入层,XWidget提供一个接口void setCentralWidget(QWidget *widget),将其它窗口集成到ContentWidget内部形成一个整体,这个与QMainWindow类似。

    3 代码实现

    1. 首先,隐藏标题栏、启用样式表,XWidget背景透明,代码如下:
    setWindowFlags(Qt::FramelessWindowHint);    //隐藏标题栏(无边框)
    setAttribute(Qt::WA_StyledBackground);      //启用样式背景绘制
    setAttribute(Qt::WA_TranslucentBackground); //背景透明
    
    1. 为了实现鼠标的位置信息获取不受子控件的影响,启动鼠标悬浮追踪,代码如下:
    setAttribute(Qt::WA_Hover);
    
    1. 随后便可以在event事件处理函数中获取到悬浮事件,将其转换为鼠标移动事件进行统一处理,代码如下:
    bool XWidget::event(QEvent *event)
    {
        if (event->type() == QEvent::HoverMove) {
            QHoverEvent *hoverEvent = static_cast<QHoverEvent *>(event);
            QMouseEvent mouseEvent(QEvent::MouseMove, hoverEvent->pos(),
                                   Qt::NoButton, Qt::NoButton, Qt::NoModifier);
            mouseMoveEvent(&mouseEvent);
        }
    
        return QWidget::event(event);
    }
    
    1. 进入鼠标移动事件,根据坐标设置鼠标对应的形状,如果鼠标为按下状态且到达XWidget边界则拉伸窗口,否则只移动窗口,代码如下:

    m_bIsPressed 是否按下鼠标
    m_bIsResizing 是否正在调整窗口,调整窗口大小时不移动窗口

    void XWidget::mousePressEvent(QMouseEvent *event)
    {
        if (event->button() == Qt::LeftButton) {
            m_bIsPressed = true;
            m_pressPoint = event->globalPos();
        }
    
        return QWidget::mousePressEvent(event);
    }
    
    void XWidget::mouseMoveEvent(QMouseEvent *event)
    {
        if (m_bIsPressed) {
            if (m_bIsResizing) {
                m_movePoint = event->globalPos() - m_pressPoint;
                m_pressPoint += m_movePoint;
            } else {
                if (!m_bIsDoublePressed && windowState() == Qt::WindowMaximized) {
                    restoreWidget();
                }
    
                QPoint point = event->globalPos() - m_pressPoint;
                move(pos() + point);
    
                m_pressPoint = event->globalPos();
            }
        }
    
        if (windowState() != Qt::WindowMaximized) {
            updateRegion(event);    //更新区域,计算鼠标状态
        }
    }
    
    void XWidget::updateRegion(QMouseEvent *event)
    {
        QRect mainRect = geometry();
    
        int marginTop = event->globalY() - mainRect.y();
        int marginBottom = mainRect.y() + mainRect.height() - event->globalY();
        int marginLeft = event->globalX() - mainRect.x();
        int marginRight = mainRect.x() + mainRect.width() - event->globalX();
    
        //计算四个方向的边距,记录鼠标状态
        if ( (marginRight >= MARGIN_MIN_SIZE && marginRight <= MARGIN_MAX_SIZE)
                && ((marginBottom <= MARGIN_MAX_SIZE) && marginBottom >= MARGIN_MIN_SIZE) ) {
            if (!m_bIsResizing) {
                m_direction = BOTTOMRIGHT;
            }
        } else if ( (marginTop >= MARGIN_MIN_SIZE && marginTop <= MARGIN_MAX_SIZE)
                    && (marginRight >= MARGIN_MIN_SIZE && marginRight <= MARGIN_MAX_SIZE)) {
            if (!m_bIsResizing) {
                m_direction = TOPRIGHT;
            }
        } else if ( (marginLeft >= MARGIN_MIN_SIZE && marginLeft <= MARGIN_MAX_SIZE)
                    && (marginTop >= MARGIN_MIN_SIZE && marginTop <= MARGIN_MAX_SIZE) ) {
            if (!m_bIsResizing) {
                m_direction = TOPLEFT;
            }
        } else if ( (marginLeft >= MARGIN_MIN_SIZE && marginLeft <= MARGIN_MAX_SIZE)
                    && (marginBottom >= MARGIN_MIN_SIZE && marginBottom <= MARGIN_MAX_SIZE)) {
            if (!m_bIsResizing) {
                m_direction = BOTTOMLEFT;
            }
            m_bIsResizing = true;
        } else if (marginBottom <= MARGIN_MAX_SIZE && marginBottom >= MARGIN_MIN_SIZE) {
            if (!m_bIsResizing) {
                m_direction = DOWN;
            }
        } else if (marginLeft <= MARGIN_MAX_SIZE && marginLeft >= MARGIN_MIN_SIZE) {
            if (!m_bIsResizing) {
                m_direction = LEFT;
            }
        } else if (marginRight <= MARGIN_MAX_SIZE && marginRight >= MARGIN_MIN_SIZE) {
            if (!m_bIsResizing) {
                m_direction = RIGHT;
            }
        } else if (marginTop <= MARGIN_MAX_SIZE && marginTop >= MARGIN_MIN_SIZE) {
            if (!m_bIsResizing) {
                m_direction = UP;
            }
        } else {
            m_bIsResizing = false;
        }
    
        if (NONE != m_direction) {
            m_bIsResizing = true;
        }
    
        //调整区域,设置鼠标形状,如果鼠标按下则调整窗口大小
        resizeRegion(marginTop, marginBottom, marginLeft, marginRight);
    }
    

    不要看着代码多就感觉复杂,上面其实就干了一件事,判断鼠标是否达到边框限定位置,达到了就把方向记录下来。

    void XWidget::resizeRegion(int marginTop, int marginBottom,
                                   int marginLeft, int marginRight)
    {
        switch (m_direction) {
        case BOTTOMRIGHT:
        {
            setCursor(Qt::SizeFDiagCursor);
            if (m_bIsPressed) {
                QRect rect = geometry();
                rect.setBottomRight(rect.bottomRight() + m_movePoint);
                setGeometry(rect);
            }
        }
            break;
        case TOPRIGHT:
        {
            setCursor(Qt::SizeBDiagCursor);
            if (m_bIsPressed) {
                if (marginLeft > minimumWidth() && marginBottom > minimumHeight()) {
                    QRect rect = geometry();
                    rect.setTopRight(rect.topRight() + m_movePoint);
                    setGeometry(rect);
                }
            }
        }
            break;
        case TOPLEFT:
        {
            setCursor(Qt::SizeFDiagCursor);
            if (m_bIsPressed) {
                if (marginRight > minimumWidth() && marginBottom > minimumHeight()) {
                    QRect rect = geometry();
                    rect.setTopLeft(rect.topLeft() + m_movePoint);
                    setGeometry(rect);
                }
            }
        }
            break;
        case BOTTOMLEFT:
        {
            setCursor(Qt::SizeBDiagCursor);
            if (m_bIsPressed) {
                if (marginRight > minimumWidth() && marginTop> minimumHeight()) {
                    QRect rect = geometry();
                    rect.setBottomLeft(rect.bottomLeft() + m_movePoint);
                    setGeometry(rect);
                }
            }
        }
            break;
        case RIGHT:
        {
            setCursor(Qt::SizeHorCursor);
            if (m_bIsPressed) {
                QRect rect = geometry();
                rect.setWidth(rect.width() + m_movePoint.x());
                setGeometry(rect);
            }
        }
            break;
        case DOWN:
        {
            setCursor(Qt::SizeVerCursor);
            if (m_bIsPressed) {
                QRect rect = geometry();
                rect.setHeight(rect.height() + m_movePoint.y());
                setGeometry(rect);
            }
        }
            break;
        case LEFT:
        {
            setCursor(Qt::SizeHorCursor);
            if (m_bIsPressed) {
                if (marginRight > minimumWidth()) {
                    QRect rect = geometry();
                    rect.setLeft(rect.x() + m_movePoint.x());
                    setGeometry(rect);
                }
            }
        }
            break;
        case UP:
        {
            setCursor(Qt::SizeVerCursor);
            if (m_bIsPressed) {
                if (marginBottom > minimumHeight()) {
                    QRect rect = geometry();
                    rect.setTop(rect.y() + m_movePoint.y());
                    setGeometry(rect);
                }
            }
        }
            break;
        default:
        {
            setCursor(Qt::ArrowCursor);
        }
            break;
        }
    
        if (!m_bIsPressed) {
            m_bIsResizing = false;
            m_direction = NONE;
        }
    }
    

    同样的,上面这段代码也只干了一件事,如果鼠标达到了边框限定位置,并且按下了鼠标按键,就跟着改变窗口大小。

    1. 对标记成员进行重置,代码如下:
    void XWidget::mouseReleaseEvent(QMouseEvent *event)
    {
        if (event->button() == Qt::LeftButton) {
            m_bIsPressed = false;
            m_bIsResizing = false;
            m_bIsDoublePressed = false;
        }
    
        QWidget::mouseReleaseEvent(event);
    }
    
    void XWidget::leaveEvent(QEvent *event)
    {
        m_bIsPressed = false;
        m_bIsDoublePressed = false;
        m_bIsResizing = false;
    
        QWidget::leaveEvent(event);
    }
    
    1. 最后实现ContentWidget边框阴影效果,代码如下:
    void XWidget::createShadow()
    {
        QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
        shadowEffect->setColor(Qt::black);
        shadowEffect->setOffset(0, 0);
        shadowEffect->setBlurRadius(13);
        ui->widgetContent->setGraphicsEffect(shadowEffect);
    }
    

    此方法虽有效,会损耗性能,复杂界面不建议使用。

    1. 由于ContentWidget和XWidget之间有间距,最大化时可能不能占满全屏,手动处理下,最大化时边距设为0,还原时恢复即可,代码如下:
    void XWidget::maximizeWidget()
    {
        ui->pushButtonRestore->show();
        ui->pushButtonMax->hide();
    
        ui->verticalLayoutShadow->setContentsMargins(0, 0, 0, 0);
    
        showMaximized();
    }
    
    void XWidget::restoreWidget()
    {
        ui->pushButtonRestore->hide();
        ui->pushButtonMax->show();
    
        ui->verticalLayoutShadow->setContentsMargins(9, 9, 9, 9);
    
        showNormal();
    }
    

    4 QSS一下

    #widgetContent {
        background-color: white;
        border: 1px solid lightgray;
        border-radius: 3px;
    }
    
    
    #widgetContent QTreeWidget {
        border: 1px solid lightgray;
    }
    
    
    #titleBarWidget QPushButton {
        min- 25px;
        max- 25px;
        min-height: 25px;
        max-height: 25px;
        qproperty-flat: true;
        border: none;
    }
    #titleBarWidget QPushButton:hover {
        background-color: #D5E1F2;
    }
    #titleBarWidget QPushButton:pressed {
        background-color: #A3BDE3;
    }
    #titleBarWidget QPushButton#pushButtonClose {
        border-image: url(:/img/titleBar/close.png) 0 0 0 0 stretch stretch;
    }
    #titleBarWidget QPushButton#pushButtonRestore {
        border-image: url(:/img/titleBar/restore.png) 0 0 0 0 stretch stretch;
    }
    #titleBarWidget QPushButton#pushButtonMax {
        border-image: url(:/img/titleBar/max.png) 0 0 0 0 stretch stretch;
    }
    #titleBarWidget QPushButton#pushButtonMin {
        border-image: url(:/img/titleBar/min.png) 0 0 0 0 stretch stretch;
    }
    #titleBarWidget QPushButton#pushButtonMenu {
        border-image: url(:/img/titleBar/menu.png) 0 0 0 0 stretch stretch;
    }
    
    
    #menuBarTabWidget::tab-bar {
        left: 65px;
    }
    #menuBarTabWidget {
        border: 1px;
    }
    #menuBarTabWidget {
        background-color: #2B579A;
    }
    #menuBarTabWidget::pane {
        border: 1px solid lightgray;
        border-left: 0px;
        border-right: 0px;
    }
    #menuBarTabWidget QTabBar::tab{
        min- 55px;
        max- 55px;
        min-height: 23px;
        max-height: 23px;
    }
    #menuBarTabWidget QTabBar::tab {
        background: transparent;
        margin-left: 4px;
        margin-right: 4px;
    }
    #menuBarTabWidget QTabBar::tab:hover {
        color: #2B579A;
    }
    #menuBarTabWidget QTabBar::tab:selected {
        border: 1px solid #BAC9DB;
        background: white;
        border-bottom-color: #FFFFFF;
    }
    #menuBarTabWidget QTabBar::tab:!selected {
        margin-top: 1px;
    }
    
    
    QMenu {
        background-color: #FCFCFC;
        border: 1px solid #8492A6;
    }
    QMenu::item {
        background-color: transparent;
    }
    QMenu::item:selected {
        color: black;
        background-color: #D5E1F2;
    }
    
    
    #pushBtnFileMenu {
        min- 58px;
        max- 58px;
        min-height: 23px;
        max-height: 23px;
        color: white;
        border: 1px solid #2B579A;
        background-color: #2B579A;
    }
    #pushBtnFileMenu::menu-indicator {
        image: none;
    }
    
    

    5 总结

    之前也看了不少Qt实现FramelessWindow的例子,不是很复杂就是不通用。通过上面的实现,现在已完成了一个通用的版本,只要将自己的窗口设置到ContentWidget即可。本次实践关键地方有以下三点:

    1. 界面的分层,感兴趣的朋友可以尝试下,如果没有XWidget这一层会有什么效果,ContentWidget边框效果会失效,这样当然就达不到预期结果了;
    2. 启用了WA_Hover鼠标悬浮追踪,如果不启用,鼠标的移动事件可能会被子控件覆盖,这样就不会知道鼠标是否到达边框位置,从而无法正确设置鼠标的形状;
    3. 窗口拉伸时有个偏移量m_movePoint,鼠标其实到达ContentWidget边界就改变形状了,拉伸是对XWidget进行的,所以这里有一定的偏移。

    可能算不上最佳实践,但是已经能够满足绝大多数使用场景了,往里面套就行,使用起来非常之简单,还是很nice的。

  • 相关阅读:
    053-157
    053-496
    053-128
    053-167
    053-250
    053-674
    离职申请
    日记


  • 原文地址:https://www.cnblogs.com/luoxiang/p/13528745.html
Copyright © 2020-2023  润新知