Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Is there a Qt layout grid that can dynamically change row and column counts to best fit the space?



  • Hello.

    In part of my new app, I've got an array of QFrame items, each of which is a wrapper for multiple smaller items (a thumbnail, some text above, a button below, etc.) It is helped, you could think of it like a results list from an image search, where thumbnails are shown. Every QFrame is the same size. The number of QFrames varies depending on the search criteria etc.

    At the moment, I've got a QGridWidget which has 3 columns as a static column count, so if I have 10 results, the frames are arranged like this:

    0 1 2
    3 4 5
    6 7 8
    9
    

    They are independent items, so which row or column they appear in is not important.

    What I would like to know is whether there is an alternative to QGridWidget, or a method of QGridWidget, which can dynamically move the frames around to best fit the available window. So for example on a particularly wide window, the same results might be shown as:

    0 1 2 3 4
    5 6 7 8 9
    

    but on a very narrow window, or if the user has dragged their window into a narrow shape, the boxes could move around so that they were arranged something like:

    0 1
    2 3
    4 5
    6 7
    8 9
    

    So my question is- is there already a Qt widget that supports this kind of dynamic behaviour, and if so, what is it please? If not, am I going to need my code to manually react to size changes and move objects on a QGridWidget accordingly, and then if so, are there any examples of this online as a starting point? The CardLayout example at https://doc.qt.io/qt-5/layout.html is I guess where I'd have to start if nothing like this already exists.

    I've Googled this but don't really know the most appropriate terms so haven't found anything- so maybe the perfect widget is right under my nose?


  • Lifetime Qt Champion

    Hi
    You can try with
    https://doc.qt.io/qt-5/qtwidgets-layouts-flowlayout-example.html
    I think you can tweak it to do like you want. Reflow the grid when window size is changed.



  • Thanks for the tip. I will start the process of trying to adapt this for PyQt5 tomorrow and will see how it goes.



  • @donquibeats
    I think I have written this flow layout in PyQt5 (https://forum.qt.io/topic/104653/how-to-do-a-no-break-qhboxlayout). I will (try to remember to!) look tomorrow and post....



  • Any existing PyQt5 examples would be of great interest. Yes please!



  • @donquibeats
    Always good to remind me... :)

    First, my flowlayout.py file, converted I believe directly from that example to PyQt5:

    #############################################################################
    #
    # This file taken from
    # https://code.qt.io/cgit/qt/qtbase.git/tree/examples/widgets/layouts/flowlayout/flowlayout.cpp?h=5.13
    # Modified/adapted by jon, 10/07/2019, to translate into Python/PyQt
    #
    # Copyright (C) 2016 The Qt Company Ltd.
    # Contact: https://www.qt.io/licensing/
    #
    # This file is part of the examples of the Qt Toolkit.
    #
    # $QT_BEGIN_LICENSE:BSD$
    # Commercial License Usage
    # Licensees holding valid commercial Qt licenses may use this file in
    # accordance with the commercial license agreement provided with the
    # Software or, alternatively, in accordance with the terms contained in
    # a written agreement between you and The Qt Company. For licensing terms
    # and conditions see https://www.qt.io/terms-conditions. For further
    # information use the contact form at https://www.qt.io/contact-us.
    #
    # BSD License Usage
    # Alternatively, you may use this file under the terms of the BSD license
    # as follows:
    #
    # "Redistribution and use in source and binary forms, with or without
    # modification, are permitted provided that the following conditions are
    # met:
    #   * Redistributions of source code must retain the above copyright
    #     notice, this list of conditions and the following disclaimer.
    #   * Redistributions in binary form must reproduce the above copyright
    #     notice, this list of conditions and the following disclaimer in
    #     the documentation and/or other materials provided with the
    #     distribution.
    #   * Neither the name of The Qt Company Ltd nor the names of its
    #     contributors may be used to endorse or promote products derived
    #     from this software without specific prior written permission.
    #
    #
    # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
    # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
    # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
    # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
    # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
    # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
    # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
    # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
    # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
    # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
    #
    # $QT_END_LICENSE$
    #
    #############################################################################
    
    
    import typing
    
    from PyQt5.QtCore import Qt, QPoint, QRect, QSize
    from PyQt5.QtWidgets import QWidget, QLayout, QLayoutItem, QStyle, QSizePolicy
    
    
    class FlowLayout(QLayout):
        def __init__(self, parent: QWidget=None, margin: int=-1, hSpacing: int=-1, vSpacing: int=-1):
            super().__init__(parent)
    
            self.itemList = list()
            self.m_hSpace = hSpacing
            self.m_vSpace = vSpacing
    
            self.setContentsMargins(margin, margin, margin, margin)
    
        def __del__(self):
            # copied for consistency, not sure this is needed or ever called
            item = self.takeAt(0)
            while item:
                item = self.takeAt(0)
    
        def addItem(self, item: QLayoutItem):
            self.itemList.append(item)
    
        def horizontalSpacing(self) -> int:
            if self.m_hSpace >= 0:
                return self.m_hSpace
            else:
                return self.smartSpacing(QStyle.PM_LayoutHorizontalSpacing)
    
        def verticalSpacing(self) -> int:
            if self.m_vSpace >= 0:
                return self.m_vSpace
            else:
                return self.smartSpacing(QStyle.PM_LayoutVerticalSpacing)
    
        def count(self) -> int:
            return len(self.itemList)
    
        def itemAt(self, index: int) -> typing.Union[QLayoutItem, None]:
            if 0 <= index < len(self.itemList):
                return self.itemList[index]
            else:
                return None
    
        def takeAt(self, index: int) -> typing.Union[QLayoutItem, None]:
            if 0 <= index < len(self.itemList):
                return self.itemList.pop(index)
            else:
                return None
    
        def expandingDirections(self) -> Qt.Orientations:
            return Qt.Orientations(Qt.Orientation(0))
    
        def hasHeightForWidth(self) -> bool:
            return True
    
        def heightForWidth(self, width: int) -> int:
            height = self.doLayout(QRect(0, 0, width, 0), True)
            return height
    
        def setGeometry(self, rect: QRect) -> None:
            super().setGeometry(rect)
            self.doLayout(rect, False)
    
        def sizeHint(self) -> QSize:
            return self.minimumSize()
    
        def minimumSize(self) -> QSize:
            size = QSize()
            for item in self.itemList:
                size = size.expandedTo(item.minimumSize())
    
            margins = self.contentsMargins()
            size += QSize(margins.left() + margins.right(), margins.top() + margins.bottom())
            return size
    
        def smartSpacing(self, pm: QStyle.PixelMetric) -> int:
            parent = self.parent()
            if not parent:
                return -1
            elif parent.isWidgetType():
                return parent.style().pixelMetric(pm, None, parent)
            else:
                return parent.spacing()
    
        def doLayout(self, rect: QRect, testOnly: bool) -> int:
            left, top, right, bottom = self.getContentsMargins()
            effectiveRect = rect.adjusted(+left, +top, -right, -bottom)
            x = effectiveRect.x()
            y = effectiveRect.y()
            lineHeight = 0
    
            for item in self.itemList:
                wid = item.widget()
                spaceX = self.horizontalSpacing()
                if spaceX == -1:
                    spaceX = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Horizontal)
                spaceY = self.verticalSpacing()
                if spaceY == -1:
                    spaceY = wid.style().layoutSpacing(QSizePolicy.PushButton, QSizePolicy.PushButton, Qt.Vertical)
    
                nextX = x + item.sizeHint().width() + spaceX
                if nextX - spaceX > effectiveRect.right() and lineHeight > 0:
                    x = effectiveRect.x()
                    y = y + lineHeight + spaceY
                    nextX = x + item.sizeHint().width() + spaceX
                    lineHeight = 0
    
                if not testOnly:
                    item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
        
                x = nextX
                lineHeight = max(lineHeight, item.sizeHint().height())
    
            return y + lineHeight - rect.y() + bottom
    
    

    And second my own "wrapper" around it (to do a touch extra I needed, and I always write wrappers anyway):

    class JFlowLayout(FlowLayout):
        # flow layout, similar to an HTML `<DIV>`
        # this is our "wrapper" to the `FlowLayout` sample Qt code we have implemented
        # we use it in place of where we used to use a `QHBoxLayout`
        # in order to make few outside-world changes, and revert to `QHBoxLayout`if we ever want to,
        # there are a couple of methods here which are available on a `QBoxLayout` but not on a `QLayout`
        # for which we provide a "lite-equivalent" which will suffice for our purposes
    
        def addLayout(self, layout: QLayout, stretch: int=0):
            # "equivalent" of `QBoxLayout.addLayout()`
            # we want to add sub-layouts (e.g. a `QVBoxLayout` holding a label above a widget)
            # there is some dispute as to how to do this/whether it is supported by `FlowLayout`
            # see my https://forum.qt.io/topic/104653/how-to-do-a-no-break-qhboxlayout
            # there is a suggestion that we should not add a sub-layout but rather enclose it in a `QWidget`
            # but since it seems to be working as I've done it below I'm elaving it at that for now...
    
            # suprisingly to me, we do not need to add the layout via `addChildLayout()`, that seems to make no difference
            # self.addChildLayout(layout)
            # all that seems to be reuqired is to add it onto the list via `addItem()`
            self.addItem(layout)
    
        def addStretch(self, stretch: int=0):
            # "equivalent" of `QBoxLayout.addStretch()`
            # we can't do stretches, we just arbitrarily put in a "spacer" to give a bit of a gap
            w = stretch * 20
            spacerItem = QtWidgets.QSpacerItem(w, 0, QSizePolicy.Expanding, QSizePolicy.Minimum)
            self.addItem(spacerItem)
    

    Put that in your pipe and smoke it ;-)



  • This is absolutely brilliant and has saved me a big chunk of time and head-scratching. I dropped the two code chunks in and was using a JFlowLayout full of QFrames() within a few minutes. A big THANK YOU for this. I owe you a virtual pint.

    I have spotted a couple of little 'but what if?' aspects to it that I might need to post here and ask questions about, but I'm going to experiment with it a bit more first before posting about those.

    Thanks again!



  • In reference to my previous post, I was able to solve the problems for myself.

    I was finding that a JFlowLayout was proving 'sticky' with window width, so the user increasing the horizontal width worked fine, but when the user tries to decrease the window size horizontally, it would sometimes refuse, as though you'd hit a minimum width. There was a knack to dragging it out-and-back that made it work, but it was an irritant. Then of course there was the problem of having so many results that the window size went off the screen.

    However both of those issues were sorted as soon as I stuck the whole JFlowLayout inside a QFrame inside a QScrollArea. Adding a QScrollArea makes it work in exactly the way I was hoping it would, and it's nicely draggable and flows in a sensible and intuitive way.

    Thanks again (again).


Log in to reply