Qt抽屉窗体实现demo

cpp 复制代码
#ifndef DRAWERITEM_H
#define DRAWERITEM_H

#include <QWidget>
#include <QLabel>
#include <QVBoxLayout>

/**
 * @brief 抽屉项: 标题栏(可点击切换) + 内容区(由调用者塞入)
 *
 * 多展开模式: 每个抽屉项独立开合,互不影响。
 * 无动画,无样式表,内容区由调用方通过 setContentWidget() 注入。
 */
class DrawerItem : public QWidget
{
    Q_OBJECT
public:
    explicit DrawerItem(const QString& title, QWidget* parent = nullptr);

    // 注入内容区。旧的 contentWidget 会被自动释放。
    void setContentWidget(QWidget* w);

    // 展开/收起
    void setExpanded(bool on);
    bool isExpanded() const { return expanded; }

    // 修改标题
    void setTitle(const QString& t);

signals:
    // 展开/收起状态变化通知(多展开模式下可选,用于外部联动)
    void expandedChanged(bool expanded);

protected:
    // 监听标题栏及其子控件的点击
    bool eventFilter(QObject* obj, QEvent* e) override;

private:
    void updateArrow();

    QWidget*     headerWidget;   // 标题栏容器
    QLabel*      arrowLabel;     // ▶ / ▼ 指示
    QLabel*      titleLabel;     // 标题文字
    QWidget*     contentWidget;  // 调用方注入的内容区
    QVBoxLayout* outerLayout;    // 自身的纵向布局
    bool         expanded;
};

#endif // DRAWERITEM_H
cpp 复制代码
#include "DrawerItem.h"
#include <QHBoxLayout>
#include <QEvent>
#include <QMouseEvent>

DrawerItem::DrawerItem(const QString& title, QWidget* parent)
    : QWidget(parent)
    , headerWidget(nullptr)
    , arrowLabel(nullptr)
    , titleLabel(nullptr)
    , contentWidget(nullptr)
    , outerLayout(nullptr)
    , expanded(false)
{
    outerLayout = new QVBoxLayout(this);
    outerLayout->setContentsMargins(0, 0, 0, 0);
    outerLayout->setSpacing(0);

    // 标题栏
    headerWidget = new QWidget(this);
    auto* headerLay = new QHBoxLayout(headerWidget);
    headerLay->setContentsMargins(8, 6, 8, 6);
    headerLay->setSpacing(6);

    arrowLabel = new QLabel(QStringLiteral("\u25B6"), headerWidget);  // ▶
    arrowLabel->setFixedWidth(12);
    arrowLabel->setAlignment(Qt::AlignCenter);

    titleLabel = new QLabel(title, headerWidget);

    headerLay->addWidget(arrowLabel);
    headerLay->addWidget(titleLabel);
    headerLay->addStretch();

    outerLayout->addWidget(headerWidget);

    // 让标题栏容器及其子控件的点击都触发开合切换
    headerWidget->installEventFilter(this);
    arrowLabel->installEventFilter(this);
    titleLabel->installEventFilter(this);
}

void DrawerItem::setContentWidget(QWidget* w)
{
    // 移除并释放旧内容区
    if (contentWidget) {
        outerLayout->removeWidget(contentWidget);
        contentWidget->deleteLater();
    }

    contentWidget = w;
    if (w) {
        w->setParent(this);
        outerLayout->addWidget(w);
        w->setVisible(expanded);  // 按当前状态决定可见性
    }
}

void DrawerItem::setExpanded(bool on)
{
    if (expanded == on) return;
    expanded = on;

    if (contentWidget)
        contentWidget->setVisible(expanded);

    updateArrow();
    emit expandedChanged(expanded);
}

void DrawerItem::setTitle(const QString& t)
{
    titleLabel->setText(t);
}

void DrawerItem::updateArrow()
{
    // 展开 ▼, 收起 ▶
    arrowLabel->setText(expanded ? QStringLiteral("\u25BC") : QStringLiteral("\u25B6"));
}

bool DrawerItem::eventFilter(QObject* obj, QEvent* e)
{
    if (e->type() == QEvent::MouseButtonPress) {
        if (obj == headerWidget || obj == arrowLabel || obj == titleLabel) {
            setExpanded(!expanded);
            return true;  // 事件已处理,不再向下传递
        }
    }
    return QWidget::eventFilter(obj, e);
}
cpp 复制代码
#include <QApplication>
#include <QMainWindow>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QGridLayout>

#include "DrawerItem.h"

// 占位内容区:模拟你自己实现的内容(状态文字 + 一组操作按钮)
// 你只需要照着这个方式 new 一个 QWidget, 然后 setContentWidget() 塞进来。
static QWidget* makePlaceholderContent(const QString& statusLine)
{
    auto* w = new QWidget;
    auto* lay = new QVBoxLayout(w);
    lay->setContentsMargins(20, 8, 8, 8);
    lay->setSpacing(8);

    auto* info = new QLabel(statusLine, w);
    lay->addWidget(info);

    // 2x2 操作按钮占位
    auto* btnLay = new QGridLayout();
    const QStringList names = {
        QStringLiteral("开机"), QStringLiteral("待机"),
        QStringLiteral("跟踪"), QStringLiteral("静默")
    };
    for (int i = 0; i < 4; ++i) {
        auto* btn = new QPushButton(names.value(i), w);
        btnLay->addWidget(btn, i / 2, i % 2);
    }
    lay->addLayout(btnLay);
    lay->addStretch();
    return w;
}

int main(int argc, char** argv)
{
    QApplication app(argc, argv);

    // 抽屉容器: 抽屉项紧密排列, 不加 addStretch(否则收起后会有空隙下坠)
    auto* container = new QWidget;
    auto* layout = new QVBoxLayout(container);
    layout->setContentsMargins(0, 0, 0, 0);
    layout->setSpacing(2);

    // 多展开模式: 每项独立开合
    auto* radar  = new DrawerItem(QStringLiteral("雷达"), container);
    radar->setContentWidget(makePlaceholderContent(
        QStringLiteral("状态: 工作 · X/S 波段\n作用距离: 48 nmi\n关联批次: 0")));
    radar->setExpanded(true);  // 默认展开第一个

    auto* ais    = new DrawerItem(QStringLiteral("AIS"), container);
    ais->setContentWidget(makePlaceholderContent(QStringLiteral("AIS 船舶自动识别系统")));

    auto* photo  = new DrawerItem(QStringLiteral("光电"), container);
    photo->setContentWidget(makePlaceholderContent(QStringLiteral("光电探测设备状态")));

    auto* esm    = new DrawerItem(QStringLiteral("电子侦察"), container);
    esm->setContentWidget(makePlaceholderContent(QStringLiteral("电子侦察 / ESM")));

    auto* sonar  = new DrawerItem(QStringLiteral("声呐"), container);
    sonar->setContentWidget(makePlaceholderContent(QStringLiteral("水下声学探测")));

    layout->addWidget(radar);
    layout->addWidget(ais);
    layout->addWidget(photo);
    layout->addWidget(esm);
    layout->addWidget(sonar);

    // 多展开必备: 滚动区, 防止所有项都展开时被裁切
    auto* scroll = new QScrollArea;
    scroll->setWidget(container);
    scroll->setWidgetResizable(true);

    QMainWindow win;
    win.setWindowTitle(QStringLiteral("设备列表 - 抽屉演示"));
    win.resize(360, 600);
    win.setCentralWidget(scroll);
    win.show();

    return app.exec();
}

优化1:

能不能往下面加一个弹簧,我觉得间距太大了

改完了。在 main.cpp#L72 末尾加了一行`layout->addStretch();` 。

效果:5 个抽屉项被推到顶部紧凑排列,下方空白由弹簧占据。

补丁2:

你看,这中间的空白很多,我希望DrawerItem是紧凑的,不要延展



改完了,两处改动:

  • DrawerItem.cpp#L42-L44:构造里加 setSizePolicy(Preferred, Maximum),禁止 vertical 方向被拉得比 sizeHint 高。
  • DrawerItem.cpp#L72:setExpanded 末尾加 updateGeometry(),让父 layout 在开合切换时重新协商高度。

改完后的效果

  • 收起状态 :`sizeHint` = 标题栏高度(约 30 像素),DrawerItem 紧贴,不会被拉高。

  • 展开状态 :`sizeHint` = 标题栏 + 内容区,`contentWidget` 不被外层拉伸,内部`addStretch()` 占 0 空间, 没有大块空白 。

  • 底部弹簧 :末尾`addStretch()`仍然把所有抽屉顶到上面,下方空白由弹簧占据。

原理说明(避免以后踩坑)

size policy (vertical) 行为
Preferred(默认) layout 可以把它拉得比 sizeHint 高 → 容易出现空白
Maximum(现在用的) 不能比 sizeHint 高 → 紧凑
Fixed 高度永远 = sizeHint,不能被压缩

我觉得这个三角图标有点丑,我在考虑还是使用真实的图片

效果说明

  • 收起:→ 系统风格的右箭头(`SP_ArrowRight` )

  • 展开:↓ 系统风格的下箭头(`SP_ArrowDown` )

  • 图标尺寸 16×16,跟随当前 QStyle(Windows 上默认是 Windows 风格的箭头,比 Unicode 三角精致很多)

cpp 复制代码
#include "DrawerItem.h"
#include <QHBoxLayout>
#include <QEvent>
#include <QMouseEvent>
#include <QStyle>
#include <QIcon>
#include <QPixmap>

DrawerItem::DrawerItem(const QString& title, QWidget* parent)
    : QWidget(parent)
    , headerWidget(nullptr)
    , arrowLabel(nullptr)
    , titleLabel(nullptr)
    , contentWidget(nullptr)
    , outerLayout(nullptr)
    , expanded(false)
{
    outerLayout = new QVBoxLayout(this);
    outerLayout->setContentsMargins(0, 0, 0, 0);
    outerLayout->setSpacing(0);

    // 标题栏
    headerWidget = new QWidget(this);
    auto* headerLay = new QHBoxLayout(headerWidget);
    headerLay->setContentsMargins(8, 6, 8, 6);
    headerLay->setSpacing(6);

    arrowLabel = new QLabel(headerWidget);
    arrowLabel->setFixedWidth(16);
    arrowLabel->setAlignment(Qt::AlignCenter);

    titleLabel = new QLabel(title, headerWidget);

    headerLay->addWidget(arrowLabel);
    headerLay->addWidget(titleLabel);
    headerLay->addStretch();

    outerLayout->addWidget(headerWidget);

    // 让标题栏容器及其子控件的点击都触发开合切换
    headerWidget->installEventFilter(this);
    arrowLabel->installEventFilter(this);
    titleLabel->installEventFilter(this);

    // 紧凑模式: vertical 方向用 Maximum, 不允许 layout 把 DrawerItem 拉得比 sizeHint 高
    // 否则展开项内部的 contentWidget 会被拉高, 内部的 stretch 会形成大块空白
    setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);

    updateArrow();   // 设置初始箭头图标(收起态)
}

void DrawerItem::setContentWidget(QWidget* w)
{
    // 移除并释放旧内容区
    if (contentWidget) {
        outerLayout->removeWidget(contentWidget);
        contentWidget->deleteLater();
    }

    contentWidget = w;
    if (w) {
        w->setParent(this);
        outerLayout->addWidget(w);
        w->setVisible(expanded);  // 按当前状态决定可见性
    }
}

void DrawerItem::setExpanded(bool on)
{
    if (expanded == on) return;
    expanded = on;

    if (contentWidget)
        contentWidget->setVisible(expanded);

    updateArrow();
    updateGeometry();   // 通知父 layout: 我的 sizeHint 变了, 请重新分配
    emit expandedChanged(expanded);
}

void DrawerItem::setTitle(const QString& t)
{
    titleLabel->setText(t);
}

void DrawerItem::updateArrow()
{
    // 用 Qt 内置标准箭头图标: 展开向下(▼), 收起向右(▶)
    QStyle::StandardPixmap sp = expanded ? QStyle::SP_ArrowDown
                                          : QStyle::SP_ArrowRight;
    QIcon icon = style()->standardIcon(sp);
    arrowLabel->setPixmap(icon.pixmap(16, 16));
}

bool DrawerItem::eventFilter(QObject* obj, QEvent* e)
{
    if (e->type() == QEvent::MouseButtonPress) {
        if (obj == headerWidget || obj == arrowLabel || obj == titleLabel) {
            setExpanded(!expanded);
            return true;  // 事件已处理,不再向下传递
        }
    }
    return QWidget::eventFilter(obj, e);
}

帮我写个qss文件,然后软件读取并全局加载

样式效果

元素 收起态 展开态
标题栏背景 #0f1c2e #142845
标题栏左边线 透明 橙色 #ff6b00(active 标识)
标题文字 #b0bec5 灰色 #00d4ff 青色高亮
按钮 透明底 + #1e3a5f 边框 + 青色文字 hover 时背景变深蓝、边框变青、文字变白
相关推荐
quantdash_cc1 小时前
Python 获取实时行情后如何进行批量筛选?从全市场快照到策略候选池
开发语言·python·数据分析·量化交易·股票数据·quantdash
论文复现现场1 小时前
论文复现看到“RTX 3090 or higher”:显存、CUDA、batch size 与 OOM 怎么判断?
开发语言·pytorch·batch·cuda·rtx3090
智鸟科技GemeOpen开发者智能设备1 小时前
GemeOpen 智能音箱 GSSM0B - 播放控制(Java示例)
java·开发语言·智能音箱
m0_380743872 小时前
PHP 8.3 接口返回数据为空怎么排查
开发语言·php
谢亮_vipxieliang2 小时前
Java 21 新特性实战:Record、Sealed、模式匹配
java·开发语言
霸道流氓气质2 小时前
LLM 应用限流与熔断机制完全指南:从多层防护架构到Java生产级弹性实战
java·开发语言·架构
大侠归来2 小时前
C语言内存管理:从栈到堆的完整指南
c语言·开发语言·python
m0_380743873 小时前
PHP7.0字符串在Docker怎么用
开发语言·php
朝朝辞暮i3 小时前
C++ 第 10 课:函数 Function
开发语言·c++