Qt 的流式布局 FlowLayout

一直苦寻于一个比较智能的布局方式,能够满足软件界面进行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源码

你可能感兴趣的:(Qt,qt,开发语言,flow,layout,流式布局)