C++ QT之抽屉效果drawer插件
在 UI 设计界,有一种经典冲突叫做:“老板想要的功能太多” vs “屏幕给的空间太少”。
每当这种时候,侧边抽屉(Drawer)就像是救命稻草——平时藏在阴影里装高冷,需要时滑出来展示各种控件,简直是空间管理界的“收纳大师”。虽然 Qt 自带的组件不少,但要在 QWidget 体系下搞一个既有平滑动画、又能自适应方向、还得长得够“现代”的抽屉,官方现成的轮子总觉得缺了点火候。
本着“能动手写插件,就绝不手动拖控件”的极客精神,我决定亲自撸一个 Qt 抽屉插件。这不仅是为了让界面看起来更高级,更是为了下次被改需求时,能优雅地回一句:“别急,我拉一下抽屉就有了。”
下面,请系好安全带,我们要进入硬核的实现环节了。

#pragmaonce/// <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~1voidsetMaskClickable(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(0, 0, 0, 20)); // 阴影颜色shadow->setOffset(0, 0); // 默认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(15, 15, 15, 15);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(30, 30);headerLayout->addWidget(m_titleLabel);headerLayout->addStretch();headerLayout->addWidget(m_closeBtn);// ===== Content =====m_contentContainer = new QWidget;m_contentContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::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());elsem_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(0, 0, m_drawerSize, parentRect.height());case DrawerType::RIGHT:return QRect(parentRect.width() - m_drawerSize, 0, m_drawerSize, parentRect.height());case DrawerType::TOP:return QRect(0, 0, 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(0, 0);case DrawerType::RIGHT:return QPoint(parentRect.width() - m_drawerSize, 0);case DrawerType::TOP:return QPoint(0, 0);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(0, 0, 0, 0);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"DrawerManager* DrawerManager::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>DrawerWidget* DrawerManager::getOrCreateDrawer(DrawerTypetype, QWidget* 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,或者有更优雅的实现方式,欢迎在评论区给我“致命一击”。如果你觉得有用,反手给个点赞或收藏,毕竟这可能是我防脱发路上的唯一慰藉了。

▼点个「
」赞,是我持续更新的动力 ▼
夜雨聆风
