4.2.5. Implementation of the User Interface
In this section, the developer will use the UI designed in the previous section, and implement
basic functionality for them. Every function needed will be implemented in the
PythonApplication/src/paegui/applicationwindow.py
file.
All the entry points for missing functionality are already defined through the Actions in the UI file, we just need to connect the missing pieces. We will begin explaining the imports.
10import time
11
12from taurus.external.qt.QtCore import QObject
13from taurus.external.qt.QtCore import Slot
14from taurus.external.qt.QtCore import Signal
15from taurus.external.qt.QtCore import QMetaObject
16from taurus.external.qt.QtWidgets import QApplication
17from taurus.external.qt.QtWidgets import QMainWindow
18from taurus.external.qt.QtWidgets import QWhatsThis
19
20from taurus.core.util.log import Logger, TraceIt
21from elt.cut.task.task import Task
22from CutColor import QePalettesModel
23
24from python_application_example.mainwindow import Ui_MainWindow # WAF will automatically generated this
25from python_application_example.applicationconfiguration import ApplicationConfiguration
Most classes in the Qt library inherit from QObject. This is true also for widgets. QObject is the base class that provides Signal/Slot connection capabilities to the Qt Toolkit. We will use them plenty through the design and implementation of GUIs. If you are not familiar with them, we suggest you to read:
The QMainWindow import is present, as this class is the implementation of a QMainWindow. At the beginning of the design section, a Main Window was selected. Therefore, the implementation class must match the top level UI element.
The taurus.Logger
class is from the taurus
module and it allows to enhance a class
with logging capabilities.
Finally, the Ui_MainWindow
import is the python code generated by the pyside6-uic
compiler. The compiler is automatically invoked by WAF, so no extra instructions are to be given.
It is important that the UI file is inside of the Python Package name paegui
, as from this
directory is where WAF will look for any UI file, and automatically compile them.
Important
To determine both the name of the Python Module and the Class Name, the name of the first element of the UI file is used:
The Python module name will be lowercased name of the top level element of the UI file
(in our case mainwindow.py
).
The resulting Class Name will be Ui_<name of top level element>
, in this case
Ui_MainWindow
.
You can see the first element name in the the Designer; Object Inspector Tab; the first element. You can also change it in the same place.
The resulting import show how the Python Module and the Class Name are used:
from python_application_example.mainwindow import Ui_MainWindow
We will now proceed to examine the Class definition:
28class ApplicationWindow(QMainWindow, Logger):
29 '''
30 Implementation of the mainwindow.ui file.
31
32 Since the UI file indicates that its root is a QMainWindow, then
33 this class should also inherit from it.
34 We should also call explicitly its parent constructors.
35
36 The implementation for this class also includes slots for
37 actions, and management of the closeEvent.
38 '''
39
40 ###########################################################################
41 # Initialization and QMainWindow Methods #
42 ###########################################################################
43
44 def __init__(self, configuration: ApplicationConfiguration):
45 QMainWindow.__init__(self)
46 Logger.__init__(self, name=QApplication.instance().applicationName())
47 # Construction of UI
48 self.ui = Ui_MainWindow()
49 self.ui.setupUi(self)
50 self._init_gui()
51 self._configuration = configuration
Line 28 declares a new class called ApplicationWindow, which inherits from QMainWindow. QMainWindow already inherits from QObject, so we can use signal and slots from this class definition.
The __init__() manually calls both its parent classes __init__() method. This is very important, as Python does not make implicit calls to these.
In line 46, the taurus.Logger
parent class is initialized, but we use as name keyworded
argument, QApplication.instance().applicationName()
. QApplication.instance()
is a static
reference to the QApplication or taurus.qt.qtgui.application.TaurusApplication
of this
process. Qt forbids running two or more QApplications in the same process, and in Python, they
offer a quick manner to obtain a reference to the QApplication object. Developers may use the
QApplication.instance()
reference to access the running QApplication.
In line 48 is where we create an instance of the UI definition we have in the mainwindow.ui file.
It is assigned to self.ui
. Then, in the next line, we invoke its setupUi() method, passing as
argument the self
reference to the QMainWindow object being initialized. The setupUi() method
in the Ui_MainWindow() class will create every element as defined in the Designer for us. It is
actually Python code that was translated from the XML. When the method finishes, every widget,
action and layout will be available for the developer at self.ui
.
Important
self.ui
is very important for UI development using Qt. We suggest the reader to familiarize
themselves with this manner of accessing the UI declared in the UI file. Developers should never
copy and archive to repositories the result of pyside6-uic
compiler. Instead, they should
archive the UI file, and WAF automatically will produce an updated version of the UI.
We also encourage to use an editor capable of Python introspection/code completion. This will
make the navigation of self.ui
much more easier.
The next section of the code has many entries like this one:
104 @Slot()
105 def on_actionNew_triggered(self):
106 '''
107 Slot that auto-connects to the actionExit.
108 actionNew is not declared in the code, but in the mainwindow.ui.
109 See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
110 '''
111 self.info('actionNew triggered')
Each of these method defined the implementation of an action. The developer may notice a pattern in the name of the method: It is a composite of an existing Action name, and a signal it has. Qt will automatically recognize this pattern, and connect that object’s signal, to this slot.
The only entry that is different is the closeEvent()
method.
65 def closeEvent(self, event):
66 '''
67 Not to be confused with actionExit, this method is special. Instead
68 of using signals to know when an application is closed, we can
69 override the closeEvent method from the QApplication class, and
70 determine here what will happen when the window is closed.
71 In this case, we are just forwarding the event to its parent class.
72 This is useful when disconnection from server or prevention of
73 current work needs to be implemented.
74
75 :param event: Event received from Qt event pump
76 :type event: QtCore.QEvent
77
78 '''
79 self.info('Application shutting down...')
80 QMainWindow.closeEvent(self, event)
Let’s remember that Qt is an event based library. It detect and translate many input performed by the user and internal system outcomes into Events. Example of these are mouse movement, click, keystrokes, closing minimizing, maximizing the application, among many more. When a user click on the close button of an application, this is translated into an event.
Each UI element in Qt has one main handleEvent()
method, and many specialized Event methods: , like closeEvent()
:
handleEvent()
is the main entry point for any event. TheQApplication.handleEvent()
method will process this event, clasify it accordingly, and then call a specialized method.closeEvent()
is the specialized method when a window or dialog is requested to close. In the case of our particular implementation, closing the application main window should exit the program. Qt does this by default, so we just call parent implementation usingQMainWindow.closeEvent()
.
At this moment, we will not concern ourselves with other kinds of methods.
At this point, the implementation is ready, and will log every action that is triggered.
4.2.5.1. Long Running Jobs
Qt runs in one thread. The main thread of the process is used by the event pump to detect and conduct the necessary operations, and drawing the GUI. For example, resizing the application will create many resizeEvent, which will request redrawing the UI to the appropriate dimensions.
But for this to happen, the main thread needs to be free. The developer may implement short and quick methods, and connect them using signal/slot mechanism. Qt will insert in between signal/slot execution many of its own events, keeping the UI smooth. But long running jobs should not be conducted in the main thread.
For sake of simplicity, a long running job can be emulated with a time.sleep() call.
Warning
An example is given below. This will cause the GUI to freeze for 5 seconds, as no other event gets executed in between, including input detection, and GUI drawing instructions.
115 @Slot()
116 def on_actionSave_triggered(self):
117 '''
118 Slot that auto-connects to the actionSave.
119 actionSave is not declared in the code, but in the mainwindow.ui.
120 See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
121 '''
122 self.info('actionSave triggered')
123 time.sleep(5)
124 self.info('actionSave finished')
In this example application, we include a solution for this. In line 115, the actionSave()
slot is queuing a long running job using the elt.cut.task
module. This class manages a threadpool used for long running operations, pushing tasks into it. It also allows you connect a slot to receive the results of the job.
122 @TraceIt()
123 @Slot()
124 def on_actionSave_triggered(self):
125 '''
126 Slot that auto-connects to the actionSave.
127 actionSave is not declared in the code, but in the mainwindow.ui.
128 See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
129 '''
130 self.info('actionSave triggered')
131 self.info('actionSave launching new Task')
132 task = Task(self.save_job)
133 task.signals.result.connect(self.save_job_slot)
134 task.start()
135 self.info('actionSave finished')
Both methods save_job()
and save_job_slot()
are defined in the code shown below. save_job()
is a method, and it is doing the long running job (in this case, sleeping for 5 seconds). When finished, the Taurus Manager will automatically invoke the callback save_job_slot()
, passing as argument the return value of the save_job()
method.
198 def save_job(self):
199 self.info('save_job sleeping for 5')
200 time.sleep(5)
201 return 'Slept for 5 seconds'
202
203 def save_job_slot(self, arg):
204 self.info('save_job_slot reporting %s' % (arg, ) )
There are also signals for progress, errors and finished conditions, to create more complete behaviors.
Now the reader may run the PaeGui using this command:
$ cutPaeGui
Logs from Taurus will appear in the console and the application will start. Please exercise the different Double Spin Boxes, and see how connections works with the Dart Boart. Also, if you press several times the save button (or its menu entry), you will see how each job request will get executed in its own separate thread:
$ cutPaeGui
MainThread INFO 2024-10-29 13:50:31,914 TaurusRootLogger: Using PySide6 (v6.7.2 with Qt 6.7.2 and Python 3.12.6)
MainThread INFO 2024-10-29 13:50:32,085 taurus.qt.qtgui.icon.icon: Setting breeze icon theme (from /usr/share/icons)
MainThread INFO 2024-10-29 13:50:32,096 TaurusRootLogger: Plugin "taurus_pyqtgraph" lazy-loaded as "taurus.qt.qtgui.tpg"
MainThread INFO 2024-10-29 13:51:00,771 Python Application Example GUI: Application shutting down...
MainThread INFO 2024-10-29 13:51:00,785 TaurusRootLogger:
Taurus logs have the following syntaxt: Thread, Level, Timestamp, Logger Name, message.
If you need lower level logs, please start the application with the following command:
$ cutPaeGui --taurus-log-level Debug
Help is always present when using the taurus.qt.qtgui.application.TaurusApplication
class:
$ cutPaeGui --help
MainThread INFO 2024-10-29 14:17:53,358 TaurusRootLogger: Using PySide6 (v6.7.2 with Qt 6.7.2 and Python 3.12.6)
MainThread INFO 2024-10-29 14:17:53,502 taurus.qt.qtgui.icon.icon: Setting breeze icon theme (from /usr/share/icons)
MainThread INFO 2024-10-29 14:17:53,512 TaurusRootLogger: Plugin "taurus_pyqtgraph" lazy-loaded as "taurus.qt.qtgui.tpg"
Usage: cutPaeGui [OPTIONS]
PythonApplicationExample GUI (or paegui), is part of a tutorial intended to
introduce how to developed a simple GUI application for the E-ELT software
Options:
--log-level [Critical|Error|Warning|Info|Debug|Trace]
Show only logs with priority LEVEL or above
[default: Info]
-m, --memory-oldb Uses in memory OLDB, disregarding remote
OLDB.
--help Show this message and exit.
If the parser was configured as in the start of the application, the taurus default options and the application options will all be handled by the same parser.
At this point, please save your work.