Skip to content

Instantly share code, notes, and snippets.

@raven-worx
Created January 11, 2021 10:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save raven-worx/28a737f7333480cfac418b42480093fc to your computer and use it in GitHub Desktop.
Save raven-worx/28a737f7333480cfac418b42480093fc to your computer and use it in GitHub Desktop.
QML GridFlow (Masonry) Layout type
#include "GridFlow.hpp"
#include <QQmlInfo>
#include <QTime>
#include <QQmlProperty>
#include <QCoreApplication>
GridFlow::GridFlow(QQuickItem *parentItem)
: QQuickItem(parentItem), m_HorizontalSpacing(5), m_VerticalSpacing(5), m_Margin(5)
, m_RowHeight(50), m_SpanningEnabled(true), m_KeepChildOrder(false), m_ColumnWidth(0), m_ColumnCount(4)
{
qsrand(QTime::currentTime().msec()); // TODO: use QRandomGenerator (since Qt 5.10)
connect(this, &QQuickItem::widthChanged, this, &GridFlow::scheduleLayout);
connect(this, &QQuickItem::heightChanged, this, &GridFlow::scheduleLayout);
this->attachToParent( parentItem );
}
GridFlow::~GridFlow()
{
}
GridFlowAttached* GridFlow::qmlAttachedProperties(QObject* attachee)
{
return new GridFlowAttached(attachee);
}
void GridFlow::doLayout()
{
if( !this->isVisible() || !this->parentItem() )
return;
const QList<QQuickItem*> items = this->childItems();
if( items.isEmpty() )
return;
m_ColumnHeights = QVector<qreal>( m_ColumnCount, m_Margin );
m_ColumnWidth = ((this->width() - 2*m_Margin) / m_ColumnCount) - m_HorizontalSpacing;
m_ColumnWidth += (m_HorizontalSpacing / m_ColumnCount);
for( int i = 0; i < items.count(); ++i )
{
QQuickItem* item = items.at(i);
if( !item->isVisible() )
continue;
GridFlowAttached* attachedLayoutItem = qobject_cast<GridFlowAttached*>( qmlAttachedPropertiesObject<GridFlow>(item,true) );
this->connectLayoutItem( attachedLayoutItem, true );
const int column = m_KeepChildOrder ? ((m_ColumnCount+i) % m_ColumnCount) : this->getMinColumnIndex();
int rowspan = 0;
if( m_SpanningEnabled )
{
const qreal spanningChance = attachedLayoutItem->m_SpanningChance;
if( qrand() <= (spanningChance*RAND_MAX) )
{
if (column - 1 > 0 && m_ColumnHeights.at(column - 1) <= m_ColumnHeights.at(column))
rowspan = -1;
else if (column + 1 < m_ColumnHeights.count() && m_ColumnHeights.at(column + 1) <= m_ColumnHeights.at(column))
rowspan = 1;
}
}
QSizeF itemSize = this->layoutItemSize( item, attachedLayoutItem, 1+qAbs(rowspan) );
if( itemSize.width() <= 0 || itemSize.height() <= 0 )
continue;
itemSize.rheight() -= std::fmod( (m_ColumnHeights.at(column) + itemSize.height() + m_VerticalSpacing), m_RowHeight );
QPointF pos = QPointF(
m_Margin + (m_ColumnWidth + m_HorizontalSpacing) * (column + (rowspan == -1 ? -1 : 0)),
m_ColumnHeights.at(column)
);
// apply property values and trigger possible connected animations
QQmlProperty(item, "x").write( QVariant::fromValue<qreal>(pos.x()) );
QQmlProperty(item, "y").write( QVariant::fromValue<qreal>(pos.y()) );
QQmlProperty(item, "width").write( QVariant::fromValue<qreal>(itemSize.width()) );
QQmlProperty(item, "height").write( QVariant::fromValue<qreal>(itemSize.height()) );
qreal next_height = m_ColumnHeights.at(column) + itemSize.height() + m_VerticalSpacing;
m_ColumnHeights[column + rowspan] = m_ColumnHeights[column] = next_height;
}
qreal implicitHeight = 0.0;
foreach( qreal h, m_ColumnHeights )
implicitHeight = qMax(implicitHeight,h);
implicitHeight += m_Margin;
QQmlProperty(this, "implicitHeight").write( QVariant::fromValue<qreal>(implicitHeight) );
}
void GridFlow::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
{
switch( change )
{
case QQuickItem::ItemVisibleHasChanged:
{
if( !value.boolValue )
break;
}
// fall through!
case QQuickItem::ItemChildAddedChange:
case QQuickItem::ItemChildRemovedChange:
{
this->scheduleLayout();
GridFlowAttached* attachedLayoutItem = qobject_cast<GridFlowAttached*>( qmlAttachedPropertiesObject<GridFlow>(value.item,false) );
this->connectLayoutItem( attachedLayoutItem, change == QQuickItem::ItemChildAddedChange );
}
break;
case QQuickItem::ItemParentHasChanged:
{
this->attachToParent( value.item );
}
break;
default:
break;
}
QQuickItem::itemChange(change,value);
}
void GridFlow::scheduleLayout()
{
QCoreApplication::postEvent(this, new QEvent(QEvent::LayoutRequest)); // make use of Qt's event-compression
}
void GridFlow::attachToParent(QQuickItem *parentItem)
{
if( !parentItem )
return;
QObject* anchors = this->property("anchors").value<QObject*>(); //private type QQuickAnchors
QQmlProperty(anchors, "fill").write( QVariant::fromValue<QQuickItem*>(parentItem) );
}
qreal GridFlow::horizontalSpacing() const
{
return m_HorizontalSpacing;
}
void GridFlow::setHorizontalSpacing(qreal spacing)
{
spacing = qMax(0.0, spacing);
if( m_HorizontalSpacing != spacing )
{
m_HorizontalSpacing = spacing;
Q_EMIT horizontalSpacingChanged(spacing);
this->scheduleLayout();
}
}
qreal GridFlow::verticalSpacing() const
{
return m_VerticalSpacing;
}
void GridFlow::setMargin(qreal margin)
{
margin = qMax(0.0, margin);
if( m_Margin != margin )
{
m_Margin = margin;
Q_EMIT marginChanged(margin);
this->scheduleLayout();
}
}
qreal GridFlow::margin() const
{
return m_Margin;
}
void GridFlow::setRowHeight(qreal height)
{
height = qMax(0.0, height);
if( m_RowHeight != height )
{
m_RowHeight = height;
Q_EMIT rowHeightChanged(height);
this->scheduleLayout();
}
}
qreal GridFlow::rowHeight() const
{
return m_RowHeight;
}
void GridFlow::setVerticalSpacing(qreal spacing)
{
spacing = qMax(0.0, spacing);
if( m_VerticalSpacing != spacing )
{
m_VerticalSpacing = spacing;
Q_EMIT verticalSpacingChanged(spacing);
this->scheduleLayout();
}
}
int GridFlow::columnCount() const
{
return m_ColumnCount;
}
void GridFlow::setColumnCount(int count)
{
count = qMax(count,1);
if( m_ColumnCount != count )
{
m_ColumnCount = count;
this->scheduleLayout();
Q_EMIT columnCountChanged(count);
}
}
void GridFlow::setSpanningEnabled(bool enabled)
{
if( m_SpanningEnabled != enabled )
{
m_SpanningEnabled = enabled;
this->scheduleLayout();
Q_EMIT spanningEnabledChanged(enabled);
}
}
bool GridFlow::isSpanningEnabled() const
{
return m_SpanningEnabled;
}
void GridFlow::setKeepChildOrder(bool keepChildOrder)
{
if( m_KeepChildOrder != keepChildOrder )
{
m_KeepChildOrder = keepChildOrder;
this->scheduleLayout();
Q_EMIT keepChildOrderChanged(keepChildOrder);
}
}
bool GridFlow::keepChildOrder() const
{
return m_KeepChildOrder;
}
#ifdef max
#undef max
#endif
int GridFlow::getMinColumnIndex()
{
qreal minHeight = std::numeric_limits<qreal>::max();
int minIdx = m_ColumnHeights.count() ? 0 : -1;
for( int i = 0; i < m_ColumnHeights.count(); ++i )
{
qreal h = m_ColumnHeights.at(i);
if( h < minHeight)
{
minHeight = h;
minIdx = i;
}
}
return minIdx;
}
QSizeF GridFlow::layoutItemSize(QQuickItem* item, GridFlowAttached* layoutItem, int rowspan) const
{
QSizeF initSize = this->getInitialItemSize(item);
if( !initSize.isValid() )
return QSizeF();
qreal w = m_ColumnWidth * rowspan;
if (rowspan > 1)
w += m_VerticalSpacing;
qreal h = (initSize.height() * w) / initSize.width();
qreal hh = qMax( layoutItem->m_MinimumHeight, h );
if( layoutItem->m_MaximumHeight > 0 )
hh = qMin( layoutItem->m_MaximumHeight, h );
qreal ww = (w * hh) / h;
return QSizeF(ww,hh);
}
QSizeF GridFlow::getInitialItemSize(QQuickItem* item) const
{
Q_ASSERT( item );
QSizeF size;
if( !item )
return size;
if( QObject* attached = qmlAttachedPropertiesObject<GridFlow>(item,false) )
{
size = attached->property("sizeHint").toSizeF();
}
if( !size.isValid() )
{
if( item->implicitWidth() > 0 )
size.rwidth() = item->implicitWidth();
else
size.rwidth() = m_ColumnWidth;
if( item->implicitHeight() > 0 )
size.rheight() = item->implicitHeight();
else
size.rheight() = m_ColumnWidth;
}
return size;
}
void GridFlow::connectLayoutItem(GridFlowAttached *item, bool connect)
{
if( !item )
return;
if( connect )
{
QObject::connect( item, &GridFlowAttached::sizeHintChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection );
QObject::connect( item, &GridFlowAttached::spanningChanceChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection );
QObject::connect( item, &GridFlowAttached::minimumHeightChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection );
QObject::connect( item, &GridFlowAttached::maximumHeightChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection );
QObject::connect( item, &GridFlowAttached::visibilityChanged, this, &GridFlow::scheduleLayout, Qt::UniqueConnection );
}
else
{
QObject::disconnect( item, &GridFlowAttached::sizeHintChanged, this, &GridFlow::scheduleLayout );
QObject::disconnect( item, &GridFlowAttached::spanningChanceChanged, this, &GridFlow::scheduleLayout );
QObject::disconnect( item, &GridFlowAttached::minimumHeightChanged, this, &GridFlow::scheduleLayout );
QObject::disconnect( item, &GridFlowAttached::maximumHeightChanged, this, &GridFlow::scheduleLayout );
QObject::disconnect( item, &GridFlowAttached::visibilityChanged, this, &GridFlow::scheduleLayout );
}
}
bool GridFlow::event(QEvent* event)
{
switch( event->type() )
{
case QEvent::LayoutRequest:
{
this->doLayout();
return true;
}
break;
default:
break;
}
return QQuickItem::event(event);
}
//------------------------------------------------------
GridFlowAttached::GridFlowAttached(QObject* attachee)
: QObject(attachee), m_SpanningChance(0.5), m_MinimumHeight(0.0), m_MaximumHeight(0.0)
{
if( QQuickItem* item = qobject_cast<QQuickItem*>(attachee) )
{
m_SizeHint = QSizeF(item->width(),item->height());
connect( item, &QQuickItem::visibleChanged, this, &GridFlowAttached::visibilityChanged );
}
}
GridFlowAttached::~GridFlowAttached()
{
}
void GridFlowAttached::setSizeHint(const QSizeF &size)
{
if( !size.isValid() )
{
QtQml::qmlInfo(this) << "Cannot set invalid size: " << size;
return;
}
if( m_SizeHint != size )
{
m_SizeHint = size;
Q_EMIT sizeHintChanged(size);
}
}
void GridFlowAttached::setSpanningChance(qreal chance)
{
if( 0.0 <= chance && chance <= 1.0 && m_SpanningChance != chance )
{
m_SpanningChance = chance;
Q_EMIT spanningChanceChanged(chance);
}
}
void GridFlowAttached::setMinimumHeight(qreal height)
{
height = qMax( 0.0, height );
if( m_MinimumHeight != height )
{
m_MinimumHeight = height;
Q_EMIT minimumHeightChanged( height );
}
}
void GridFlowAttached::setMaximumHeight(qreal height)
{
height = qMax( 0.0, height );
if( m_MaximumHeight != height )
{
m_MaximumHeight = height;
Q_EMIT maximumHeightChanged( height );
}
}
#ifndef GRIDFLOW_HPP
#define GRIDFLOW_HPP
#include <QQuickItem>
class GridFlow;
class GridFlowAttached : public QObject
{
Q_OBJECT
Q_PROPERTY( QSizeF sizeHint MEMBER m_SizeHint WRITE setSizeHint NOTIFY sizeHintChanged )
Q_PROPERTY( qreal spanningChance MEMBER m_SpanningChance WRITE setSpanningChance NOTIFY spanningChanceChanged )
Q_PROPERTY( qreal minimumHeight MEMBER m_MinimumHeight WRITE setMinimumHeight NOTIFY minimumHeightChanged )
Q_PROPERTY( qreal maximumHeight MEMBER m_MaximumHeight WRITE setMaximumHeight NOTIFY maximumHeightChanged )
friend class GridFlow;
public:
void setSizeHint( const QSizeF & size );
void setSpanningChance( qreal chance );
void setMinimumHeight( qreal height );
void setMaximumHeight( qreal height );
Q_SIGNALS:
void visibilityChanged();
void sizeHintChanged(const QSizeF & sizeHint);
void spanningChanceChanged(qreal chance);
void minimumHeightChanged(qreal height);
void maximumHeightChanged(qreal height);
protected:
explicit GridFlowAttached( QObject* attachee );
~GridFlowAttached();
QSizeF m_SizeHint;
qreal m_SpanningChance;
qreal m_MinimumHeight;
qreal m_MaximumHeight;
};
QML_DECLARE_TYPE(GridFlowAttached)
//--------------------------------------------------------------
class GridFlow : public QQuickItem
{
Q_OBJECT
QRW_ANDROID_CLASSINFO
Q_PROPERTY( int columnCount READ columnCount WRITE setColumnCount NOTIFY columnCountChanged )
Q_PROPERTY( qreal horizontalSpacing READ horizontalSpacing WRITE setHorizontalSpacing NOTIFY horizontalSpacingChanged )
Q_PROPERTY( qreal verticalSpacing READ verticalSpacing WRITE setHorizontalSpacing NOTIFY verticalSpacingChanged )
Q_PROPERTY( bool spanningEnabled READ isSpanningEnabled WRITE setSpanningEnabled NOTIFY spanningEnabledChanged )
Q_PROPERTY( qreal margin READ margin WRITE setMargin NOTIFY marginChanged )
Q_PROPERTY( qreal rowHeight READ rowHeight WRITE setRowHeight NOTIFY rowHeightChanged )
Q_PROPERTY( bool keepChildOrder READ keepChildOrder WRITE setKeepChildOrder NOTIFY keepChildOrderChanged )
public:
explicit GridFlow(QQuickItem *parentItem = Q_NULLPTR);
~GridFlow();
static GridFlowAttached* qmlAttachedProperties(QObject* attachee);
void setHorizontalSpacing(qreal spacing);
qreal horizontalSpacing() const;
void setVerticalSpacing(qreal spacing);
qreal verticalSpacing() const;
void setMargin(qreal margin);
qreal margin() const;
void setRowHeight(qreal height);
qreal rowHeight() const;
void setColumnCount(int);
int columnCount() const;
void setSpanningEnabled(bool enabled);
bool isSpanningEnabled() const;
void setKeepChildOrder(bool keepChildOrder);
bool keepChildOrder() const;
Q_SIGNALS:
void columnCountChanged(int count);
void horizontalSpacingChanged(qreal spacing);
void verticalSpacingChanged(qreal spacing);
void marginChanged(qreal margin);
void rowHeightChanged(qreal height);
void spanningEnabledChanged(bool enabled);
void keepChildOrderChanged(bool keepChildOrder);
protected Q_SLOTS:
void doLayout();
private:
int getMinColumnIndex();
QSizeF layoutItemSize(QQuickItem* item, GridFlowAttached* layoutItem, int rowspan) const;
QSizeF getInitialItemSize(QQuickItem* item) const;
void connectLayoutItem( GridFlowAttached* item, bool connect );
protected:
virtual bool event(QEvent* event) Q_DECL_OVERRIDE;
virtual void itemChange(ItemChange change, const ItemChangeData &value) Q_DECL_OVERRIDE;
void scheduleLayout();
void attachToParent(QQuickItem* parentItem);
qreal m_HorizontalSpacing;
qreal m_VerticalSpacing;
qreal m_Margin;
qreal m_RowHeight;
bool m_SpanningEnabled;
bool m_KeepChildOrder;
qreal m_ColumnWidth;
int m_ColumnCount;
QVector<qreal> m_ColumnHeights;
};
QML_DECLARE_TYPE(GridFlow)
QML_DECLARE_TYPEINFO(GridFlow, QML_HAS_ATTACHED_PROPERTIES)
#endif // GRIDFLOW_HPP
@amifelipek
Copy link

Hey, could you help getting this to work on my application?

@raven-worx
Copy link
Author

what exactly is your issue?
Register this class as a QML type (qmlRegisterType() method)
Example usage in QML can be found here: https://forum.qt.io/topic/81267/qrwandroid-qml-plugin/6

@amifelipek
Copy link

amifelipek commented May 20, 2022

I want to use this with a model, following the example you linked, all the delegates appear to be in the same row:

Screenshot_20220520_154609

I'm expecting it to look something like this:

Screenshot_20220520_042457

This is my code:

GridFlow {
    Layout.fillWidth: true
    Layout.fillHeight: true

    columnCount: 3
    horizontalSpacing: Kirigami.Units.gridUnit
    verticalSpacing: Kirigami.Units.gridUnit * 50
    spanningEnabled: true
    margin: Kirigami.Units.gridUnit
    rowHeight: Kirigami.Units.gridUnit * 100
    keepChildOrder: true

    Repeater {
        model: fontsModel
        FontDelegate {
            GridFlow.sizeHint: Qt.size(implicitWidth, implicitHeight)
            GridFlow.spanningChance: 0.2
            GridFlow.minimumHeight: Kirigami.Units.gridUnit * 5
            GridFlow.maximumHeight: implicitHeight
        }
    }
}

Another thing is that it segfaults everytime I close a page in the application, not sure how they're related though...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment