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?
When you're building a GUI that responds to user actions — like selecting files — you often need to create widgets on the fly. Adding a single widget per action is straightforward enough, but what happens 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.
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.
layout.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 Window
Let'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.
import sys
import os
from 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 += 1
app = 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 Works
The add_image_row method is where the dynamic creation happens. Each time it's called, it:
- Creates three
QLineEditwidgets — 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_rowso 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 Dimensions
The get_image_size method above is a placeholder. If you want to extract actual image dimensions, you can use QImageReader from PyQt6 itself — no extra libraries needed:
from PyQt6.QtGui import QImageReader
def get_image_size(self, path):
reader = QImageReader(path)
size = reader.size()
if size.isValid():
return size.width(), size.height()
return 0, 0
This reads only the image header, so it's fast even for large files.
Adding a Computed Column
Since 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.
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))
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 += 1
You'd also want to add a matching "Area" header label in the __init__ method.
Encapsulating a Row as a Class
As 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.
class 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:
row = ImageInfoRow(filename, width, height, self.grid_layout, self.current_row)
self.image_rows.append(row)
self.current_row += 1
Storing 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 Approaches
QGridLayout is a great fit here, but there are other ways to achieve similar results:
- QVBoxLayout with QHBoxLayout rows — Make each row a
QWidgetwith its ownQHBoxLayoutcontaining 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
QTableViewbacked by aQStandardItemModel(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 Example
Here's the full example with real image size extraction and a computed area column:
import sys
import os
from PyQt6.QtGui import QImageReader
from 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, 0
app = 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.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PyQt6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!