浅析 Qt 布局系统

Qt 布局系统介绍

布局系统

作为一名 iOS 开发人员, 见证着 iOS 布局系统的不断完善, 从绝对布局, Autoresizing 到 Autolayout. 使得开发人员的工作效率越来越高, 项目界面的可读性和易维护性越来越强. 如今 IDE 中的可视化界面工具已经非常强大, 许多网友”戏称” iOS 开发者为”UI 拖拽师”, 可见, iOS 开发中界面布局系统的高效. 所以, 优秀的布局系统的使命在于让开发者花更少的时间来完成更易维护的界面.

同样的, 在 Qt 中, 系统提供了强大的排版机制来为窗口中的视图进行布局排版, 经过了对 Qt 布局一个初步的探索, 不得不对 Qt 布局系统的简洁高效而又功能强大表示赞叹.

布局系统的功能

在 Qt 中, 布局系统可以完成

  • 定位子控件
  • 得知窗体默认大小
  • 得知窗体最小大小
  • 窗体大小变化时进行布局排版
  • 内容改变(字体大小文本等, 隐藏或显示, 移除)时进行布局排版

布局系统的结构

Qt 提供了 QLayout 类及其子类来为界面进行排版布局. 结构如下图:

布局系统结构图

QLayout 是布局系统中的抽象基类, 继承自 QObject 和 QLayoutItem, 其中四个子类分别为

  • QBoxLayout(箱式布局)
  • QFormLayout(表单布局)
  • QGridLayout(网格布局)
  • QStackedLayout(栈布局)

在真实使用场景中, 往往需要通过多种布局的相结合来完成界面的设计, 接下来将分别介绍四中布局.

QBoxLayout 箱式布局

箱式布局提供了两个子类分别处理水平(QHBoxLayout)和垂直(QVBoxLayout)两个方向的排版, 可以使视图排成一行或者一列来显示. 简单说, 就是可以让控件进行排排站, 比如在我们的 AlphaBox 中, 顶部的头像, 姓名, 和刷新按钮排成了一排, 这就是水平箱式布局:

什么叫排排站

你以为我要讲一下这个东西如何实现? NO, 我偏偏要以垂直箱式布局为例, 用一个最简单的例子来介绍箱式布局的使用, 首先创建一个基于 QWidget 的界面, 添加我们需要使用的头文件:

#include <QVBoxLayout>
#include <QPushButton>

并在构造函数中添加如下代码

    //  添加两个按钮
    QPushButton *okBtn  = new QPushButton;
    okBtn ->setText(tr("我在上面, 我最牛"));
    QPushButton *celBtn = new QPushButton;
    celBtn->setText(tr("我在下面, 我不服"));

    //  创建一个垂直箱式布局, 将两个按钮扔进去
    QVBoxLayout *layout = new QVBoxLayout;
    layout->addWidget(okBtn);
    layout->addWidget(celBtn);

    //  设置界面的布局为垂直箱式布局
    setLayout(layout);

运行看一下效果, 什么? 这就可以运行了? 坐标呢? 尺寸呢? 是的, 没看错…点击运行:

最简单的箱式布局

两个按钮已经一上一下, 乖乖的在垂直方向自己站好了位置, 就是这么强大, 就是这么省心.

QFormLayout 表单布局

强大的 AlphaBox 是很外向的, 可以很轻松的将你的资料分享给其他用户, 当我们分享的时候, 会有这样一个界面:

在 AlphaBox 中共享资料

看到这个界面, 聪明的你可能会说, 这很简单啊, 好几个水平箱式布局就可以实现, 可是, 更聪明的 Qt 提供了更高效的方式帮助你完成这样一个界面, 那就是 QFormLayout.

在我所学习 Qt 所使用的书籍中, 将 QFormLayout 翻译为窗体布局, 我个人认为, 将其翻译为表单布局更为贴切, 因为 QFormLayout 的强大之处正是可以使用最快的速度完成一个用户输入的表单界面的搭建.

那么, 让我们揭开 AlphaBox 的神秘面纱, 看看这样一个界面是怎么实现的.

首先, 拖拽一个 Form Layout 到 Widget 中.

添加表单布局

双击之后即可为表单增加一行.

为表单增加一行

相信大家看到这张图时, 就已经能理解到表单布局是如何使用的, 提供了标签作为用户输入内容的指引, 提供字段类型作为用户输入的控件, 作为 iOS 开发者, 深知这样一个界面的搭建所需要的繁杂的工作量. 当我第一次打开这个界面时, 被这样创建界面的方式所惊呆了.

  1. 按照图中, 创建表单的第一行, 共享给哪个用户的输入框, 可以为输入框填写占位文字.
  2. 双击 Form Layout 创建字段类型为 QComboBox (多选框)的一行. 填写允许的权限内容.
  3. 设置整个 Widget 布局为垂直箱式布局
  4. 在 Form Layout 下拖拽过去一个 Horizontal Layout(水平箱式布局)
  5. 在箱式布局中添加 Horizontal Spacer (水平占位) 后拖拽两个 Push Button 完成界面布局

共享界面的布局

快不快? 快不快! 快不快!!!

同样的, 如果是使用纯代码表单布局的话可以使用addRow()的方法来添加一行.

QGridLayout 网格布局

强大的 AlphaBox 是这样的

事实上, 强大的 AlphaBox 是这样的, 我们可以共享给多个用户, 而且, 下方会有一个列表, 展示共享的用户以及权限列表. 这时, 表单布局就没办法满足我们, 只好另求新欢 QGridLayout - 网格布局.

网格布局顾名思义, 可以将界面分割成行列来进行布局管理, 在每个单元格中来摆放控件. 所以 AlphaBox 分享的界面使用了一个 两行三列 的网格布局来实现的.

QGridLayout - 网格布局

当然, 更更复杂的界面, 用 Qt 布局的效率也是非常高的, 我做了一个外链分享的布局 Demo, 可以将内部资料生成一个下载链接共享给任何人去下载.

外链分享界面

这个界面中, 我在Tab之内使用了网格布局, 布局如图:

外链分享界面布局

从图中可以看出, 网格布局像是在操作一个 Excel 一样简单, 布局单元格, 合并单元格, 等等.

在这个界面中, 更灵活的使用了 QLayout 的属性来完成了界面布局排版.

同样的, 在代码中, 可以使用如下等的 Api 来为网格视图添加一个从几行几列开始占据几行几列的控件:

void addWidget(QWidget *, int row, int column, int rowSpan, int columnSpan)

QStackedLayout 栈布局

如在 AlphaBox 中, 我们可以通过云端文件浏览器直接查看和操作云端文件, 在加载的过程中, 会有一个转菊花的界面.

在转菊花的 AlphaBox

加载失败时的错误提示:

菊花转失败了的 AlphaBox

以及加载成功时:

通常情况下我们能看见的 AlphaBox

通常应用的界面会根据不同的状态有不同的内容, 这时就可以使用 QStackedLayout 栈布局, 栈布局提供了一个页面的栈, 每个页面有完全独立的界面布局. 可以非常清晰的对不同状态下的界面进行布局管理.

在 Qt 的可视化布局工具中, 通过 Stacked Widget 来完成界面的栈布局

Stacked Widget

通过右键来进行页面的插入移除和排序等操作.

布局相关属性

控件大小

对于控件大小, 最重要的两个属性是 sizeHintminimumSizeHint , 这是 QWidget 的属性, 是只读属性. 其中, sizeHint 属性为控件的建议大小, 对于不同的控件, 有不同的建议大小, 同理 minimumSizeHint 为建议的最小大小. 知道了这两个属性才可以理解布局中控件的大小是如何控制的. 如果手动设置了最小尺寸的话(minimumSize), minimumSizeHint 是会被忽略的.

大小策略

大小策略属性 sizePolicy 也是 QWidget 类的属性, 这个属性在水平和垂直两个方向分别起作用, 控制着控件大小变化的策略.

在可视化工具中可以直观的看到几种大小策略, 以垂直为例如图:

大小策略

  • QSizePolicy::Fixed 只能使用 sizeHint 的大小, 任何操作都不会改变控件大小
  • QSizePolicy::Minimum sizeHint 为最小大小, 控件可以被拉伸
  • QSizePolicy::Maximum sizeHint 为最大大小, 控件可以被压缩
  • QSizePolicy::Preferred sizeHint 为建议大小, 控件既可以被压缩也可以被拉伸
  • QSizePolicy::MinimumExpanding sizeHint 为最小大小, 不能被压缩, 被拉伸的优先级更高
  • QSizePolicy::Expanding sizeHint 为建议大小, 可以被压缩, 被拉伸的优先级更高
  • QSizePolicy::Ignored sizeHint 的值将会被忽略

在网上或者书中, 关于这些策略的说明会有很多, 可是如果不是真的自己尝试一下, 很难很好的理解在复杂布局的情况下, 大小策略是如何控制布局的, 尤其是 MinimumExpanding, Expanding, Ignored 这三种.

对于优先级的概念大家肯定不会陌生, 这里我认为优先级来解释这些策略是更清晰易懂的.

关于拉伸 Expanding , MinimumExpanding 优先级相同, 同时要比 PreferredIgnored 拉伸优先级更高, PreferredIgnored 相同.

换句话说, 如果两个控件在一个水平箱式布局中管理, 其中一个水平大小策略为 Preferred 另一个为 Expanding 或者 MinimumExpanding 如果水平拉伸窗体, 则 Preferred的控件大小不会改变, Expanding 或者是 MinimumExpanding 会被拉伸.

同理, 如果两个控件水平大小策略一个为Expanding, 一个是MinimumExpanding, 这时拉伸窗体, 则两个控件均会拉伸.

多说一句, 如果两个控件都为 Fixed 无法拉伸时, 控件间的间隙会被拉伸.

关于压缩 如果达到了minimumSizeHint是不会被继续压缩了, 但是Ignored是会忽略 sizeHintminimumSizeHint 的属性的, 所以是会继续被压缩的.

伸缩性

在 QLayout 中提供了一个和控件大小策略相关的属性, layoutStretch 布局伸缩性, 这个值是一个比例, 在可视化工具中可以更直观的看到这个值的设置, 如果在布局中有三个控件, 则是三个控件的占比, 用逗号分隔, 如: 1, 1, 1 .

伸缩性就好理解一些了, 但是要注意的是, 只有会被压缩或者拉伸的控件才会受该属性值影响(如 Fixed 是不会受该属性影响)

还有一点非常重要的是设置了伸缩性的比值(如果都为0, 则表示不设置) 刚刚提到的大小策略的优先级将会被忽略, 还是刚刚的例子, 如果两个控件在一个水平箱式布局中管理, 其中一个水平大小策略为 Preferred 另一个为 Expanding, 设置水平箱式布局的 layoutStretch2, 1 则拉伸时, 并不会像刚刚所说, 只有 Expanding 的控件会被拉伸, 而是都会被拉伸, 按照一个 2 : 1 的拉伸比例拉伸.

窗体大小约束策略

最后想介绍一下 QLayout 的 layoutSizeConstraint 属性, 用来约束窗体大小, 只影响窗体, 所以该属性只对最顶级的 QLayout 起作用.

layoutSizeConstraint

关于这几个属性同样的, 简单的介绍网上和书上会有很多, 如果不尝试一下, 浅显的字面意思无法理解这几个属性的作用. 根据我的尝试, 总结如下:

  • QLayout::SetDefaultConstraint 窗体最小值被设置为 minimumSize 值无法再缩小, 如果 QLayout 内控件有更大的minimumSize, 则会取更大的minimumSize.
  • QLayout::SetNoConstraint 窗体没有约束策略
  • QLayout::SetFixedSize,窗体大小被设定为 sizeHint 的大小,无法改变
  • QLayout::SetMinimumSize 窗体最小为 minimumSize 无法再缩小, 如果 QLayout 内控件有更小的minimumSize, 则会取更小的minimumSize., 总结就是的话, 和 Default 不同的地方就是尽可能的小.
  • QLayout::SetMaxmumSize 同理, 窗体最大值为 maxmumSize , 无法再放大
  • QLayout::SetMinAndMaxSize 窗体最小为 minimumSize 无法再缩小, 窗体最大值为 maxmumSize , 无法再放大

其他属性

关于表单布局和网格布局还有其他的属性约束单元格的一些策略, 如 layoutFieldGrowthPolicy 控件的变化方式策略等等有兴趣可以查看官方文档, 更多的属性间隙, 间隔, 对其方式等等都比较好理解了, 在此也不赘述了.

官方文档

Qt 官方文档点这里

参考

<< Qt Creator 快速入门>> 第三版, 霍亚飞著