发布于: 2023-9-27最后更新: 2023-9-28字数 00 分钟

Qt

事件系统

事件
事件是对各种应用程序需要知道的由应用程序内部或者外部产生的事情或者动作的通称。Qt 中使用一个对象来表示一个事件,继承自 QEvent 类。
需要说明的是,事件与信号并不相同,比如单击一下界面上的按钮,那么就会产生鼠标事件 QMou­seEvent (不是按钮产生的 ),而因为按钮被按下了 ,所以它会发出 clicked() 单击信号(是按钮产生的)。这里一般只关心按钮的单击信号,而不用考虑鼠标事件,但是如果要设计一个按钮,或者当单击按钮时让它产生别的效果,那么就要关心鼠标事件了。可以看到,事件与信号是两个不同层面的东西,发出者不同,作用也不同。在 Qt 中,任何 QObject 子类实例都可以接收和处理事件。
事件的处理
一个事件由一个特定的 QEvent 子类来表示,但是有时一个事件又包含多个事件类型,比如鼠标事件又可以分为鼠标按下、双击和移动等多种操作。这些事件类型都由 QEvent 类的枚举型 QEvent::Type 来表示,其中包含了 一百多种事件类型,可以在 QEvent 类的帮助文档中查看。虽然 QEvent 的子类可以表示一个事件,但是却不能用来处理事件,那么应该怎样来处理一个事件呢?在 QCoreApplication 类的 notify() 函数的帮助文档处给出了 5 种处理事件的方法:
  • 方法一:重新实现部件的 paintEvent()、mousePressEvent() 等事件处理函数。这是最常用的一种方法,不过它只能用来处理特定部件的特定事件。
  • 方法二:重新实现 notify() 函数。这个函数功能强大,提供了完全的控制,可以在事件过滤器得到事件之前就获得它们。但是,它一次只能处理一个事件。
  • 方法三:向 QApplication 对象上安装事件过滤器。因为一个程序只有一个 QApplication 对象,所以这样实现的功能与使用 notify() 函数是相同的,优点是可以同时处理多个事件。
  • 方法四:重新实现 event() 函数。QObject 类的 event() 函数可以在事件到达默认的事件处理函数之前获得该事件。
  • 方法五:在对象上安装事件过滤器。使用事件过滤器可以在一个界面类中同时处理不同子部件的不同事件。
在实际编程中,最常用的是方法一,其次是方法五。因为方法二需要继承自 QApplication 类;而方法三要使用一个全局的事件过滤器,这将减缓事件的传递,所以,虽然这两种方法功能很强大,但是却很少被用到。
事件的传递
在每个程序的 main() 函数的最后都会调用 QApplication 类的 exec() 函数,它会使 Qt 应用程序进人事件循环,这样就可以使应用程序在运行时接收发生的各种事件。一旦有事件发生,Qt 便会构建一个相应的 QEvent 子类的对象来表示,然后将它传递给相应的 QObject 对象或其子对象。下面通过例子来看一下 Qt 中的事件传递过程。(示例放到后面几节,进行深入分析)
这个图不错(但其实这个图只是示意,实际上不是这样的,准确一点的话最外层应该是WidgetWindow。懒得重画了,看下面实际的解析):可以看到,事件的传递顺序是这样的:先是事件过滤器,然后是焦点部件的 event() 函数,最后是焦点部件的事件处理函数,例如这里的键盘按下事件函数;如果焦点部件忽略了该事件,那么会执行父部件的事件处理函数,如上图所示。注意,event() 函数和事件处理函数,是在该部件内进行重新定义的,而事件过滤器却是在该部件的父部件中进行定义的。
notion image

exec的执行

通过源码的debug,深入了解开启事件循环的exec的执行过程:
  1. 逐步深入,最后调用的是QCoreApplication::exec()。最后本质上是启动了一个局部对象eventLoopexec.
    1. notion image
  1. QEventLoop::exec 本质上也是调用了processEvents。 启动了一个循环,不断处理事件,直到exit()被调用。这里loadAcquire使用的是内存模型同步。
    1. QEventLoop::processEvents根据官方文档:This function is simply a wrapper for QAbstractEventDispatcher::processEvents(). 这里根据不同平台,选择对应的eventDispatcher。顺便说一下源码中大量出现的d、Q_D,QObjectPrivateClass之类的,QT封装的数据的方式也很有意思。
      1. 总之,会调用具体分发器的事件处理. 当然Windows下也还有细分。
        1. 最终真正的实现是在QEventDispatcherWin32::processEvents核心代码也是一个事件处理的循环。
          1. 这就涉及Windows平台的事件系统,比如句柄、消息等了,就不往下展开了。
        可简化上述模型为:
        举一个例子分析:
        我们来看一下比较典型的事件流程是什么样的。假设某个界面程序上有一个 QPushButton 按钮,它的clicked信号被连接到了一个doWork()槽函数上。当用户在按钮上点击鼠标时,操作系统会给程序发送一个鼠标点击的事件,事件循环被唤醒,并将其转换为 QMouseEvent 对象,然后添加到事件队列中。随后在下一次循环时,事件从队列中取出,发送给 QPushButton 对象进行处理,进而在 QPushButton 事件处理函数的内部触发了clicked信号,最终使得doWork()函数被调用。假设点击事件发生后没有其他外部事件产生,doWork()函数执行完成后,也没有产生新的内部事件,那么此时队列没有其他事件可以继续处理,事件循环再次进入等待状态,否则就继续处理队列中的其他事件,直到将事件队列为空。 在事件循环中,事件只能一个接一个的进行处理,在前面的事件处理完成之前,后续的事件只能在队列中等待。在上面的例子中,如果doWork()的执行时间很长会发生什么呢?事件循环必须等待doWork()函数执行完返回后才能处理下一个事件,在这期间内,事件循环相当于被卡住了。卡住的事件循环无法处理任何事件,也无法收到新的外部事件,所以此时在这个程序内,界面无法更新,计时器也无法触发,网络 IO 也得不到任何反馈,表面上看就是“卡住了”。并且系统的窗口管理器也能检测到程序不再处理任何事件了,所以提示用户“程序失去响应”。

        示例演示和分析

        代码:
        子控件类
        父控件类
        代码里主要包括:
        1. 子控件类重写事件处理函数,即前面事件处理的方法1. MyLineEdit::keyPressEvent
        1. 子控件类重写event函数,即前面事件处理的方法4。 MyLineEdit::event
        1. 父控件类重写事件处理函数,即前面事件处理的方法1. Widget::keyPressEvent
        1. 父控件类重写事件过滤器函数,即前面事件处理的方法5. Widget::eventFilter. 这个生效的前提是给要过滤事件的控件进行注册:lineEdit->installEventFilter(this);
        运行代码,结果是:
        notion image
        下面结合源码调用堆栈对这四个顺序进行深入分析:
        1. Widget::eventFilter
          1. notion image
            从堆栈可以看到很多前面提到的函数调用。比如:
            QEventDispatcherWin32::processEvents,具体是调用下面的代码。通过DispatchMessage分发消息。然后内部调用USER32。
            事件系统的一系列处理接口,QWindowsGuiEventDispatcher::sendPostedEventsQGuiApplicationPrivate::processKeyEvent。
            重要的接口QApplication::notify这时已经拆解出事件接收者和事件了,接受者是从QWindowSystemInterfacePrivate::WindowSystemEvent里拆出来的window,这里就是WidgetWindow,即我们定义的Widget最外层的界面
            notion image
            notion image
            notify的官方文档是”Sends event to receiver: receiver->event(event).”
            因此接下来就调用WidgetWindow的event,然后调用forwardEvent向上抛出(抛给这个顶层界面内部的具有焦点的控件)
            然后再次调用notify.这时接收者已经是输入框的控件。
            notion image
            notify_helper中,有一段处理处理,返送给eventFilters处理。这段代码说明两点,一个是EventFilters要起作用,那肯定是要添加到事件接收者那个控件内才对其起作用。第二是如果如果其中的eventFilter返回了true,那就会直接捕捉完成这个事件,将不再继续处理该事件。
        1. MyLineEdit::event
          1. 前面部分跟上一个都是一样的。只是在前面处理完成后,在notify_helper代码中后一行,就会调用事件接受者(MyLineEdit)的event函数。注意event函数是处理所有事件的,所以重写event只需要判断其中自己关注的事件即可。
        1. MyLineEdit::keyPressEvent
          1. 前面的同上一点。只是处理完自己的event函数后,往上返回父类的event,直到使用QWidget::event提供默认的各种事件处理函数,这里就是keyPressEvent.然后调用这个虚事件处理函数。
        1. Widget::keyPressEvent
          1. 前面的还是同上。处理完事件接收者上的所有事件处理方式后,如果这个事件还是还没标记为处理完,即使用责任链的方式,将这个事件传递给它的父亲控件处理。可以看到有一个While循环进行处理。最终同上一点一样,会调用到keyPressEvent函数。

        强制处理事件

        在实际开发中,确实经常有一些很耗时的操作需求,比如复制大量文件,读写大量数据等等。这种情况一般有两种实现方案,一种是开启子线程进行耗时逻辑处理,避免主线程的事件循环阻塞,这种方式本文不做详细讨论,这里主要解释另一种:强制让事件循环处理事件。
        调用 QCoreApplication::processEvents() 函数可以强制使事件循环分发处理队列中剩下的事件,直到队列中没有事件为止,或者直到给定的时间结束。
        还是用前面的例子,假设doWork()函数是要向某个文件写入大量数据,执行时间很长,如果不采取特别的措施,在doWork()执行过程中肯定会阻塞事件循环,使界面失去响应。但如果在写入数据的过程中,没间隔固定一段,比如每写入 4MB,就调用一次processEvents(),就相当于在这个非常耗时的逻辑中,不时的间隔暂定一下,转而去处理事件队列中的事件。这样一种方式,就能在执行耗时逻辑的同时,穿插处理事件,而不至于让界面失去响应。当然,这个间隔需要开发者自己把握,如果间隔太小调用太频繁,会导致效率降低,如果间隔太大调用又太少,还是会导致一段时间内无法无法处理事件,界面卡顿。
        调用QCoreApplication::processEvents的原理是:
        记得事件循环前面的简化模型吗:
        这里相当于在调用dispatch_next_event时,分发前面一个事件处理耗费时间太长了,然后自己手动调用processEvents就相当于跳过了while循环中这一个事件的处理,然后继续处理其他事件了。
        测试代码:
        堆栈就可以看出来。在MyLineEdit一个函数迟迟完不成,然后手动调用processEvents,就恢复到启动事件循环时的processEvents一样了。
        notion image

        特殊的事件处理

        测试过程发现中文输入法下键盘输入无法正常被捕获为事件键盘事件,需特殊处理。具体参考:

        QObject体系对象数据系统

        C++

        内存模型

        C++11的内存模型是一种规定了多线程并发操作如何交互的规则。这个模型主要由两部分组成:原子操作(atomic operations)和内存顺序(memory orders)。
        原子操作
        原子操作是一种特殊的操作,它们在执行期间不能被中断。换句话说,原子操作在执行过程中不会被其他线程的操作所影响。C++11为各种基础类型(如intfloatdouble,指针等)提供了原子版本,它们都在std::atomic命名空间下。
        例如:
        std::atomic提供了一系列的操作,比如load(), store(), fetch_add(), fetch_sub()等,它们都是原子的。
        内存顺序
        内存顺序定义了原子操作的执行顺序。C++11提供了以下几种内存顺序:
        • std::memory_order_relaxed:不强制执行任何顺序,只保证了单个线程中的操作顺序。
        • std::memory_order_consume:保证了本线程中,所有依赖于当前原子操作的后续操作,都不会在当前操作之前执行。
        • std::memory_order_acquire:标记读。保证了本线程中,所有在当前原子读操作之后的读写操作,都不会在当前操作之前执行。
        • std::memory_order_release:标记写。保证了本线程中,所有在当前原子写操作之前的读写操作,都不会在当前操作之后执行。
        • std::memory_order_acq_rel:同时具有acquirerelease的效果。
        • std::memory_order_seq_cst:全序,即在所有线程中都按照一致的顺序看待原子操作。
        这些内存顺序对于控制线程间的同步非常有用。例如,使用std::memory_order_acquirestd::memory_order_release可以确保数据在被一个线程写入后,再被另一个线程读取。
        示例
        以下是一个使用std::memory_order的示例:
        在这个例子中,std::memory_order_acquirestd::memory_order_release一起使用,确保了数据的正确同步。
        这个模型的一个主要优点是,它提供了更精细的控制,以及可能的性能优化。然而,它也增加了编程的复杂性,需要更深入的理解以避免错误。

        Qt多线程使用疑难杂症
        Qt多线程使用疑难杂症
        秋招复盘9.25:随便重新复习一下
        秋招复盘9.25:随便重新复习一下