Bootstrap

✅ Qt流式布局

Qt流式布局

前段时间,曾经对某个软件的一个“流式布局”有点感兴趣,什么叫“流式布局”呢?请看下图:

简而言之,流式布局就是布局应能够根据界面尺寸的变化自动调整其内部控件的位置。然而,Qt 提供的标准布局(如 QBoxLayout、QGridLayout 和 QFormLayout)并不能直接实现这一功能。因此,我们需要自行设计一个布局来达到目的。

🛠️ 创建自定义布局

在官方提供的布局管理文档,有这样一段描述:

理论上,通过继承QLayout,实现特定的函数,我们可以实现任何我们想要的布局效果。Qt官方提供了一个流式布局的例子(为什么不加到官方的控件里🤔),并对各个实现做了详细的解释。下面我们简单来分析分析这些函数的作用。具体的实现效果,我将在后续为大家展示。

🕵️‍♂️ 函数分析

  1. addItem/itemAt/takeItem

这组方法负责管理布局中的项。addItem用于向布局中添加一个新的项,itemAt用于检索指定索引处的项,而takeItem则用于从布局中移除一个特定的项。由于QLayout并不继承自QObject,因此它没有使用父对象机制来管理其子项,而是通过这组方法自行管理布局内的项目。

  1. horizontalSpacing/verticalSpacing

这两个属性或方法用于控制布局内各个元素之间的水平和垂直间距。

  1. hasHeightForWidth/heightForWidth

这些方法用于处理当宽度改变时,高度如何相应调整的问题。例如,当QLabel启用了自动换行(word wrap)功能时,它会实现heightForWidth方法,以根据文本换行情况动态调整自身的高度。这种机制对于确保内容在不同尺寸的容器中正确显示非常有用。有关于这部分的源码分析,挖个坑😉,之后我们有机会再一起来学习一下QLabel的换行机制是什么样的。

  1. sizeHint/minimumSizeHint/setGeometry

sizeHintminimumSizeHint分别返回布局推荐的大小和最小大小提示,这对于嵌套布局或界面设计特别重要,可以保证每个组件都能获得适当的空间setGeometry方法则是用于设置布局的具体几何形状,即它将如何根据给定的矩形区域重新定位和调整它内部控件的大小

  1. doLayout

上面那些函数都是Qt提供给我们重载的,而这个函数是Qt提供的例子中,用来实现布局的函数。对这个函数,稍微进行部分布局逻辑分析。

这些方法共同作用,确保了Qt应用程序中的用户界面既灵活又高效,能够适应不同的设备和屏幕尺寸。通过理解和运用这些布局管理,我们能够灵活的根据自己的需求来创建界面。

🔑 doLayout

int FlowLayout::doLayout(const QRect &rect, bool testOnly) const
{
    int left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);
    // [1] 去除边距,获取实际有效的区域
    QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
    int x = effectiveRect.x();
    int y = effectiveRect.y();
    int lineHeight = 0;

    // [2] 遍历布局中所有item
    for (QLayoutItem *item : std::as_const(itemList)) {
        const QWidget *wid = item->widget();
        int spaceX = horizontalSpacing();
        if (spaceX == -1)
            spaceX = wid->style()->layoutSpacing(
                QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Horizontal);
        int spaceY = verticalSpacing();
        if (spaceY == -1)
            spaceY = wid->style()->layoutSpacing(
                QSizePolicy::PushButton, QSizePolicy::PushButton, Qt::Vertical);

        // [3] 计算下一个控件的x坐标: 当前x + 当前item的推荐宽度 + 布局的横向间距
        int nextX = x + item->sizeHint().width() + spaceX;
        // [4] 下一个控件需要的宽度超出了界面的宽度,
        //     将y坐标整体往下移动,移动的大小为当前行的控件的最大高度
        if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {
            x = effectiveRect.x();
            y = y + lineHeight + spaceY;
            nextX = x + item->sizeHint().width() + spaceX;
            lineHeight = 0;
        }

        if (!testOnly)
            item->setGeometry(QRect(QPoint(x, y), item->sizeHint()));

        x = nextX;
        // [5] 计算布局当前行的高度
        lineHeight = qMax(lineHeight, item->sizeHint().height());
    }
    return y + lineHeight - rect.y() + bottom;
}

该函数的核心在于:

  1. 根据当前区域(Geometry)及每个控件所需占用的宽度,来计算每个控件的 x、y 坐标当 x 坐标超出当前区域时,通过向下扩展 y 坐标来确保控件位置的合理

  1. 根据控件的sizeHint来设置控件的大小(所以控件的sizeHint很重要🤓

⚙️ 工程实战

最终,实现了下面的效果,就直接贴代码了:

✅ 布局管理

int ParamComboLayout::doLayout(const QRect & rect, bool testOnly) const
{
    int left, top, right, bottom;
    getContentsMargins(&left, &top, &right, &bottom);
    QRect effectiveRect = rect.adjusted(+left, +top, -right, -bottom);
    int x = effectiveRect.x();
    int y = effectiveRect.y();
    int lineHeight = LineHeight;

    m_bIsForm = effectiveRect.width() <= 350;

    QList<QRect> itemsRect;
    if (m_bIsForm) {
        int width = effectiveRect.width() - CheckBoxWidth - m_hSpace;
        switch (m_eType)
        {
        case ParamComboLayout::String:
        case ParamComboLayout::Datetime:
        {
            int px = x + CheckBoxWidth + m_hSpace;
            itemsRect.append(QRect(px, y, width, lineHeight));

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(px, y, width, lineHeight));

            break;
        }
        case ParamComboLayout::Enum:
        {
            itemsRect.append(QRect(x, CheckYStartPos, CheckBoxWidth, CheckBoxHeight));

            x += CheckBoxWidth + m_hSpace;
            itemsRect.append(QRect(x, y, width, lineHeight));

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(x, y, width, lineHeight));

            if (!m_bIsCombineEnable)
                break;

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(x, y, width, lineHeight));
            break;
        }
        case ParamComboLayout::Number:
        {
            itemsRect.append(QRect(x, CheckYStartPos, CheckBoxWidth, CheckBoxHeight));

            x += CheckBoxWidth + m_hSpace;
            itemsRect.append(QRect(x, y, width, lineHeight));

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(x, y, width, lineHeight));

            if (!m_bIsCombineEnable)
                break;

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(x, y, width, lineHeight));

            y += (LineHeight + m_vSpace);
            itemsRect.append(QRect(x, y, width, lineHeight));
            break;
        }
        default:
            break;
        }
    }

    if (!testOnly) {
        for (int i = 0; i < itemsRect.size(); ++i) {
            itemList.at(i)->setGeometry(itemsRect.at(i));
        }
    }

    return y + lineHeight - rect.y() + bottom;
}

✅ 布局宽高自适应

在调整界面宽度时,布局中的控件位置会相应地发生变化。如果界面宽度减小到当前高度无法容纳所有内部控件,可以通过增加界面高度,将控件向下布局。那怎样能够让布局自动处理这种问题呢?

答案是通过heightForWidth函数来解决问题

// ParamComboLayout是继承自FlowLayout

bool FlowLayout::hasHeightForWidth() const
{
    return true;
}

int FlowLayout::heightForWidth(int width) const
{
    int height = doLayout(QRect(0, 0, width, 0), true);
    return height;
}

📚 参考

  1. Flow Layout Example | Qt Widgets 5.15.17
  2. Layout Management | Qt Widgets 5.15.17
  3. Trading Height for Width
;