PyQt5多线程编程
单位的公共仪器需要预约才能够使用,但是自从预约时间改到了早上,就很不好约了,所以决定写一个程序实现自动预约。由于还要分享给实验室其它同学使用,就得写一个GUI了,这一下代码量瞬间变成了原来的10倍,不过学到了些新东西,这波不亏。
在图形化编程中一个很重要的一点就是使用多线程,将UI线程独立出来。如果你的程序不存在会造成线程阻塞的操作,不使用多线程倒也没什么大问题,但是如果存在像联网、读写文件等可能需要等待的操作,使用单线程就可能会造成UI假死,这对于用户体验来说是非常不友好的。例如微软的office套件为了兼容老式CPU,IO等操作都是使用的UI的线程,打开或保存大文件时,用户界面无响应;而像photoshop这样的程序由于使用了多线程,即使有大量的IO行为,UI也可以响应用户的操作。
Qt引以为豪的对象通讯机制被称为信号-槽(signal-slot)机制。当特定事件被触发时(如子线程结束)将发送一个信号,而与该信号建立的连接槽,则可以接收到该信号并做出反应(根据子线程的返回值执行操作)。
在我的程序中,需要在仪器开放预约前不断查询,在开放预约的第一刻立即提交表单,而为了不拖垮服务器,需要在每次查询后让线程休眠一会。如果所有这些操作都在主线程中完成,那么线程休眠的时候UI就会无响应,在我的程序中就表现为点击开始查询后UI就一直处于无响应状态,直到预约成功后才恢复响应。
因此,就需要新建一个线程,这个线程只用来发送信号调用查询函数,发送完毕后即休眠,等待下一次发送或任务完成后结束线程。
灵魂画手的流程图要创建一个新线程,需要在程序中定义一个类,这个类要继承QtCore.QThread
,然后把要执行的操作放进run()
函数中,线程启动后,就会自动运行这个函数。结束线程时通过调用线程的terminate()
方法来中止线程:
class QueryWaitThread(QtCore.QThread):
# 定义线程需要用到的信号
query_signal = QtCore.pyqtSignal() # 查询信号
on_complete = QtCore.pyqtSignal(int) # 每次查询完成时发送,可以发送参数,
此处参数int为查询次数
terminate_thread = QtCore.pyqtSignal() # 结束线程信号
def __init__(self):
super(QueryWaitThread, self).__init__()
def run(self): # 只发送信号,然后休眠。
i = 1
while True:
self.on_complete.emit(i) # 调用信号的emit()方法
i += 1
self.query_signal.emit()
sleep(60)
def kill_thread(self):
self.terminate()
我的查询函数与验证函数:
def auto_query(self):
################
# 查询操作 #
################
if self.good_to_submit():
self.btnstop.hide()
self.btn_resel.show()
main.Appoint(self)
def good_to_submit(self):
if # 还无法提交表单:
return False
self.T.terminate_thread.emit() # 验证可以提交表单后发送中止信号
return True
def print_current_count(self, i):
self.normalOutputWritten("当前为第 %i 次查询...\n" % i)
self.normalOutputWritten()
是一个我自己定义的函数,用于在窗体的文本框中打印信息,你也可以定义自己的这类函数。
定义好信号以后,接下来需要调用信号的connect()
方法将信号连接到需要执行函数上。下面的start()
和stop()
是窗体按钮按下后执行的操作,我们在这两个函数中使用刚刚定义好的信号:
def start(self):
################
# do something #
################
self.T = QueryWaitThread()
self.T.query_signal.connect(self.auto_query)
self.T.on_complete.connect(self.print_current_count)
self.T.terminate_thread.connect(self.T.kill_thread)
self.normalOutputWritten("已开启自动查询,当有符合条件的时间段时将自动提交预约...\n")
self.T.start() # 开始执行线程函数
self.btnstop.show()
self.btn_resel.hide()
def stop(self):
self.btnstop.hide()
self.btn_resel.show()
self.T.terminate_thread.emit() # 发送中止信号
self.normalOutputWritten("用户终止查询。\n")
当按下开始按钮后,程序会构造出一个线程T
,执行到T.start()
后,其中的run()
函数便会进入while
循环,不断发送查询信号,直到good_to_submit()
函数返回True
表明已经可以提交表单,或者当停止按钮按下后终止。
改进后的程序只是将定时功能分离了出来,实际上联网查询功能还是使用主线程完成的,在程序等待服务器返回数据的过程中UI还是会有短暂的无响应时间,不过这是在用户点击后产生的,因此对体验影响不大。但是,严格来说联网也应该分离出一个线程,毕竟服务器有时候返回会很慢,这时候如果你的用户是一个暴躁老哥,还是会对体验有影响的。