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

Shifting elements in a grid layout



  • I want to have multiple rows of widgets, each with 4 columns. When i get rid of a widget , I want each element to be shifted up so that it maintains having 4 widgets per row, as shown below when widget c is deleted:
    Screenshot 2020-11-09 at 19.47.06.png

    I was thinking a grid layout would be useful, but I'm not sure how to implement it exactly.

    Thanks for the help.



  • @sam1
    If you use QGridLayout you have to move all the items yourself in code.

    If you want to take Flow Layout Example, that may be what you are looking for if you want to widgets to flow around.



  • @JonB Thank you, I'll take a look at that later. Is it also in pyqt though? the link you sent was a c++ implementation. Hopefully this solves my problem



  • @sam1
    I forgot you were Python. As it happens I ported it to Python, and made some fix. I'll try to remember to dig it out tomorrow and post here.



  • @JonB Hello, Any chance you can share it here now?



  • @sam1
    Here is a standalone file for you to use.

    #############################################################################
    #
    # 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 then I wrapped it into my own class, with a couple of comments:

    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)
    

Log in to reply