在PyQt 5.15.0和PySide 6.2.0版本中,QThreadPool
的.start()
方法被扩展为除了只接受一个Python函数、一个Python方法或一个PyQt/PySide槽,还可以接受一个QRunnable
对象。这简化了在后台运行Python代码的过程,避免了为每个任务创建一个QRunnable
对象的麻烦。
关于为多线程创建QRunnable
对象的更多信息,请参阅多线程教程。
.start()
方法将一个函数/方法/槽的执行安排在一个单独的线程上,使用QThreadPool
,所以它避免了阻塞你的应用程序的主GUI线程。因此,如果你有一个或多个长期运行的任务需要完成或在后台运行,将它们传递给.start()
,就可以了。
我们将建立一个简单的演示应用程序,模拟一个长期运行的任务,以展示.start()
如何将一个用户定义的Python函数/方法或PyQt/PySide槽转移到一个单独的线程上。
但首先,让我们从一个有缺陷的方法开始。
阻断GUI
我们的演示程序是一个从1开始向上计数的绵羊计数器。在这个过程中,你可以按一个按钮来挑选一只绵羊。而由于挑选一只羊是很难的,所以需要一些时间来完成。
这就是我们的演示应用程序的样子。
确保你使用的是PyQt 5.15.0+或PySide 6.2.0+;否则,演示程序将不能为你工作:
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QTimer
from PySide6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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 (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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 (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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()
当你运行演示程序并按下 ***挑选一只羊!***按钮时,你会注意到有5秒钟,GUI是完全没有反应的。这可不好。
GUI响应性的延迟来自于行time.sleep(5)
,它使Python代码的执行暂停了5秒。这是为模拟一个长期运行的任务而添加的。然而,我们可以通过线程来改善这个问题,你将在后面看到。
请随意试验,增加延迟的长度--将一个大于5的数字传给.sleep()
--你可能会注意到你的操作系统开始抱怨演示应用程序没有响应。
在一个单独的线程上运行一个任务
那么,我们怎样才能提高我们的演示应用程序的响应速度呢?这就是QThreadPool
的扩展.start()
方法的用处!
首先,我们需要导入QThreadPool
,所以让我们来做这件事:
- PySide6
- PyQt6
- PyQt5
from PySide6.QtCore import QThreadPool
from PyQt6.QtCore import QThreadPool
from PyQt5.QtCore import QThreadPool
接下来,我们需要创建一个QThreadPool
实例。让我们把
self.thread_manager = QThreadPool()
到MainWindow
类的__init__
块中。
现在,让我们创建一个pick_sheep_safely()
槽。它将使用.start()
方法来调用长期运行的pick_sheep()
槽,并将其从GUI主线程移到一个单独的线程上:
- 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!
此外,确保你将pick_sheep_safely()
槽与self.pick_sheep_button
的pressed
信号连接起来。因此,在MainWindow
类的__init__
块中,你应该有
self.pick_sheep_button.pressed.connect(self.pick_sheep_safely)
而如果你一直跟着,我们改进后的演示应用程序的代码现在应该是:
- PySide6
- PyQt6
- PyQt5
import time
from PySide6.QtCore import Slot, QThreadPool, QTimer
from PySide6.QtWidgets import (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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 .start() is used!
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 (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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 .start() is used!
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 (
QLabel,
QWidget,
QMainWindow,
QPushButton,
QVBoxLayout,
QApplication,
)
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 .start() is used!
if __name__ == "__main__":
app = QApplication([])
main_window = MainWindow()
main_window.show()
app.exec()
当你按下 选一只羊!按钮时,pick_sheep()
槽会在一个单独的线程上执行,不再阻塞GUI的主线程。绵羊计数继续进行,GUI保持响应--尽管我们的演示应用程序仍然要在后台完成一个长期运行的任务。
现在试着增加延迟的长度--例如,time.sleep(10)
--并注意到它不再影响GUI了。
结论
就这样吧!.start()
我希望你会发现QThreadPool
的扩展方法在你的PyQt/PySide GUI应用中很有帮助,因为这些应用有长期运行的任务要在后台执行。