QBoxLayout的布局算法及一些影响widget大小的因素

QLayout

  • 相关的类:

QLayout继承自QLayoutItem, 常用的子类有QBoxLayout和QGridLayout.
一个布局里会很多个QLayoutItem.

QWidget是通过QWidgetItem这个适配器放到QLayout里到的, QWidgetItem继承自QLayoutItem,
QWidgetItem还有一个子类, QWidgetItemV2, 在QWidgetItem的基础上加了个缓存机制.
现在qt往layout里添加widget时用的就是QwidgetItemV2.

QLayout只是widget的一个布局器, 只和QLayoutItem关联.
不会变成widget的parent, 在父子链中布局器和布局器里的widget是兄弟关系.
但是在往布局器里加widget的时候, 布局器还是会对widget的parent进行调整的.

layout的激活过程

多层layout嵌套的情况:

从最外层的layout开始激活, 如果当前不是最外层的布局就直接转调去激活上一层的布局.

bool QLayout::activate()
{
    if (!d->topLevel)
        return static_cast<QLayout*>(parent())->activate();
    if (d->activated)
        return false;

    activateRecursiveHelper(this);
    
    // 开始真正的布局
    // ...
}

void QLayout::activateRecursiveHelper(QLayoutItem *item)
{
    item->invalidate();     // <== <== <== <== <== <== <== <==

    QLayout *layout = item->layout();
    if (layout) {
        QLayoutItem *child;
        int i=0;
        while ((child = layout->itemAt(i++)))
            activateRecursiveHelper(child);
        layout->d_func()->activated = true;
    }
}

布局过程

先把最外层的大小固定, 再调整里面每一项的大小.
从里往外汇总出整个布局的大小. 再从外往里给每个布局项分配大小, 同时调整每个布局项的位置.

bool QLayout::activate()
{
    // ...

     => mw->setFixedSize(totalSizeHint());

     => d->doResize(mw->size());
             => q->setGeometry(rect);  // QBoxLayout
                     => d->setupGeom();
                        =*= 统计每一项的布局信息(minimumSize/maximumSize/sizeHint)
                        =*= 汇总出整体的布局信息

                     => qGeomCalc(a, 0, n, pos, space);
                        =*= 给每一项分配空间

                     => for (int j = 0; j < n; j++) {
                            QBoxLayoutItem *box = d->list.at(i);
                            box->item->setGeometry(QRect(a.at(i).pos, s.y(), a.at(i).size, s.height()));
                                =*= 调整每一项的位置
                        }

     => mw->updateGeometry();
        =*= 向父widget发LayoutRequest事件, 触发父widget重新布局.
}

QBoxLayout的空间分配算法

void qGeomCalc(QVector<QLayoutStruct> &chain, int start, int count,
               int pos, int space, int spacer)
{
    有三种情况

    一. 可用空间比最小值还小, 所有项按最小值分配都不够分
        从最大的项开始减. 就像给一条往上增长的曲线设置了一个最大值, 超过最大值的部分全部截断为最大值.

    二. 可用空间介于最小值和参考值之间
        把需要压缩的空间平均到每一项上, 每项都压缩相同的值.
        但是会有某一项压缩之后小于最小值的情况, 这时候直接把该项设成最小值, 然后重新计算需要压缩的空间, 剩下的项重新平均分配.

    三. 可用空间大于参考值

        1. 先按比例分配, 同时分配的过程中统计哪些是给多了(实际分配的值和最大值的差)哪些是给少了(实际分配值和参考值的差)
        2. 给少了的就重新调整为最大大小
        3. 给多了的就重新调整为合适大小
        4. 再回到第一步, 还没分配的项继续按比例分配剩下的空间
        5. 循环结束条件: 
            所有项都走了第(2)或者第(3)步的分配,
            或者,
            第(1)步中统计的多分配的和少分配的相平衡(相等).

        理想结果:
            所有项都按比例分配可用空间, 并且都没有超出每项的最小值和最大值的限制范围.

        两个极端:
            最小情况:
                所有项都分值配到了合适的大小
            最大情况:
                所有项都分配到了最大的空间, 还剩下的空间分配给各项的间距.
}

SizeConstraint

widget大小的约束规则

搜代码, 用到的地方就QLayout::activate一个. 每次触发布局的时候都会根据sizeConstraint调整一下widget的最大值/最小值或者固定值.

用得比较多的是SetFixedSize, 会根据布局的内容算出一个合适的大小, 然后把widget固定到这个大小. 这在某些控件隐藏后自动调整整个widget的大小的时候非常有用.

其他的都只是调整一下最小值和最大值, 不会直接改变widget的大小.

QLayout::activate
{
    switch (d->constraint) {
    case SetFixedSize:
        // will trigger resize
        mw->setFixedSize(totalSizeHint());
        break;
    case SetMinimumSize:
        mw->setMinimumSize(totalMinimumSize());
        break;
    case SetMaximumSize:
        mw->setMaximumSize(totalMaximumSize());
        break;
    case SetMinAndMaxSize:
        mw->setMinimumSize(totalMinimumSize());
        mw->setMaximumSize(totalMaximumSize());
        break;
    case SetDefaultConstraint: {
        bool widthSet = explMin & Qt::Horizontal;
        bool heightSet = explMin & Qt::Vertical;
        if (mw->isWindow()) {
            QSize ms = totalMinimumSize();
            if (widthSet)
                ms.setWidth(mw->minimumSize().width());
            if (heightSet)
                ms.setHeight(mw->minimumSize().height());
            mw->setMinimumSize(ms);
        } else if (!widthSet || !heightSet) {
            QSize ms = mw->minimumSize();
            if (!widthSet)
                ms.setWidth(0);
            if (!heightSet)
                ms.setHeight(0);
            mw->setMinimumSize(ms);
        }
        break;
    }
    case SetNoConstraint:
        break;
    }

    d->doResize(mw->size());
}

SizeHint

widget根据自身的需要给出的参考大小, 布局器会在这个基础上进行调整.

SizePolicy

伸缩策略, 在sizeHint的基础上还可以做哪些调整, 会影响widget的最小大小和最大大小.
而最终widget分配到的大小受限于最大大小和最小大小.

官方定义

QSizePolicy::GrowFlag       1   The widget can grow beyond its size hint if necessary.
QSizePolicy::ExpandFlag     2   The widget should get as much space as possible.
QSizePolicy::ShrinkFlag     4   The widget can shrink below its size hint if necessary.
QSizePolicy::IgnoreFlag     8   The widget's size hint is ignored. The widget will get as much space as possible.

代码里用到的地方

  • ExpandFlag
class Q_WIDGETS_EXPORT QSizePolicy
{
    // ...

    QT_SIZEPOLICY_CONSTEXPR Qt::Orientations expandingDirections() const Q_DECL_NOTHROW {
        return ( (verticalPolicy()   & ExpandFlag) ? Qt::Vertical   : Qt::Orientations() )
             | ( (horizontalPolicy() & ExpandFlag) ? Qt::Horizontal : Qt::Orientations() ) ;
    }

    // ...
};

直接影响QWidgetItem的expandingDirection,
而对于QBoxLayout来说, 只有里面有一项是可以伸展的, 那整个layout就是可伸展的.

最终布局器分配空间的时候会把多余空间平均分配给可伸展的项.

  • ShrinkFlag
Q_WIDGETS_EXPORT QSize qSmartMinSize(const QSize &sizeHint, const QSize &minSizeHint,
                                 const QSize &minSize, const QSize &maxSize,
                                 const QSizePolicy &sizePolicy)
{
    QSize s(0, 0);

    if (sizePolicy.horizontalPolicy() != QSizePolicy::Ignored) {
        if (sizePolicy.horizontalPolicy() & QSizePolicy::ShrinkFlag)
            s.setWidth(minSizeHint.width());
        else
            s.setWidth(qMax(sizeHint.width(), minSizeHint.width()));
    }

    if (sizePolicy.verticalPolicy() != QSizePolicy::Ignored) {
        if (sizePolicy.verticalPolicy() & QSizePolicy::ShrinkFlag) {
            s.setHeight(minSizeHint.height());
        } else {
            s.setHeight(qMax(sizeHint.height(), minSizeHint.height()));
        }
    }

    s = s.boundedTo(maxSize);
    if (minSize.width() > 0)
        s.setWidth(minSize.width());
    if (minSize.height() > 0)
        s.setHeight(minSize.height());

    return s.expandedTo(QSize(0,0));
}

这段代码是在用算最小大小, 计算的结果会直接影响到QWidgetItemV2::minimumSize()和QWidgetItem::minimumSize()的返回值,
即最终widget能分配到的大小的下限.

不难看出, 在有ShrinkFlag标记的时候, 直接返回minimumSizeHint, 而这个值又直接来自QWidget::minimumSizeHint();
如果没有ShrinkFlag标记, 就返回minimumSizeHint和sizeHint中较大的.

Shrink, 收缩的意思, 在有这个标记的时候收缩到minimumSizeHint的大小.

  • GrowFlag
Q_WIDGETS_EXPORT QSize qSmartMaxSize(const QSize &sizeHint,
                                 const QSize &minSize, const QSize &maxSize,
                                 const QSizePolicy &sizePolicy, Qt::Alignment align)
{
    if (align & Qt::AlignHorizontal_Mask && align & Qt::AlignVertical_Mask)
        return QSize(QLAYOUTSIZE_MAX, QLAYOUTSIZE_MAX);
    QSize s = maxSize;
    QSize hint = sizeHint.expandedTo(minSize);
    if (s.width() == QWIDGETSIZE_MAX && !(align & Qt::AlignHorizontal_Mask))
        if (!(sizePolicy.horizontalPolicy() & QSizePolicy::GrowFlag))
            s.setWidth(hint.width());

    if (s.height() == QWIDGETSIZE_MAX && !(align & Qt::AlignVertical_Mask))
        if (!(sizePolicy.verticalPolicy() & QSizePolicy::GrowFlag))
            s.setHeight(hint.height());

    if (align & Qt::AlignHorizontal_Mask)
        s.setWidth(QLAYOUTSIZE_MAX);
    if (align & Qt::AlignVertical_Mask)
        s.setHeight(QLAYOUTSIZE_MAX);
    return s;
}

这里是在计算layoutItem的最大大小的时候调的, 计算结果会直接影响QWidgetItemV2::maximumSize()和QWidgetItem::maximumSize()的返回值,
即最终widget能分配到的大小的上限.

GrowFlag是反过来用的, 为0的时候才会把最终的返回值限制到sizeHint, 因为最大大小默认就是一个非常大的值(QWIDGETSIZE_MAX).

多层widget嵌套的情况:

QLayout::activate 
    => Qwidget::updateGeometry
        => parentWidget->layout->invalidate
            => postEvent(parentWidget, QEvent::LayoutRequest)
                => QLayout::widgetEvent
                    => QLayout::activate

单来说就是, 里层widget布局的改变(activate), 会通过LaoutRequest事件逐层往外传, 触发外层widget重新布局.
但是, 布局器在重新布局的时候一般不会重新调整widget的大小, 只是给每个布局项重新分配空间.
所以, 里层widge会先按照已有的大小重新调整好布局, 然后再触发外层widget重新布局,
要是外层widget在布局的时候把里层widget的大小改了, 里层的widget就需要重新布局一次.

强调一点, QApplication在处理LayoutRequest事件时是先经过QLayout::widgetEvent,再经过eventFilter最后才到QWdiget::event().

需要注意的是,
QWidget::setGeometry的时候还是会受最大大小和最小大小限制的.
这就意味着, 布局器在空间不够时虽然会自动压缩布局项, 但是最终还是要走QWidget::setGeometry来调整widget的大小.
所以, 要是压缩后的大小小过widget本身的最小大小, 就会看不到布局器的自动压缩效果, 甚至会出现重叠的情况.

布局器默认情况下不会调整widget的大小(SetDefaultConstraint), 但是在多层widget嵌套的时候我们却能看到里层widget的大小可以随着内容动态调整, 这其实是外层widget的布局器的作用.
里层widget在updateGeometry的时候会往外层发一个LayoutRequest事件, 触发外层widget重新布局,
外层widget在重新布局的时候把里层widget的大小给调整了.
当然, 最外层的widget也可以通过把sizeConstraint设为SetFixedSize来达到这个效果.

本文福利,费领取Qt开发学习资料包、技术视频,内容包括(C++语言基础,C++设计模式,Qt编程入门,QT信号与槽机制,QT界面开发-图像绘制,QT网络,QT数据库编程,QT项目实战,QSS,OpenCV,Quick模块,面试题等等)↓↓↓↓↓↓见下面↓↓文章底部点击费领取↓↓

猜你喜欢

转载自blog.csdn.net/m0_60259116/article/details/130068382
今日推荐