现代Qt开发教程新手篇3.1——布局系统基础相关仓库仍然已经开源正在积极火热的建设之中欢迎各位大佬提Issue和PR链接地址https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeQt1. 前言 / 为什么需要布局管理器最最开始的时候我是真的会以为GUI编程中控件的排列是靠手动算每个控件的 x、y 坐标然后用move()和resize()把它们摆到指定位置艹想想就抽象是不是。窗口大小写死 800x600你别说你真别说。我真看过一个Qt项目一瞅窗口大小写死的甚至以为是客户的需求但是实际上真的只是说写代码的不知道Layout这个东西。。。太烦布局了直接写死控件大小的看起来还行。然后用户把窗口一拉大——所有控件还是挤在左上角右边一大片空白丑到没法看。更要命的是不同系统的字体大小不一样Windows 上排版正常到了 Linux 上文字就溢出了。后来才知道Qt 有一整套布局管理器专门解决这些问题。布局管理器的核心思想很简单你告诉它控件之间的相对关系——“这个按钮在左边的标签旁边”、“这些控件从上到下排列”——然后布局管理器会根据窗口的实际大小自动计算每个控件的位置和尺寸。窗口拉大控件跟着变大窗口缩小控件自动收缩。不同平台、不同字体、不同 DPI 之下界面都能保持合理的比例。Qt 提供了四种常用的布局管理器QHBoxLayout 做水平排列、QVBoxLayout 做垂直排列、QGridLayout 做网格排列、QFormLayout 做表单排列。还有一个 QStackedLayout 用于多页面切换。这五个加上 stretch 和 spacing 的调节手段基本覆盖了你能见到的所有界面排版需求。2. 环境说明本篇代码适用于 Qt 6.5 版本CMake 3.26C17 或更高标准。所有布局类都在 QtWidgets 模块中所以示例代码只需链接 Widgets 和 Gui 两个模块。示例可以在任何支持 Qt6 的桌面平台上编译运行布局管理器会自动处理不同平台的字体和 DPI 差异。3. 核心概念讲解3.1 QHBoxLayout 和 QVBoxLayout一维排列的基础QHBoxLayout 和 QVBoxLayout 是最基础的两个布局管理器——前者让控件从左到右水平排列后者让控件从上到下垂直排列。它们俩的用法完全一样只是方向不同。我们先看一个最简单的例子三个按钮水平排列。auto*layoutnewQHBoxLayout;layout-addWidget(newQPushButton(按钮1));layout-addWidget(newQPushButton(按钮2));layout-addWidget(newQPushButton(按钮3));setLayout(layout);就这样三个按钮会自动平分水平空间从左到右排成一行。当你拉伸窗口时三个按钮会等比例变宽。垂直排列同理把 QHBoxLayout 换成 QVBoxLayout 即可。控件会从上到下依次排列每个控件默认占据其sizeHint()推荐的高度剩余空间由设置了 stretch 的控件分配。这里有一个初学者经常搞混的概念布局不是 Widget它没有自己的视觉表示。布局是附加在 Widget 上的负责管理这个 Widget 的子控件的排列方式。所以使用布局的标准流程是创建一个容器 Widget创建一个布局把子控件加到布局里然后用setLayout()把布局绑定到容器 Widget 上。// 标准流程auto*containernewQWidget;// 容器auto*layoutnewQVBoxLayout(container);// 创建布局直接传入容器// auto *layout new QVBoxLayout; // 也可以先创建再绑定// container-setLayout(layout);layout-addWidget(newQLabel(标题));layout-addWidget(newQTextEdit);container-show();你会发现new QVBoxLayout(container)这种写法同时完成了创建布局和绑定到容器两步比先 new 再 setLayout 更简洁。两种写法效果完全一样选哪种看个人习惯。3.2 布局嵌套组合出复杂界面真正的界面很少只有一个层级的布局。比如一个典型的对话框顶部是一个文本框垂直方向占大部分空间底部是一排水平排列的按钮。这需要 QVBoxLayout 套一个 QHBoxLayoutauto*mainLayoutnewQVBoxLayout;// 上半部分文本编辑区auto*textEditnewQTextEdit;mainLayout-addWidget(textEdit,1);// 第二个参数是 stretch1 表示占据剩余空间// 下半部分按钮栏水平排列auto*buttonLayoutnewQHBoxLayout;buttonLayout-addWidget(newQPushButton(确定));buttonLayout-addWidget(newQPushButton(取消));buttonLayout-addStretch();// 弹性空间把按钮推到左边mainLayout-addLayout(buttonLayout);// 把子布局加到主布局中setLayout(mainLayout);addLayout()是把一个布局加到另一个布局中的方法这就是布局嵌套的关键。你可以无限层级地嵌套——QVBoxLayout 里套 QHBoxLayout再套 QGridLayout——直到满足你的排版需求。实际开发中一个中等复杂度的界面嵌套三四层布局是很正常的。addWidget 的第二个参数 stretch 非常重要它决定了控件在分配额外空间时的权重。stretch 为 0 表示控件不会自动拉伸保持 sizeHint 推荐的大小stretch 为 1 表示控件参与均分剩余空间stretch 为 2 表示这个控件分到的额外空间是 stretch 为 1 的控件的两倍。// 三个控件中间的占据所有额外空间layout-addWidget(newQLabel(固定标签),0);// stretch0不拉伸layout-addWidget(newQTextEdit,1);// stretch1占据剩余空间layout-addWidget(newQLabel(固定标签),0);// stretch0不拉伸3.3 QGridLayout网格布局与行列 span当你需要控件按照二维网格排列时QGridLayout 就是最合适的工具。它把空间划分成行和列组成的网格每个控件占据其中一个单元格。auto*gridnewQGridLayout;// addWidget(widget, row, col) —— 指定行列位置grid-addWidget(newQLabel(用户名:),0,0);// 第 0 行第 0 列grid-addWidget(newQLineEdit,0,1);// 第 0 行第 1 列grid-addWidget(newQLabel(密码:),1,0);// 第 1 行第 0 列grid-addWidget(newQLineEdit,1,1);// 第 1 行第 1 列setLayout(grid);行列编号从 0 开始这个没什么歧义。但有一个容易被忽略的细节QGridLayout 会根据你添加的控件自动推断行数和列数。你不需要提前声明这个网格有 3 行 2 列只要往里面加控件就行了。更有用的是行列 span——让一个控件跨多行或多列。比如一个对话框的底部按钮栏你可能希望它横跨整行// addWidget(widget, row, col, rowSpan, colSpan)grid-addWidget(newQTextEdit,0,0,1,2);// 第 0 行跨 1 行 2 列grid-addWidget(newQPushButton(提交),1,0,1,1);// 正常占 1 格grid-addWidget(newQPushButton(重置),1,1,1,1);// 正常占 1 格span 参数的后两个数字分别表示跨越的行数和列数。1, 2的意思是这个控件从当前位置开始占据 1 行 2 列。这在做复杂排版的时候非常好用比如一个计算器界面数字键都是 1x1等号键跨两行0 键跨两列。你还可以通过setColumnStretch()和setRowStretch()设置各列各行的伸缩比例效果和 BoxLayout 的 stretch 参数类似grid-setColumnStretch(0,1);// 第 0 列 stretch1grid-setColumnStretch(1,2);// 第 1 列 stretch2宽度是第 0 列的两倍3.4 QFormLayout表单布局QFormLayout 是专门为标签输入控件这种表单式界面设计的布局。你可能觉得用 QGridLayout 也能实现同样的效果——确实能但 QFormLayout 提供了更简洁的 API 和更好的平台适配行为。auto*formnewQFormLayout;form-addRow(姓名:,newQLineEdit);form-addRow(邮箱:,newQLineEdit);form-addRow(电话:,newQLineEdit);form-addRow(备注:,newQTextEdit);setLayout(form);addRow(const QString labelText, QWidget *field)这个签名非常方便一行代码搞定标签和输入控件的配对。如果你需要对标签做更多定制也可以传入一个 QLabel 对象addRow(new QLabel(姓名:), new QLineEdit)。QFormLayout 有一个 QGridLayout 做不到的事它会根据当前桌面的风格自动决定标签放在输入框的左边还是上面。在大多数桌面环境下标签默认在左边但在某些特殊平台或者 Qt 的特定配置下标签可能会被放到输入框上方。这种自适应行为在你做跨平台应用时很有价值。你还可以通过setRowWrapPolicy()和setFieldGrowthPolicy()来控制具体的排版策略。比如QFormLayout::ExpandingFieldsGrow会让所有设置了 expending size policy 的输入框自动拉伸QFormLayout::WrapAllRows会让所有标签都显示在输入框上方。这些细节在你需要精细调整表单外观的时候再查文档就行。3.5 QStackedLayout页面切换QStackedLayout 的用途跟前面几个不太一样——它不是用来排版的而是用来做页面切换的。它管理一组 Widget但同一时间只显示其中一个。你通过setCurrentIndex()或setCurrentWidget()来切换当前显示的页面。这个布局最常见的应用场景是选项卡式的界面——左侧一个 QListWidget 做导航菜单右侧一个 QStackedLayout 根据菜单选择切换不同的设置页面。auto*stackedLayoutnewQStackedLayout;auto*page1newQWidget;auto*page1LayoutnewQVBoxLayout(page1);page1Layout-addWidget(newQLabel(这是第一页));// ... 添加更多控件auto*page2newQWidget;auto*page2LayoutnewQVBoxLayout(page2);page2Layout-addWidget(newQLabel(这是第二页));stackedLayout-addWidget(page1);// index 0stackedLayout-addWidget(page2);// index 1// 切换到第二页stackedLayout-setCurrentIndex(1);// 或者根据 Widget 指针切换// stackedLayout-setCurrentWidget(page2);QStackedLayout 和 QStackedWidget 的关系就跟 QLayout 和 QWidget 的关系一样——QStackedWidget 是对 QStackedLayout 的一层封装提供了一个更方便的 Widget 接口。如果你不需要精细控制布局行为直接用 QStackedWidget 就行。3.6 addStretch、setSpacing 和 setContentsMargins这三个方法不属于任何一个特定的布局类而是所有布局类共有的调节手段。addStretch()在 BoxLayout 中插入一段弹性空白。它的效果是占据所有剩余空间。最常见的用法是把按钮推到右边或底部auto*layoutnewQHBoxLayout;layout-addWidget(newQPushButton(左边的按钮));layout-addStretch();// 弹性空白把后面的控件推到最右边layout-addWidget(newQPushButton(右边的按钮));addStretch()也可以传入一个 stretch 参数在多个 stretch 之间按比例分配空间。但大多数情况下你只需要一个无参数的addStretch()就够了。setSpacing(int)设置控件之间的间距单位是像素。默认值通常是取决于平台风格的某个值一般在 5-10 像素之间。如果你觉得控件之间太挤了或者太松了调这个值就行。setContentsMargins(int left, int top, int right, int bottom)设置布局的外边距——也就是布局区域和 Widget 边缘之间的距离。如果你发现控件紧贴着窗口边缘不好看把 margins 设大一点就好了。layout-setSpacing(10);// 控件之间间距 10pxlayout-setContentsMargins(15,15,15,15);// 四周外边距 15px还有一个简化版本setContentsMargins(int all)可以一次性设置四个方向相同的外边距用起来更方便。到这里你可以想一个问题如果你要做一个包含标题、内容区域和底部按钮栏的界面应该怎么组合使用这些布局标题固定高度、内容区域占满剩余空间、按钮栏固定在底部——用 QVBoxLayout 嵌套 QHBoxLayout配合 stretch 就能搞定。如果你能在脑子里把这个结构画出来说明布局系统的核心你已经掌握了。4. 踩坑预防第一个坑是不设布局就直接用move()和resize()定位控件。这种方式叫做绝对定位不是说不能用但它的缺点非常明显窗口大小变了控件不会跟着变不同平台的字体 DPI 不同排版也会乱。除非你做的是固定尺寸的嵌入式界面或者游戏 UI否则都应该用布局管理器。绝对定位属于知道有这个东西就好日常别用的范畴。第二个坑是布局的父子关系搞混。当你写new QVBoxLayout(widget)的时候布局会自动成为这个 widget 的子对象并且自动管理添加到布局中的所有控件的父子关系。但如果你写new QVBoxLayout不传 widget然后又忘了setLayout()这个布局就不会被任何 widget 管理也不会生效。更糟糕的是你往里面加的控件也不会有正确的 parent可能导致内存泄漏。养成习惯创建布局的时候直接传容器 widget或者创建完立刻setLayout()。第三个坑是混用同一个布局上的 stretch 和 sizePolicy。stretch 决定了布局给控件分配额外空间的比例而 sizePolicy 决定了控件自己愿不愿意接受额外空间。如果你给一个控件设了setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed)那不管你给它多大的 stretch它都死活不变大。这两个概念是配合工作的stretch 管给多少sizePolicy 管收不收。第四个坑是在已经设置了布局的 Widget 上再次调用setLayout()。Qt 的一个 Widget 只能有一个顶层布局如果你试图设第二个会直接报 warning 并且行为不可预测。如果你需要动态改变布局结构不要替换整个布局而是用removeWidget()和addWidget()来调整内容。5. 练习项目我们来做一个综合练习用五种布局组合实现一个类似设置面板的界面。左侧是一个垂直排列的导航菜单三个按钮个人信息、外观设置、关于右侧是一个 QStackedLayout 切换三页内容。个人信息页用 QFormLayout 排列表单姓名、邮箱、电话输入框外观设置页用 QGridLayout 排列颜色选择按钮和字体大小调节关于页用 QVBoxLayout 居中显示版本信息。完成标准是主窗口用 QHBoxLayout 分为左右两栏左栏宽度固定用setFixedWidth或 stretch 控制、右栏自适应三个导航按钮用 QVBoxLayout 排列底部加一个addStretch()让按钮靠上QStackedLayout 管理三页内容点击导航按钮时调用setCurrentIndex()切换所有布局设置合理的 spacing 和 margins控件之间不拥挤不松散。几个提示左栏导航可以用一个独立的 QWidget 作为容器对这个容器设置 QVBoxLayout 而不是对整个窗口QStackedLayout 添加页面的顺序决定了currentIndex的对应关系要跟导航按钮的信号对应上QGridLayout 的 setColumnStretch 可以让输入框列自动拉伸而标签列保持固定宽度。6. 官方文档参考链接Qt 文档 · Layout Management – Qt 布局系统的完整概述包含布局管理器的工作原理和使用建议Qt 文档 · QHBoxLayout – 水平布局的 API 文档addWidget / addStretch / setSpacing 等方法详解Qt 文档 · QVBoxLayout – 垂直布局的 API 文档与 QHBoxLayout 接口一致Qt 文档 · QGridLayout – 网格布局的 API 文档包含行列 span 和 stretch 设置Qt 文档 · QFormLayout – 表单布局的 API 文档addRow 的多种重载和排版策略Qt 文档 · QStackedLayout – 页面切换布局的 API 文档setCurrentIndex / currentChanged 信号到这里Qt 的布局系统你就入门了。五大布局管理器各有各的适用场景掌握它们的关键不在于死记 API而在于拿到一个界面设计稿的时候能快速判断这里该用什么布局、嵌套关系是怎样的。下一篇文章我们进入事件处理与传播的机制那是理解 Qt 应用怎么响应用户操作的核心。相关阅读现代Qt开发教程新手篇1.15——正则与文本处理 - 相似度 100%通用GUI编程技术——Win32 原生编程实战五十四——Hook 机制 - 相似度 100%通用GUI编程技术——图形渲染实战四十四——D3D12命令列表、队列与围栏GPU同步核心 - 相似度 100%