Python GUIs: Dynamically Adding Rows of Widgets in PyQt6 — How to use QGridLayout to add multiple widgets per row at runtime

Wait 5 sec.

I have a GUI where I dynamically add a QLineEdit for each image a user selects. That part works. But now I want to extend it so that each image gets a row of three text boxes — one for the filename, one for the width, and one for the height. How do I dynamically add multiple widgets horizontally for each new entry?Adding a single widget per action is straightforward enough, but when you need a whole row of widgets each time? That's where QGridLayout becomes your best friend.In this tutorial, we'll walk through how to dynamically add rows of QLineEdit widgets using QGridLayout in PyQt6. Each time the user selects images, a new row appears with three text boxes: the filename, image width, and image height. We'll also look at how you can use a custom widget to group related fields together.Why QGridLayout?If you've been using QVBoxLayout to stack widgets vertically, you've probably noticed that it only gives you one column. To place multiple widgets side by side and keep adding new rows, you need a two-dimensional layout. QGridLayout lets you place widgets by specifying a row and column, which makes it perfect for building table-like arrangements dynamically.pythonlayout.addWidget(some_widget, row, column)Each time you add a new file, you increment the row number and place your three widgets at columns 0, 1, and 2.Setting Up the WindowLet's start with a basic main window that has a button to open a file dialog and a grid layout ready to receive our dynamic rows.pythonimport sysimport osfrom PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QPushButton, QGridLayout, QLineEdit, QFileDialog, QLabel, QVBoxLayout,)class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Image Selector") # Central widget and outer layout central_widget = QWidget() self.setCentralWidget(central_widget) outer_layout = QVBoxLayout() central_widget.setLayout(outer_layout) # Button to open file dialog self.button_open = QPushButton("Open Images") self.button_open.clicked.connect(self.load_images) outer_layout.addWidget(self.button_open) # Header labels header_layout = QGridLayout() header_layout.addWidget(QLabel("Filename"), 0, 0) header_layout.addWidget(QLabel("Width"), 0, 1) header_layout.addWidget(QLabel("Height"), 0, 2) outer_layout.addLayout(header_layout) # Grid layout for dynamic rows self.grid_layout = QGridLayout() outer_layout.addLayout(self.grid_layout) # Stretch at the bottom to push everything up outer_layout.addStretch() # Track how many rows we've added self.current_row = 0 def load_images(self): paths, _ = QFileDialog.getOpenFileNames( self, "Select Images", "", "Images (*.png *.jpg *.bmp);;All Files (*)" ) for path in paths: filename = os.path.basename(path) width, height = self.get_image_size(path) self.add_image_row(filename, width, height) def get_image_size(self, path): # Placeholder — replace with your own logic return 0, 0 def add_image_row(self, filename, width, height): line_edit_name = QLineEdit(filename) line_edit_width = QLineEdit(str(width)) line_edit_height = QLineEdit(str(height)) self.grid_layout.addWidget(line_edit_name, self.current_row, 0) self.grid_layout.addWidget(line_edit_width, self.current_row, 1) self.grid_layout.addWidget(line_edit_height, self.current_row, 2) self.current_row += 1app = QApplication(sys.argv)window = MainWindow()window.show()sys.exit(app.exec())Run this and click "Open Images." Each file you select gets its own row with three editable text boxes. Click the button again to add more.How It WorksThe add_image_row method is where the dynamic creation happens. Each time it's called, it:Creates three QLineEdit widgets — one for the filename, one for width, one for height.Adds them to the grid layout at the current row, in columns 0, 1, and 2.Increments self.current_row so the next call places widgets on the next row.Because QGridLayout handles positioning for you, you don't need to manually manage spacing or alignment. The widgets line up in neat columns automatically.Extracting Real Image DimensionsThe get_image_size method above is a placeholder. If you want to extract actual image dimensions, you can use QImageReader from PyQt6 itself:pythonfrom PyQt6.QtGui import QImageReaderdef get_image_size(self, path): reader = QImageReader(path) size = reader.size() if size.isValid(): return size.width(), size.height() return 0, 0This reads only the image header, so it's fast even for large files.Adding a Computed ColumnSince the original question mentioned calculating the area from width and height, let's add a fourth column that shows the computed area. We can make this column read-only so users don't accidentally edit it.pythondef add_image_row(self, filename, width, height): line_edit_name = QLineEdit(filename) line_edit_width = QLineEdit(str(width)) line_edit_height = QLineEdit(str(height)) line_edit_area = QLineEdit(str(width * height)) line_edit_area.setReadOnly(True) self.grid_layout.addWidget(line_edit_name, self.current_row, 0) self.grid_layout.addWidget(line_edit_width, self.current_row, 1) self.grid_layout.addWidget(line_edit_height, self.current_row, 2) self.grid_layout.addWidget(line_edit_area, self.current_row, 3) self.current_row += 1You'd also want to add a matching "Area" header label in the __init__ method.Encapsulating a Row as a ClassAs your rows get more complex, it helps to group the widgets for each row into their own class. This keeps MainWindow clean and makes it easy to access or update individual rows later.pythonclass ImageInfoRow: def __init__(self, filename, width, height, grid_layout, row): self.line_edit_name = QLineEdit(filename) self.line_edit_width = QLineEdit(str(width)) self.line_edit_height = QLineEdit(str(height)) self.line_edit_area = QLineEdit(str(width * height)) self.line_edit_area.setReadOnly(True) grid_layout.addWidget(self.line_edit_name, row, 0) grid_layout.addWidget(self.line_edit_width, row, 1) grid_layout.addWidget(self.line_edit_height, row, 2) grid_layout.addWidget(self.line_edit_area, row, 3)Then in your main window, creating a row becomes a single line:pythonrow = ImageInfoRow(filename, width, height, self.grid_layout, self.current_row)self.image_rows.append(row)self.current_row += 1Storing each ImageInfoRow in a list (self.image_rows) lets you access or modify any row later — for example, to read back edited values or to remove a row.Alternative ApproachesQGridLayout is a great fit here, but there are other ways to achieve similar results:QVBoxLayout with QHBoxLayout rows — Make each row a QWidget with its own QHBoxLayout containing three line edits, then add each row widget to a vertical layout. This gives you self-contained row widgets that are easy to add and remove.QTableView with a model — If you're dealing with many images or want features like sorting and filtering, a QTableView backed by a QStandardItemModel (or a custom model) is a more scalable approach. See the PyQt6 QTableView tutorial for details.For a handful of images, the grid layout approach is simple and works well. For dozens or hundreds, consider switching to a model/view approach.Complete Working ExampleHere's the full example with real image size extraction and a computed area column:pythonimport sysimport osfrom PyQt6.QtGui import QImageReaderfrom PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QPushButton, QGridLayout, QLineEdit, QFileDialog, QLabel, QVBoxLayout,)class ImageInfoRow: """Holds a row of QLineEdits for one image.""" def __init__(self, filename, width, height, grid_layout, row): self.line_edit_name = QLineEdit(filename) self.line_edit_width = QLineEdit(str(width)) self.line_edit_height = QLineEdit(str(height)) self.line_edit_area = QLineEdit(str(width * height)) self.line_edit_area.setReadOnly(True) grid_layout.addWidget(self.line_edit_name, row, 0) grid_layout.addWidget(self.line_edit_width, row, 1) grid_layout.addWidget(self.line_edit_height, row, 2) grid_layout.addWidget(self.line_edit_area, row, 3)class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("Image Selector") self.setMinimumWidth(600) # Central widget and outer layout central_widget = QWidget() self.setCentralWidget(central_widget) outer_layout = QVBoxLayout() central_widget.setLayout(outer_layout) # Button to open file dialog self.button_open = QPushButton("Open Images") self.button_open.clicked.connect(self.load_images) outer_layout.addWidget(self.button_open) # Header labels header_layout = QGridLayout() header_layout.addWidget(QLabel("Filename"), 0, 0) header_layout.addWidget(QLabel("Width"), 0, 1) header_layout.addWidget(QLabel("Height"), 0, 2) header_layout.addWidget(QLabel("Area"), 0, 3) outer_layout.addLayout(header_layout) # Grid layout for dynamic rows self.grid_layout = QGridLayout() outer_layout.addLayout(self.grid_layout) # Push rows to the top outer_layout.addStretch() # Track rows self.current_row = 0 self.image_rows = [] def load_images(self): paths, _ = QFileDialog.getOpenFileNames( self, "Select Images", "", "Images (*.png *.jpg *.bmp);;All Files (*)", ) for path in paths: filename = os.path.basename(path) width, height = self.get_image_size(path) row = ImageInfoRow( filename, width, height, self.grid_layout, self.current_row, ) self.image_rows.append(row) self.current_row += 1 def get_image_size(self, path): reader = QImageReader(path) size = reader.size() if size.isValid(): return size.width(), size.height() return 0, 0app = QApplication(sys.argv)window = MainWindow()window.show()sys.exit(app.exec())Click "Open Images," select a few image files, and you'll see a row appear for each one with the filename, dimensions, and computed area. Click the button again to add more images — each new selection appends to the existing list.From here you could add scroll support (wrap the grid in a QScrollArea), add a "Remove" button per row, or switch to a QTableView if your needs grow. The core pattern — tracking a row counter and placing widgets into a QGridLayout — stays the same regardless of how you extend it. For an in-depth guide to building Python GUIs with PyQt6 see my book, Create GUI Applications with Python & Qt6.