1. Call Types
Using Python within a Graphical User Interface (GUI) differs significantly from its typical usage in a console application, requiring special considerations.
The following examples are not specific to PyMpc classes, functions, or methods, but rather serve to illustrate various approaches to working with Python in the STKO interface. Familiarity with GUI programming fundamentals can help unlock the full potential of the Python interface in STKO, enabling you to write effective scripts both as a user and as a developer creating scripts for others. This section provides a series of examples on call types, which are different methods for invoking processes from the GUI.
1.1. Standard
The first example demonstrates a standard call type with no additional setup.
To simulate a time-consuming operation, the sleep
function is used.
Running this script will cause the GUI to freeze for 0.5 seconds when the sleep
function is called.
In contrast, running this script from a console application would not freeze the visualization,
as the thread running the program is not responsible for generating a graphical interface.
Instead, it would execute sequentially, as expected.
import os
from time import sleep
print(__file__)
print(os.getcwd())
print("start")
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
print("end")
Unlike console applications, GUIs do not run scripts from start to finish on a single thread. Instead, they create an infinite loop called the event loop, which processes user interactions and updates the graphical interface. When a standard call type is executed, it shares the same thread as the event loop. As a result, if a function takes more than a second to complete, the STKO application will become unresponsive, appearing to freeze. However, this does not indicate a crash; the application is simply busy executing the script and unable to update the GUI.
To effectively perform finite element analysis using Tcl or Python, it is essential to understand the available commands in each scripting language, as well as any new commands specific to the STKO interface. Note that the Tcl and Python languages have distinct command sets, which must be taken into account when scripting.
1.2. Process events
To prevent the GUI from freezing while running scripts, several approaches can be taken.
One of the simplest solutions is to utilize the processEvents
method from the App sub-module.
This function enables the thread to divide its attention between the script and the GUI, allowing for more responsive interaction.
By inserting processEvents
at the end of each iteration in the standard call type example,
you can now interact with the GUI while the script is running, although you may experience some lag.
This subtle feedback informs the user that the software is still operational, merely busy processing the script.
To see this in action, try running the following script:
import os
from time import sleep
from PyMpc import App
print(__file__)
print(os.getcwd())
print("start")
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
App.processEvents()
print("end")
By incorporating processEvents
, you can create a more user-friendly experience,
even when running computationally intensive scripts.
This technique helps to maintain a responsive GUI,
providing reassurance that the software is functioning as intended.
1.3. Working thread
An alternative approach to prevent the GUI from freezing while running a process is to utilize a worker thread. This requires leveraging the PySide2 library, which is used to build the STKO GUI and is already installed on the Python embedded in STKO. If you’re using a different Python environment, you can install PySide2 by referring to the link provided.
To implement this approach, you’ll need to be familiar with Python concepts such as polymorphism, child and parent classes, and inheritance. The example script below uses the same lengthy function as before, but instead of calling it directly, we create a Worker class that utilizes the signal and slot delivery mechanism provided by PySide2. This mechanism enables communication between objects by emitting signals when specific events occur, which in turn trigger the execution of designated functions (slots).
For more information on signal and slot mechanisms, please refer to the Qt Documentation or conduct your own research.
In the script below, we define a Worker class that includes a signal to notify the main thread when our process is complete. Within this class, we define a run function tagged as a slot using the @ symbol, which executes the lengthy function on a parallel worker thread.
import os
from time import sleep
from PySide2.QtCore import Qt, QObject, Signal, Slot, QThread, QEventLoop
class Worker(QObject):
finished = Signal() #signals
@Slot() #lengthy function
def run(self): #paste your function here, inside run...
print(__file__)
print(os.getcwd())
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
self.finished.emit() # Done
By using a worker thread and the signal and slot delivery mechanism, we can decouple the GUI from the processing task, ensuring a responsive and interactive user experience even when performing computationally intensive tasks.
loop = QEventLoop()
thread = QThread()
worker = Worker()
worker.moveToThread(thread)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.started.connect(worker.run)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(loop.quit)
print('Starting thread...')
thread.start()
print(' Thread started')
loop.exec_()
print('DONE!')
To ensure that the thread has completed its execution, it’s recommended to use the exec_()
function from the QEventLoop class.
This function blocks the main thread until the event loop is finished, preventing potential crashes or errors in STKO.
When running the example script, you’ll notice that interacting with the STKO GUI is possible while the lengthy function is executing. However, this flexibility also introduces a potential risk: if the user modifies the model while the script is running, it may alter the data required by the script, leading to application crashes. Similarly, if the same script is re-run before the first process has completed, it may also cause the application to crash.
To mitigate these risks, it’s essential to implement proper synchronization mechanisms, such as using event loops, to ensure that the script execution is properly coordinated with the GUI interactions. By doing so, you can prevent potential crashes and ensure a more robust and reliable user experience.
It’s worth noting that using event loops can help prevent common issues such as:
Crashes caused by concurrent access to shared data
Errors resulting from incomplete or inconsistent data
Unexpected behavior due to concurrent execution of multiple scripts
By incorporating event loops into your script design, you can create more robust and reliable scripts that interact seamlessly with the STKO GUI.
1.4. Working thread with GUI-operation and callback
When working with GUI applications, it’s essential to remember that all GUI-related operations must be performed on the GUI thread. This thread is responsible for listening to operating system events, and attempting to perform GUI operations on a parallel thread can lead to crashes.
One common GUI operation is displaying a message box to communicate with the user and wait for input. However, if you try to perform this operation on a parallel thread, your STKO application will likely crash.
To demonstrate this, let’s examine the following script, which adds a GUI operation to the run function from the previous example.
import os
from time import sleep
from PySide2.QtCore import Qt, QObject, Signal, Slot, QThread, QEventLoop
from PySide2.QtWidgets import QMessageBox
class Worker(QObject):
finished = Signal()
@Slot()
def run(self):
QMessageBox.information(None, "Title", "Message: Hi there!")
print(__file__)
print(os.getcwd())
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
self.finished.emit()
loop = QEventLoop()
thread = QThread()
worker = Worker()
worker.moveToThread(thread)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.started.connect(worker.run)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(loop.quit)
print('Starting thread...')
thread.start()
print(' Thread started')
loop.exec_()
print('DONE!')
As you can see, the script attempts to display a message box on a parallel thread, which can cause the application to crash.
To avoid this issue, you can create a method within the Worker class that runs specific functions on the main thread. One way to achieve this is by using a QBlockingSlot class, which is derived from QObject. This class requests the call for a function and connects it to the parallel thread.
Here’s an updated script that demonstrates this approach:
import os
from time import sleep
from PySide2.QtCore import Qt, QObject, Signal, Slot, QThread, QEventLoop
from PySide2.QtWidgets import QMessageBox, QApplication
class QBlockingSlot(QObject):
requestCall = Signal()
done = Signal()
def __init__(self, function, parent = None):
super(QBlockingSlot, self).__init__(parent)
self.requestCall.connect(self.run)
self.function = function
def run(self):
self.function()
self.done.emit()
def call(self):
loop = QEventLoop()
self.done.connect(loop.quit)
self.requestCall.emit()
loop.exec_()
def runOnMainThread(what):
bslot = QBlockingSlot(what)
bslot.moveToThread(QApplication.instance().thread())
bslot.call()
class Worker(QObject):
finished = Signal()
@Slot()
def run(self):
print(QThread.currentThread())
def what_to_run():
print(QThread.currentThread())
print(QApplication.instance().thread())
QMessageBox.information(None, 'Title', 'Hi There!')
runOnMainThread(lambda : what_to_run())
print(__file__)
print(os.getcwd())
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
self.finished.emit()
loop = QEventLoop()
thread = QThread()
worker = Worker()
worker.moveToThread(thread)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.started.connect(worker.run)
thread.finished.connect(thread.deleteLater)
thread.finished.connect(loop.quit)
print('Starting thread...')
thread.start()
print(' Thread started')
loop.exec_()
print('DONE!')
In this script, we create a QBlockingSlot class that allows us to run a function on the main thread. We then use this class to display a message box on the main thread, while the parallel thread continues to run.
The print statements in the script show that the first call is different from the others, and both the main and parallel threads are involved in the operations. This demonstrates that we have successfully run a GUI operation on the main thread, while avoiding potential crashes.
By using this approach, you can ensure that your GUI applications remain stable and responsive, even when performing complex operations on parallel threads.
1.5. Blocking thread
To ensure a seamless user experience, it’s essential to provide visual feedback when performing time-consuming operations. One way to achieve this is by using a blocking dialog, which prevents the user from interacting with the GUI while the operation is in progress.
In this example, we’ll modify the previous script to include a WorkerDialog class, derived from QDialog. This dialog will display a progress bar and a label indicating that the operation is in progress.
The key difference between this approach and the previous one is that the dialog will remain visible until the operation is complete, providing a clear indication of the progress. Additionally, the dialog will not close when the user presses the button or the Escape key, ensuring that the thread continues to run uninterrupted.
Here’s the updated script:
import os
from time import sleep
from PySide2.QtCore import Qt, QObject, Signal, Slot, QThread, QEventLoop, QTimer
from PySide2.QtWidgets import QMessageBox, QApplication, QDialog, QLabel, QProgressBar, QVBoxLayout
class QBlockingSlot(QObject):
requestCall = Signal()
done = Signal()
def __init__(self, function, parent = None):
super(QBlockingSlot, self).__init__(parent)
self.requestCall.connect(self.run)
self.function = function
def run(self):
self.function()
self.done.emit()
def call(self):
loop = QEventLoop()
self.done.connect(loop.quit)
self.requestCall.emit()
loop.exec_()
def runOnMainThread(what):
bslot = QBlockingSlot(what)
bslot.moveToThread(QApplication.instance().thread())
bslot.call()
class WorkerDialog(QDialog):
def __init__(self, parent = None):
super(WorkerDialog, self).__init__(parent)
self.setWindowFlags(Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
self.setAttribute(Qt.WA_DeleteOnClose)
self.setLayout(QVBoxLayout())
self.layout().addWidget(QLabel("Work in progres. Please wait"))
self.pbar = QProgressBar()
self.pbar.setRange(1, 100)
self.pbar.setTextVisible(True)
self.layout().addWidget(self.pbar)
self.setWindowOpacity(0.0)
self.delay = 0.0
self.timer = QTimer(self)
self.timer.setInterval(25)
self.timer.timeout.connect(self.onTimerTimeOut)
def showEvent(self, event):
print('showEvent')
super(WorkerDialog, self).showEvent(event)
self.timer.start()
def reject(self):
pass
@Slot(int)
def setPercentage(self, value):
self.pbar.setValue(value)
@Slot()
def onTimerTimeOut(self):
if self.delay < 1.0:
self.delay += 0.05
else:
self.setWindowOpacity(self.windowOpacity() + 0.05)
if self.windowOpacity() == 1.0:
self.timer.stop()
self.timer.timeout.disconnect(self.onTimerTimeOut)
class Worker(QObject):
sendPercentage = Signal(int)
finished = Signal()
@Slot()
def run(self):
print(QThread.currentThread())
def what_to_run():
print(QThread.currentThread())
print(QApplication.instance().thread())
QMessageBox.information(None, 'Title', 'Hi There!')
runOnMainThread(lambda : what_to_run())
print(__file__)
print(os.getcwd())
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
self.sendPercentage.emit(int(float(i+1)/10.0*100.0))
self.finished.emit()
As you can see, the WorkerDialog class is designed to display a progress bar and a label, while also preventing the user from interacting with the GUI. The showEvent method is overridden to start a timer that gradually increases the dialog’s opacity, creating a smooth fade-in effect.
The Worker class remains largely unchanged, with the addition of a sendPercentage signal that emits the progress percentage to the dialog. This allows the dialog to update the progress bar accordingly.
The rest of the script is similar to the previous example, with the addition of a new connection between the Worker and the WorkerDialog to send the progress percentage.
dialog = WorkerDialog()
thread = QThread()
worker = Worker()
worker.moveToThread(thread)
worker.finished.connect(thread.quit)
worker.finished.connect(worker.deleteLater)
thread.started.connect(worker.run)
thread.finished.connect(thread.deleteLater)
worker.sendPercentage.connect(dialog.setPercentage)
thread.finished.connect(dialog.accept)
print('Starting thread...')
thread.start()
print(' Thread started')
dialog.exec_()
print('DONE!')
By using this approach, you can provide a more intuitive and user-friendly experience, while also ensuring that the GUI remains responsive and stable. This is the recommended way to interact with STKO from the Python API, as it provides a clear indication of the progress and prevents the user from interacting with the GUI while the operation is in progress.
1.6. ThreadUtils
To simplify the process of running scripts with a blocking thread, we’ve created a utility file called ThreadUtils.py. This file contains a set of classes and functions that can be used to run scripts with a blocking thread, without having to repeat the same code in every script.
The ThreadUtils.py file includes the following classes and functions:
QBlockingSlot: a simple
QObject
that wraps a user-defined function and calls it, waiting for it to finish.runOnMainThread: a utility function that runs a function with a
QBlockingSlot
on the GUI thread.WorkerDialog: a simple worker dialog with a progress bar to track the percentage of an operation.
runOnWorkerThread: a function that runs a worker (an instance of a class derived from
Worker
) on a working thread using the provided dialog.
Here’s an example of using ThreadUtils in your script:
import os
from time import sleep
import importlib
import utils.ThreadUtils
importlib.reload(utils.ThreadUtils)
import utils.ThreadUtils as tu
from PySide2.QtCore import Qt, QObject, Signal, Slot, QThread
from PySide2.QtWidgets import QMessageBox, QApplication
class Worker(QObject):
sendPercentage = Signal(int)
finished = Signal()
@Slot()
def run(self):
print(QThread.currentThread())
def what_to_run():
print(QThread.currentThread())
print(QApplication.instance().thread())
QMessageBox.information(None, 'Title', 'Hi There!')
tu.runOnMainThread(lambda : what_to_run())
print(__file__)
print(os.getcwd())
counter = 0
for i in range(10):
sleep(0.5)
counter += 1
print('iteration {}'.format(counter))
self.sendPercentage.emit(int(float(i+1)/10.0*100.0))
self.finished.emit()
tu.runOnWorkerThread(Worker())
In this example, we define a Worker class that inherits from QObject and has the necessary signals and slots. We then use the runOnWorkerThread function from ThreadUtils to run the worker on a working thread, using the provided dialog.
Additionally, if you want to add exception handling to your function before emitting the signal to the thread that the process is finished, you can use the IO sub-module, referenced in the Input/Output section. Here’s an example:
except Exception as ex:
IO.write_cerr(str(ex))
finally:
self.finished.emit() # Done