Build a Desktop App with PySide6 #4: Qt Designer and UI Files — Draw the Screen, Then Load It
In “Build a Desktop App with PySide6 #2: Widgets and Layouts — Assembling the Screen” we assembled every layout in code. For a screen with a handful of widgets that approach is fine, but as the form grows it becomes hard to picture the screen just by reading code, and even a small layout tweak means editing several places. Qt provides a dedicated tool for this problem: you draw the screen with your mouse. That tool, Qt Designer, is the subject of this post.
It runs in seven parts.
- #1 What PySide6 is — desktop apps with Qt and Python
- #2 Widgets and layouts — assembling the screen
- #3 Signals and slots — the core of event handling
- #4 Qt Designer and UI files — draw the screen, then load it ← this post
- #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
In this post we redraw the todo app screen in Designer, compare the two ways of loading the resulting .ui file into Python code, and finish with a workflow that keeps signal connections in code.
Launching Qt Designer — it is already installed #
Qt Designer needs no separate installation. It comes as a command-line tool when you install PySide6.
pyside6-designerOn launch, a new-form dialog opens. We are building a standalone window, so choose Main Window. If your screen only needs a single dialog, one of the Dialog templates works too.
The interface — three panels are all you need #
The Designer window looks busy at first, but only three panels matter for actual work.
- Widget Box (left) — the list of available widgets. Drag them from here onto the form.
- Object Inspector (top right) — the tree of widgets placed on the form. Parent-child relationships and the layout structure are visible at a glance.
- Property Editor (bottom right) — edits the properties of the selected widget. The most important property here is
objectName.
There is a reason to emphasize objectName. When we later load the .ui file from code, each widget becomes a Python attribute under exactly that objectName. If you leave defaults like pushButton and lineEdit, you cannot tell which button is which in code, so make it a habit to assign a role-revealing name like addButton or todoInput the moment you drop a widget.
Building the form — the todo app screen #
We rebuild the screen we wrote in code in #2, this time in Designer. There are three steps.
- Drag a Line Edit from the Widget Box to the top of the form and set its objectName to
todoInput. - Place a Push Button next to it, set its objectName to
addButton, and set its text property to “Add”. - Place a List Widget below and set its objectName to
todoList.
At this point the widgets are floating at absolute coordinates on the form. Resize the window and they do not follow. We need to apply layouts.
Applying layouts and spacers #
Layouts in Designer are the same concepts as QHBoxLayout and QVBoxLayout in code, applied visually.
- Select
todoInputandaddButtontogether and click Lay Out Horizontally on the toolbar. The two widgets are grouped into a horizontal layout. - Click an empty spot on the form to select the window itself, then click Lay Out Vertically. The horizontal group and
todoListare stacked vertically, and the widgets now stretch when you resize the window.
When you need empty space between widgets, drag in a Horizontal Spacer or Vertical Spacer from the Widget Box. Spacers are invisible on screen but absorb leftover space, which lets you build arrangements like pushing a button to the far right.
When you are done, save the form as main_window.ui.
What a .ui file really is — XML #
Open the saved file in an editor and there is nothing exotic about it. It is an XML document holding the widget tree and properties as they are.
<widget class="QLineEdit" name="todoInput"/>
<widget class="QPushButton" name="addButton">
<property name="text">
<string>Add</string>
</property>
</widget>
<widget class="QListWidget" name="todoList"/>The key point is that this file contains only screen structure and no behavior. What happens when something is clicked is still the job of your Python code. Because screen and logic are separated at the file level, layout changes happen in Designer and behavior changes happen in code, each independently.
Loading, option 1 — generate code with pyside6-uic #
The first approach converts the .ui file into Python code that you import.
pyside6-uic main_window.ui -o ui_main_window.pyThe generated ui_main_window.py contains a Ui_MainWindow class whose setupUi method reproduces the widget tree you drew in Designer. The usage pattern looks like this.
import sys
from PySide6.QtWidgets import QApplication, QMainWindow
from ui_main_window import Ui_MainWindow
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
self.ui.addButton.clicked.connect(self.add_todo)
def add_todo(self):
text = self.ui.todoInput.text().strip()
if text:
self.ui.todoList.addItem(text)
self.ui.todoInput.clear()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()The objectName you assigned in Designer becomes an attribute, as in self.ui.addButton. The generated file is import-only, so never edit it directly. When you change the form in Designer, rerun the conversion command to refresh it.
Loading, option 2 — runtime loading with QUiLoader #
The second approach reads the .ui file directly at run time, with no conversion step.
import sys
from PySide6.QtWidgets import QApplication
from PySide6.QtUiTools import QUiLoader
app = QApplication(sys.argv)
loader = QUiLoader()
window = loader.load("main_window.ui")
window.addButton.clicked.connect(
lambda: window.todoList.addItem(window.todoInput.text())
)
window.show()
app.exec()Edit the file, run again, and the change shows up immediately, so it takes less effort. In exchange, code alone cannot tell which widgets window carries, so you lose editor autocompletion and type checking, and because load returns a finished widget, this approach also fits poorly with the pattern of extending your window class through inheritance.
The two approaches compared #
| Aspect | pyside6-uic (code generation) | QUiLoader (runtime loading) |
|---|---|---|
| Conversion step | Required (rerun on every .ui change) | None |
| Autocompletion and type checks | Work against the generated code | Not supported |
| Extending via class inheritance | Natural with the setupUi pattern | Awkward |
| Distribution | Ship only the generated .py | Ship the .ui file alongside |
| Best fit | Larger apps, team projects | Quick experiments, small tools |
The remaining parts of this series use the setupUi pattern. An environment with working autocompletion is also better for learning.
.ui files, the common approach is a one-line shell script or a Makefile target that converts them all.Keep signal connections in code #
Designer also has an editing mode that wires signals and slots on screen. Press F4 and the view switches to one where you drag between widgets to connect signals. This series does not use it. Once connection relationships hide inside the .ui file, you can no longer find which button triggers which action through a code search. Keeping the boundary — Designer owns the screen structure, code owns behavior and connections — pays off more as the app grows.
clicked and textChanged to methods in code with connect stays the same regardless of how the screen was built.Wrapping up #
Three key points from this post.
- Qt Designer is the screen-editing tool included with PySide6, and the resulting
.uifile is XML holding only screen structure. The objectName becomes the name you use to reach each widget from code. - There are two loading approaches. The setupUi pattern via
pyside6-uicwins on autocompletion and extensibility, whileQUiLoaderreads the file directly at the cost of type information. - Signal connections belong in code, not in Designer. A clear boundary between screen and behavior keeps the app manageable as it grows.
So far the todo list has pushed plain strings into QListWidget. To carry a due date and a completion flag per item, we need a structure that separates data from display. The next post, “Build a Desktop App with PySide6 #5: Model/View — Connecting Data to Lists and Tables”, covers Qt’s model/view architecture.