跳转到内容
新建笔记

QScrollArea 的内容容器与动态滚动条

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