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. The QApplication.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 using QMainWindow.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.