经过上一节课的学习,我们发现编写界面程序并不难,但是,我们仔细的去审查代码,就会发现每界面个程序的最后一行代码都是
app.exec()
。这行代码到底做了什么呢,本节课我们就来深度剖析这行代码。
我们知道 python 的界面库有很多,无论是 Qt,还是
wxPython,GTK,任何界面程序库的实现都离不开消息循环,包括其他语言的界面库也一样。Qt 的消息循环是在app.exec()
函数里实现的。
我们先来分析一下界面程序是怎么运行的,界面程序运行后,我们可以在界面上做各种动作,比如拖动该界面,点击界面内的按钮,在界面内的文本框内输入字符等等操作,
我们所有的操作都会转化为一个消息,比如拖动界面是 mouseMove
消息,点击界面内的按钮是 click
消息,在界面内的文本框内输入字符是
keydown
消息等等,Qt 会把每个消息放入消息队列中,然后一直检查消息队列里面有没有新的消息到来,如果有的话就处理该消息,然后把该消息从消息队列中移除。这种投递消息和处理消息都是在
app.exec()
函数中完成的,该函数就是 Qt 的消息循环函数。
初次接触界面库的同学们可能会想,界面库是不是至少要有两个线程,一个子线程做消息投递的行为,而主线程中的 app.exec()
来做消息处理的行为,其实整个界面库是单线程的,也就是说消息投递和消息处理都是在
app.exec()
函数中实现的,包括其他主流的界面库也是这样的。
本节课,为了让大家更深刻的了解界面库的消息循环,我们就自己编程来实现消息循环。因为老鸟说过一句名言:如果你想彻底了解它,你就亲自去实现它。
import sys msg = [] # 用全局变量定义一个消息队列 class MsgLoop(object): def __init__(self): super(MsgLoop, self).__init__() def getmessage(self): try: newmsg = msg[0] del msg[0] return newmsg except: return def msg_clicked(self): print("do clicked msg") def msg_keydown(self): print("do keydown msg") # 处理键盘消息 def msg_mousemove(self): print("do mousemove msg") # 处理鼠标移动消息 def msgcheck(self): while True: onemsg = input("投递一个消息:") # 输入要模拟的消息 msg.append(onemsg) # 投递消息 newmsg = self.getmessage() # 从消息队列里面取出消息 if newmsg == "clicked": self.msg_clicked() # 处理点击消息 elif newmsg == "keydown": self.msg_keydown() # 处理键盘消息 elif newmsg == "mousemove": self.msg_mousemove() # 处理鼠标移动消息 else: pass class MyApplication(object): def __init__(self, argv): super(MyApplication, self).__init__() self.msgloop = MsgLoop() def _exec(self): self.msgloop.msgcheck() # 我们自定义的消息循环 app = MyApplication(sys.argv) # 类似 qt 的 app = QApplication(sys.argv) print("显示界面") app._exec() # 进入消息循环
结果如下:
显示界面 投递一个消息:keydown do keydown msg 投递一个消息:clicked do clicked msg 投递一个消息:cry 投递一个消息:mousemove do mousemove msg 投递一个消息:
实际上的界面程序的消息都是我们在界面上一系列的动作产生的,比如点击,移动鼠标,键盘输入等等。在上面的程序中,我们用输入来模拟产生一个消息,本质上都是一样的。
通过上面例子,我们发现在主线程的消息循环函数 _exec()
中,我们同时实现了消息投递和消息处理。
在上面的例子中,我们用一个全局的 list 变量来定义消息队列,一般情况下,界面库用的消息队列是阻塞的,比如用 queue 对象来实现阻塞队列。
最后大家要注意,处理消息的函数如果是阻塞的或者是耗时的,程序就会出现卡住或者卡顿,所以,我们一般在做消息处理函数时,都要保证里面的逻辑是非阻塞的和非耗时的,如果你的消息处理函数里面有导致程序阻塞的网络 IO 或者磁盘 IO,你可以使用非阻塞的 IO 操作,比如非阻塞套接字,非阻塞文件描述符;如果你的程序里面有耗时的 CPU 计算,你可以把该计算分开来计算,比如,分成 N 个消息进行投递,调用 N 次消息处理函数来完成整个计算需求。但是,如果你的消息处理函数必然要阻塞或者耗时,你可以单开一个线程去做,我们在此给大家演示一下。
import sys import time import threading msg = [] # 用全局变量定义一个消息队列 class MsgLoop(object): def __init__(self): super(MsgLoop, self).__init__() def getmessage(self): try: newmsg = msg[0] del msg[0] return newmsg except: return def msg_clicked(self): # 阻塞或耗时的操作 time.sleep(10) print("do clicked msg") def msg_keydown(self): print("do keydown msg") # 处理键盘消息 def msgcheck(self): while True: onemsg = input("投递一个消息:") # 输入要模拟的消息 msg.append(onemsg) # 投递消息 newmsg = self.getmessage() # 从消息队列里面取出消息 if newmsg == "clicked": t = threading.Thread(target=self.msg_clicked) t.start() # 开启一个新的线程处理阻塞或耗时的行为 elif newmsg == "keydown": self.msg_keydown() # 处理键盘消息 else: pass class MyApplication(object): def __init__(self, argv): super(MyApplication, self).__init__() self.msgloop = MsgLoop() def _exec(self): self.msgloop.msgcheck() # 我们自定义的消息循环 app = MyApplication(sys.argv) # 类似 qt 的 app = QApplication(sys.argv) print("显示界面") app._exec() # 进入消息循环
结果如下:
显示界面 投递一个消息:clicked 投递一个消息:keydown do keydown msg do clicked msg 投递一个消息:
我们投递一个 clicked
消息后,在处理该消息的函数里面,我们用
time.sleep(10)
来模拟阻塞或耗时行为,我们的程序并不需要等该消息处理完成,可以继续投递
keydown
消息,并处理该消息,这样就让我们的界面程序从而变得平滑。同样,我们编写 Qt 的界面程序,如果碰到类似的问题,也需要一样的处理方案。
知道什么是消息循环。
知道什么是消息队列。
用阻塞队列 queue 定义消息队列,编程实现自定义消息循环的例子。
但是Qt有个bug 事件分发过程中,遇到异步处理需要PostMessage分发,在调用过程中,某些情况下postmessage会阻塞,在高响应场景能不用emit就不用emit,举个神奇的例子,插入usb的瞬间调用emit会阻塞