In PyQt5 version 5.15.0 the .start()
method of QThreadPool
got a new signature that accepts either a Python function, a Python method, or a PyQt/PySide slot. This greatly simplifies the process of running Python code in a separate thread, avoiding the need to create a QRunnable
first.
The .start()
method schedules the execution of the given function/method/slot on a new thread using the QThreadPool
and so avoids blocking your app's main GUI thread. If you have a simple task that you want to execute on another thread, you can now just pass it to .start()
and be done with it.
For more information on using a QRunnable
for threading, see the multithreading tutorial.
PySide was a little slower to add that signature, but it finally happened in version 6.2.0
To demonstrate how to use .start()
to run user-defined Python functions/methods, we'll build a simple GUI with a long-running task.
A bad GUI design
Our demo application is a simple sheep counter which continuously counts upwards from 1. While the sheep are being counted, you can press a button to pick the currently counted sheep. Picking sheep is hard, so this task takes a long time.
This is how the demo GUI app looks like.
If you use PyQt5
, make sure that it is of version 5.15.0 or greater, otherwise the demo GUI app won’t work for you.
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QTimer
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@Slot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@Slot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt6.QtCore import pyqtSlot, QTimer
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt5.QtCore import pyqtSlot, QTimer
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function represents a long-running task!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
If you run the demo GUI app and you press on the Pick a sheep! button, you’ll notice that for a period of 5 seconds the GUI is completely unresponsive. That's not good.
Have you spotted the problem? The delay comes from the line time.sleep(5)
which pauses execution of the Python code for 5 seconds. We've added this to simulate a long-running task – which can be helped by threading! You can experiment by increasing the length of the delay and may notice that your operating system starts complaining about your application not responding.
A good GUI design
So, what can we do to fix our delay issue? Well, this is where the new .start()
method of QThreadPool
comes in handy!
First, we need to import QThreadPool
, so let’s do that.
- PySide6
- PyQt6
- PyQt5
from PySide6.QtCore import QThreadPool
from PyQt6.QtCore import QThreadPool
from PyQt5.QtCore import QThreadPool
Next, we need to create a QThreadPool
instance. Let’s add
self.thread_manager = QThreadPool()
to the __init__
block of the MainWindow
class.
Now, let’s create a pick_sheep_safely()
slot. This new slot will use the .start()
method of QThreadPool
to call the long-running pick_sheep()
slot and move it from the main GUI thread to a completely new thread.
- PySide
- PyQt
@Slot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # This is where the magic happens!
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # This is where the magic happens!
Also, make sure that you connect the pick_sheep_safely()
slot with the pressed
signal of self.pick_sheep_button
. So, in the __init__
block of the MainWindow
class you should have
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
If you made all those changes, your demo GUI app code should now be:
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QThreadPool, QTimer
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@Slot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@Slot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@Slot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since it is called by this start() method
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt6.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since it is called by this start() method
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
import time
from PyQt5.QtCore import pyqtSlot, QThreadPool, QTimer
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.setFixedSize(250, 100)
self.setWindowTitle("Sheep Picker")
self.sheep_number = 1
self.timer = QTimer()
self.picked_sheep_label = QLabel()
self.counted_sheep_label = QLabel()
self.layout = QVBoxLayout()
self.main_widget = QWidget()
self.thread_manager = QThreadPool()
self.pick_sheep_button = QPushButton("Pick a sheep!")
self.layout.addWidget(self.counted_sheep_label)
self.layout.addWidget(self.pick_sheep_button)
self.layout.addWidget(self.picked_sheep_label)
self.main_widget.setLayout(self.layout)
self.setCentralWidget(self.main_widget)
self.timer.timeout.connect(self.count_sheep)
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
self.timer.start()
@pyqtSlot()
def count_sheep(self):
self.sheep_number += 1
self.counted_sheep_label.setText(f"Counted {self.sheep_number} sheep.")
@pyqtSlot()
def pick_sheep(self):
self.picked_sheep_label.setText(f"Sheep {self.sheep_number} picked!")
time.sleep(5) # This function doesn't affect GUI responsiveness anymore...
@pyqtSlot()
def pick_sheep_safely(self):
self.thread_manager.start(self.pick_sheep) # ...since it is called by this start() method
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
Now, if you press on the Pick a sheep! button the pick_sheep
method is executed in a separate thread and does not block the main GUI thread. Your application will remain responsive, and the sheep counting will continue normally -- even though it still has to complete a long-running task in the background.
Try increasing the length of the delay now – for example, sleep(10)
– and you'll notice that it has no effect on the responsiveness of the UI.
Conclusion
And that’s it! I hope you’ll find this new .start()
method of QThreadPool
useful in any of your PyQt
/PySide
GUI apps that have some kind of a long-running task to be performed while the app’s main GUI thread is running.
For more, see the complete PyQt5 tutorial.
from Planet Python
via read more
No comments:
Post a Comment