背景
最近在开发过程中,遇到了一个问题,我们需要通过点击一个按钮来执行一个耗时操作。若是一般的耗时操作,则进行一个异步设计即可。
但是此点特殊的情况在于,此耗时操作是个循环,且耗时极长,还需要在每次单个循环结束的时候刷新UI。这就导致异步操作变得不可行。我们只能选择通过创建子线程的方式去达成目标。这本是一个基础的UI开发问题,但是由于小组内缺少有经验的UI开发人员,只能其他开发边学边写。
详细思考与开发过程
首先先对到我手中的代码状态进行下说明,经过几次修正之后,到我手中的代码非常之神奇。阻塞是照阻不误的,刷新居然是隔几分钟刷新一次的,虽然我的算法部分依旧在运行,但是这个UI状态……如何让产品满意?领导可都是产品人员,这不得被diao死?下面是经过我调研理解之后的代码迭代过程:
step1:DEMO设计
在一开始的可行性开发DEMO如下,简单说其实很就是,点击按钮连接到点击槽函数上,槽函数执行单次的耗时操作,此耗时操作使用了Qthread线程摘出去运行。代码示例如下:
class TestWorker(QThread):
result_ready = pyqtSignal(bool, int, str) # 发送结果、行号和测试项文本
def __init__(self):
super().__init__()
def run(self):
# 执行耗时操作
result = func()
self.result_ready.emit(result, self.row, self.item_text)
class MainWindow(QWidget):
'''
主窗口,用以定义主窗口布局以及总控系统UE、线程
类变量:
无
'''
global result_global
def __init__(self):
'''
初始化类,定义实例变量与窗口初始化
'''
super().__init__()
self.initUI() # 窗口初始化
self.current_row = 0
def on_start_clicked(self):
# 开始遍历左侧测试内容 从表单第一行开始
if self.current_row < num:
worker = TestWorker()
worker.result_ready.connect(self.update_widget)
worker.start()
else:
print("所有项目已测试完成!")
#更新布局
def update_widget(self):
'''
刷新UI界面
'''
# 具体的刷新操作……
self.right_list_widget.show()
def initUI(self):
# 创建主垂直布局
main_vlayout = QVBoxLayout(self)
# 创建水平布局,包含“开始”按钮
button_hlayout = QHBoxLayout()
self.start_button = QPushButton("开始")
self.start_button.clicked.connect(self.on_start_clicked)
button_hlayout.addWidget(self.start_button)
# 将按钮的水平布局添加到主垂直布局的底部
main_vlayout.addLayout(button_hlayout)
# 设置窗口标题和大小
self.setWindowTitle('test')
if __name__ == '__main__':
print("UI初始化")
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
step2:加入循环,问题暴露
在上诉的DEMO开发演示过程中,并没有出现什么问题,同事就直接加入了while循环。聚焦于函数如下:
def on_start_clicked(self):
# 开始遍历左侧测试内容 从表单第一行开始
while self.current_row < num:
worker = TestWorker()
worker.result_ready.connect(self.update_widget)
worker.start()
self.current_row += 1
有经验的UI开发人员可能一眼就看出了问题……报错信息如下:
QThread: Destroyed while thread is still running。
解释说明:worker作为临时变量,它生命周期就是单个循环,也就是说,worker.start()、current自增后,worker作为一个变量就结束了,但线程才刚刚开始,所以报错。
step3:尝试解决
直接无脑增加了一个wait……
def on_start_clicked(self):
while self.current_row < num:
worker = TestWorker()
worker.result_ready.connect(self.update_widget)
worker.start()
worker.wait()
self.current_row += 1
现在虽然解决了报错,但是……这比没有开线程更差。主线程照样阻塞不说,还平白多了n个线程的创建和销毁过程……而且UI刷新?那是不可能刷新的。主循环已经死在这个while循环里面了,根本不会执行刷新的操作。
step4:神之一手
同事在经过上诉挫折之后,想出了一个神之方案,我到如今都没有理解代码是如何达成这个谜之结果的。具体的思路如下:
- 按下按钮之后开始执行on_start_clicked,并跳转到start_next_test()执行Qthread线程,
- 并把Qthread线程的结束信号连接到next_test_finished()函数,
- 在next_test_finished函数中又开始调用start_next_test……
现象是:阻塞是照阻不误的,刷新居然是隔几分钟也能刷新一次的。代码示例如下:
def on_start_clicked(self):
'''
当开始按钮被点击时,开始执行
'''
self.start_next_test()
def start_next_test(self):
if self.current_row < num:
worker = TestWorker()
worker.result_ready.connect(self.update_widget)
worker.test_finished.connect(self.next_test_finished)
worker.start()
worker.wait()
self.current_row += 1
def next_test_finished(self):
self.start_next_test()
step5:代码重构
到在下手中的时候就是上诉的状态,没想到我一个算法开发人员还需要进行UI的开发工作……
没办法,直接开始重构吧。
设定固定刷新机制
对于Qt的UI刷新机制,主循环会不断监听各种事件的发生,并响应。 所以针对上诉UI刷新相关的问题,我直接在主循环中每100ms提交一次UI刷新任务,在ui_init中运行即可。
def Mytimer(self):
timer = QTimer(self)
timer.timeout.connect(self.update_widget)
timer.start(100)
while循环全部放入Qthread中去
上诉代码最大的问题就是while循环放在了主循环中运行,使得主线程直接阻塞了,这才是根本原因,若不更正这个问题,后续所有的补救措施都是没用的。因为我们需要在子线程中更新主线程的UI,我直接设定了一个共享变量resultlist,Qthread线程实时的去更新该变量,而主线程也根据该变量每100ms刷新一次UI。
Qthread线程如下:
class TestWorker(QThread):
result_ready = pyqtSignal(bool, int, str) # 发送结果、行号和测试项文本
def __init__(self, num):
super().__init__()
self.num = num
def run(self):
global current_row
global test_cases
# 执行自动化脚本
while current_row < self.num:
result = func()
resultlist[current_row] = str(result)
current_row += 1
修改主线程UI刷新函数
如下图for循环所示,根据resultlist共享变量来刷新UI。注:update_widget就是上面Mytime函数链接的函数
def update_widget(self):
'''
刷新UI界面
'''
#刷新
for i,text in enumerate(resultlist):
item = self.right_list_widget.item(i)
item.setText(text)
if text == "False":
self.right_list_widget.item(i).setForeground(QBrush(Qt.darkRed))
else:
self.right_list_widget.item(i).setForeground(QBrush(Qt.darkGreen))
self.right_list_widget.show()
结尾
说实话,小组成员是第一次操作Qt中的Qthread模块,也是第一次对Qt UE进行编写,错漏难免,小生在解决上诉问题之后,记笔记到此,增进经验。