一直苦寻于一个比较智能的布局方式,能够满足软件界面进行resize的时候,对已经存在的布局进行重新布局。能够合理的判断界面的size,在界面放大的时候,显示的item的行数减少,相反,界面缩小的时候,显示的 item 行数能相应的增加,也就是能够满足界面是充盈的并且不会有超出界面的显示 item。
而我们已知的在Qt中已有的几种布局方式,都没办法直接达到我们的预期。比较接近的是栅格布局,但也要进行二次开发才能满足需求。
这种方式比较适合每个 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 是等大的时候比较有效。
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()); 得到每行的固定高度。
使用也是比较简单。
这样通过定义成员变量的方式,会方便后续对布局内的 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());
...
}
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源码