Qt侧边栏布局的实现示例

前言

侧边栏(Sidebar)是现代桌面应用里最常见的布局形态之一:左边一条窄的导航/工具栏,右边一大块主内容区,中间还可以拖动分隔条调整宽度。VSCode、Typora、Qt Creator 自不必说,连很多工具类小软件也都会做成这个样子。

在 Qt 里实现侧边栏其实有至少四五种路子:QHBoxLayout 手搭、QSplitter 拖拽、QDockWidget 停靠、QToolBox 折叠、QPropertyAnimation 做抽屉动画。每种都有它的适用场景和坑。

本文会从最基础的布局原理讲起,逐步深入到"可折叠 + 带动画"的侧边栏,并且重点说清那个让无数新手抓狂的问题:为什么我设置了布局,界面却纹丝不动?

一、先搞清楚:Qt 布局到底是怎么工作的

要理解侧边栏,必须先理解 Qt 的布局系统。它的核心是三个概念:

  1. 父子关系(parent-child):一个 widget 的位置和尺寸由它的父 widget 决定;
  2. 布局管理器(layout):按照一定规则,把子 widget 排列在父 widget 的客户区里;
  3. 尺寸策略(size policy):告诉布局"我这个 widget 希望占多大、能不能被拉伸"。

侧边栏的本质,就是一个"宽度受限、但高度撑满"的子 widget。所以它需要:

cpp 复制代码
sidebar->setFixedWidth(200);   // 或者 setMinimumWidth / setMaximumWidth

然后把它和一个主内容区一起塞进水平布局:

cpp 复制代码
auto *layout = new QHBoxLayout;

layout->addWidget(sidebar);

layout->addWidget(content, 1);   // 拉伸因子 1,让内容区吃掉剩余空间

这里 addWidget(content, 1) 的第二个参数是拉伸因子(stretch)。这是关键:侧边栏没有拉伸因子(默认 0),内容区是 1,那么所有多余空间都给内容区,侧边栏保持自己的宽度。

尺寸策略简表

SizePolicy 含义 侧边栏该用吗
Fixed 完全按 sizeHint,不可拉伸 固定宽度侧边栏合适
Minimum sizeHint 是最小值,可拉大 可以
Preferred 最好按 sizeHint,也可以拉大 一般
Expanding 尽量占满可用空间 ❌ 侧边栏绝对不要用
Ignored 无所谓的,随便拉伸 ❌

新手最常犯的错就是把侧边栏设成 Expanding,结果侧边栏把整个窗口占满,内容区被挤成一条缝。

二、方案一:QHBoxLayout 手搭(最简单)

先看一个能跑的最小实现。

cpp 复制代码
#include <QApplication>

#include <QWidget>

#include <QHBoxLayout>

#include <QVBoxLayout>

#include <QLabel>

#include <QPushButton>

#include <QListWidget>

#include <QStackedWidget>



int main(int argc, char *argv[])

{

    QApplication app(argc, argv);



    auto *window = new QWidget;

    window->setWindowTitle(QStringLiteral("侧边栏布局"));

    window->resize(800, 520);



    // ---- 左侧:侧边栏 ----

    auto *sidebar = new QWidget;

    sidebar->setObjectName("sidebar");          // 给 QSS 用

    sidebar->setFixedWidth(200);



    auto *navList = new QListWidget(sidebar);

    navList->addItems({QStringLiteral("首页"),

                       QStringLiteral("消息"),

                       QStringLiteral("设置")});



    auto *sideLayout = new QVBoxLayout(sidebar);

    sideLayout->setContentsMargins(0, 0, 0, 0);

    sideLayout->setSpacing(0);

    sideLayout->addWidget(navList, 1);



    // ---- 右侧:内容区 ----

    auto *content = new QStackedWidget;

    for (int i = 0; i < 3; ++i) {

        auto *page = new QLabel(QStringLiteral("第 %1 页内容").arg(i + 1));

        page->setAlignment(Qt::AlignCenter);

        content->addWidget(page);

    }



    // ---- 合并 ----

    auto *root = new QHBoxLayout(window);

    root->setContentsMargins(0, 0, 0, 0);

    root->setSpacing(0);

    root->addWidget(sidebar);

    root->addWidget(content, 1);       // 关键:内容区拉伸因子为 1



    QObject::connect(navList, &QListWidget::currentRowChanged,

                     content, &QStackedWidget::setCurrentIndex);



    window->show();

    return app.exec();

}

配套 .pro:

bash 复制代码
QT += widgets

CONFIG += c++17

TARGET = SidebarDemo

SOURCES += main.cpp

或者 CMake:

cmake 复制代码
cmake_minimum_required(VERSION 3.16)

project(SidebarDemo LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)

set(CMAKE_AUTOMOC ON)

find_package(Qt6 REQUIRED COMPONENTS Widgets)

add_executable(SidebarDemo main.cpp)

target_link_libraries(SidebarDemo PRIVATE Qt6::Widgets)

这个方案简单直接,缺点是宽度不能拖、不能折叠。

三、方案二:QSplitter 实现可拖拽分隔条

QSplitter(分裂器)自带一个可拖拽的分隔条,用户能自由调整两侧比例,还能"拖到尽头就折叠"。

cpp 复制代码
#include <QSplitter>

#include <QListWidget>

#include <QTextEdit>



auto *splitter = new QSplitter(Qt::Horizontal);

splitter->addWidget(sidebarList);

splitter->addWidget(textEdit);



// 关键:设置初始比例和伸缩特性

splitter->setStretchFactor(0, 0);   // 侧边栏不随窗口拉伸

splitter->setStretchFactor(1, 1);   // 内容区拉伸

splitter->setSizes({220, 580});     // 初始宽度分配



// 不允许子项被完全折叠(0 像素),保留最小宽度

splitter->setChildrenCollapsible(false);

sidebarList->setMinimumWidth(140);

几个 QSplitter 的重要特性:

特性 说明
setStretchFactor(i, f) 窗口缩放时各子项按 f 分多余空间
setSizes({...}) 设初始尺寸分配
setChildrenCollapsible(false) 防止拖到 0 宽度
setCollapsible(i, false) 只禁止某一个子项折叠
handleWidth 分隔条宽度(配合 QSS 可做细线)
saveState() / restoreState() 保存/恢复用户拖拽的比例

saveState() 这个很有用:用户辛苦调好的比例,重启程序就没了会很烦躁。存到 QSettings 里即可。

cpp 复制代码
// 退出时保存

QSettings settings;

settings.setValue("splitter/state", splitter->saveState());



// 启动时恢复

const QByteArray state = settings.value("splitter/state").toByteArray();

if (!state.isEmpty())

    splitter->restoreState(state);

四、方案三:可折叠侧边栏 + 动画

实际产品里,侧边栏往往要能"收起来",给内容区腾地方。下面这个方案用 QPropertyAnimation 做宽度动画。

cpp 复制代码
// collapsiblesidebar.h

class CollapsibleSidebar : public QWidget

{

    Q_OBJECT

    Q_PROPERTY(int animatedWidth READ animatedWidth WRITE setAnimatedWidth)

public:

    explicit CollapsibleSidebar(QWidget *parent = nullptr);

    bool isExpanded() const { return m_expanded; }

    int  animatedWidth() const { return width(); }

public slots:

    void setExpanded(bool expanded);

    void toggle();

private slots:

    void setAnimatedWidth(int w);

private:

    QPropertyAnimation *m_anim = nullptr;

    bool m_expanded = true;

    int  m_expandedWidth;

};
cpp 复制代码
// collapsiblesidebar.cpp

#include "collapsiblesidebar.h"



#include <QPropertyAnimation>

#include <QVBoxLayout>

#include <QListWidget>



CollapsibleSidebar::CollapsibleSidebar(QWidget *parent)

    : QWidget(parent)

    , m_expandedWidth(220)

{

    setObjectName(QStringLiteral("sidebar"));

    setMinimumWidth(0);

    setMaximumWidth(m_expandedWidth);



    auto *list = new QListWidget(this);

    list->addItems({QStringLiteral("文件"), QStringLiteral("搜索"),

                    QStringLiteral("源码管理"), QStringLiteral("调试")});



    auto *layout = new QVBoxLayout(this);

    layout->setContentsMargins(0, 0, 0, 0);

    layout->addWidget(list);



    m_anim = new QPropertyAnimation(this, "animatedWidth", this);

    m_anim->setDuration(180);

    m_anim->setEasingCurve(QEasingCurve::InOutQuad);

}



void CollapsibleSidebar::setAnimatedWidth(int w)

{

    // 宽度受 setMaximumWidth 限制,所以这里直接改固定宽度

    setFixedWidth(w);

}



void CollapsibleSidebar::setExpanded(bool expanded)

{

    if (m_expanded == expanded)

        return;

    m_expanded = expanded;



    m_anim->stop();

    m_anim->setStartValue(width());

    m_anim->setEndValue(expanded ? m_expandedWidth : 0);

    m_anim->start();

}



void CollapsibleSidebar::toggle()

{

    setExpanded(!m_expanded);

}

主窗口里这样用:

cpp 复制代码
auto *root = new QHBoxLayout(window);

root->setContentsMargins(0, 0, 0, 0);

root->setSpacing(0);



auto *sidebar = new CollapsibleSidebar;

root->addWidget(sidebar);

root->addWidget(content, 1);



auto *toggleBtn = new QPushButton(QStringLiteral("☰"));

QObject::connect(toggleBtn, &QPushButton::clicked,

                 sidebar, &CollapsibleSidebar::toggle);

动画方案的关键点

要点 说明
Q_PROPERTY + QPropertyAnimation 属性必须有 READ/WRITE 访问器
用 setFixedWidth() 而非 resize() 布局会覆盖 resize() 的结果
setStartValue(width()) 从当前宽度动,中途打断也平滑
动画期间布局自动跟随 setFixedWidth 会触发 updateGeometry,兄弟 widget 自动重排

五、方案对比:到底该用哪个

方案 可拖拽 可折叠 动画 代码量 适用场景
QHBoxLayout ❌ 手动 ❌ 极少 固定宽度侧边栏
QSplitter ✅ 可选 ❌ 少 用户需要自定义比例
QPropertyAnimation 自绘 ❌ ✅ ✅ 中 现代风格折叠侧栏
QDockWidget ✅ ✅ ❌ 少 需要停靠/浮动/多面板
QToolBox ❌ ✅(分组) ❌ 少 竖向分组堆叠面板

关于 QDockWidget:它是 QMainWindow 专属的,自带标题栏、浮动按钮、停靠区域(上下左右)。如果你的侧边栏需要"用户能拖出来变成浮动窗口"的能力,它最省事,但外观比较难定制。

cpp 复制代码
auto *dock = new QDockWidget(QStringLiteral("导航"), mainWindow);

dock->setWidget(navList);

mainWindow->addDockWidget(Qt::LeftDockWidgetArea, dock);

常见坑点

坑点 1:设置了布局却没有任何效果

这是新手第一大坑。布局必须挂到一个 widget 上才会生效。下面这种写法是无效的:

❌ 错误写法:

cpp 复制代码
auto *layout = new QHBoxLayout;     // 没有父对象

layout->addWidget(sidebar);

layout->addWidget(content);

window->show();                     // 布局和 window 一点关系都没有

✅ 正确写法,三种等价方式任选:

cpp 复制代码
// 方式 A:构造时传父 widget

auto *layout = new QHBoxLayout(window);



// 方式 B:后置 setLayout

auto *layout = new QHBoxLayout;

window->setLayout(layout);



// 方式 C:父 widget 构造后,用在子 widget 上,但必须往上追到顶层

sidebar->setLayout(sideLayout);     // 这是给 sidebar 设布局,不是给 window

还有一种变体坑:给 QMainWindow 用 setLayout()。

❌ 错误写法:

cpp 复制代码
auto *w = new QMainWindow;

w->setLayout(new QHBoxLayout);      // Qt 会警告 "already has a layout"

✅ 正确写法:QMainWindow 有自己的布局槽位,中间区域要用 setCentralWidget()。

cpp 复制代码
auto *central = new QWidget;

auto *layout  = new QHBoxLayout(central);

layout->addWidget(sidebar);

layout->addWidget(content, 1);

w->setCentralWidget(central);

坑点 2:侧边栏把窗口撑满了

❌ 错误写法:

cpp 复制代码
sidebar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);

// 侧边栏会疯狂抢空间

✅ 正确写法:

cpp 复制代码
sidebar->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding);

sidebar->setFixedWidth(200);

// 或者用拉伸因子:layout->addWidget(content, 1);

顺带说,addWidget(w, 1) 的拉伸因子只在"有剩余空间"时起作用;侧边栏若是 Fixed,压根不参与分配。

坑点 3:折叠后留了一块空白

❌ 错误写法:

cpp 复制代码
navList->hide();     // 列表没了,但 sidebar 容器还在,占着 200px

✅ 正确写法,隐藏整个侧边栏容器:

cpp 复制代码
sidebar->hide();     // 或 hide() 之后调 updateGeometry()

另外 hide() 和 setVisible(false) 等价,但动画场景下不要立刻 hide() ,否则会瞬间消失、没有过渡。折中做法是展开前先 show(),动画结束后再真正 hide():

cpp 复制代码
void CollapsibleSidebar::setExpanded(bool expanded)

{

    if (expanded) show();                 // 先显示再动

    m_anim->setStartValue(width());

    m_anim->setEndValue(expanded ? m_expandedWidth : 0);

    if (!expanded) {

        QObject::disconnect(m_anim, &QPropertyAnimation::finished, nullptr, nullptr);

        QObject::connect(m_anim, &QPropertyAnimation::finished, this, [this]{

            if (!m_expanded) hide();      // 结束后再隐藏,避免 1px 残影

        });

    }

    m_anim->start();

}

坑点 4:动画属性没有生效

QPropertyAnimation 要求目标属性是已注册的 Q_PROPERTY ,且有 READ 和 WRITE。

❌ 错误写法:

cpp 复制代码
QPropertyAnimation anim(sidebar, "width");   // width 不是可写属性,无效

✅ 正确写法,自己声明一个:

cpp 复制代码
class CollapsibleSidebar : public QWidget {

    Q_OBJECT

    Q_PROPERTY(int animatedWidth READ animatedWidth WRITE setAnimatedWidth)

    // ...

};

而且注意:如果 setAnimatedWidth() 里调用 setFixedWidth(),setFixedWidth 内部会设 min=max=w,同时把 size policy 改成 Fixed------这正是我们想要的效果,因为这样布局才会立即重排。

坑点 5:QSS 给侧边栏设背景色没效果

QWidget 子类默认不绘制背景,QSS 里的 background-color 对裸 QWidget 无效。

❌ 错误写法:

cpp 复制代码
sidebar->setStyleSheet("QWidget { background: #2b2b2b; }");

// 大概率还是白的

✅ 正确写法,两种方案:

cpp 复制代码
// 方案 A:用 objectName 精准指定 + 声明 WA_StyledBackground

sidebar->setObjectName("sidebar");

sidebar->setAttribute(Qt::WA_StyledBackground, true);

sidebar->setStyleSheet("#sidebar { background-color: #2b2b2b; }");



// 方案 B:直接把侧边栏做成 QFrame

auto *sidebar = new QFrame;

sidebar->setFrameShape(QFrame::NoFrame);

sidebar->setStyleSheet("QFrame#sidebar { background-color: #2b2b2b; }");

WA_StyledBackground 属性是让 QWidget 走样式表绘制背景的开关,很多"QSS 不生效"的问题都是因为它。

坑点 6:QListWidget 选中项有难看的虚线框

侧边栏导航列表选中后,Qt 默认会画一圈焦点虚线。QSS 里用 outline: none 处理:

cpp 复制代码
navList->setStyleSheet(R"(

    QListWidget { border: none; background: transparent; outline: none; }

    QListWidget::item { height: 36px; padding-left: 16px; outline: none; }

    QListWidget::item:selected { background: #3a3a3a; color: #fff; }

    QListWidget::item:hover { background: #333; }

)");

注意 outline: none 要同时写在 QListWidget 和 QListWidget::item 两处,只写一处可能不管用。

总结

侧边栏布局的实现思路可以浓缩成一句话:一个宽度受限的 widget,加一个拉伸因子为 1 的内容区,塞进水平布局。

选型上:

需求 推荐方案
只要固定宽度 QHBoxLayout
用户要能拖比例 QSplitter + saveState()
要折叠动画 QPropertyAnimation + Q_PROPERTY
要能停靠浮动 QDockWidget(限 QMainWindow)

而所有"布局不生效"的问题,90% 都逃不出这三种原因:布局没挂到任何 widget 上、QMainWindow 用了 setLayout、侧边栏的 size policy 设成了 Expanding。把这三条记熟,再配上 setObjectName + WA_StyledBackground 处理 QSS 背景,侧边栏这块基本就不会再翻车了。

相关推荐
zh_xuan1 小时前
c++ 23 std::out_ptr用法
开发语言·c++23·out_ptr
陆卿之1 小时前
Java对接DeepSeek
java·开发语言·人工智能
庄园特聘拆椅狂魔1 小时前
Java 后端转全栈的第一课:从前端项目搭建到技术选
java·开发语言·前端
汉克老师2 小时前
GESP2026年9月认证C++五级( 第三部分编程题(1、哥德巴赫猜想))精讲
c++·gesp·小学生·学c++编程
小灰灰搞电子2 小时前
C++ final 关键字详解:从虚函数到类继承的完整指南
c++·final
weixin_307779132 小时前
C++代码实现MATLAB中的pade函数功能
开发语言·c++·算法·matlab
ProcessOn官方账号2 小时前
java IO操作 你快速认识IO的相关基本知识
java·开发语言
神仙别闹2 小时前
基于C++实现的(控制台)学校开课查询系统
开发语言·c++
Dylan的码园3 小时前
A+B问题 进制处理【逻辑控制 运算符】
c++·算法