Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. Is there a Qt layout grid that can dynamically change row and column counts to best fit the space?
Forum Updated to NodeBB v4.3 + New Features

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

Scheduled Pinned Locked Moved Unsolved General and Desktop
8 Posts 3 Posters 6.2k Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • D Offline
    D Offline
    donquibeats
    wrote on last edited by
    #1

    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?

    1 Reply Last reply
    0
    • mrjjM Offline
      mrjjM Offline
      mrjj
      Lifetime Qt Champion
      wrote on last edited by mrjj
      #2

      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.

      1 Reply Last reply
      4
      • D Offline
        D Offline
        donquibeats
        wrote on last edited by
        #3

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

        JonBJ 1 Reply Last reply
        0
        • D donquibeats

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

          JonBJ Offline
          JonBJ Offline
          JonB
          wrote on last edited by JonB
          #4

          @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....

          1 Reply Last reply
          2
          • D Offline
            D Offline
            donquibeats
            wrote on last edited by
            #5

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

            JonBJ 1 Reply Last reply
            0
            • D donquibeats

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

              JonBJ Offline
              JonBJ Offline
              JonB
              wrote on last edited by
              #6

              @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 ;-)

              D 1 Reply Last reply
              4
              • JonBJ JonB

                @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 ;-)

                D Offline
                D Offline
                donquibeats
                wrote on last edited by
                #7

                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!

                1 Reply Last reply
                0
                • D Offline
                  D Offline
                  donquibeats
                  wrote on last edited by
                  #8

                  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).

                  1 Reply Last reply
                  1

                  • Login

                  • Login or register to search.
                  • First post
                    Last post
                  0
                  • Categories
                  • Recent
                  • Tags
                  • Popular
                  • Users
                  • Groups
                  • Search
                  • Get Qt Extensions
                  • Unsolved