6.2 水平和垂直布局器

关于 Qt 布局管理,有专门的帮助文档页面 Layout Management。本章的主要内容就是介绍布局管理的知识,Qt 设计师里面不仅有布局器的控件可以拖动使用,还可以在窗体里面选择控件,然后点击设计师上面的工具按钮自动添加布局器。本节首先大致介绍一下 Qt 设计师里面关于布局器的操作界面,主要介绍两个基本的水平布局器 QHBoxLayout 和垂直布局器 QVBoxLayout, 将控件和布局器由小到大搭成一个完整的界面。

6.2.1 布局器概览

我们以下图的 Qt 设计师界面来说明布局功能,QtCreator 设计模式的布局功能与 Qt 设计师是一样的。
designer
在设计师左边列表,可以看到 Layouts 栏目里有四个布局器:
◆ 垂直布局器 QVBoxLayout:将内部的控件按照垂直方向排布,一行一个。
◆ 水平布局器 QHBoxLayout:将内部的控件按照水平方向排布,一列一个。
◆ 网格布局器 QGridLayout:按照多行、多列的网格排布内部控件,单个控件可以占一个格子或者占据连续多个格子。
◆ 表单布局器 QFormLayout:Qt 设计师里把这个布局器称为窗体布局器,窗体布局器这个叫法不准。这个布局器就是对应网页设计的表单,通常用于接收用户输入。该布局器就如它的图标一样,就是固定的两列控 件,第一列通常是标签,第二列是输入控件或含有输入控件的布局器。
◆ Qt 另外还有一个堆栈布局器 QStackedLayout,通常用于容纳多个子窗口布局,每次只显示其中一个。这个布局器隐含在堆栈部件 QStackedWidget 内部,一般直接用 QStackedWidget 就行了,不需要专门设置堆栈布局器。堆栈部件和堆栈布局器留到后面容器类控件的 章节讲解。

与布局紧密关联的是两个空白条(或叫弹簧条):Horizontal Spacer 水平空白条和 Vertical Spacer 垂直空白条,空白条的作用就是填充无用的空隙,如果不希望看到控件拉伸后变丑,就可以塞一个空白条到布局器里面,布局器通常会优先拉伸空白条。两种空白条的类名都是 QSpacerItem,两种空白条只是默认的拉伸方向不一样。

对界面进行布局有两种方式,第一种方式是预先设计好整体布局,先拖入布局器,后拖入功能控件到布局器里面,这种方式不太方便,因为脑海里得提前做好布局规划。第二 种方式才是是最常用的,先把所有功能控件拖入主界面,然后根据设置好的功能控件来决定如何进行布局。Qt 的布局器中既可以添加普通功能控件,也可以添加其他布局器,所以布局器的使用是非常灵活的。界面里的控件,可以先按行排列布局,再按列排列布局;或者反过来,先排好列,再 排好行;当然也可以直接用网格布局器或表单布局器。条条大道通罗马,可以按实际控件的关系和用户喜好进行布局。

Qt 设计师左边列的四个布局器,其实不是经常需要拖动它们到主界面,更为常见的操作是选中控件,然后点击设计师上面布局工具栏里的快捷按钮实现布局,这些快捷按钮的功能更丰 富,也更常用。布局工具栏有 8 个按钮,下面依次介绍:
① 将选中控件添加到水平布局器排列。
② 将选中控件添加到垂直布局器排列。
③ 将选中控件添加到水平分裂器排列。
④ 将选中控件添加到垂直分裂器排列。
⑤ 将选中控件添加到网格布局器排布,行列的数目不限。
⑥ 将选中控件添加到表单布局器排布,该布局器固定为两列控件。
⑦ 打破布局,即保留布局器内部的控件和子布局,消除当前选中的布局器。
⑧ 根据需要显示的内容,自动调整控件或窗体的尺寸,相当于调用一次 adjustSize() 函数。

这里需要说明一下,布局器和空白条的基类其实都是 QLayoutItem,布局器仅用于辅助功能,帮助自动调整窗口里的控件布局,并不是实体控件,没有 show() 之类的显示函数,不能单独存在,必须要有实体控件才能设置布局器。
我们上一章介绍的都是实体控件,基类都是 QWidget ,都可以单独存在,有 show() 之类的显示函数。
分裂器具有布局功能,但分裂器的基类是 QFrame,分裂器是一个实体控件,分裂器不同于布局器,我们到 6.6 节专门讲分裂器。

6.2.2 QBoxLayout

水平布局器 QHBoxLayout 和垂直布局器 QVBoxLayout 的基类都是 QBoxLayout,只是二者排列方向不同。水平和垂直布局器的主要功能函数都位于基类 QBoxLayout 里面,我们这里专门介绍一下这个基类的功能。

QBoxLayout 构造函数和 setDirection() 都可以指定布局器的方向:
QBoxLayout(Direction dir, QWidget * parent = 0)
void setDirection(Direction direction)
QBoxLayout 布局器的方向 QBoxLayout::​Direction 枚举不仅可以指定水平和垂直,还能指定反方向排列:

枚举常量 数值 描述
QBoxLayout::LeftToRight 0 水平布局,从左到右排列
QBoxLayout::RightToLeft 1 水平布局,从右到左排列
QBoxLayout::TopToBottom 2 垂直布局,从上到下排列
QBoxLayout::BottomToTop 3 垂直布局,从下到上排列

水平布局器 QHBoxLayout 和垂直布局器 QVBoxLayout 默认是其中的两种:QBoxLayout::LeftToRight 和 QBoxLayout::TopToBottom 。

布局器是一定要往里面添加控件才有用,添加控件的函数如下:
void addWidget(QWidget * widget, int stretch = 0, Qt::Alignment alignment = 0)
void insertWidget(int index, QWidget * widget, int stretch = 0, Qt::Alignment alignment = 0)
widget 就是要添加的控件指针,stretch 是伸展因子(到 6.5 节再讲这个,本节先不管),伸展因子越大,窗口变大时拉伸越 多,alignment 一般不需要指定,用默认的即可。第一个 addWidget() 是将控件添加到布局里面的控件列表末尾,第二个 insertWidget() 是将控件插入到布局里控件列表序号为 index 的位置。
对于布局器里的各个控件,可以设置相邻控件之间默认的间距:
void setSpacing(int spacing)
spacing 就是间隔的像素点数目。如果不设置 spacing,那么布局器会根据默认策略决定如何添加控件之间的间隙,一般是根据父窗口或父布局器的策略来 定。

布局器中不仅可以添加控件,还可以直接添加其他布局:
void addLayout(QLayout * layout, int stretch = 0)
void insertLayout(int index, QLayout * layout, int stretch = 0)
参数里添加的 layout 布局器会作为一个整体,与父布局器里其他控件一块排布,stretch 也是伸展因子。

对于 QBoxLayout、QHBoxLayout 、QVBoxLayout 布局器,通常不需要手动新建空白条对象,因为它们自带相关函数:
void QBoxLayout::​addSpacing(int size)    //添加 size 固定尺寸空白条到布局器
void QBoxLayout::​addStretch(int stretch = 0)  //添加自动拉伸的空白条到布局器
void QBoxLayout::​insertSpacing(int index, int size)  //插入 size 固定尺寸空白条到布局器
void QBoxLayout::​insertStretch(int index, int stretch = 0)  //插入自动拉伸的空白条到布局器
对于 add* 添加函数,因为布局器内部通常有多个控件,添加函数是把空白条添加到最后。
而 insert* 插入函数,是把空白条插入到指定序号 index 的位置。

如果要添加自己创建的空白条对象,也是可行的:
void addSpacerItem(QSpacerItem * spacerItem)
void insertSpacerItem(int index, QSpacerItem * spacerItem)
另外,还可以自己从 QLayoutItem 派生新的布局器条目,对布局器条目进行自定义,这些新的布局器条目可以用如下函数添加:
virtual void addItem(QLayoutItem * item)
void insertItem(int index, QLayoutItem * item)

讲了如何添加控件和其他布局器,当然也可以计算布局器里面的条目计数:
virtual int count() const
如果要获得布局器中某个序号的条目:
virtual QLayoutItem * itemAt(int index) const
如果要删除布局器中某个序号的条目:
virtual QLayoutItem * takeAt(int index)
布局器中无论是填充普通控件还是其他布局器,每个条目都是用 QLayoutItem 封装的,对于获得的 QLayoutItem 指针(非空指针),如果要获取里面封装的控件、布局器或空白条,使用如下函数:
QWidget * QLayoutItem::​widget()
QLayout * QLayoutItem::​layout()
QSpacerItem * QLayoutItem::​spacerItem()
注意判断以上函数的返回值是否为 NULL 指针,如果是非空指针才能进行其他操作。关于水平和垂直布局器的内容介绍这么多,下面通过例子展示怎么用这些布局器。

6.2.3 网络参数示例的布局

前一章 5.2.4 节有一个网络参数输入的数据验证器示例 netparas ,我们以这个项目为底板构造本节的布局例子。
主界面的窗体都可以通过如下函数,将一个布局器设置为主布局器(或叫总布局器、顶级布局器):
void QWidget::​setLayout(QLayout * layout)
参数里的 layout 布局器就是主布局器,会自动占满窗体内部所有区域。我们可以设置好主布局器,然后通过该函数把主布局器设置给窗体。或者,如果在 layout 布局器构造参数里用主界面窗体的指针作为该布局器父指针 parent,主界面窗体只有这唯一一个直属的布局器(其他子布局器的 parent 设置为该布局器或其他子布局器),那么这个唯一直属布局器自动成为主布局器。

下面我们把 5.2.4 小节 D:\QtProjects\ch05\ 目录里的 netparas 子文件夹复制一份,
保存到第 6 章例子目录 D:\QtProjects\ch06\ 里面,然后进行下面操作:
① 把新的 netparas 文件夹重命名为 netparasnew,并把 netparasnew 里面的 netparas.pro.user 文件删掉。
② 进入 netparasnew 文件夹,把 netparas.pro 重命名为 netparasnew.pro 。
③ 用记事本打开 netparasnew.pro ,修改里面的 TARGET 一行,变成下面这句:
TARGET = netparasnew
这样我们就得到了这个新例子 netparasnew 项目。

双击打开 netparasnew.pro 文件或者用 QtCreator 打开这个 netparasnew.pro 项目,在配置项目界面选择所有套件并点 击 "Configure Project" ,配置好项目后,打开 widget.ui 界面文件,进入 QtCreator 设计模式:
ui
本节只用到图上标出的两个工具栏按钮,即水平布局和垂直布局。下面教大家两套布局操作,第一套操作,另外需要在 widget.cpp 文件构造函数里加一句 ​setLayout() 函数设置主布局器。第二套操作,不需要手动添加任何代码,单纯用设计师操作。

◆ 先看第一套布局操作:
按照下图,选中第一行的两个控件,点击设计模式上面的水平布局按钮:
layout1
点击水平布局按钮之后,看到如下图中红框框的布局器:
layout2
然后类似地,选中第二行控件,点击设计模式上面的水平布局按钮;
再选中第三行控件,点击设计模式上面的水平布局按钮,得到如下图所示的三行布局器:
layout3
下面要把三行水平布局器在垂直方向在进行一次布局,得到主布局器。选中三行水平布局器,然后点击设计模式上面的垂直布局按钮:
layout4
最后得到包括所有控件的主布局器:
layout5
这时候垂直布局器 verticalLayout 就是我们需要的主布局器,在 widget.cpp 的构造函数里面末尾的位置还需要添加一句代码:
    //第一种布局操作需要的代码
    setLayout(ui->verticalLayout);
这是第一套操作的过程。

◆ 再看第二套布局操作的过程
我们回到窗体没有任何布局的状态,可以按快捷键 Ctrl+A ,选中刚才全部的布局器,点击设计模式的打破布局按钮:
break
上图打破布局按钮的功能是这样的,如果选中单个布局器,它就打破选中的那一个布局器;
如果选中所有的控件和布局器,它就打破所有的布局器,只剩下普通控件。打破所有布局后,回到没有布局器的状态:
break2
然后我们从头开始布局,选中第一行的两个控件,点击上面水平布局按钮;
选中第二行的两个控件,点击上面的水平布局按钮;
再选中第三行的两个控件,也点击上面的水平布局按钮。得到三行水平布局:
lay1
得到三行水平布局之后,我们需要设置最后的主布局器,注意这里第二套操作的过程,与之前第一套的操作不同。

我们现在点击一下主界面窗体的标题或者空白位置,不选中任何布局和控件(其实就是唯一选中主 界面窗口自身),直接点击上面的垂直 布局按钮,主界面会变成下面所示的:
lay2
不选中任何布局和控件,直接在上面点击垂直布局器,这时候垂直布局器自动成为主窗体的主布局器,并且在右下角的属性编辑一栏会多出来一个 Layout 属性。
因为现在 verticalLayout 自动成为了主布局器,它填满了整个主窗体,原先的三行水平布局器的高度都变高了,每个水平布局器大约是主窗体的三分之一 高。
一般情况下,主界面的窗体 Widget 没有 Layout 属性,只有像刚才那样操作,不选中布局和控件,直接点击上面的布局器,才会自动为主界面的窗体添加主布局器,Widget 才有 Layout 属性,就是主布局器。
无论是主布局器,还是一般的子布局器,都有几个可以设置的属性条目:
① layoutName,布局器名称,uic 自动为界面生成窗体代码时,这个名字就是布局器对象的指针,比如 ui->verticalLayout 。
② layoutLeftMargin、layoutTopMargin、layoutRightMargin、layoutBottomMargin,四个边距,布局器内部 的控件或子布局器距离该布局器四个边的距离。设置了边距之后,控件距离布局四周会有个空隙,这样界面看起来不会太挤。上图的 verticalLayout 四个边距都是 9 。
③ layoutSpacing,布局器内部的控件或子布局器的固定间距,上图的三个水平布局器之间的间隙就是 6 。
④ layoutStretch,布局器内的控件或子布局器的伸缩因子,默认都是 0,这个留到 6.5 节专门讲。
⑤ layoutSizeConstraint,布局器的尺寸约束,一般用默认的 QLayout::SetDefaultConstraint 即可。

我们按照第二套操作方法,就不需要修改 *.h 和 *.cpp 中的源代码,界面布局的设置与源代码是分离的,只需要从设计师操作 *.ui 文件就够了,这是 非常方便的。

当然,上面的主界面还有一点瑕疵,就是三个标签控件宽度不一样,会看起来比较别扭。第一行的标签宽度是 18,第二行的标签宽度是 12,第三行的标签宽度是 24,我们设置三个标签控件的 minimumSize 属性,把最小宽度都设置成 24,那么三个标签就自动对齐了:
lay3
设置好标签最小宽度之后,界面里的控件就全都对齐了,而且窗体无论变大变小,都能自动适应。

一般修改并保存界面文件之后,需要从 QtCreator 的菜单,点击【 构建--> 重新构建项目 "项目名" 】,需要手动重新构建项目,这样 ui_*.h 文件才会根据 *.ui 文件做更新,手动重新构建项目之后,我们运行例子程序:
run
现在可以随意拉伸或缩小程序窗口,里面的控件布局会自动适应窗口大小。
这样我们就不需要手动编写任何代码,Qt 布局器会自动完成控件的分布和尺寸调整。

6.2.4 简易 HTML 查看器示例的布局
前一章 5.3.4 节有一个简易 HTML 查看器示例 simplebrowser,我们还是用水平和垂直布局器对它进行布局。Qt 的多种布局器都可以作为窗口的主布局器,上个例子使用垂直布局器作为窗口的主布局器,现在我们试一下把水平布局器设置成窗口的主布局器。
以后设置主布局器我们都是用前面介绍的第二套布局操作,自动设置窗口的主布局器,而不需要添加任何布局代码。

我们把 5.3.4 小节 D:\QtProjects\ch05\ 目录里的 simplebrowser 子文件夹复制一份,
保存到第 6 章例子目录 D:\QtProjects\ch06\ 里面,然后进行下面操作:
① 把新的 simplebrowser 文件夹重命名为 simplebrowsernew,并把 simplebrowsernew 文件夹里的 simplebrowser.pro.user 删掉。
② 进入 simplebrowsernew 文件夹,把 simplebrowser.pro 文件重命名为 simplebrowsernew.pro 。
③ 用记事本打开 simplebrowsernew.pro ,修改里面的 TARGET 一行,变成下面这句:
TARGET = simplebrowsernew
这样我们就得到了新的 simplebrowsernew 项目。

双击打开新项目或者用 QtCreator 打开这个 simplebrowsernew.pro 项目文件,在配置项目界面选择所有套件并点击 "Configure Project" ,配置好项目后,打开 widget.ui 界面文件,进入 QtCreator 设计模式:
ui
这个窗体里的控件是比较有规律的,中间一道缝,两边各一半。
下面有三个按钮,我们先处理一下按钮的布局。
选中左边两个按钮,点击设计模式上面的水平布局按钮,得到下图所示的布局器:
lay1
然后选中左边的 textBrowser 控件和两个按钮的布局器,点击设计模式上面的垂直布局按钮,得到下图所示的左半部分布局器:
lay2
这时候我们就需要注意了,这两个按钮被拉得太宽,如果布局器跟着窗体变宽,那么按钮会被拉伸得非常宽,会比较丑,这不科学。

我们希望避免按钮被拉伸,就可以使用前面 6.2.1 节的空白条,我们拖动一个水平空白条,塞到第二个按钮的右边的细缝里:
lay3
这样左半边的布局就设置好了。

对于空白条的拖动,可以先设置布局,再塞入空白条。当然也可以预先拖一个空白条,然后选中空白条和控件一起进行布局。
有了左半边布局的经验,我们也希望右边的按钮不被拉伸,我们预先拖一个水平空白条到 "打开HTML" 按钮的左边:
lay4
我们现在选中新的空白条和 "打开HTML" 按钮,点击设计模式上面的水平布局按钮,得到下图的布局器:
lay5
然后我们选中右边带空白条的布局器和 plainTextEdit 控件,点击设计模式上面的垂直布局按钮,会生成右半部的布局器:
lay6
两个半边的布局器都设置好了,现在我们点击主窗体下方的空白区域,不选中任何布局器和控件(其实就是唯一选中主界面窗口自身),直接点击设计模式上面的水平布局按 钮,这样新的水平布局自动成为主窗体 的 Layout 属性,就是主布局器:
lay7
现在布局工作就完成了。我们点击 QtCreator 菜单【 构建--> 重新构建项目 "项目名" 】,重新构建例子,然后运行它:
run
现在随便拉伸程序的窗口都可以,Qt 布局会自动调整里面的控件分布和尺寸,整个布局过程不需要添加任何一句代码。
通过这个例子,主要是介绍空白条的使用,可以先布局,再塞空白条;或者先拖好空白条,再进行布局。两种操作方式都可以。

6.2.5 ui_*.h 文件中布局器的内幕代码

虽然我们不需要手动编写布局器的代码,但是应该学习 uic 功能生成的布局器内幕代码,因为难免遇到需要自己编写布局器代码的时候。Qt 设计师也不是万能的,学 习 ui_*.h 里面的代码是很有必要的。
我们就以刚才的简易 HTML 查看器示例布局为例,学习这个例子的 ui_widget.h 里面的代码,这个文件位于影子构建目录:
D:\QtProjects\ch06\build-simplebrowsernew-Desktop_Qt_5_4_0_MinGW_32bit-Debug
现在把它的内容贴出来:
#ifndef UI_WIDGET_H
#define UI_WIDGET_H

#include <QtCore/QVariant>
#include <QtWidgets/QAction>
#include <QtWidgets/QApplication>
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QHBoxLayout>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QPlainTextEdit>
#include <QtWidgets/QPushButton>
#include <QtWidgets/QSpacerItem>
#include <QtWidgets/QTextBrowser>
#include <QtWidgets/QVBoxLayout>
#include <QtWidgets/QWidget>

QT_BEGIN_NAMESPACE

class Ui_Widget
{
public:
    QHBoxLayout *horizontalLayout_3;
    QVBoxLayout *verticalLayout;
    QTextBrowser *textBrowser;
    QHBoxLayout *horizontalLayout;
    QPushButton *pushButtonBackward;
    QPushButton *pushButtonForeward;
    QSpacerItem *horizontalSpacer;
    QVBoxLayout *verticalLayout_2;
    QPlainTextEdit *plainTextEdit;
    QHBoxLayout *horizontalLayout_2;
    QSpacerItem *horizontalSpacer_2;
    QPushButton *pushButtonOpen;

    void setupUi(QWidget *Widget)
    {
        if (Widget->objectName().isEmpty())
            Widget->setObjectName(QStringLiteral("Widget"));
        Widget->resize(630, 350);
        horizontalLayout_3 = new QHBoxLayout(Widget);
        horizontalLayout_3->setSpacing(6);
        horizontalLayout_3->setContentsMargins(11, 11, 11, 11);
        horizontalLayout_3->setObjectName(QStringLiteral("horizontalLayout_3"));
        verticalLayout = new QVBoxLayout();
        verticalLayout->setSpacing(6);
        verticalLayout->setObjectName(QStringLiteral("verticalLayout"));
        textBrowser = new QTextBrowser(Widget);
        textBrowser->setObjectName(QStringLiteral("textBrowser"));

        verticalLayout->addWidget(textBrowser);

        horizontalLayout = new QHBoxLayout();
        horizontalLayout->setSpacing(6);
        horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));
        pushButtonBackward = new QPushButton(Widget);
        pushButtonBackward->setObjectName(QStringLiteral("pushButtonBackward"));

        horizontalLayout->addWidget(pushButtonBackward);

        pushButtonForeward = new QPushButton(Widget);
        pushButtonForeward->setObjectName(QStringLiteral("pushButtonForeward"));

        horizontalLayout->addWidget(pushButtonForeward);

        horizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);

        horizontalLayout->addItem(horizontalSpacer);


        verticalLayout->addLayout(horizontalLayout);


        horizontalLayout_3->addLayout(verticalLayout);

        verticalLayout_2 = new QVBoxLayout();
        verticalLayout_2->setSpacing(6);
        verticalLayout_2->setObjectName(QStringLiteral("verticalLayout_2"));
        plainTextEdit = new QPlainTextEdit(Widget);
        plainTextEdit->setObjectName(QStringLiteral("plainTextEdit"));

        verticalLayout_2->addWidget(plainTextEdit);

        horizontalLayout_2 = new QHBoxLayout();
        horizontalLayout_2->setSpacing(6);
        horizontalLayout_2->setObjectName(QStringLiteral("horizontalLayout_2"));
        horizontalSpacer_2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);

        horizontalLayout_2->addItem(horizontalSpacer_2);

        pushButtonOpen = new QPushButton(Widget);
        pushButtonOpen->setObjectName(QStringLiteral("pushButtonOpen"));

        horizontalLayout_2->addWidget(pushButtonOpen);


        verticalLayout_2->addLayout(horizontalLayout_2);


        horizontalLayout_3->addLayout(verticalLayout_2);


        retranslateUi(Widget);

        QMetaObject::connectSlotsByName(Widget);
    } // setupUi

    void retranslateUi(QWidget *Widget)
    {
        Widget->setWindowTitle(QApplication::translate("Widget", "Widget", 0));
        pushButtonBackward->setText(QApplication::translate("Widget", "\345\220\216\351\200\200", 0));
        pushButtonForeward->setText(QApplication::translate("Widget", "\345\211\215\350\277\233", 0));
        pushButtonOpen->setText(QApplication::translate("Widget", "\346\211\223\345\274\200HTML", 0));
    } // retranslateUi

};

namespace Ui {
    class Widget: public Ui_Widget {};
} // namespace Ui

QT_END_NAMESPACE

#endif // UI_WIDGET_H
它包含的头文件就是例子用到的控件类以及其他预先包含的头文件。

我们现在解释一下 Ui_Widget 类开始处的成员变量,直接用注释形式说明:
class Ui_Widget
{
public:
    QHBoxLayout *horizontalLayout_3;//主布局器
    //左半边
    QVBoxLayout *verticalLayout;    //左半边布局器
    QTextBrowser *textBrowser;      //丰富文本浏览器
    QHBoxLayout *horizontalLayout;  //两个按钮和空白条的水平布局器
    QPushButton *pushButtonBackward;//后退按钮
    QPushButton *pushButtonForeward;//前进按钮
    QSpacerItem *horizontalSpacer;  //左边的空白条
    //右半边
    QVBoxLayout *verticalLayout_2;  //右半边布局器
    QPlainTextEdit *plainTextEdit;  //普通文本编辑器
    QHBoxLayout *horizontalLayout_2;//右边按钮和空白条的水平布局器
    QSpacerItem *horizontalSpacer_2;//右边的空白条
    QPushButton *pushButtonOpen;    //打开按钮
解释了成员变量之后,我们来看看 setupUi() 函数,主窗体的布局器代码都在这里面,我们对代码进行分片讲解,代码内容以注释形式讲解:
    void setupUi(QWidget *Widget)
    {
        //Widget 是要进行界面构造的窗体
        //先设置窗体的对象名称
        if (Widget->objectName().isEmpty())
            Widget->setObjectName(QStringLiteral("Widget"));
        Widget->resize(630, 350);   //设置窗体尺寸

        //主布局器构造
        horizontalLayout_3 = new QHBoxLayout(Widget);   //新建布局器
        horizontalLayout_3->setSpacing(6);              //设置内部控件或子布局器间隙
        //设置内部控件或子布局器距离四个边的边距
        //设计师原本是(9,9,9,9),因为是主布局器,uic 工具额外增加了 2 的边距
        horizontalLayout_3->setContentsMargins(11, 11, 11, 11);
        //设置主布局器对象名称
        horizontalLayout_3->setObjectName(QStringLiteral("horizontalLayout_3"));
该函数内部先设置主窗体 Widget 的对象名和尺寸。
然后构造了主布局器,并设置主布局器的属性。我们在 QtCreator 设计模式原本看到的 layoutLeftMargin、layoutTopMargin、layoutRightMargin、layoutBottomMargin,四个边距都是 9,但是这里调用的是 setContentsMargins(11, 11, 11, 11),因为是主布局器,额外增了 2 的边距,与窗体边界多设置了一些 间隙。只有主布局器会额外加边距,内部子布局器不会。

然后是左边控件和布局器的代码:
        //左半边
        verticalLayout = new QVBoxLayout(); //左半边大布局器
        verticalLayout->setSpacing(6);      //内部控件和子布局器间隙也是 6
        verticalLayout->setObjectName(QStringLiteral("verticalLayout"));//布局器对象名称
        //左边丰富文本浏览器控件
        textBrowser = new QTextBrowser(Widget); //新建丰富文本浏览器
        textBrowser->setObjectName(QStringLiteral("textBrowser"));  //对象名称

        verticalLayout->addWidget(textBrowser); //添加丰富文本浏览器到左边大布局器

        //左边两个按钮的布局器
        horizontalLayout = new QHBoxLayout();   //新建布局器
        horizontalLayout->setSpacing(6);        //设置内部控件间隙
        horizontalLayout->setObjectName(QStringLiteral("horizontalLayout"));//对象名称
        pushButtonBackward = new QPushButton(Widget);   //新建后退按钮
        pushButtonBackward->setObjectName(QStringLiteral("pushButtonBackward"));//对象名称

        horizontalLayout->addWidget(pushButtonBackward);//添加到按钮布局器

        pushButtonForeward = new QPushButton(Widget);   //新建前进按钮
        pushButtonForeward->setObjectName(QStringLiteral("pushButtonForeward"));//对象名称

        horizontalLayout->addWidget(pushButtonForeward);//添加到按钮布局器
        //新建左边的水平空白条,水平策略是伸展方式 QSizePolicy::Expanding
        horizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);

        horizontalLayout->addItem(horizontalSpacer);    //添加空白条到按钮布局器


        verticalLayout->addLayout(horizontalLayout);    //添加按钮布局器到左边的大布局器


        horizontalLayout_3->addLayout(verticalLayout);  //添加左半边大布局器到主布局器
左半边的布局器和控件创建过程是,先建立左边大布局器 verticalLayout ,设置该布局器的属性;
新建左上方的丰富文本浏览器,设置对象名称,添加到 verticalLayout ;
新建两个按钮的水平布局器 horizontalLayout,设置布局间隙和对象名称;
新建后退按钮,设置对象名,添加到按钮水平布局器 horizontalLayout;
新建前进按钮,设置对象名,添加到按钮水平布局器 horizontalLayout;
新建空白条,空白条的最优大小是 40*20,水平策略 QSizePolicy::Expanding,是尽可能伸展的意思,垂直策略是 QSizePolicy::Minimum,是尽可能不拉伸,高度达到最优高度 20 以后,就不会再变高;
然后用 addItem() 函数把空白条添加到按钮水平布局器 horizontalLayout;
把 horizontalLayout 布局器添加到左半边大布局器 verticalLayout;
再把左半边的 verticalLayout 添加到主布局器 horizontalLayout_3 。

这个构建和布局的过程,其实就是对布局树的遍历过程,我们在 QtCreator 设计模式右上角能看到这个布局树:
tree
从布局树上看,先构建根节点,然后构建子节点,再构建孙子节点,依此类推;
从窗体图形界面上看,同一层级的布局或控件,是按照窗体图形里从上到下、从左到右的方式构建。


构建的基本规律就是上面描述的,现在来看看窗体右半部分的构建和布局代码:
        //右半边
        verticalLayout_2 = new QVBoxLayout();   //右半边大布局器
        verticalLayout_2->setSpacing(6);        //布局间隙
        verticalLayout_2->setObjectName(QStringLiteral("verticalLayout_2"));//对象名称
        plainTextEdit = new QPlainTextEdit(Widget); //普通文本编辑器
        plainTextEdit->setObjectName(QStringLiteral("plainTextEdit"));  //对象名称

        verticalLayout_2->addWidget(plainTextEdit); //添加到右半边大布局器

        //右边按钮的布局器
        horizontalLayout_2 = new QHBoxLayout(); //新建右边按钮的水平布局器
        horizontalLayout_2->setSpacing(6);      //布局间隙
        horizontalLayout_2->setObjectName(QStringLiteral("horizontalLayout_2"));//对象名称
        //新建右边的空白条,最优尺寸 40*20,水平策略是尽量伸展,垂直策略是高度到 20 之后不变高
        horizontalSpacer_2 = new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum);

        horizontalLayout_2->addItem(horizontalSpacer_2);//添加空白条到按钮的布局器

        pushButtonOpen = new QPushButton(Widget);   //新建打开按钮
        pushButtonOpen->setObjectName(QStringLiteral("pushButtonOpen"));//对象名称

        horizontalLayout_2->addWidget(pushButtonOpen);  //添加到按钮的水平布局器


        verticalLayout_2->addLayout(horizontalLayout_2);//添加到右半边的大布局器


        horizontalLayout_3->addLayout(verticalLayout_2);//添加到主布局器

右半边的构造和布局器过程与左半边是类似的。
新建右半边的大布局器 verticalLayout_2,设置布局间隙和对象名;
新建普通文本编辑器,设置对象名,添加到右边布局器 verticalLayout_2;
新建右边按钮的布局器,设置布局间隙和对象名;
新建右边的水平空白条,最优尺寸 40*20,水平策略 QSizePolicy::Expanding,垂直策略 QSizePolicy::Minimum;
用 addItem() 函数把空白条添加到按钮的水平布局器 horizontalLayout_2;
新建打开按钮,设置对象名,添加到按钮水平布局器 horizontalLayout_2;
把按钮布局器 horizontalLayout_2 添加到右半边大布局器 verticalLayout_2;
再把右半边大布局器 verticalLayout_2 添加到主布局器 horizontalLayout_3。

关于界面构建和布局的代码就是上面那么多。下面把 Ui_Widget 剩下的代码概略看看:
        retranslateUi(Widget);  //翻译界面

        QMetaObject::connectSlotsByName(Widget);//自动关联槽函数
    } // setupUi 函数

    void retranslateUi(QWidget *Widget)
    {
        //设置窗口标题
        Widget->setWindowTitle(QApplication::translate("Widget", "Widget", 0));
        //后退按钮的文本
        //八进制 "\345\220\216"  UTF-8 编码的 "后" 
        //八进制 "\351\200\200"  UTF-8 编码的 "退" 
        pushButtonBackward->setText(QApplication::translate("Widget", "\345\220\216\351\200\200", 0));
        //设置前进按钮文本,八进制表示的 UTF-8 文本 "前进"
        pushButtonForeward->setText(QApplication::translate("Widget", "\345\211\215\350\277\233", 0));
        //设置打开HTML按钮文本,八进制和英文字母表示的 "打开HTML"
        pushButtonOpen->setText(QApplication::translate("Widget", "\346\211\223\345\274\200HTML", 0));
    } // retranslateUi 函数

};//Ui_Widget 
setupUi 函数最后两句是翻译界面和实现自动关联槽函数。
retranslateUi() 函数是设置窗体的标题文本、按钮的文本。

以上是 ui_widget.h 里面的主要代码,是 uic 工具根据 widget.ui 文件自动生成的代码。以后我们自己如果需要写布局代码,可以效仿上面的代码,当然不需要那么严格,我们可以按照自己的规划从小到大、从少到多地构建界面,不一定要像上面那 样死板,可以自己灵活写代码,比如水平空白条就不需要单独新建,直接调用一句 horizontalLayout->addStretch() 即可。

本节介绍的是水平和垂直布局器,对应大部分的窗体其实这两个布局器都够用了。Qt 还另外提供了网格布局器、表单布局器,无论用哪些布局器,其实都可以实现类似的布局效果,条条大道通罗马。不需要纠结用哪些布局器,也不要拘泥于一两种布局器,只要能实现 效果就够了,实现的过程可以按照界面的需求和自己的喜好来定。



prev
contents
next