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 PySide2.QtCore import QObject
13from PySide2.QtCore import Slot
14from PySide2.QtCore import Signal
15from PySide2.QtCore import QMetaObject
16from PySide2.QtWidgets import QMainWindow
17
18from taurus.core.util.log import Logger
19from elt.cut.task import Task
20
21from paegui.mainwindow import Ui_MainWindow # WAF will automatically generated this
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 Logger import is from taurus
, and it allows to enhance a class with logging capabilities.
Finally, the Ui_MainWindow
import is the python code generated by the pyside2-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 paegui.mainwindow import Ui_MainWindow
We will now proceed to examine the Class definition:
19class ApplicationWindow(QMainWindow, Logger):
20 '''
21 Implementation of the mainwindow.ui file.
22
23 Since the UI file indicates that its root is a QMainWindow, then
24 this class should also inherit from it.
25 We should also call explicitly its parent constructors.
26
27 The implementation for this class also includes slots for
28 actions, and management of the closeEvent.
29 '''
30
31 def __init__(self):
32 QMainWindow.__init__(self)
33 Logger.__init__(self,name=qApp.applicationName())
34 # Construction of UI
35 self.ui = Ui_MainWindow()
36 self.ui.setupUi(self)
37 self._init_gui()
Line 19 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 33, the Logger parent class is initialized, but we use as name keyworded argument, qApp.applicationName()
. qApp
is a global reference to the QApplication or 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 qApp
global object.
In line 35 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 pyside2-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:
66 @Slot()
67 def on_actionNew_triggered(self):
68 '''
69 Slot that auto-connects to the actionNew.
70 actionNew is not declared in the code, but in the mainwindow.ui.
71 See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
72 '''
73 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.
168 def closeEvent(self, event):
169 '''
170 Not to be confused with actionExit, this method is special. Instead
171 of using signals to know when an application is closed, we can
172 override the closeEvent method from the QApplication class, and
173 determine here what will happen when the window is closed.
174 In this case, we are just forwarding the event to its parent class.
175 This is useful when disconnection from server or prevention of
176 current work needs to be implemented.
177
178 :param event: Event received from Qt event pump
179 :type event: QtCore.QEvent
180
181 '''
182 self.info('Application shutting down...')
183 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.
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 self.info('actionSave launching new Task')
124 task = Task(self.save_job)
125 task.signals.result.connect(self.save_job_slot)
126 task.start()
127 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.
159 def save_job(self):
160 self.info('save_job sleeping for 5')
161 time.sleep(5)
162 return 'Slept for 5 seconds'
163
164 def save_job_slot(self, arg):
165 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:
$ paegui
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:
$ paegui
MainThread WARNING 2023-02-02 11:22:29,135 TaurusRootLogger: <frozen importlib._bootstrap>:228: DeprecationWarning: taurus.core.util.argparse is deprecated since 4.5.4. Use argparse or (better) click instead
MainThread INFO 2023-02-02 11:22:29,172 TaurusRootLogger: Using PySide2 (v5.15.2 with Qt 5.15.2 and Python 3.9.13)
MainThread INFO 2023-02-02 11:22:29,666 TaurusRootLogger: Plugin "taurus_pyqtgraph" lazy-loaded as "taurus.qt.qtgui.tpg"
MainThread INFO 2023-02-02 11:22:32,693 Python Application Example GUI: actionNew triggered
MainThread INFO 2023-02-02 11:22:33,332 Python Application Example GUI: actionOpen triggered
MainThread INFO 2023-02-02 11:22:33,948 Python Application Example GUI: actionSave triggered
MainThread INFO 2023-02-02 11:22:33,948 Python Application Example GUI: actionSave launching new Task
MainThread INFO 2023-02-02 11:22:33,949 Python Application Example GUI: actionSave finished
Dummy-1 INFO 2023-02-02 11:22:33,949 Python Application Example GUI: save_job sleeping for 5
MainThread INFO 2023-02-02 11:22:35,044 Python Application Example GUI: actionWhats_This triggered
MainThread INFO 2023-02-02 11:22:38,954 Python Application Example GUI: save_job_slot reporting Slept for 5 seconds
MainThread INFO 2023-02-02 11:22:40,517 Python Application Example GUI: Application shutting down...
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:
$ paegui --taurus-log-level Debug
Help is always present when using the TaurusApplication class:
$ paegui --help
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.