一直苦寻于一个比较智能的布局方式,能够满足软件界面进行resize的时候,对已经存在的布局进行重新布局。能够合理的判断界面的size,在界面放大的时候,显示的item的行数减少,相反,界面缩小的时候,显示的 item 行数能相应的增加,也就是能够满足界面是充盈的并且不会有超出界面的显示 item。
而我们已知的在Qt中已有的几种布局方式,都没办法直接达到我们的预期。比较接近的是栅格布局,但也要进行二次开发才能满足需求。
1、 利用栅格布局和 QScrollArea 实现类似的流式布局
这种方式比较适合每个 item size 相同的情况,实现的原理也就是在调用 resizeEvent() 函数的以后,对界面已经存在的 item 先全部移除,再重新添加。
测试的时候发现,如果每个 item 的 size 不做限制,可能不同列的 item 宽度会存在不一样的情况,但至少每列的 item 均是一样大小。
如下,首先我们 new 一个 QGridLayout
, 当然这种方式可以通过在 Qt designer 直接拖的方式创建,我这样做是因为我是直接在 Qt FlowLayout 的例子中做了测试,方便后面切换成 FlowLayout 。
QGridLayout* layout = new QGridLayout;
ui->scrollAreaWidgetContents->setLayout(layout);
QStringList list{"Short", "Longer", "Different text", "More text", "Even longer button text" };for(auto& t : list)
{auto btn = new QPushButton(t);layout->addWidget(btn);m_list.push_back(btn);
}
接下来就是如何在调用 resizeEvent 函数的时候对界面上的 item 重新布局。
首先就是计算出当前的界面最多能放几列。我下面直接用了一个固定的值,实际使用的时候,如果所有的 item 是固定大小的,则直接使用固定大小的宽度即可。
接下来就是先将界面上所有的 item 移除,然后再根据重新计算的数值将所有的 item 再次添加,为了方便,我用了一个 QPoint 类型的变量来记录当前是栅格布局的某个栅格。
void Window::resizeEvent(QResizeEvent *event)
{int column = event->size().width() / 100;auto layout = ui->scrollAreaWidgetContents->layout();for (int index = 0, size = layout ->count(); index < size ; ++index){auto btn = layout ->itemAt(index)->widget();layout ->removeWidget(btn);}QPoint pt(0, 0);for (auto& btn : m_list){if(pt.y() == column){pt.setX(pt.x() + 1);pt.setY(0);}static_cast<QGridLayout*>(layout)->addWidget(btn, pt.x(), pt.y());pt.setY(pt.y() + 1);}
}
这种方法在布局 item 是等大的时候比较有效。
2、使用 Qt 给的例子 Flowlayout
Qt 提供了一个 FlowLayout 的例子,但是并没有将其收录进模块,而是以源码的形式直接给出了可运行测试的例子程序。该类继承自 QLayout。
class FlowLayout : public QLayout
{
public:explicit FlowLayout(QWidget *parent, int margin = -1, int hSpacing = -1, int vSpacing = -1);explicit FlowLayout(int left, int top, int right, int bottom, int hSpacing = -1, int vSpacing = -1);explicit FlowLayout(int margin = -1, int hSpacing = -1, int vSpacing = -1);~FlowLayout();void addItem(QLayoutItem *item) override;void setSpacing(int hSpacing = -1, int vSpacing = -1);int horizontalSpacing() const;int verticalSpacing() const;Qt::Orientations expandingDirections() const override;bool hasHeightForWidth() const override;int heightForWidth(int) const override;int count() const override;QLayoutItem *itemAt(int index) const override;QSize minimumSize() const override;void setGeometry(const QRect &rect) override;QSize sizeHint() const override;QLayoutItem *takeAt(int index) override;private:int doLayout(const QRect &rect, bool testOnly) const;int smartSpacing(QStyle::PixelMetric pm) const;
};
通过重载的方式,实现部分成员函数,最后再通过调用 doLayout(const QRect &rect, bool testOnly) 成员函数实现布局显示。
因此,该类的主要功能集中在该函数中。
int FlowLayout::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 = 0;QLayoutItem *item = nullptr;foreach(item, m_itemList) {QSize sizeItemHint(item->sizeHint());if (item == m_itemList.last()){sizeItemHint = QSize(qMax(m_minimumSize.width(), sizeItemHint.width()), qMax(m_minimumSize.height(), sizeItemHint.height()));}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);}int nextX = x + sizeItemHint.width() + spaceX;if (nextX - spaceX > effectiveRect.right() && lineHeight > 0) {x = effectiveRect.x();y = y + lineHeight + spaceY;nextX = x + sizeItemHint.width() + spaceX;lineHeight = 0;}if (!testOnly){item->setGeometry(QRect(QPoint(x, y), sizeItemHint));}x = nextX;lineHeight = qMax(lineHeight, sizeItemHint.height());}return y + lineHeight - rect.y() + bottom;
}
函数功能,主要是:通过遍历item list, 计算每个 item 绘制的坐标,如果矩形的剩余长度不够 item widget 的长度及两个控件中间的间距,则将 item widget 的 x 坐标设为矩形的起始,也就是另起一行。另起一行之后则需要考虑 item widget 所处的 y 坐标,最后通过取当前 item 的高度和 lineHeight 的较大值, lineHeight = qMax(lineHeight, sizeItemHint.height()); 得到每行的固定高度。
使用也是比较简单。
1、可以通过定义成员变量的方式
这样通过定义成员变量的方式,会方便后续对布局内的 widget 进行操作,比如如下:
m_layout = new FlowLayout(this);
m_layout->setMargin(0);
m_layout->setSpacing(10, 10);
ui->wdgCard->setLayout(m_layout);
使用时:
auto ptr = new XXXX(this);
m_layout->addWidget(ptr);...for(int index = 0, size = m_layout->size(); index < size; ++index)
{auto ptr = static_cast<XXXX*>(m_layout->item(index)->widget());...
}
2、使用 Qt 给的例子 Flowlayout
auto layout = new FlowLayout(this);
layout->setMargin(0);
layout->setSpacing(10, 10);
ui->wdgCard->setLayout(layout);
使用时:
auto ptr = new XXXX(this);
ui->wdgCard->layout()->addWidget(ptr);
...
while (ui->wdgCard->layout()->count())
{auto ptr = ui->wdgCard->layout()->itemAt(0)->widget();ui->wdgCard->layout()->removeWidget(wdg);wdg->deleteLater();
}
FlowLayout源码