《老鸟python 系列》视频上线了,全网稀缺资源,涵盖python人工智能教程,爬虫教程,web教程,数据分析教程以及界面库和服务器教程,以及各个方向的主流实用项目,手把手带你从零开始进阶高手之路!点击 链接 查看详情




消息循环

阅读:223606944    分享到

经过上一节课的学习,我们发现编写界面程序并不难,但是,我们仔细的去审查代码,就会发现每界面个程序的最后一行代码都是 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 定义消息队列,编程实现自定义消息循环的例子。


如果以上内容对您有帮助,请老板用微信扫一下赞赏码,赞赏后加微信号 birdpython 领取免费视频。


登录后评论

user_image
Anonymous
2020年3月26日 11:44 回复

但是Qt有个bug 事件分发过程中,遇到异步处理需要PostMessage分发,在调用过程中,某些情况下postmessage会阻塞,在高响应场景能不用emit就不用emit,举个神奇的例子,插入usb的瞬间调用emit会阻塞


user_image
1不2
2019年6月8日 08:13 回复

写的挺好的


user_image
小 坤
2019年4月8日 10:16 回复

卧槽,好屌


user_image
feemung
2019年2月23日 02:38 回复

大神多多更新Qt


user_image
yonka
2019年1月15日 06:26 回复

太强了,学习了。


user_image
DCjanus
2019年1月2日 18:17 回复

涨知识了。[赞][赞]