PyQt子線程處理業(yè)務事件的問題解決
在PyQt中是不推薦使用UI主線程來處理耗時操作的,會造成窗口組件阻塞。耗時操作一般放在子線程中。子線程處理完成后,可能需要更新窗口組件,但是PyQt不推薦使用子線程來更新主線程(也不是不能更新),這就用到了信號槽機制來更新主線程。
- 在QObject的一個子類中創(chuàng)建一個信號(
PyQt5.QtCore.pyqtSignal)屬性 - 將這個信號屬性和其他類中的函數(shù)綁定,綁定的這個函數(shù)叫做整個信號的槽函數(shù)。一個信號可以和多個槽函數(shù)綁定。
- 該信號發(fā)出時,就會調用對應的槽函數(shù)
可能會有疑問,槽函數(shù)被執(zhí)行時所在的線程和發(fā)送信號的線程是不是同一個?
需要注意,信號一定義在QObject或其子類中。調用該屬性的emit方法發(fā)出信號后,和該信號綁定的槽函數(shù)都將要被調用,但是調用的線程并不一定是發(fā)送信號的這個線程,這和PyQt中的線程親和性(Thread Affinity)有關。
線程親和性(Thread Affinity)
在 PyQt 中,一個對象可以被移動到不同的線程中,但一個對象在同一時刻只能屬于一個線程。這是因為 Qt 使用線程親和性(Thread Affinity)的概念來管理對象所屬的線程。
每個 Qt 對象都與一個特定的線程相關聯(lián),即它的線程親和性。對象的線程親和性決定了該對象的槽函數(shù)是在哪個線程中執(zhí)行。默認情況下,對象在創(chuàng)建時會與創(chuàng)建它的線程相關聯(lián),但可以使用 moveToThread 方法將對象移動到另一個線程中。
錯誤示例:
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog
from PyQt5.QtCore import QThread,pyqtSignal,QObject
import sys, threading
class MyWindow(QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.button = QPushButton('Hi')
self.button.clicked.connect(self.on_click)
self.setCentralWidget(self.button)
def on_click(self):
print("on_click",threading.current_thread().name)
self.thread = MyThread(self)
self.thread.start()
def set_text(self,file_name):
print("setText",threading.current_thread().name)
self.button.setText(file_name)
class MyThread(QThread):
def __init__(self,mv:QMainWindow) -> None:
super().__init__(None)
self.mv = mv
def run(self):
print('run',threading.current_thread().name)
QThread.sleep(5)
self.mv.set_text("Hello World")
if __name__ == '__main__':
app = QApplication([])
window = MyWindow()
window.show()
sys.exit(app.exec_())
輸出結果:
on_click MainThread
run Dummy-1
setText Dummy-1 //子線程更新UI,不推薦
使用信號槽機制
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog
from PyQt5.QtCore import QThread,pyqtSignal,QObject
import sys, threading
class MyWindow(QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.button = QPushButton('Hi')
self.button.clicked.connect(self.on_click)
self.setCentralWidget(self.button)
def on_click(self):
print("on_click",threading.current_thread().name)
self.thread = MyThread(self)
self.thread.pyqtSignal.connect(self.set_text)
self.thread.start()
def set_text(self,file_name):
print("setText",threading.current_thread().name)
self.button.setText(file_name)
class MyThread(QThread):
pyqtSignal = pyqtSignal(str)
def __init__(self,mv:QMainWindow) -> None:
super().__init__(None)
self.mv = mv
def run(self):
print('run',threading.current_thread().name)
QThread.sleep(5)
self.pyqtSignal.emit("Hello World")
if __name__ == '__main__':
app = QApplication([])
window = MyWindow()
window.show()
sys.exit(app.exec_())
輸出結果:
on_click MainThread
run Dummy-1
setText MainThread //更新UI時,執(zhí)行的線程為主線程
setText槽函數(shù)為什么會被主函數(shù)執(zhí)行,就是因為線程親和性,槽函數(shù)所在對象和MainThread綁定,當然會被主線程所執(zhí)行。
但是這種將事務直接寫在run,PyQt5是不推薦的,正確寫法如下
創(chuàng)建一個類集成QObject,來做業(yè)務的處理。并將這個對象和新創(chuàng)建的線程通過moveToThread綁定,作為這個對象的親和線程。將QThread的started信號和這個業(yè)務事件綁定。線程啟動,發(fā)送started信號,業(yè)務對象開始處理業(yè)務,完成之后發(fā)送信號給主線程槽函數(shù)。
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog
from PyQt5.QtCore import QThread,pyqtSignal,QObject
import sys, threading
class MyWindow(QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.button = QPushButton('Hi')
self.button.clicked.connect(self.on_click)
self.setCentralWidget(self.button)
def on_click(self):
print("on_click",threading.current_thread().name)
self.thread = QThread()
self.myHander = MyHandler()
self.myHander.moveToThread(self.thread)
self.myHander.pyqtSignal.connect(self.set_text)
self.thread.started.connect(self.myHander.handle)
self.thread.start()
def set_text(self,file_name):
print("setText",threading.current_thread().name)
self.button.setText(file_name)
class MyHandler(QObject):
pyqtSignal = pyqtSignal(str)
def handle(self):
print('handle',threading.current_thread().name)
self.pyqtSignal.emit("Hello World")
if __name__ == '__main__':
app = QApplication([])
window = MyWindow()
window.show()
sys.exit(app.exec_())
子線程中調用QFileDialog
如果在子線程中調用了QFileDialog窗口選擇文件,QFileDialog窗口出現(xiàn)后幾秒后程序會崩潰,代碼如下
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton,QFileDialog
from PyQt5.QtCore import QThread,pyqtSignal,QObject
import sys, threading
class MyWindow(QMainWindow):
def __init__(self, parent=None):
super(MyWindow, self).__init__(parent)
self.button = QPushButton('Hi')
self.button.clicked.connect(self.on_click)
self.setCentralWidget(self.button)
def on_click(self):
print("on_click",threading.current_thread().name)
self.thread = MyThread(self)
self.thread.pyqtSignal.connect(self.set_text)
self.thread.start()
def set_text(self,file_name):
print("setText",threading.current_thread().name)
self.button.setText(file_name)
class MyThread(QThread):
pyqtSignal = pyqtSignal(str)
def __init__(self,mv:QMainWindow) -> None:
super().__init__(None)
self.mv = mv
def run(self):
print('run',threading.current_thread().name)
file_name = QFileDialog.getOpenFileName(self.mv, '選擇文件', './', 'Excel files(*.xlsx , *.xls)')
print(file_name)
self.pyqtSignal.emit("Hello World")
if __name__ == '__main__':
app = QApplication([])
window = MyWindow()
window.show()
sys.exit(app.exec_())
輸出結果:
on_click MainThread
run Dummy-1
QObject::setParent: Cannot set parent, new parent is in a different thread
CoCreateInstance failed (操作成功完成。)
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x21fb451d190), parent's thread is QThread(0x21fb443b430), current thread is MyThread(0x21fb8788df0)
CoCreateInstance failed (操作成功完成。)
QObject::startTimer: Timers cannot be started from another thread
問題原因
PyQt中,必須在主線程中來創(chuàng)建子對象。
到此這篇關于PyQt子線程處理業(yè)務事件的問題解決的文章就介紹到這了,更多相關PyQt子線程內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Python開發(fā)圍棋游戲的實例代碼(實現(xiàn)全部功能)
圍棋是一種古老而復雜的策略棋類游戲,起源于中國,已有超過2500年的歷史,本文介紹了如何用Python開發(fā)一個簡單的圍棋游戲,實例代碼涵蓋了游戲的基本規(guī)則、界面設計、棋盤實現(xiàn)、棋子管理、游戲邏輯等多個方面,通過逐步實現(xiàn)落子、吃子、判斷勝負等功能2024-12-12

