Build a Desktop App with PySide6 #2: Widgets and Layouts — Assembling the Screen
In the last post we built an app where a single button filled the window. A real app’s screen needs an input field, a list, and several buttons, each in its proper place, and the arrangement has to adjust naturally when the window is resized. This post covers the two ingredients responsible for that arrangement: widgets and layouts.
It runs in seven parts.
- #1 What PySide6 is — desktop apps with Qt and Python
- #2 Widgets and layouts — assembling the screen ← this post
- #3 Signals and slots — the core of event handling
- #4 Qt Designer and UI files — draw the screen, then load it
- #5 Model/View — connecting data to lists and tables
- #6 Threads and timers — keeping the UI responsive
- #7 Packaging and distribution — building executables with PyInstaller
By the end of this post, the todo app’s screen skeleton is complete. It does nothing yet, but the structure comes alive the moment we connect signals in the next post.
A catalog of everyday widgets #
Widgets are the parts a screen is built from. Qt ships dozens of them, but an app at the level of a work tool is mostly assembled from these seven.
| Widget | Purpose |
|---|---|
QLabel | Displays text or an image. Not editable |
QLineEdit | Single-line text input |
QPushButton | A clickable button |
QCheckBox | An on/off checkbox |
QComboBox | A dropdown selection list |
QTextEdit | Multi-line text input and display |
QSpinBox | Numeric input with up/down arrows |
Add QListWidget, which shows a list of items, and you have every ingredient this post needs. You can look up each widget’s detailed options in the official docs when you need them; what matters now is knowing which parts exist.
The trap of coordinate placement, and layout managers #
The most primitive way to place widgets is by coordinates — pinning pixel positions directly, as in button.move(50, 100). This approach collapses quickly in practice. Widgets do not follow when the window is resized, and on systems with different font sizes they overlap or get clipped.
So Qt hands placement to layout managers. You simply add widgets to a layout, and every time the window size changes, the layout recalculates each widget’s position and size. There are four basic layouts.
from PySide6.QtWidgets import (
QVBoxLayout, # stack top to bottom
QHBoxLayout, # line up left to right
QGridLayout, # place in a grid of rows and columns
QFormLayout, # align label-input pairs in two columns
)QVBoxLayout and QHBoxLayout are the staples. A calculator screen that needs a grid calls for QGridLayout, and a settings-style form with repeating “name: input” pairs fits QFormLayout.
# Grid placement — addWidget(widget, row, column)
grid = QGridLayout()
grid.addWidget(QPushButton("7"), 0, 0)
grid.addWidget(QPushButton("8"), 0, 1)
grid.addWidget(QPushButton("9"), 0, 2)
# Form placement — addRow(label, input widget)
form = QFormLayout()
form.addRow("Name", QLineEdit())
form.addRow("Port", QSpinBox())Nesting — layouts inside layouts #
A real screen never ends with a single layout. Sketch the todo app’s screen and it divides like this.
┌─────────────────────────────┐
│ [ input field ] [Add] │ ← horizontal row (QHBoxLayout)
├─────────────────────────────┤
│ │
│ todo list (QListWidget) │ ← center, takes all remaining space
│ │
├─────────────────────────────┤
│ [Delete Selected] [Clear] │ ← horizontal row (QHBoxLayout)
└─────────────────────────────┘
↑ stack the whole thing vertically (QVBoxLayout)Two horizontal rows and one list, stacked in a vertical layout. Translated into code, it looks like this.
import sys
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget,
QVBoxLayout, QHBoxLayout,
QLineEdit, QPushButton, QListWidget,
)
class TodoWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Todo Manager")
self.resize(400, 500)
# Top input row — input field + add button
self.todo_input = QLineEdit()
self.todo_input.setPlaceholderText("Enter a todo")
self.add_button = QPushButton("Add")
input_row = QHBoxLayout()
input_row.addWidget(self.todo_input)
input_row.addWidget(self.add_button)
# Center list
self.todo_list = QListWidget()
# Bottom button row — right aligned
self.delete_button = QPushButton("Delete Selected")
self.clear_button = QPushButton("Clear All")
button_row = QHBoxLayout()
button_row.addStretch()
button_row.addWidget(self.delete_button)
button_row.addWidget(self.clear_button)
# Assemble — stack vertically
layout = QVBoxLayout()
layout.addLayout(input_row)
layout.addWidget(self.todo_list)
layout.addLayout(button_row)
container = QWidget()
container.setLayout(layout)
self.setCentralWidget(container)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TodoWindow()
window.show()
sys.exit(app.exec())Run it and a window matching the sketch appears, and when you stretch the window, the list area grows with it. There are three assembly rules.
- addWidget for widgets, addLayout for layouts — when nesting a layout inside another layout, use addLayout.
- A layout must ride on a widget to appear on screen. Only widgets can go in the center of a
QMainWindow, so you load the layout onto an emptyQWidgetwith setLayout and pass that widget to setCentralWidget. - Keep widgets as attributes on self. Only widgets you hold on to, like
self.todo_input, can have signals connected or values read in the next post.
Margins, alignment, and leftover space #
Three tools for polishing the arrangement.
- stretch — the
addStretch()in the bottom button row is a spring that occupies empty space. Because it comes before the buttons, they are pushed to the right. - Margins and spacing —
layout.setContentsMargins(12, 12, 12, 12)sets the layout’s outer margins, andlayout.setSpacing(8)sets the gap between widgets. - Size ratios — give a stretch value, as in
layout.addWidget(self.todo_list, stretch=1), and that widget takes priority in claiming leftover space when the window grows.
Wrap-up #
This post comes down to three points.
- Screens are assembled from widgets as parts and layouts as placement rules; coordinate placement is not used.
- Real screens are mostly expressed by nesting QHBoxLayout and QVBoxLayout, and a layout rides on an empty QWidget that goes onto the window via setCentralWidget.
- Use addStretch, margins, and stretch ratios to polish the arrangement.
Right now the todo app does nothing when you click its buttons. In the next post, “Build a Desktop App with PySide6 #3: Signals and Slots — the Core of Event Handling,” we learn Qt’s core event-handling model, signals and slots, and wire real behavior into this screen.