Qt自定义标题栏

Qt自定义标题栏

    • 效果图
    • 源码
    • 使用方法
    • 实现思路及部分代码
    • 参考

效果图



1.设置为无边框窗口后拉伸、移动、放大缩小等事件需要自己重新实现,我只实现标题栏拖动、双击放大/还原、窗体边缘可拉伸等基本功能,有其他需求的小伙伴可参考和修改源码


2.拉伸右、下、右下这三个方向不会抖动,拉伸其他方向均会抖动,貌似很多自定义窗口的软件都是这样,包括微软的Edge浏览器、QQ、网易云音乐等


3.事件实现看过很多别人的方法,思路都是差不多的,但仅实现放大、缩小、移动、拉伸的话,很多人的代码都写得过于复杂,不便于我这种菜鸡阅读和理解。我的代码中拉伸实现主要参考了qt 无边框窗口,可拖动,可拉伸 这篇文章


4.试过刘典武大佬的无抖动方案源码,一样是会抖动的,相对来说没那么抖
Qt开源作品38-无边框窗体方案(无抖动,支持win、linux、mac等系统,侧边半屏顶部全屏)

Qt自定义标题栏_第1张图片

源码

链接: https://pan.baidu.com/s/1HtN-ciYU_-HaVTId6gjMKw
提取码: 5dmy

使用方法

  1. titlebar.h,titlebar.cpp,titlebar.ui 文件加入到自己的项目中
  2. 包含头文件 #include "titlebar.h"
  3. 创建标题栏对象 TitleBar title;
  4. 创建主窗口对象 QWidget w;
  5. 调用函数将主窗口添加入标题栏窗口 title.setMainWidget(&w);
  6. 调用显示函数 title.show();
  7. 2021.10.21注:如果实例化的是普通对象,则3和4顺序不可倒换,因为title.setMainWidget(&w)是title把w作为子对象,构造时应先构造title再构造w,析构时先析构w再析构title。如果顺序倒换,则先析构了title,这时再析构w就会报错。如果创建的是对象指针则实例化顺序不影响

实现思路及部分代码

声明

#ifndef TITLEBAR_H
#define TITLEBAR_H

#include 
#include 
#include 

QT_BEGIN_NAMESPACE
namespace Ui { class TitleBar; }
QT_END_NAMESPACE

class TitleBar : public QWidget
{
    Q_OBJECT

public:
    TitleBar(QWidget *parent = nullptr);
    ~TitleBar();

    /**
     * @brief msetStyleSheet 通过文件设置全局样式
     * @param styleSheet css/qss等样式文件
     */
    static void msetStyleSheet(const QString &styleSheet);

    /**
     * @brief setMainWidget 设置主窗口
     * @param widget 窗口指针
     */
    void setMainWidget(QWidget *widget);

    /**
     * @brief setTitleBarText 设置标题
     * @param text 文本
     */
    void setTitleBarText(const QString &text);

    /**
     * @brief setTitleBarIcon 设置标题栏图标
     * @param icon 图标
     */
    void setTitleBarIcon(const QString &icon);

    /**
     * @brief setTitleBarStyleSheet 设置标题栏样式
     * @param sheet 样式
     */
    void setTitleBarStyleSheet(const QString &sheet);

    /**
     * @brief setTitleBarBackGround 设置标题栏背景颜色
     * @param r 红色值
     * @param g 绿色值
     * @param b 蓝色值
     */
    void setTitleBarBackGround(const int &r, const int &g, const int &b);

    /**
     * @brief setTitleTextFont 设置标题栏字体
     * @param font 字体样式
     */
    void setTitleTextFont(const QFont &font);

    /**
     * @brief setMarginSize 设置拉伸边距
     * @param size 边距值
     */
    void setMarginSize(const int &size);

    /**
     * @brief The MousePosition enum 鼠标区域枚举
     * 左上角(1,1)   上(1,2)   右上角(1,3)
     * 左(2,1)      中(2,2)   右(2,3)
     * 左下角(3,1)  下(3,2)   右下角(3,3)
     */
    enum MousePosition
    {
        PosLeftTop = 11,
        PosTop = 12,
        PosRightTop = 13,
        PosLeft = 21,
        PosMid = 22,
        PosRight = 23,
        PosLeftBottom = 31,
        PosBottom = 32,
        PosRightBottom = 33,
    };

protected:
    bool eventFilter(QObject *watched, QEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;

    /**
     * @brief getMouseArea 获取鼠标所在区域
     * @param pos 全局坐标
     * @return
     */
    int getMouseArea(const QPoint &pos);

    /**
     * @brief setMouseCursor 设置鼠标样式
     * @param pos 全局坐标
     */
    void setMouseCursor(const QPoint &pos);

private:
    Ui::TitleBar *ui;

    QWidget *mainWidget;//主窗口指针
    QPoint dragPos;//窗口拖动位置
    bool isMoveEvent;//标题栏移动事件标志

    QPoint lastPos;//左键按下时停留的坐标
    bool leftBtnPress;//左键按下标志
    int mousePressArea; //鼠标点击的区域
    int marginSize;//边缘边距值
    bool isMidArea;//在非边缘区域标志
    int minWidth;//窗体(可缩小至)宽度
    int minHeight;//窗体(可缩小至)高度
};
#endif // TITLEBAR_H

1. 窗体布局
Qt自定义标题栏_第2张图片

以一个Widget作为整体窗口,在此基础上添加一个Widget作为标题栏,然后提供一个添加主窗口的接口函数 setMainWidget,布局属性边距、垂直策略那些啥的在ui文件里设置了

void TitleBar::setMainWidget(QWidget *widget)
{
    if (widget != nullptr) {
        mainWidget = widget;
        mainWidget->setParent(this);
        mainWidget->show();
        mainWidget->installEventFilter(this);
        //重新布局标题栏和窗口
        QLayout *mainLayout = this->layout();
        mainLayout->removeWidget(ui->titleWidget);
        mainLayout->addWidget(ui->titleWidget);
        mainLayout->addWidget(mainWidget);
    }
}


2. 标题栏拖动窗口及双击最大化


用事件过滤器限定窗口整体拖动只在标题栏事件中

窗口拖动应该不难,偏移量就是左键按下时鼠标的位置相对于整体窗口的左上角的位置

双击放大就没啥好说了,但是会和拖动冲突,现象就是双击放大后窗口移动到了鼠标的位置,所以用isMoveEvent这个标志来解决冲突

bool TitleBar::eventFilter(QObject *watched, QEvent *event)
{
    //标题栏区域事件
    if (isMidArea && watched == ui->titleWidget) {
        setCursor(Qt::ArrowCursor);
        QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event);
        switch (event->type()) {
            case QEvent::MouseButtonPress:
                dragPos = mouseEvent->globalPos() - frameGeometry().topLeft();//计算偏移量
                isMoveEvent = true;
                break;
            case QEvent::MouseButtonDblClick:
                isMoveEvent = false;
                emit ui->btnMax->clicked();
                break;
            case QEvent::MouseMove:
                if (isMoveEvent && !isMaximized())
                    move(mouseEvent->globalPos() - dragPos);//移动窗口
                break;
            case QEvent::MouseButtonRelease:
                isMoveEvent = false;
                break;
            default:
                break;
        }
    }
    //主窗口区域事件
    if (isMidArea && watched == mainWidget)
        setCursor(Qt::ArrowCursor);

    return QWidget::eventFilter(watched, event);
}

3. 光标样式及拉伸动作

3.1. 光标样式
Qt自定义标题栏_第3张图片
Qt自定义标题栏_第4张图片

上图把整体窗口分为了9个区域,光标在哪个区域就根据图上的划分计算(函数getMouseArea),得出区域值后便根据区域值对应的区域设置光标(函数setMouseCursor),在mouseMoveEvent中调用setMouseCursor

区域枚举值

/**
 * @brief The MousePosition enum 鼠标区域枚举
 * 左上角(1,1)   上(1,2)   右上角(1,3)
 * 左(2,1)      中(2,2)   右(2,3)
 * 左下角(3,1)  下(3,2)   右下角(3,3)
 */
enum MousePosition
{
    PosLeftTop = 11,
    PosTop = 12,
    PosRightTop = 13,
    PosLeft = 21,
    PosMid = 22,
    PosRight = 23,
    PosLeftBottom = 31,
    PosBottom = 32,
    PosRightBottom = 33,
};
int TitleBar::getMouseArea(const QPoint &pos)
{
    int posX = pos.x();//全局x坐标
    int posY = pos.y();//全局y坐标
    int mainWidth = width();//全局宽度
    int mainHeight = height();//全局高度
    int areaX = 0;//x所在区域
    int areaY = 0;//y所在区域

    //判断x所在区域
    if (posX > (mainWidth - marginSize))
        areaX = 3;
    else if (posX < marginSize)
        areaX = 1;
    else
        areaX = 2;

    //判断y所在区域
    if (posY > (mainHeight - marginSize))
        areaY = 3;
    else if (posY < marginSize)
        areaY = 1;
    else
        areaY = 2;

    //返回区域值,如区域1:1 + 1*10 = 11
    return areaX + areaY*10;
}

void TitleBar::setMouseCursor(const QPoint &pos)
{
    Qt::CursorShape cursor;
    //获取区域值
    int area = getMouseArea(pos);
    switch (area) {
        case PosLeftTop:
        case PosRightBottom:
            cursor = Qt::SizeFDiagCursor;
            break;
        case PosRightTop:
        case PosLeftBottom:
            cursor = Qt::SizeBDiagCursor;
            break;
        case PosLeft:
        case PosRight:
            cursor = Qt::SizeHorCursor;
            break;
        case PosTop:
        case PosBottom:
            cursor = Qt::SizeVerCursor;
            break;
        case PosMid:
            cursor = Qt::ArrowCursor;
            break;
        default:
            break;
    }
    setCursor(cursor);
}


3.2. 拉伸动作

重点在重写mouseMoveEvent函数,核心在switch语句中,根据光标所在区域以及偏移量进行运算,条件判断用于防止过度拉伸,理解代码时可不看

void TitleBar::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        leftBtnPress = true;//左键按下标志
        lastPos = event->globalPos();//按下时鼠标坐标
        mousePressArea = getMouseArea(event->pos());//按下时鼠标所在区域
    }
    return QWidget::mousePressEvent(event);
}

void TitleBar::mouseMoveEvent(QMouseEvent *event)
{
    //最大化状态时不能拉伸
    if (isMaximized())
        return;
    //根据位置设置鼠标样式
    if (!leftBtnPress)
        setMouseCursor(event->pos());

    if (leftBtnPress && (event->buttons() & Qt::LeftButton)) {
        if (mousePressArea != PosMid)
            isMidArea = false;
        QPoint offset = event->globalPos() - lastPos;//偏移量=当前鼠标坐标-按下时鼠标坐标
        int offsetX = offset.x();
        int offsetY = offset.y();
        QRect globalGeometry = frameGeometry();//获取窗口矩形
        int globalWidth = globalGeometry.width();
        int globalHeight = globalGeometry.height();
        //拉伸后位置=当前位置+偏移量
        //加入条件判断防止过度拉伸
        switch (mousePressArea) {
            case PosLeftTop:
                if (minWidth <= (globalWidth - offsetX))
                    globalGeometry.setLeft(globalGeometry.left() + offsetX);
                if (minHeight <= (globalHeight - offsetY))
                    globalGeometry.setTop(globalGeometry.top() + offsetY);
                //globalGeometry.setTopLeft(globalGeometry.topLeft() + offset);
                break;
            case PosTop:
                if (minHeight <= (globalHeight - offsetY))
                    globalGeometry.setTop(globalGeometry.top() + offsetY);
                break;
            case PosRightTop:
                if (minWidth <= (globalWidth + offsetX))
                    globalGeometry.setRight(globalGeometry.right() + offsetX);
                if (minHeight <= (globalHeight - offsetY))
                    globalGeometry.setTop(globalGeometry.top() + offsetY);
                //globalGeometry.setTopRight(globalGeometry.topRight() + offset);
                break;
            case PosRight:
                if (minWidth <= (globalWidth + offsetX))
                    globalGeometry.setRight(globalGeometry.right() + offsetX);
                break;
            case PosRightBottom:
                if (minWidth <= (globalWidth + offsetX))
                    globalGeometry.setRight(globalGeometry.right() + offsetX);
                if (minHeight <= globalHeight + offsetY)
                    globalGeometry.setBottom(globalGeometry.bottom() + offsetY);
                //globalGeometry.setBottomRight(globalGeometry.bottomRight() + offset);
                break;
            case PosBottom:
                if (minHeight <= (globalHeight + offsetY))
                    globalGeometry.setBottom(globalGeometry.bottom() + offsetY);
                break;
            case PosLeftBottom:
                if (minWidth <= (globalWidth - offsetX))
                    globalGeometry.setLeft(globalGeometry.left() + offsetX);
                if (minHeight <= (globalHeight + offsetY))
                    globalGeometry.setBottom(globalGeometry.bottom() + offsetY);
                //globalGeometry.setBottomLeft(globalGeometry.bottomLeft() + offset);
                break;
            case PosLeft:
                if (minWidth <= (globalWidth - offsetX))
                    globalGeometry.setLeft(globalGeometry.left() + offsetX);
                break;
            default:
                break;
        }
        //设置拉伸后坐标并记录
        setGeometry(globalGeometry);
        lastPos = event->globalPos();
    }

    return QWidget::mouseMoveEvent(event);
}

void TitleBar::mouseReleaseEvent(QMouseEvent *event)
{
    leftBtnPress = false;//左键释放
    isMidArea = true;
    setCursor(Qt::ArrowCursor);
    return QWidget::mouseReleaseEvent(event);
}


4. 其他接口函数声明

/**
 * @brief msetStyleSheet 通过文件设置全局样式
 * @param styleSheet css/qss等样式文件
 */
static void msetStyleSheet(const QString &styleSheet);

/**
 * @brief setMainWidget 设置主窗口
 * @param widget 窗口指针
 */
void setMainWidget(QWidget *widget);

/**
 * @brief setTitleBarText 设置标题
 * @param text 文本
 */
void setTitleBarText(const QString &text);

/**
 * @brief setTitleBarIcon 设置标题栏图标
 * @param icon 图标
 */
void setTitleBarIcon(const QString &icon);

/**
 * @brief setTitleBarStyleSheet 设置标题栏样式
 * @param sheet 样式
 */
void setTitleBarStyleSheet(const QString &sheet);

/**
 * @brief setTitleBarBackGround 设置标题栏背景颜色
 * @param r 红色值
 * @param g 绿色值
 * @param b 蓝色值
 */
void setTitleBarBackGround(const int &r, const int &g, const int &b);

/**
 * @brief setTitleTextFont 设置标题栏字体
 * @param font 字体样式
 */
void setTitleTextFont(const QFont &font);

/**
 * @brief setMarginSize 设置拉伸边距
 * @param size 边距值
 */
void setMarginSize(const int &size);

参考

[1]: qt 无边框窗口,可拖动,可拉伸
[2]: qt 无边框窗体拖动、拉伸

你可能感兴趣的:(qt,c++)