乐于分享
好东西不私藏

C++ QT之抽屉效果drawer插件

C++ QT之抽屉效果drawer插件

在 UI 设计界,有一种经典冲突叫做:“老板想要的功能太多” vs “屏幕给的空间太少”。

每当这种时候,侧边抽屉(Drawer)就像是救命稻草——平时藏在阴影里装高冷,需要时滑出来展示各种控件,简直是空间管理界的“收纳大师”。虽然 Qt 自带的组件不少,但要在 QWidget 体系下搞一个既有平滑动画、又能自适应方向、还得长得够“现代”的抽屉,官方现成的轮子总觉得缺了点火候。

本着“能动手写插件,就绝不手动拖控件”的极客精神,我决定亲自撸一个 Qt 抽屉插件。这不仅是为了让界面看起来更高级,更是为了下次被改需求时,能优雅地回一句:“别急,我拉一下抽屉就有了。”

下面,请系好安全带,我们要进入硬核的实现环节了。

老习惯先预览效果:
功能点:
1、分为:左右上下抽屉4个方向
2、抽屉内容自定义布局
3、遮罩层可开启和关闭,可开启点击回调
4、可开启footer,底部按钮
源码分类3个文件,插件核心源码drawerwidget.h,方向类drawertype.h,管理单例类,提供外部调用drawermanager.h
drawertype源码如下:
#pragma once/// <summary>/// 抽屉样式枚举/// </summary>enum classDrawerType{    LEFT,    TOP,    RIGHT,    BOTTOM};

drawerwidget源码如下:

#pragma once#include<QWidget>#include"drawertype.h"#include<QPropertyAnimation>#include<QResizeEvent>#include<QLabel>#include<QPushButton>#include<qboxlayout.h>class DrawerWidget : public QWidget{    Q_OBJECTpublic:    explicitDrawerWidget(QWidget* parent = nullptr, DrawerType type = DrawerType::LEFT);    ~DrawerWidget();    voidsetDrawerSize(int size);    voidsetTitle(const QString& title);    voidsetContentWidget(QWidget* widget);    voidsetMaskOpacity(qreal opacity)// 0~1    voidsetMaskClickable(bool enable);    voidsetShowFooter(bool enable);    voidopen();    voidcloseDrawer();    voidsetOnConfirm(const std::function<void()>& cb);    voidsetOnCancel(const std::function<void()>& cb);    voidsetOnClose(const std::function<void()>& cb);protected:    voidresizeEvent(QResizeEvent* event)override;    booleventFilter(QObject* obj, QEvent* event)override;private:    voidinitUI();    voidupdateGeometryPosition();    QRect getStartGeometry();    QRect getEndGeometry();    QPoint getStartPos();    QPoint getEndPos();private:    DrawerType m_type = DrawerType::LEFT;    int m_drawerSize = 300;    qreal m_maskOpacity = 0.5;    bool m_maskClickable = true;    QWidget* m_maskWidget = nullptr;    QWidget* m_drawerPanel = nullptr;    QLabel* m_titleLabel = nullptr;    QPushButton* m_closeBtn = nullptr;    QPushButton* m_cancelBtn = nullptr;    QPushButton* m_confirmBtn = nullptr;    QWidget* m_contentContainer = nullptr;    QPropertyAnimation* m_animation = nullptr;    std::function<void()> m_onConfirm;    std::function<void()> m_onCancel;    std::function<void()> m_onClose;    bool m_isOpen = false;};
#include "drawerwidget.h"#include <QApplication>#include <QMouseEvent>#include <qboxlayout.h>#include <QGraphicsOpacityEffect>DrawerWidget::DrawerWidget(QWidget* parent, DrawerType type)    : m_type(type), QWidget(parent){    setAttribute(Qt::WA_StyledBackground);    setStyleSheet("background: transparent;");    setGeometry(parent->rect());    parent->installEventFilter(this);    initUI();}DrawerWidget::~DrawerWidget(){    delete m_maskWidget;    delete m_drawerPanel;    delete m_animation;}void DrawerWidget::initUI(){    // ===== 遮罩 =====    m_maskWidget = new QWidget(this);    m_maskWidget->setStyleSheet("background-color: rgba(0,0,0,128);");    m_maskWidget->installEventFilter(this);    // ===== 抽屉主体 =====    m_drawerPanel = new QFrame(this);    m_drawerPanel->setStyleSheet("background:white; border-radius:0px;");    QGraphicsDropShadowEffect* shadow = new QGraphicsDropShadowEffect(this);    shadow->setBlurRadius(10);      // 模糊半径    shadow->setColor(QColor(00020));  // 阴影颜色    shadow->setOffset(00);        // 默认    int offset = 8;    switch (m_type)    {    case DrawerType::LEFT:        shadow->setOffset(offset, 0);     // 向右投影        break;    case DrawerType::RIGHT:        shadow->setOffset(-offset, 0);    // 向左投影        break;    case DrawerType::TOP:        shadow->setOffset(0, offset);     // 向下投影        break;    case DrawerType::BOTTOM:        shadow->setOffset(0, -offset);    // 向上投影        break;    }    m_drawerPanel->setGraphicsEffect(shadow);    QVBoxLayout* mainLayout = new QVBoxLayout(m_drawerPanel);    mainLayout->setContentsMargins(15151515);    mainLayout->setSpacing(10);    // ===== Header =====    QHBoxLayout* headerLayout = new QHBoxLayout;    m_titleLabel = new QLabel("Title");    m_titleLabel->setStyleSheet("font-size:18px; font-weight:bold;");    m_closeBtn = new QPushButton("×");    m_closeBtn->setStyleSheet("font-size:20px;");    m_closeBtn->setFixedSize(3030);    headerLayout->addWidget(m_titleLabel);    headerLayout->addStretch();    headerLayout->addWidget(m_closeBtn);    // ===== Content =====    m_contentContainer = new QWidget;    m_contentContainer->setSizePolicy(QSizePolicy::ExpandingQSizePolicy::Expanding);    // ===== Footer =====    QHBoxLayout*  footerLayout = new QHBoxLayout;    footerLayout->addStretch();    m_cancelBtn = new QPushButton("Cancel");    m_confirmBtn = new QPushButton("Confirm");    footerLayout->addWidget(m_cancelBtn);    footerLayout->addWidget(m_confirmBtn);    m_cancelBtn->setVisible(false);    m_confirmBtn->setVisible(false);    mainLayout->addLayout(headerLayout);    mainLayout->addWidget(m_contentContainer);    mainLayout->addLayout(footerLayout);    // ===== 动画 =====	m_animation = new QPropertyAnimation(m_drawerPanel, "pos");//geometry 动画会有问题,改为pos动画    m_animation->setDuration(300);    // ===== 信号连接 =====    connect(m_closeBtn, &QPushButton::clicked, this, [this] {        closeDrawer();        });    connect(m_cancelBtn, &QPushButton::clicked, this, [this]() {        if(m_onCancel) m_onCancel();        closeDrawer();        });    connect(m_confirmBtn, &QPushButton::clicked, this, [this]() {        if(m_onConfirm) m_onConfirm();        closeDrawer();        });}void DrawerWidget::resizeEvent(QResizeEvent*){    setGeometry(parentWidget()->rect());    m_maskWidget->setGeometry(rect());    updateGeometryPosition();}void DrawerWidget::updateGeometryPosition(){   /* if (m_isOpen) {        m_drawerPanel->setGeometry(getEndGeometry());    }    else {        m_drawerPanel->setGeometry(getStartGeometry());    }*/    QRect parentRect = rect();    // 固定尺寸,不再动画尺寸    switch (m_type)    {    case DrawerType::LEFT:    case DrawerType::RIGHT:        m_drawerPanel->resize(m_drawerSize, parentRect.height());        break;    case DrawerType::TOP:    case DrawerType::BOTTOM:        m_drawerPanel->resize(parentRect.width(), m_drawerSize);        break;    }    if (m_isOpen)        m_drawerPanel->move(getEndPos());    else        m_drawerPanel->move(getStartPos());}void DrawerWidget::setShowFooter(bool enable){    m_cancelBtn->setVisible(enable);    m_confirmBtn->setVisible(enable);}QRect DrawerWidget::getStartGeometry(){    QRect parentRect = rect();    switch (m_type)    {    case DrawerType::LEFT:        return QRect(-m_drawerSize, 0, m_drawerSize, parentRect.height());    case DrawerType::RIGHT:        return QRect(parentRect.width(), 0, m_drawerSize, parentRect.height());    case DrawerType::TOP:        return QRect(0, -m_drawerSize, parentRect.width(), m_drawerSize);    case DrawerType::BOTTOM:        return QRect(0, parentRect.height(), parentRect.width(), m_drawerSize);    }    return QRect();}QRect DrawerWidget::getEndGeometry(){    QRect parentRect = rect();    switch (m_type)    {    case DrawerType::LEFT:        return QRect(00, m_drawerSize, parentRect.height());    case DrawerType::RIGHT:        return QRect(parentRect.width() - m_drawerSize, 0, m_drawerSize, parentRect.height());    case DrawerType::TOP:        return QRect(00, parentRect.width(), m_drawerSize);    case DrawerType::BOTTOM:        return QRect(0, parentRect.height() - m_drawerSize, parentRect.width(), m_drawerSize);    }    return QRect();}QPoint DrawerWidget::getStartPos(){    QRect parentRect = rect();    switch (m_type)    {    case DrawerType::LEFT:        return QPoint(-m_drawerSize, 0);    case DrawerType::RIGHT:        return QPoint(parentRect.width(), 0);    case DrawerType::TOP:        return QPoint(0, -m_drawerSize);    case DrawerType::BOTTOM:        return QPoint(0, parentRect.height());    }    return QPoint();}QPoint DrawerWidget::getEndPos(){    QRect parentRect = rect();    switch (m_type)    {    case DrawerType::LEFT:        return QPoint(00);    case DrawerType::RIGHT:        return QPoint(parentRect.width() - m_drawerSize, 0);    case DrawerType::TOP:        return QPoint(00);    case DrawerType::BOTTOM:        return QPoint(0, parentRect.height() - m_drawerSize);    }    return QPoint();}void DrawerWidget::open(){    m_isOpen = true;    show();   /* m_animation->stop();    m_animation->setStartValue(getStartGeometry());    m_animation->setEndValue(getEndGeometry());    m_animation->start();*/    updateGeometryPosition();   // 先确保尺寸正确    m_animation->stop();    m_animation->setStartValue(getStartPos());    m_animation->setEndValue(getEndPos());    m_animation->setEasingCurve(QEasingCurve::OutCubic);    m_animation->start();}void DrawerWidget::closeDrawer(){    m_isOpen = false;    /*m_animation->stop();    m_animation->setStartValue(getEndGeometry());    m_animation->setEndValue(getStartGeometry());    connect(m_animation, &QPropertyAnimation::finished, this, [this]() {        if (m_onClose) m_onClose();        });    m_animation->start();*/    m_animation->stop();    m_animation->setStartValue(getEndPos());    m_animation->setEndValue(getStartPos());    connect(m_animation, &QPropertyAnimation::finished, this, [this]() {        this->hide();        if (m_onClose) m_onClose();        });    m_animation->setEasingCurve(QEasingCurve::OutCubic);    m_animation->start();    //遮罩透明度消失    QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(m_maskWidget);    m_maskWidget->setGraphicsEffect(effect);    QPropertyAnimation* fadeAnim = new QPropertyAnimation(effect, "opacity");    fadeAnim->setDuration(3000);    fadeAnim->setStartValue(1.0);    fadeAnim->setEndValue(0.0);    connect(fadeAnim, &QPropertyAnimation::finished, [this, effect, fadeAnim]() {        effect->deleteLater();        fadeAnim->deleteLater();     });    fadeAnim->start();}bool DrawerWidget::eventFilter(QObject* obj, QEvent* event){    if (obj == m_maskWidget && m_maskClickable)    {        if (event->type() == QEvent::MouseButtonPress)        {            closeDrawer();            return true;        }    }    //窗口改变 重置窗体位置    if (obj == parentWidget() && event->type() == QEvent::Resize)    {        setGeometry(parentWidget()->rect());        m_maskWidget->setGeometry(rect());        updateGeometryPosition();    }    return QWidget::eventFilter(obj, event);}void DrawerWidget::setDrawerSize(int size){    m_drawerSize = size;}void DrawerWidget::setTitle(const QString& title){    m_titleLabel->setText(title);}void DrawerWidget::setContentWidget(QWidget* widget){    if (m_contentContainer->layout())        delete m_contentContainer->layout();    QVBoxLayout* layout = new QVBoxLayout(m_contentContainer);    layout->setContentsMargins(0000);    layout->addWidget(widget);}void DrawerWidget::setMaskOpacity(qreal opacity){    m_maskOpacity = opacity;    int alpha = opacity * 255;    m_maskWidget->setStyleSheet(        QString("background-color: rgba(0,0,0,%1);").arg(alpha));}void DrawerWidget::setMaskClickable(bool enable){    m_maskClickable = enable;}void DrawerWidget::setOnClose(const std::function<void()>& cb){    m_onClose = cb;}void DrawerWidget::setOnCancel(const std::function<void()>& cb){    m_onCancel = cb;}void DrawerWidget::setOnConfirm(const std::function<void()>& cb){    m_onConfirm = cb;}

drawermanager源码:

#pragma once#include<QObject>#include<QMap>#include"DrawerWidget.h"class DrawerManager : public QObject{    Q_OBJECTpublic:    static DrawerManager* instance();    DrawerWidget*  showDrawer(DrawerType type,        QWidget* parent,        QWidget* content,        const QString& title = "",        int size = 300        const std::function<void()>& onClose = nullptr,        const std::function<void()>& onConfirm = nullptr,        const std::function<void()>& onCannel = nullptr);    voidcloseDrawer(DrawerWidget* drawer);private:    explicitDrawerManager(QObject* parent = nullptr);    ~DrawerManager();    DrawerWidget* getOrCreateDrawer(DrawerType type, QWidget* parent);private:};
#include "drawermanager.h"DrawerManagerDrawerManager::instance(){    static DrawerManager manager;    return &manager;}//析构函数DrawerManager::~DrawerManager(){}DrawerManager::DrawerManager(QObject* parent)    : QObject(parent){}/// <summary>/// 创建抽屉/// </summary>/// <param name="type"></param>/// <param name="parent"></param>/// <returns></returns>DrawerWidgetDrawerManager::getOrCreateDrawer(DrawerTypetypeQWidget* parent){    DrawerWidget* drawer = new DrawerWidget(parent,type);    return drawer;}/// <summary>/// 显示抽屉/// </summary>/// <param name="type"></param>/// <param name="parent"></param>/// <param name="content"></param>/// <param name="title"></param>/// <param name="size"></param>DrawerWidget*  DrawerManager::showDrawer(DrawerTypetype,    QWidget* parent,    QWidget* content,    const QString& title,    int size,    const std::function<void()>& onClose,    const std::function<void()>& onConfirm,    const std::function<void()>& onCannel){    DrawerWidget* drawer = getOrCreateDrawer(type, parent);    drawer->setOnClose(onClose);    drawer->setOnConfirm(onConfirm);    drawer->setOnCancel(onCannel);    drawer->setDrawerSize(size);    drawer->setTitle(title);    drawer->setContentWidget(content);    drawer->open();    return drawer;}// <summary>/// 关闭抽屉/// </summary>/// <param name="type"></param>void DrawerManager::closeDrawer(DrawerWidget* drawer){     drawer->closeDrawer();}

上面就是插件的全部源码。

下面是调用插件的demo

左抽屉调用效果:

调用源码如下:

ui->pushButtonLeft 为按钮对象,按你实际对象替换

//左抽屉connect(ui->pushButtonLeft, &QPushButton::clicked, [this]() {	QWidget* custom = new QWidget;	QVBoxLayout* layout = new QVBoxLayout(custom);	layout->addWidget(new QLabel("这里是左抽屉内容22222"));	DrawerWidget* drawer = DrawerManager::instance()->showDrawer(		DrawerType::LEFT,		this->window(),		custom,		"左抽屉",		400, [] {			qDebug() << "点击了关闭";		}, [] {			qDebug() << "confirm";			}, [] {				qDebug() << "cannel";				}				);	drawer->setShowFooter(true);	//关闭	//DrawerManager::instance()->closeDrawer(drawer);	});

显示底部按钮 confirm \ cannel , 并注册回调函数。

顶部抽屉调用效果:

调用源码如下:

//上抽屉connect(ui->pushButtonUp, &QPushButton::clicked, [this]() {	QWidget* custom = new QWidget;	QVBoxLayout* layout = new QVBoxLayout(custom);	layout->addWidget(new QLabel("这里是上抽屉内容"));	DrawerWidget* drawer = DrawerManager::instance()->showDrawer(		DrawerType::TOP,		this,		custom,		"上抽屉",		400, [] {			qDebug() << "点击了关闭";		});	drawer->setMaskOpacity(0);//非透明	drawer->setMaskClickable(false);//取消遮罩点击	//关闭	//DrawerManager::instance()->closeDrawer(drawer);	});

下面根据返回的抽屉对象,对遮罩的功能进行调用

drawer->setMaskOpacity(0);//非透明drawer->setMaskClickable(false);//取消遮罩点击

右侧抽屉调用效果:

调用源码如下:

//右抽屉connect(ui->pushButtonRight, &QPushButton::clicked, [this]() {	QWidget* custom = new QWidget;	QVBoxLayout* layout = new QVBoxLayout(custom);	layout->addWidget(new QLabel("这里是右抽屉内容"));	DrawerWidget* drawer = DrawerManager::instance()->showDrawer(		DrawerType::RIGHT,		this,		custom,		"右抽屉",		400, [] {			qDebug() << "点击了关闭";		});	//关闭	//DrawerManager::instance()->closeDrawer(drawer);	});

底部抽屉调用效果:

调用源码如下:

//下抽屉connect(ui->pushButtonBottom, &QPushButton::clicked, [this]() {	QWidget* custom = new QWidget;	QVBoxLayout* layout = new QVBoxLayout(custom);	layout->addWidget(new QLabel("这里是下抽屉内容"));	DrawerWidget* drawer = DrawerManager::instance()->showDrawer(		DrawerType::BOTTOM,		this,		custom,		"下抽屉",		400, [] {			qDebug() << "点击了关闭";		});	//关闭	//DrawerManager::instance()->closeDrawer(drawer);	});

好了,就看到这里啦,插件介绍完毕,是否对你有帮助啵!

写插件的乐趣就在于:一次痛苦,终身享福。虽然折腾这个 Drawer 耗费了一个下午,但当你看到那个侧边栏顺滑地弹出、缩回,那种指哪打哪的掌控感,是不是比多吃一顿火锅还解压?(好吧,火锅还是更香一点)。

这个插件满足基本功能,后续其实还有很多脑洞可以挖:

  • 比如加入毛玻璃滤镜,让抽屉变得更有“果里果气”的高级感。

  • 或者支持手势横扫弹出,让你的 PC 软件强行体验移动端的丝滑。

代码我已经毫无保留地摊在上面了。如果你在使用过程中发现了 Bug,或者有更优雅的实现方式,欢迎在评论区给我“致命一击”。如果你觉得有用,反手给个点赞或收藏,毕竟这可能是我防脱发路上的唯一慰藉了。

关注我获取更多基础编程知识 
一个热爱编程、分享的 Bug 战士

👆🏻👆🏻👆🏻扫码关注👆🏻👆🏻👆🏻

点个「」赞,是我持续更新的动力 

本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » C++ QT之抽屉效果drawer插件

评论 抢沙发

8 + 4 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮