If you're building a PyQt5 desktop application, there's a good chance you're using Qt Designer to lay out your interface visually. And if you want to package that application into a standalone executable, fbs (fman build system) is a popular choice that simplifies the whole process.
The question is: how do you connect these two tools? Specifically, can you load .ui files directly in your fbs application — without converting them to Python code first using pyuic5?
Yes, you can. In this tutorial, we'll walk through exactly how to do it.
The usual workflow (and why it's annoying)
When you design a window in Qt Designer, the result is a .ui file — an XML description of your layout. To use it in Python, many developers run pyuic5 to convert it into a .py file:
pyuic5 mainwindow.ui -o mainwindow_ui.py
This works, but it adds friction. Every time you tweak the layout in Qt Designer — adjusting a label, adding a button, rearranging a group box — you need to re-run that conversion command before your changes appear. If you forget, you'll be scratching your head wondering why nothing changed.
A smoother approach is to load the .ui file directly at runtime using uic.loadUiType(). This way, you edit in Qt Designer, save, and immediately run your application to see the result. No extra conversion step needed.
Loading .ui files with uic.loadUiType()
The PyQt5.uic module provides a function called loadUiType() that reads a .ui file and returns two things: a generated form class and the Qt base class it's built on. You can then use these with multiple inheritance to create your main window class.
Here's a minimal standalone example (no fbs yet):
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
# Load the .ui file. This returns a tuple: (form_class, base_class)
Ui_MainWindow, QtBaseClass = uic.loadUiType("mainwindow.ui")
class MainWindow(QMainWindow, Ui_MainWindow):
def __init__(self):
super().__init__()
self.setupUi(self)
# Now you can access any widget you named in Qt Designer.
# For example, if you have a QPushButton called "myButton":
# self.myButton.clicked.connect(self.on_button_clicked)
def on_button_clicked(self):
print("Button clicked!")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
After calling self.setupUi(self), every widget you named in Qt Designer becomes an attribute on self. If you named a QLineEdit widget searchInput in Designer, you can access it as self.searchInput in your Python code. No need for findChild() calls or any special loop — setupUi() handles all of that for you.
Setting up an fbs project
If you haven't used fbs before, start by installing it and creating a new project:
pip install fbs PyQt5
fbs startproject
This creates a project structure like:
src/
└── main/
├── icons/
│ └── base/
│ └── ...
├── python/
│ └── main.py
└── resources/
└── base/
└── ...
The main.py file inside src/main/python/ is your application entry point. The src/main/resources/base/ directory is where fbs looks for resource files that get bundled with your app.
Where to put the .ui file
For fbs to include your .ui file when packaging, place it in the resources directory:
src/
└── main/
├── python/
│ └── main.py
└── resources/
└── base/
└── mainwindow.ui
The ApplicationContext class from fbs provides a get_resource() method that resolves the correct path to files in this directory — both during development (fbs run) and after packaging (fbs freeze). This is how you make sure the .ui file is always found, regardless of where or how the app is launched.
Putting it all together: .ui files with fbs
Here's a complete, working example that loads a .ui file inside an fbs application:
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from fbs_runtime.application_context.PyQt5 import (
ApplicationContext,
cached_property,
)
class AppContext(ApplicationContext):
def run(self):
self.main_window.show()
return self.app.exec_()
@cached_property
def main_window(self):
return MainWindow(self)
class MainWindow(QMainWindow):
def __init__(self, ctx):
super().__init__()
self.ctx = ctx
# Use ctx.get_resource() to find the .ui file in the
# resources folder. This works both during development
# and after freezing with fbs.
ui_path = self.ctx.get_resource("mainwindow.ui")
Ui_MainWindow, QtBaseClass = uic.loadUiType(ui_path)
# We need to apply the UI to self. Create a temporary
# mixin and call setupUi.
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# Connect signals and slots, set initial values, etc.
# Access widgets via self.ui.<widget_name>
# For example:
# self.ui.myButton.clicked.connect(self.on_button_clicked)
def on_button_clicked(self):
print("Button was clicked!")
if __name__ == "__main__":
appctxt = AppContext()
exit_code = appctxt.run()
sys.exit(exit_code)
There's a subtle difference from the standalone example earlier. Because uic.loadUiType() is called inside __init__ (after resolving the resource path), we can't use the returned classes in the class statement's inheritance list. Instead, we create an instance of the UI class (self.ui = Ui_MainWindow()) and call self.ui.setupUi(self). Widgets are then accessible through self.ui — for example, self.ui.myButton.
If you prefer accessing widgets directly on self (without the self.ui prefix), you can restructure things slightly by moving the loadUiType call to module level and using multiple inheritance:
import sys
import os
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from fbs_runtime.application_context.PyQt5 import (
ApplicationContext,
cached_property,
)
class AppContext(ApplicationContext):
def run(self):
self.main_window.show()
return self.app.exec_()
@cached_property
def main_window(self):
return MainWindow(self)
@cached_property
def ui_base_classes(self):
"""Load and cache the UI classes from the .ui file."""
ui_path = self.get_resource("mainwindow.ui")
return uic.loadUiType(ui_path)
class MainWindow(QMainWindow):
def __init__(self, ctx):
super().__init__()
self.ctx = ctx
# Get the generated UI class and call setupUi
Ui_MainWindow, _ = ctx.ui_base_classes
self.ui_class = Ui_MainWindow()
self.ui_class.setupUi(self)
# Widgets are now accessible directly on self:
# self.myButton.clicked.connect(self.on_button_clicked)
if __name__ == "__main__":
appctxt = AppContext()
exit_code = appctxt.run()
sys.exit(exit_code)
When setupUi(self) is called with self (the QMainWindow) as the argument, it sets up all the widgets as attributes on that window. So self.myButton, self.searchInput, and any other named widget from your .ui file are all available directly.
Creating a simple .ui file to test with
If you don't already have a .ui file, open Qt Designer and create a new Main Window. Add a few widgets — perhaps a QLabel, a QLineEdit, and a QPushButton. Give them descriptive object names in the property editor (for example, greetingLabel, nameInput, and greetButton).
Save the file as mainwindow.ui in your src/main/resources/base/ folder.
A complete working example
Here's everything together, including signal connections, using a simple greeting app as an example. This assumes you have a mainwindow.ui file with a QLabel named greetingLabel, a QLineEdit named nameInput, and a QPushButton named greetButton.
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow
from fbs_runtime.application_context.PyQt5 import (
ApplicationContext,
cached_property,
)
class AppContext(ApplicationContext):
def run(self):
version = self.build_settings["version"]
self.main_window.setWindowTitle(
"Greeting App v" + version
)
self.main_window.show()
return self.app.exec_()
@cached_property
def main_window(self):
return MainWindow(self)
class MainWindow(QMainWindow):
def __init__(self, ctx):
super().__init__()
self.ctx = ctx
# Load the .ui file from fbs resources
ui_path = self.ctx.get_resource("mainwindow.ui")
Ui_MainWindow, _ = uic.loadUiType(ui_path)
# Apply the UI to this window
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
# Set initial state
self.ui.greetingLabel.setText("Enter your name below:")
# Connect signals
self.ui.greetButton.clicked.connect(self.greet)
def greet(self):
name = self.ui.nameInput.text().strip()
if name:
self.ui.greetingLabel.setText(f"Hello, {name}!")
else:
self.ui.greetingLabel.setText("Please enter your name first.")
if __name__ == "__main__":
appctxt = AppContext()
exit_code = appctxt.run()
sys.exit(exit_code)
Run this with:
fbs run
You should see your Designer-created interface appear, fully functional. Edit the .ui file in Qt Designer, save, and run fbs run again — your changes will appear immediately, no conversion step needed.
Packaging your app
When you're ready to create a distributable executable:
fbs freeze
Because the .ui file lives in src/main/resources/base/, fbs automatically includes it in the packaged application. The get_resource() call resolves to the correct bundled path at runtime, so everything works the same way in the packaged version as it does during development.
You can also build an installer:
fbs installer
This workflow — designing visually in Qt Designer and loading the .ui file directly at runtime — gives you the best of both worlds: rapid visual prototyping with the full power of Python for application logic. Combined with fbs for packaging, you have a complete pipeline from design to distributable application.
Create GUI Applications with Python & Qt6 by Martin Fitzpatrick
(PySide6 Edition) The hands-on guide to making apps with Python — Over 15,000 copies sold!