I have created a simple application to replicate this issue. Basically, if you click away or click on the app (replicating an event that is handled by the application), then it sets it into the queue and stops updating. This also prompts a "Not Responding" notification. For whatever reason with the code I have just typed up, it will not witness this issue until the 6th iteration.
I have done some research on threading, but nothing seems to help. Maybe I am missing something.
Here is how the code works. All you need to do is click on the run button. This should prompt the application to update all 5 edit boxes on the right with a new incremented number value every 1 second. And the app will also alert the user in the text field on the right.
My main curiosity is this: is there a way to create a thread that queues all events such that they do not affect the app from updating the user on what is going on internally? Or is there a way to disable all events all together to make sure that the only thing that happens during operation is updating the user on events within the app.
Here is the code:
import sys
import time
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLineEdit
class MyApp(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
self.setWindowTitle('Increment App')
self.setGeometry(100, 100, 600, 300)
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
# Layouts
main_layout = QHBoxLayout(central_widget)
input_layout = QVBoxLayout()
output_layout = QVBoxLayout()
text_layout = QVBoxLayout()
main_layout.addLayout(input_layout)
main_layout.addLayout(output_layout)
main_layout.addLayout(text_layout)
# Input Edit Fields
self.input_fields = []
for i in range(5):
input_field = QLineEdit(self)
input_field.setText("0")
input_layout.addWidget(input_field)
self.input_fields.append(input_field)
# Output Edit Fields
self.output_fields = []
for i in range(5):
output_field = QLineEdit(self)
output_field.setReadOnly(True)
output_layout.addWidget(output_field)
self.output_fields.append(output_field)
# Text Box
self.text_box = QTextEdit(self)
text_layout.addWidget(self.text_box)
# Run Button
self.run_button = QPushButton('Run', self)
self.run_button.clicked.connect(self.run_button_clicked)
text_layout.addWidget(self.run_button)
def run_button_clicked(self):
try:
# Get the current values from input fields
input_values = [int(input_field.text()) for input_field in self.input_fields]
while input_values[0] < 101:
# Increment each value and update the output fields
for i in range(5):
input_values[i] += 1
self.output_fields[i].setText(str(input_values[i]))
# Update the text box
self.text_box.append("Updated values in output fields.")
self.run_button.setEnabled(False)
self.repaint()
time.sleep(1)
self.text_box.append("All values have reached 100.")
except ValueError:
self.text_box.append("Invalid input. Please enter valid integers.")
def main():
app = QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
See the attached image for an idea of what it will look like after an event is witnessed by the app.
I have tried using what is recommended in this question: Background thread with QThread in PyQt
But I still have no luck even when using threading.
For instance, there are many examples of answers on SO that dwell with QThreads, and explain their usage. This is one I hold it close since I started learning / using them. It basically tells you how to create a custom thread/worker, and make code run on another thread.
Now, about what musicamante had said on all of these comments: he's indeed correct:
These are straight forward guidelines of how to use threading in Qt5 + Python. I'll try to explain them to you if you're interested. If not, just skip to the bottom there's a script example in there.
So, why you shouldn't call QWidget's methods in another thread?
I'm no expert, but I am using QThreads for a while. Try to imagine the following scenario: on the main thread, once you call QApplication.exec(), Qt5 starts an Event Loop. Think of it as a while (True) loop that executes as long as the user doesn't click in the X button of the window.
Inside this infinite loop, widgets are being constantly updated and redrawn over time. So, their methods are being called over and over without the user's knowledge.
If you create a QThread, and call a QWidget method inside that thread, there's going to be a chance that there will be a conflict: the same instance of a QWidget will execute method A in the main thread, and that instance will execute method B in the QThread at the same time. Once it happens, it can cause a series of reading/writting operations on the same variable (memory, files, pointers of data, ...), at the same time.
This is bad.
In order to fix that, we could use a QMutex. However, we cannot edit the Qt5's source code, and we don't even want to. It would be easy to mess things up. As we can't do that, QWidget's methods are not thread-safe. So it's unwise to call them in another thread.
How can we do that, then?
We use signals.
Qt5 runs an event loop. Think of it as queue (First in, First out). You schedule an event, and at some point, that event is readen, and processed. Once you emit a signal, that signal is catched by the Qt5 event loop, and eventually it is processed, executing every connected listener.
So, in order to execute a method from a QWidget inside the QThread, you don't call the method directly:
What happens in the background once you emit a signal, is that it doesn't execute the method right away. It sends a notification to the main thread's event loop, and when the notification is readen, it is processed and then the respective listener (QWidget's method) is called.
So there's always a delay of a few frames.
However, even if there's a delay of emitting signals and executing listeners, you can always rest assured that listeners will be executed when a respective signal is emitted. So it's a reliable way of exchanging data.
Why time.sleep is bad?
As I've written above, Qt5 runs an infinite event loop once QApplication.exec() is called. If you call time.sleep(), it will freeze the current thread, the main thread for a while.
This will cause the event loop to halt immediately for X seconds. What can be seen by the user, is that the application freezes and becomes unresponsive. The cursor's icon changes into an hourglass/blue spinner or something (like in windows), and the application gives itself away to be stuck at some heavy processing.
So, in terms of using GUIs, this is bad for the user experience. And the user will probably start clicking erratically and the application will be forced to shutdown. It happens to all of us.
Now, I'll provide you an example of what you should do IMHO:
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QPushButton, QLineEdit
import copy
import sys
# Our worker is agnostic of the GUI. It's simply used to process some
# data and once it's done or has some output available, it can send it
# back to the main thread using signals.
class Worker(QObject):
# Parameter is a tuple: (index: int, text: str)
updateOutput = pyqtSignal(object)
# Parameter is a string.
appendTextBox = pyqtSignal(str)
def __init__(self):
super().__init__()
self.input_values = []
# On this example, we're fetching data on the main thread, and keep
# the list's reference on the on the thread side.
#
# So, while we're calling this function on the main thread, we know
# for sure that the thread hadn't started yet.
#
# If you plan to use data that is referenced both on the main thread,
# AND the QThread side, guard the variable getter/setter with QMutexes.
def setInputValues(self, ivs):
self.input_values = ivs
# Everything in this scope executes in another thread.
def run(self):
try:
while (self.input_values[0] < 101):
# Increment each value and update the output fields
for i in range(len(self.input_values)):
self.input_values[i] += 1
# Don't ever call QWidgets methods in another thread.
#
# Use a signal to schedule that method to be executed
# on the main thread instead.
#
# Pass whichever parameter you want to that function.
self.updateOutput.emit((i, str(self.input_values[i])))
# Simulate some heavy processing or simply force a way
# to visualize the animation.
#
# Note: for some reason, in my case at least (PySide2 Win11), I
# must call self.thread() instead of using self directly to call
# QThread.msleep.
self.thread().msleep(1000)
# Again, emit signals instead of calling QWidgets methods on the other thread.
self.appendTextBox("Updated values in output fields.")
except ValueError:
# Again, emit signals instead of calling QWidgets methods on the other thread.
self.appendTextBox.emit("Invalid input. Please enter valid integers.")
# I've skipped the QMainWindow on purpose to diminish code.
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.initUI()
# Runs on main thread
def initUI(self):
self.setWindowTitle('Increment App')
self.setGeometry(100, 100, 600, 300)
# Layouts
main_layout = QHBoxLayout()
input_layout = QVBoxLayout()
output_layout = QVBoxLayout()
text_layout = QVBoxLayout()
main_layout.addLayout(input_layout)
main_layout.addLayout(output_layout)
main_layout.addLayout(text_layout)
self.setLayout(main_layout)
# Input Edit Fields
self.input_fields = []
for i in range(5):
input_field = QLineEdit(self)
input_field.setText("0")
input_layout.addWidget(input_field)
self.input_fields.append(input_field)
# Output Edit Fields
self.output_fields = []
for i in range(5):
output_field = QLineEdit(self)
output_field.setReadOnly(True)
output_layout.addWidget(output_field)
self.output_fields.append(output_field)
# Text Box
self.text_box = QTextEdit(self)
text_layout.addWidget(self.text_box)
# Run Button
self.run_button = QPushButton('Run', self)
self.run_button.clicked.connect(self.run_button_clicked)
text_layout.addWidget(self.run_button)
# -------------------------------------------------
# - Thread Initialization
# -------------------------------------------------
# 1. Instantiate a QObject Worker.
# - This is a matter of taste, I prefer to use the moveToThread approach
# when dealing with QThreads.
self.worker = Worker()
# 2. Create the thread and use moveToThread.
self.update_thread = QThread()
self.worker.moveToThread(self.update_thread)
# 3. pyqtSignal Logic:
# - When Worker.updateOutput signal is emitted, it will execute
# updateIndexedOutput on the main thread with the respective parameters:
# - its a tuple (index, text)
self.worker.updateOutput.connect(self.updateIndexedOutput)
# 4. pyqtSignal Logic:
# - When Worker.appendTextBox signal is emitted, it will execute
# QTextEdit.append method on the main thread with a string parameter
# provided by the worker (executing on the other thread).
self.worker.appendTextBox.connect(self.text_box.append)
# 5. Set which functions will run on the other thread.
# In this case only this one:
self.update_thread.started.connect(self.worker.run)
# Runs on main thread
def run_button_clicked(self):
try:
# Get the current values from input fields
input_values = [int(input_field.text()) for input_field in self.input_fields]
# Start the update thread
self.worker.setInputValues(input_values)
self.update_thread.start()
# Disable the run button
self.run_button.setEnabled(False)
except:
self.text_box.append("Invalid input. Please enter valid integers.")
# Runs on main thread
def updateIndexedOutput(self, data):
try:
# Extract values from tuple
index, text = data
self.output_fields[index].setText(text)
except Exception as error:
print(error)
####
app = QApplication()
win = MyApp()
win.show()
app.exec_()
In the script above, I use QThread.msleep to simulate heavy processing. You shouldn't really call this for anything else. A function that gets any thread stuck for no reason, is not useful at all, specially if you want to close the application (you'd have to wait for it to finish). In this example, however, we are not waiting the QThread to finish its process to clean things properly on shutdown.
If you're waiting for another process output, or a peripheral device to provide you with data, you can just set a while (True) loop and wait for the result. As you're using another thread, a while (True) loop will not freeze the main application anymore.
If the script doesn't work, just let me know. I use PySide2 on Win11 so there might be some typos in that case.