Laying Out Multiple Widgets in a Scrollable Grid with PyQt5

How to dynamically arrange group boxes in a grid layout that scrolls and resizes cleanly
Heads up! You've already completed this tutorial.

When building UIs you can sometimes find yourself needing to display a variable number of widgets arranged in a tidy grid — say, three columns and as many rows as the data requires. And when there are more items than fit on screen, you want the whole thing to scroll.

This is a common pattern, but it's easy to run into trouble. If you set fixed sizes on parent containers, adding more items can cause everything to squash together or overflow. In this tutorial, we'll walk through how to set up a QGridLayout inside a QScrollArea so your grid grows naturally and stays scrollable, no matter how many items you add.

The problem with fixed geometry

A tempting approach when starting out is to use setGeometry() on every widget — placing them at exact pixel positions. This works when you know exactly how many items you'll have, but as soon as the number changes (say, from 15 to 30 or 300), things fall apart. Widgets overlap, get clipped, or the container doesn't grow to fit them all.

The solution is to let Qt's layout system handle the sizing and positioning for you. That's what layouts are for — they arrange child widgets automatically and respond to changes in content and window size.

Setting up the scrollable grid

Let's start with a minimal example: a QMainWindow containing a QScrollArea, which holds a widget with a QGridLayout. We'll populate it with some placeholder group boxes.

python
import sys

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QScrollArea,
    QFrame, QGridLayout, QGroupBox, QHBoxLayout,
    QPushButton, QLabel,
)


class Window(QMainWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.setGeometry(100, 100, 920, 560)

        # Central widget with its own layout
        central_widget = QWidget(self)
        central_layout = QGridLayout(central_widget)

        # Scroll area
        scroll_area = QScrollArea(central_widget)
        scroll_area.setWidgetResizable(True)
        central_layout.addWidget(scroll_area, 0, 0)

        # A content widget that will live inside the scroll area
        scroll_content = QWidget()
        scroll_content_layout = QGridLayout(scroll_content)

        # The frame that holds our grid of cards
        staff_frame = QFrame(scroll_content)
        staff_frame_layout = QGridLayout(staff_frame)

        # The actual grid where group boxes go
        self.staff_grid = QGridLayout()
        staff_frame_layout.addLayout(self.staff_grid, 0, 0)

        scroll_content_layout.addWidget(staff_frame, 0, 0)
        scroll_area.setWidget(scroll_content)

        self.setCentralWidget(central_widget)

        # Populate the grid
        self.create_boxes()

    def create_boxes(self):
        number = 30
        columns = 3

        for index in range(number):
            row = index // columns
            col = index % columns

            box = QGroupBox()
            box.setFixedSize(250, 130)
            self.staff_grid.addWidget(box, row, col)

            layout = QHBoxLayout()
            box.setLayout(layout)

            label = QLabel(f"Staff #{index + 1}")
            layout.addWidget(label)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    sys.exit(app.exec_())

Run this and you'll see 30 group boxes arranged in a 3-column grid, inside a scrollable area. Try changing number = 30 to number = 100 or number = 7 — the scroll area adjusts automatically.

Grid of staff boxes in a scroll area

There are a few things making this behave properly:

  • setWidgetResizable(True) on the QScrollArea tells it to resize its internal widget to fill available space, while still allowing scrolling when the content exceeds the visible area. For more on how QScrollArea works, see our dedicated QScrollArea tutorial.
  • No setGeometry() on inner widgets. Instead of fixing positions manually, we let the QGridLayout calculate where each group box goes. The layout grows as items are added, and the scroll area responds.
  • setFixedSize(250, 130) on each group box gives them a consistent card-like appearance, but the frame and scroll content are not fixed — they grow as needed.

Summary

The main takeaways for building dynamic grid layouts in PyQt5:

Packaging Python Applications with PyInstaller by Martin Fitzpatrick — This step-by-step guide walks you through packaging your own Python applications from simple examples to complete installers and signed executables.

Get the book

  • Use layouts instead of setGeometry() for anything that needs to adapt to varying content. If you're new to PyQt5 layouts, our PyQt5 layouts tutorial covers the different layout types in detail.
  • Put a QGridLayout inside a QScrollArea with setWidgetResizable(True) so the content scrolls naturally as it grows.
  • Calculate row and column from a single index using // and % to handle any number of items, including non-multiples of the column count.

If you want to take your layouts further with custom-designed interfaces, you can also use Qt Designer to visually build your GUI layout and then populate it programmatically.

PyQt6 Crash Course by Martin Fitzpatrick — The important parts of PyQt6 in bite-size chunks

See the course

Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak
Martin Fitzpatrick

Laying Out Multiple Widgets in a Scrollable Grid with PyQt5 was written by Martin Fitzpatrick.

Martin Fitzpatrick is the creator of Python GUIs, and has been developing Python/Qt applications for the past 12+ years. He has written a number of popular Python books and provides Python software development & consulting for teams and startups.