Qt 布局负责计算控件的位置和尺寸,控件对象本身通常由宿主窗口的对象树管理。把“由布局排列”和“由谁删除”分开,就能解释为什么 removeWidget() 以后控件还活着,甚至仍然显示在原来的位置。
一个宿主只安装一个顶层布局
跳转到“一个宿主只安装一个顶层布局”new QHBoxLayout(this) 会把布局安装到 this 上,无须紧接着重复 setLayout()。另一种合法写法是先创建没有宿主的布局,最后调用宿主的 setLayout()。同一个 QWidget 不能同时安装两个顶层布局;需要复杂结构时,把没有宿主的子布局通过 addLayout() 加到顶层布局中。
子布局中的控件仍以实际宿主 QWidget 为父控件,不以 QLayout 为父控件。布局本身是 QObject 与 QLayoutItem 的组合;普通控件、嵌套布局、Spacer 分别通过不同的布局项参与计算。窗口销毁时会处理自己的布局和子控件;单独销毁布局并不等于删除它排列的控件。Qt:布局管理
Designer 项目还要分清 ui 和窗口:生成的 Ui::Form 通常只是普通辅助类,没有 layout() 方法。访问实际窗口的 layout(),或者访问 ui->horizontalLayout 等生成成员;QMainWindow 的内容布局通常装在 centralWidget() 上。不能在 setupUi() 之后不加检查地给已有布局的宿主再安装一个顶层布局。参见 [[10-knowledge/computing/software-development/application-development/desktop/qt/core/designer-generated-classes|Designer 生成类与所有权]]。
removeWidget 不等于隐藏或删除
跳转到“removeWidget 不等于隐藏或删除”对于这里使用的 QHBoxLayout、QVBoxLayout:
| 操作 | 布局管理 | 可见性 | 对象与父对象 |
|---|---|---|---|
layout->removeWidget(widget) | 从该布局移除 | 不替你隐藏 | 对象还在,父对象保持不变 |
widget->hide() | 仍在布局中时,通常不再占用布局空间 | 隐藏 | 对象还在,父对象保持不变 |
重新 addWidget(widget) | 重新参与该布局 | 若曾显式隐藏,按需 show() | 添加过程会根据宿主处理父控件关系 |
delete widget | Qt 会处理该控件的布局关联 | 不再显示 | 对象立即销毁,裸指针随即失效 |
widget->deleteLater() | 销毁执行前对象仍可能存在 | 不保证立即隐藏 | 交给相应事件循环延后销毁 |
removeWidget() 只对目标布局中的条目操作,不负责递归搜索所有子布局。移除后,应明确选择重新安排几何尺寸、放入另一布局、隐藏或销毁。hide() 的占位行为还可能被 QSizePolicy::retainSizeWhenHidden 改变,不能把它当作所有布局的无条件规则。堆叠容器有额外的页面隐藏语义,见 [[10-knowledge/computing/software-development/application-development/desktop/qt/widgets/stacked-pages|QStackedLayout 与 QStackedWidget]]。Qt:removeWidget、Qt:隐藏时保留尺寸
原来的图表场景是在 640×480 窗口内显示 A、B、C、D、E 五组值 1、2、3、4、5,纵轴范围 0~10,随后移除图表控件。图表的数据和坐标轴与移除语义是两层问题;原片段没有提供自定义 BalancingBarChart 的定义,无法单独编译。下面改用标准按钮和标签演示相同的布局行为;实际项目只需把 upper 换成已经构造好的图表指针,并使用真正包含它的布局。
可运行例子:左右布局与控件重新加入
跳转到“可运行例子:左右布局与控件重新加入”左侧按钮与右侧上下标签分别使用伸缩系数 1 和 3。它们表达剩余空间分配的相对倾向,不保证所有窗口尺寸下实际像素宽度恰好为 1:3,还要满足控件最小尺寸、尺寸策略、间距等约束。
#include <QApplication>#include <QHBoxLayout>#include <QLabel>#include <QPointer>#include <QPushButton>#include <QVBoxLayout>#include <QWidget>#include <cassert>
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget host; auto *outer = new QHBoxLayout(&host); auto *right = new QVBoxLayout; // 嵌套布局不要再安装到 host auto *button = new QPushButton("Left"); auto *upper = new QLabel("Upper"); auto *lower = new QLabel("Lower"); outer->addWidget(button, 1); outer->addLayout(right, 3); right->addWidget(upper); right->addWidget(lower); host.resize(640, 480); host.show(); app.processEvents(); assert(host.layout() == outer); assert(right->parentWidget() == &host); assert(upper->parentWidget() == &host); assert(!upper->isHidden());
QPointer<QLabel> observed = upper; right->removeWidget(upper); assert(right->indexOf(upper) == -1); assert(observed && upper->parentWidget() == &host); assert(!upper->isHidden()); // 移除布局没有替我们调用 hide upper->hide(); assert(upper->isHidden()); right->insertWidget(0, upper); upper->show(); assert(right->indexOf(upper) == 0); if (app.arguments().contains("--check")) return 0; return app.exec();}不要在清空布局时一律 delete item->widget() 后忘记 delete item。takeAt() 交还的是布局项,它可能包含控件、子布局或者 Spacer;递归清空需要明确每一种对象是否仍要保留。仅从某个布局取出布局项,不会自动把其内部的所有对象全部销毁。Qt:takeAt
Spacer 的方向、策略与 sizeHint
跳转到“Spacer 的方向、策略与 sizeHint”Spacer 是不绘制内容的 QSpacerItem,不是透明的 QWidget,也没有控件那套父对象关系。Designer 中的水平、垂直 Spacer 表达希望在哪个方向安排空白;实际留出的范围仍由所在布局和两个方向的尺寸策略共同决定。
把 sizeHint 设为 40×20,是在表达期望宽高,不是锁定几何尺寸,也不表示窗口缩放时这个提示值会自动改变。geometry() 才是布局本次分配的矩形。下表按单个方向解释常见策略,实际结果还要结合最小/最大尺寸等约束:
| 策略 | 对该方向的主要含义 |
|---|---|
Fixed | 以提示尺寸为固定目标,不主动伸缩 |
Minimum | 提示尺寸作为下限,可以变大 |
Maximum | 提示尺寸作为上限,可以变小 |
Preferred | 偏好提示尺寸,允许变大或变小 |
MinimumExpanding | 不低于提示尺寸,并希望取得额外空间 |
Expanding | 可变大或变小,并希望取得额外空间 |
Ignored | 布局计算忽略这一方向的提示尺寸;不代表其他尺寸约束也失效 |
Expanding 与 MinimumExpanding 更适合希望吸收多余空间的方向;要恒定间距可使用 addSpacing(),要伸缩空白可使用 addStretch()。直接操作 QSpacerItem 时,加入布局后的 changeSize() 要配合布局 invalidate(),让新参数参与下一次计算。Qt:QSizePolicy、Qt:QSpacerItem
下面的第二个完整程序演示修改提示值。断言比较提示值和已知约束,不把某个桌面样式下的最终像素宽度当作跨平台常量。
#include <QApplication>#include <QHBoxLayout>#include <QPushButton>#include <QSizePolicy>#include <QSpacerItem>#include <QWidget>#include <cassert>
int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget host; auto *row = new QHBoxLayout(&host); row->addWidget(new QPushButton("Left")); auto *gap = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum); row->addItem(gap); // 布局管理这个布局项 row->addWidget(new QPushButton("Right")); host.resize(480, 120); host.show(); app.processEvents(); assert(gap->sizeHint() == QSize(40, 20)); assert(gap->expandingDirections().testFlag(Qt::Horizontal)); gap->changeSize(60, 20, QSizePolicy::Fixed, QSizePolicy::Minimum); row->invalidate(); row->activate(); assert(gap->sizeHint() == QSize(60, 20)); assert(gap->minimumSize().width() == 60); assert(gap->maximumSize().width() == 60); if (app.arguments().contains("--check")) return 0; return app.exec();}两个程序分别保存成各自项目中的 main.cpp,不要把两个 main 放进同一个目标。两者都可使用下面的构建文件;用 Debug 构建后传入 --check 会在断言完成后退出。
cmake_minimum_required(VERSION 3.20)project(LayoutExample LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)find_package(Qt6 REQUIRED COMPONENTS Widgets)add_executable(layout_example main.cpp)target_link_libraries(layout_example PRIVATE Qt6::Widgets)