Here is a tiny dialog example I wrote for
this thread in March 2024, slightly edited.
First, you create an
.ui file, say
example.ui, attached as
example.ui.txt. You can open it in Qt Creator, and modify it as you like.
For ease of use, name your interactive widgets so that in the Python code can automatically implement handlers for the corresponding events (signals in Qt parlance) by naming the handler
on_widgetName_signal.
With a fixed-size display, make it a fullscreen window of the correct size, and you don't need to worry about the intricacies of scaling. This example does not set the scaling properties, so if you resize the window, the widgets will stay in its upper left corner. For proper scaling (so widgets will expand to the available space), you need to use a geometry manager, some subclass of
QLayout; I typically use
QFormLayout for stuff like this dialog, and
QGridLayout for everything else.
The corresponding Python code for the example dialog is
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# SPDX-License-Identifier: CC0-1.0
#
# This is in public domain. Written by Nominal Animal, 2024.
#
import sys
try:
# Prefer PyQt5,
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.uic import loadUi
except ImportError as err:
# but fall back on PySide2.
from PySide2 import QtCore, QtGui, QtWidgets, QtUiTools
def loadUi(uifile):
return QtUiTools.QUiLoader().load(uifile)
class ExampleDialog(object):
def __init__(self):
super().__init__()
# Load UI,
self.ui = loadUi("example.ui")
# and connect on_<widget>_<signal> to corresponding members as handlers:
for slot in dir(self):
if slot.startswith("on_") and slot.find("_", 3) > 3:
method = getattr(self, slot)
widgetName, signalName = slot[3:].rsplit("_", 1)
source = self.ui.findChild(QtCore.QObject, widgetName)
signal = getattr(source, signalName, None)
if signal:
signal.connect(lambda *args, **kwargs: method(source, *args, **kwargs))
# Display this ui.
self.show = self.ui.show
# Example handler
def on_radioButton_3_USB_clicked(self, widget, clicked):
print("clicked on %s" % widget.objectName())
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
# Set these. Among other things, QtCore.QSettings(scope) uses these.
app.setOrganizationName('The Name of Your Organization')
app.setApplicationName('The Name of This Application')
win = ExampleDialog()
win.show()
status = app.exec_()
if win.ui.result() == QtWidgets.QDialog.Accepted:
if win.ui.findChild(QtWidgets.QRadioButton, "radioButton_1_LAN").isChecked():
print("LAN: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP").text())
elif win.ui.findChild(QtWidgets.QRadioButton, "radioButton_2_COM").isChecked():
print("COM: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP_2").text())
elif win.ui.findChild(QtWidgets.QRadioButton, "radioButton_3_USB").isChecked():
print("USB: %s" % win.ui.findChild(QtWidgets.QLineEdit, "lineEdit_IP_3").text())
else:
print("None selected")
else:
print("Canceled")
del win
del app
sys.exit(status)
The first
try ..
except clause uses PyQt5 bindings if available, and falls back to PySide2 otherwise, in a way that makes the two practically identical to our Python code. If you wanted to control the bindings using environment variable, that part expands by about 20 lines, where an environment variable, typically
QT_API. Instead of this, you can use
QtPy instead, but I dislike the added abstraction and maintenance load of yet another dependency.
Like all widget toolkits, but unlike
SDL, your program releases all control to the widget toolkit, and then the toolkit will trigger events and call your code. You can implement an idle handler, which will be called whenever all pending events have been processed, and you can implement timeouts that cause an event to be generated in the future, for animations and such.
In Qt, for graphical UI stuff, you first instantiate a
QtWidgets.QApplication class. (Code that does not use any QtWidgets can use
QtGui.QGuiApplication, and code that runs in batch mode without any graphics can use
QtCore.QCoreApplication.)
This object then represents the application itself, and owns all windows you create. There are some nifty tricks there for multidocument handling, but most importantly, you use the
app.postEvent() to send events to your widgets from other threads. You can also use
app.setStyle() to set the application look and feel (see
QtWidgets.QStyle).
(If you use threads, do not subclass subclass
QtCore.QThread, but use the plain QtCore.QThread. In its
.run() method, instantiate your own subclass of
QtCore.QObject for the communication with other threads. This way, Qt will handle the signal passing between threads correctly, with each thread owning the Qt widget or object running the event code.)
Qt provides a way to get and set settings objects in both system (machine) and user scopes, via
systemSettings = QtCore.QSettings(QtCore.QSettings.SystemScope) userSettings = QtCore.QSettings(QtCore.QSettings.UserScope)To set a value, use the
.setValue(name,value) method;
name is always a string, but
value can be of any type. Conversely,
.value(name) or
.value(name,default) method to access them. These are automatically loaded
and saved for you. You can also use the
.sync() method to save the contents, but I don't bother. Note that your constructors (
__init__() methods) and other functions can access the settings this same way. To find out if there have been any errors, check the
.status() method. If everything is okay, it should be equal to
QtCore.QSettings.NoError.
Next, you instantiate your windows. They do not need to become instantly visible, as they by default stay hidden until you call
.show() on them or their parents. In Python, the
__init__() method is the constructor, called whenever the class is instantiated. The
super().__init__() is the Python 3 way of calling the
__init__() method of the parent type first, so you almost always have it first in your subclass constructors. If there are parameters that can be passed to the constructor, you can use form
class myClassName(extendsClass): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)where
args is the list of positional parameters (and
*args expands that list in place for function parameters, much like varargs in C), and
kwargs is a dictionary of named parameters (and
**kwargs expands that for function parameters).
Because I load the entire graphical user interface from an
.ui file, my main window class is not actually an Qt object, but a plain Python one. Its
.ui member will have the widget hierarchy. I prefer this approach, because this way I will have no collisions with the Qt instance or class methods and properties.
When the
.exec_() method of the application object is called, control passes to Qt. This call returns only when the application is closing. The return value is usually 0, but if one uses
application.exit(value) or similar methods, that value is returned.
In this dialog example, the main program then calls the dialog window
.result() function, to see whether the selection was accepted or canceled. (Closing the window is the same as canceling.)
Finally, I make it easier for the Python garbage controller by deleting the main window object first, then the application object. If you passed a reference to the application object to your window hierarchy, I recommend you set it to
None before deleting the main window object. This ensures that the various destructors get called, and in the correct order.
sys.exit(value) passes the status code, zero being No Errors (but all others application-specific and between 1 and 127), to the operating system.
If you want to create custom widgets, like say a speedometer like gauge, and you want to use it in the
.ui file, things become a bit messier since you'd need to provide the implementation to Qt Designer also.
I don't usually do that. Instead, I use the base class in Qt Designer, basically setting it in the layout, and then create a copy of the
.ui file with the class name replaced, using
sed -e 's| class="QClass"| class="myclass"|g' original.ui > runtime.uiI then also use ONLY
PySide2 bindings, since that makes the extension easier, via
def loadUi(uifile, custom=None, parent=None):
from PySide2 import QtUiTools
loader = QtUiTools.QUiLoader()
if custom is not None:
for cls in custom:
loader.registerCustomWidget(cls)
ui = loader.load(uifile, parent)
del loader
del QtUiTools
return ui
where you supply a list of your custom classes as the second parameter to
loadUi().
Note that the Qt
.ui file syntax is based on XML, and is more or less human-readable.
So, I wouldn't say this is a lot of work. A bit complicated, maybe, because of the many details, but I've listed the key points above.
In Linux, you can install both
PySide2 and
PyQt5 bindings at the same time. I do find
PySide2 a bit more complete, considering the
.ui file loading mechanisms, so I guess I should prefer that.
For massive data processing, I recommend using Python built-in
multiprocessing module, as the current Python interpreter can only execute code in one thread at a time per process. Using parallel processes allows your Python code to use more than one CPU core concurrently. Even better solution is to write it in C or some other compiled code into a dynamic library; just make the Python-facing API thread-safe, and use threads (
pthreads in C).
One particular benefit of using Qt is that its graphics primitives are pretty well optimized to work on things like OpenGL ES; even moreso if you use
QtWidgets.QOpenGLWidget. See
QtGui.QOpenGLFunctions for what is available (via
.context().functions()).
For example, if you wanted an oscilloscope-type display, then having a list of
QtCore.QPoint values defining the samples, each list will be rendered by hardware in a single call. Similarly for blitting textures. Remember that PNG images have transparency, and OpenGL and OpenGL ES supports textures with transparency. A speedometer dial you could render by having three images: the background image, an image of the dial arrow shadow in center position (horizontal or vertical), and an image of the dial arrow in center position. When rendering, the background would be copied, then the rotated shadow (with alpha channel) drawn, and finally the rotated arrow (with alpha channel) drawn. With a bit of preparation, the three images would be stored in an OpenGL buffer, so the entire thing would basically be rendered in hardware. Even though you're using Python.