界面阻塞问题
前面我们的练习里开发了一个类似 Postman 的HTTP接口测试工具。
其中,具体发送请求消息的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| def sendRequest(self):
method = self.ui.boxMethod.currentText() url = self.ui.editUrl.text() payload = self.ui.editBody.toPlainText()
headers = {}
req = requests.Request(method, url, headers=headers, data=payload )
prepared = req.prepare()
self.pretty_print_request(prepared) s = requests.Session() try: r = s.send(prepared) self.pretty_print_response(r) except: self.ui.outputWindow.append( traceback.format_exc())
|
这里有一个问题:
我们 点击发送按钮 发送HTTP消息消息,如果服务端接收处理的比较慢,就会导致下面这行代码中的send方法要比较长的时间才能返回。
这会导致什么问题呢?
假设10秒钟后,才接收到服务端的响应消息,这时候,界面就会 僵死 10秒钟。
这期间,你点击界面没有任何反应。
为什么呢?
原因
这是因为,我们现在的代码都是在主线程中执行的。
其中最末尾的代码
其实会让主线程进入一个死循环,循环不断的处理 用户操作的事件。
当我们点击发送按钮后,Qt的 核心代码就会接受到这个 点击事件,并且调用相应的 slot函数去处理。
因为我们代码做了这样的设置
1 2
| self.ui.buttonSend.clicked.connect(self.sendRequest)
|
指定了点击发送按钮由 sendRequest 方法处理。
如果这个sendRequest 很快能接收到 服务端的相应,那么sendRequest就可以很快的返回。
返回后, 整个程序又进入到 app.exec_() 里面接收各种 事件,并且调用相应的函数去处理。界面就不会僵死,因为所有的操作界面的事件,都能得到及时的处理。
但是,如果这个sendRequest 要很长时间才能返回,这段时间内,整个程序就停在 下面这行代码处
自然就没有机会去处理其他的用户操作界面的事件了,当然程序就僵死了。
子线程处理
典型的一种解决方法就是使用多线程去处理。
关于Python的多线程的讲解,可以点击参考我们这里的教程
修改代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| def sendRequest(self):
method = self.ui.boxMethod.currentText() url = self.ui.editUrl.text() payload = self.ui.editBody.toPlainText()
headers = {}
req = requests.Request(method, url, headers=headers, data=payload )
prepared = req.prepare()
self.pretty_print_request(prepared) s = requests.Session()
thread = Thread(target = self.threadSend, args= (s, prepared) ) thread.start()
def threadSend(self,s,prepared):
try: r = s.send(prepared) self.pretty_print_response(r) except: self.ui.outputWindow.append( traceback.format_exc())
|
这样,通过创建新的线程去执行发送方法,服务器响应再慢,也只会在新线程中阻塞
主线程启动新线程后,就继续执行后面的代码,返回继续运行Qt的事件循环处理 ,可以响应用户的操作,就不会僵死了。
VIP 实战班学员请联系老师获取完整代码示例。
子线程发信号更新界面
点击这里,边看视频讲解,边学习下面的内容
上面的示例中,我们在子线程里面操作了界面,如下代码所示
1 2 3 4 5 6 7 8 9 10
| def threadSend(self,s,prepared):
try: r = s.send(prepared) self.pretty_print_response(r) except: self.ui.outputWindow.append( traceback.format_exc())
|
Qt建议: 只在主线程中操作界面 。
在另外一个线程直接操作界面,可能会导致意想不到的问题,比如:输出显示不全,甚至程序崩溃。
但是,我们确实经常需要在子线程中 更新界面。比如子线程是个爬虫,爬取到数据显示在界面上。
怎么办呢?
这时,推荐的方法是使用信号。
前面我们曾经看到过 各种 Qt 控件可以发出信号,比如 被点击、被输入等。
我们也可以自定义类,只要这个类继承QObject类,就能发出自己定义的各种Qt信号,具体做法如下:
自定义一个Qt 的 QObject类,里面封装一些自定义的 Signal信号
怎么封装自定义的 Signal信号?参考下面的示例代码。
一种信号定义为 该类的 一个 静态属性,值为Signal 实例对象即可。
可以定义 多个 Signal静态属性,对应这种类型的对象可以发出的 多种 信号。
注意:Signal实例对象的初始化参数指定的类型,就是 发出信号对象时,传递的参数数据类型。因为Qt底层是C++开发的,必须指定类型。
定义主线程执行的函数处理Signal信号(通过connect方法)
在新线程需要操作界面的时候,就通过自定义对象 发出 信号
通过该信号对象的 emit方法发出信号, emit方法的参数 传递必要的数据。参数类型 遵循 定义Signal时,指定的类型。
主线程信号处理函数,被触发执行,获取Signal里面的参数,执行必要的更新界面操作
一个示例代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| from PySide2.QtWidgets import QApplication, QTextBrowser from PySide2.QtUiTools import QUiLoader from threading import Thread
from PySide2.QtCore import Signal,QObject
class MySignals(QObject):
text_print = Signal(QTextBrowser,str)
update_table = Signal(str)
global_ms = MySignals()
class Stats:
def __init__(self): self.ui = QUiLoader().load('main.ui')
global_ms.text_print.connect(self.printToGui)
def printToGui(self,fb,text): fb.append(str(text)) fb.ensureCursorVisible()
def task1(self): def threadFunc(): global_ms.text_print.emit(self.ui.infoBox1, '输出内容') thread = Thread(target = threadFunc ) thread.start()
def task2(self): def threadFunc(): global_ms.text_print.emit(self.ui.infoBox2, '输出内容')
thread = Thread(target=threadFunc) thread.start()
|