QScrollArea 只滚动通过 setWidget() 指定的那个内容控件。把一个 QWidget 构造成它的子对象,还不足以把它登记为滚动内容;内容的最小尺寸、布局约束和视口大小,才共同决定是否有可滚动的范围。
先建立内容,再交给滚动区域
跳转到“先建立内容,再交给滚动区域”常见的动态列表需要四层关系:窗口中的 QScrollArea、滚动区域内部的 viewport()、用 setWidget() 安装的内容容器,以及内容容器中的布局和条目。滚动区域接管内容容器;安装后它的实际父控件是视口。
先给内容容器安装布局,再调用 setWidget(),可让滚动区域从一开始就获得有效的内容尺寸信息。setWidget() 不是简单的 setParent(),也不是设置布局;一个滚动区域只有一个这样的内容控件。Qt:setWidget
原来“父对象指向 scrollArea 就可以得到滚动条”的表述省略了这一步。真正需要检查的是 scrollArea->widget() 是否等于预期容器,随后才是容器能否被压缩到当前视口。
widgetResizable 与最小尺寸一起决定滚动范围
跳转到“widgetResizable 与最小尺寸一起决定滚动范围”| 设置 | 尺寸行为 |
|---|---|
widgetResizable == false,默认值 | 保持内容控件的尺寸,超出视口时提供滚动范围 |
widgetResizable == true | 尽量调整内容尺寸以利用视口空间,或避免不必要的滚动条;仍须满足内容的尺寸约束 |
| 内容布局的最小高度大于视口高度 | 无法继续压缩时,在允许显示滚动条的策略下产生纵向滚动范围 |
ScrollBarAlwaysOn | 强制显示滚动条外观,本身不能证明内容确实有可滚动范围 |
因此,setWidgetResizable(true) 不表示永远没有滚动条。动态添加条目时,要让容器的布局把最小尺寸变化传递出去。下面使用 QLayout::SetMinimumSize;官方也给出了根据应用需要采用 SetMinAndMaxSize 的方式。不要只把条目画在容器之外,却让容器尺寸和尺寸提示维持原样。Qt:滚动区域的尺寸提示和布局
完整例子:顶部对齐的动态分组列表
跳转到“完整例子:顶部对齐的动态分组列表”此例保留零边距、零间距、顶部对齐和“添加分组”按钮的结构。新增标签插入按钮之前,每行至少高 32 像素;条目增多后,内容最小高度超过视口,纵向滚动范围自然增加。
#include <QApplication>#include <QLabel>#include <QPushButton>#include <QScrollArea>#include <QScrollBar>#include <QVBoxLayout>#include <QWidget>#include <cassert>
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget host; auto *outer = new QVBoxLayout(&host); auto *scrollArea = new QScrollArea; outer->addWidget(scrollArea); scrollArea->setWidgetResizable(true); scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
auto *content = new QWidget; auto *list = new QVBoxLayout(content); list->setContentsMargins(0, 0, 0, 0); list->setSpacing(0); list->setAlignment(Qt::AlignTop); list->setSizeConstraint(QLayout::SetMinimumSize); auto *addGroupButton = new QPushButton("Add group"); list->addWidget(addGroupButton); scrollArea->setWidget(content);
QObject::connect(addGroupButton, &QPushButton::clicked, content, [content, list, count = 0]() mutable { auto *row = new QLabel(QString("Group %1").arg(++count), content); row->setMinimumHeight(32); list->insertWidget(list->count() - 1, row); }); host.resize(320, 200); host.show();
if (app.arguments().contains("--check")) { for (int i = 0; i < 12; ++i) addGroupButton->click(); // 测试中让布局请求和视口尺寸更新得到处理。 for (int i = 0; i < 4; ++i) app.processEvents(); assert(scrollArea->widget() == content); assert(content->parentWidget() == scrollArea->viewport()); assert(list->count() == 13); assert(content->minimumHeight() > scrollArea->viewport()->height()); assert(scrollArea->verticalScrollBar()->maximum() > 0); return 0; } return app.exec();}processEvents() 在这里用于让短测试观察布局计算后的结果;实际应用由正常的 app.exec() 驱动事件循环,不需要每增加一行就强制调用它。程序没有强制把水平滚动条关闭;若希望始终只纵向滚动,应先保证内容的宽度提示和最小宽度适配视口,避免用隐藏滚动条掩盖无法访问的横向内容。
将代码保存为 main.cpp,配合以下构建文件。使用 Debug 构建后传入 --check 可执行断言,普通启动可交互添加分组。
cmake_minimum_required(VERSION 3.20)project(ScrollAreaExample LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(scroll_area main.cpp)target_link_libraries(scroll_area PRIVATE Qt6::Widgets)更新内容时的几个边界
跳转到“更新内容时的几个边界”再次调用 setWidget(newContent) 会替换并删除旧内容,所以不能先保存旧内容的裸指针,替换后再继续使用。需要把内容交还调用者时,使用 takeWidget(),并为取出的控件安排新的宿主或负责删除它。新增行与移除行则通常只操作现有内容布局;删除或隐藏条目的行为见 [[10-knowledge/computing/software-development/application-development/desktop/qt/gui/layout-ownership-and-removal|布局与控件移除]]。
如果要滚动到刚加入的控件,可以在布局更新后调用 ensureWidgetVisible(row),让它调整滚动位置;它不负责创建条目,也不改变条目的所有权。排查“有内容却不滚动”时,依次看内容是否正确安装、内容尺寸约束是否增长、视口大小和滚动条策略是否符合预期。Qt:takeWidget、Qt:ensureWidgetVisible