7. 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.
10 11 12 13 14 15 16 17 18 19 20 21 | import time
from PySide2.QtCore import QObject
from PySide2.QtCore import Slot
from PySide2.QtCore import Signal
from PySide2.QtCore import QMetaObject
from PySide2.QtWidgets import QMainWindow
from taurus.core.util import Logger
from elt.cut.task import Task
from 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:
19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class ApplicationWindow(QMainWindow, Logger):
'''
Implementation of the mainwindow.ui file.
Since the UI file indicates that its root is a QMainWindow, then
this class should also inherit from it.
We should also call explicitly its parent constructors.
The implementation for this class also includes slots for
actions, and management of the closeEvent.
'''
def __init__(self):
QMainWindow.__init__(self)
Logger.__init__(self,name=qApp.applicationName())
# Construction of UI
self.ui = Ui_MainWindow()
self.ui.setupUi(self)
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 67 68 69 70 71 72 73 | @Slot()
def on_actionNew_triggered(self):
'''
Slot that auto-connects to the actionNew.
actionNew is not declared in the code, but in the mainwindow.ui.
See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
'''
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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | def closeEvent(self, event):
'''
Not to be confused with actionExit, this method is special. Instead
of using signals to know when an application is closed, we can
override the closeEvent method from the QApplication class, and
determine here what will happen when the window is closed.
In this case, we are just forwarding the event to its parent class.
This is useful when disconnection from server or prevention of
current work needs to be implemented.
:param event: Event received from Qt event pump
:type event: QtCore.QEvent
'''
self.info('Application shutting down...')
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.
7.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. 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.
@Slot()
def on_actionSave_triggered(self):
'''
Slot that auto-connects to the actionSave.
actionSave is not declared in the code, but in the mainwindow.ui.
See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
'''
self.info('actionSave triggered')
self.info('actionSave launching new Job')
time.sleep(5)
In this example application, we include a solution for this. In line 90, the actionSave():samp: 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.
90 91 92 93 94 95 96 97 98 99 | @Slot()
def on_actionSave_triggered(self):
'''
Slot that auto-connects to the actionSave.
actionSave is not declared in the code, but in the mainwindow.ui.
See: https://doc.qt.io/qt-5/designer-using-a-ui-file.html#widgets-and-dialogs-with-auto-connect
'''
task = Task(self.save_job)
task.signals.result.connect(self.save_job_slot)
task.start()
|
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 160 161 162 163 164 165 | def save_job(self):
self.info('save_job sleeping for 5')
time.sleep(5)
return 'Slept for 5 seconds'
def save_job_slot(self, arg):
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:
(base) [eltdev@cut python_application]$ paegui
MainThread WARNING 2021-06-17 17:21:27,065 TaurusRootLogger: <frozen importlib._bootstrap>:219: DeprecationWarning: taurus.core.util.argparse is deprecated since 4.5.4. Use argparse or (better) click instead
MainThread INFO 2021-06-17 17:21:27,108 TaurusRootLogger: Using PySide2 (v5.14.1 with Qt 5.14.0 and Python 3.7.6)
MainThread INFO 2021-06-17 17:21:27,184 taurus.qt.qtgui.icon.icon: Setting Tango icon theme (from /opt/anaconda38/lib/python3.7/site-packages/taurus/qt/qtgui/icon/)
MainThread INFO 2021-06-17 17:21:27,480 TaurusRootLogger: Plugin "taurus_pyqtgraph" loaded as "taurus.qt.qtgui.tpg"
MainThread INFO 2021-06-17 17:21:29,058 Python Application Example GUI: actionSave triggered
MainThread INFO 2021-06-17 17:21:29,058 Python Application Example GUI: actionSave launching new Job
MainThread INFO 2021-06-17 17:21:29,059 Task: Created QThreadPool with 8 threads
Dummy-1 INFO 2021-06-17 17:21:29,059 Python Application Example GUI: save_job sleeping for 5
Dummy-1 INFO 2021-06-17 17:21:34,064 Task: RTN: Slept for 5 seconds
Dummy-1 INFO 2021-06-17 17:21:34,064 Task: Is a common reply
MainThread INFO 2021-06-17 17:21:34,066 Python Application Example GUI: save_job_slot reporting Slept for 5 seconds
MainThread INFO 2021-06-17 17:21:36,384 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.