「Qt」事件概念

0、引言:

        在本文所属专栏的前面的文章里,我们介绍了Qt的 信号(Signal)槽(Slot) 机制。信号(Signal)槽(Slot) 机制是 Qt 框架用于多个对象之间通信的,是 Qt 的核心特性,也是 Qt 与其他框架最大的不同之处。Qt 的元对象系统是信号和槽实现的基础。以 QPushButton 的父类 QAbstractButton 为例,QAbstractButton 类提供了一些常见的信号

void clicked(bool checked = false)        //点击信号(当鼠标指针在按钮范围内按下随后释放时该信号会被发出)
void pressed()                            //按下信号(按钮按下时该信号被发出)
void released()                           //释放信号(按钮释放时该信号被发出)
void toggled(bool checked)                //切换信号(checkable按钮状态改变时该信号被发出)

下面使用 QPushButton 对象验证这些信号细节:

  • 按钮能被释放的前提按钮已经被按下了,这是常识。所以鼠标指针在按钮区域外时按下,保持按下状态进入按钮区域内随后松开,不会发出 released() 释放信号;
  • 鼠标指针在按钮区域内按下并保持按下的状态离开按钮区域时,会发出 released() 释放信号。这就像我们拿手指按住键盘上一个按键然后慢慢挪开,挪开时按键会自动弹起来,对吧?这符合常识;

  • QPushButton 默认不是checkable按钮,所以如果我们不主动设置“checkable”属性为真时,它任何时候都不会发出 toggled() 切换信号(设置“checkable”属性为真后 QPushButton 具备了两种状态:按下或弹起);

  • QCheckBox(复选框) 和 QRadioButton(单选按钮) 默认都是checkable按钮,只要状态发生改变就会发出 toggled() 切换信号;

        接下来,要介绍 Qt 中另外一个概念 —— 事件。还是以 QAbstractButton 为例,QAbstractButton 类提供了一些常见的事件处理函数

virtual void changeEvent(QEvent *e) override                //改变事件处理函数(在按钮的一些属性发生改变时被调用)
virtual bool event(QEvent *e) override                      //事件分发函数
virtual void focusInEvent(QFocusEvent *e) override          //获得焦点事件处理函数
virtual void focusOutEvent(QFocusEvent *e) override         //失去焦点事件处理函数
virtual void keyPressEvent(QKeyEvent *e) override           //键盘按下事件处理函数
virtual void keyReleaseEvent(QKeyEvent *e) override         //键盘释放事件处理函数
virtual void mouseMoveEvent(QMouseEvent *e) override        //鼠标移动事件处理函数
virtual void mousePressEvent(QMouseEvent *e) override       //鼠标按下事件处理函数
virtual void mouseReleaseEvent(QMouseEvent *e) override     //鼠标释放事件处理函数
virtual void paintEvent(QPaintEvent *e) override = 0        //绘制事件处理函数
virtual void timerEvent(QTimerEvent *e) override            //定时器事件处理函数

        细心的小伙伴可能已经发现了:事件处理函数中也有鼠标按下和释放事件的处理函数,鼠标按下事件pressed() 按下信号似乎是差不多的东西?似乎都表示鼠标被按下了?反正只要按下按钮,mousePressEvent(QMouseEvent *e) 肯定会被调用,pressed() 信号肯定也会被发出?

        答案当然是“事件信号是不同的概念,但有一定联系”,它们的区别将会在本文第二章详细罗列。会产生这样的疑惑也很正常,这是因为 Qt 框架自带的信号大多是在相应的事件处理函数中被发出的(当然有些也不是),所以有时我们可能认为这俩是差不多的东西,但其实并不一样。

        还是以 QAbstractButton 类为例:

  • pressed() 信号是在 mousePressEvent() 事件处理函数中被发出的;

  • released() 信号可能是在 mouseReleaseEvent() / mouseMoveEvent() / focusOutEvent() 这几个事件处理函数中被发出的:
    • mouseReleaseEvent() 会触发发出 released() 信号,这是正常的,对应的情况是“在按钮区域内按下并释放鼠标”;

    • mouseMoveEvent() 触发发出 released() 信号对应的情况是“在按钮区域内按下并保持按下的状态离开按钮区域时发出 released() 信号”;

    • focusOutEvent() 触发发出 released() 信号对应的情况是“只要失去焦点之前按钮处于按下状态,那么失去焦点时就会释放按钮并发出 released() 信号”;

  • clicked() 信号是在 mouseReleaseEvent() 事件处理函数中被发出的,对应的情况是“在按钮区域内按下并释放鼠标”;

  • toggled() 信号是在 setChecked() 函数中被发出的。如果按钮是checkable的,那么在鼠标释放事件处理函数 mouseReleaseEvent() 中也会调用 setChecked() 函数,从而发出 toggled() 信号;

(注:以上所有点都可以自行验证,验证方法为写一个类继承自 QPushButton (或者别的 QAbstractButton 子类的)类并重写它的一些事件处理函数,然后在设计界面添加一个 QPushButton 按钮并将其"提升为"我们的子类就可以了)

        从上面的多个GIF图中可以看到:当我们重写了特定事件的处理函数时,需要去调用基类的对应事件处理函数来执行默认的事件处理;否则该事件可能不会得到正确的处理(比如信号可能不会被发出等)。当然如果我们想要有意地屏蔽基类的事件处理,就不用调用基类的事件处理函数了。

1、事件的概念:

        几乎所有的图形用户界面(GUI)程序都是采用基于事件驱动的编程模型的Qt 也不例外。

        当然常见的GUI框架同时也是支持数据驱动的编程模型的,比如 QtWinFormWPF 等。本文属于 Qt 专栏,这里就只介绍 Qt 的数据驱动的编程模型(作为本文的拓展内容):

        Qt 提供了一些内置的数据模型,比如 QAbstractItemModelQAbstractListModelQAbstractTableModel 等,也可以自定义数据模型来适应不同的需求。Qt 还提供了一些视图类,比如 QListViewQTableViewQTreeView 等,可以显示不同类型的数据模型,并通过双向绑定机制与数据模型同步。Qt 还提供了一些委托类,比如 QItemDelegateQStyledItemDelegate 等,可以自定义视图中的单元格的显示和编辑方式。

为了使用数据驱动的编程模型,Qt 还需要使用数据库驱动来连接不同类型的数据库,比如MySQLSQLiteOracle等。数据库驱动是一种插件,它负责与数据库进行通信,执行 SQL 语句,返回查询结果等。Qt 提供了一些常用的数据库驱动,比如QMYSQLQSQLITEQOCI等,也可以自己编译数据库驱动来支持其他类型的数据库。为了使用数据库驱动,Qt 还提供了一些类,比如 QSqlDatabaseQSqlQueryQSqlRecord 等,可以方便地操作数据库。

        Qt事件是一种描述程序内部或外部发生的动作或消息的对象,继承自QEvent类。Qt事件可以由系统产生,比如鼠标点击、键盘输入等,也可以由程序产生,比如定时器事件、重绘事件等。

        Qt事件的处理和传递是基于Qt事件循环的,当程序调用 QApplication::exec() 时,就进入了事件循环。事件循环会从系统消息队列中读取并转换为QEvent对象,然后将其分发给相应的QObject对象或其子对象。QObject对象可以通过重写 event() 函数或者特定的事件处理函数(如 mousePressEvent()keyPressEvent() 等)来处理事件,也可以通过安装事件过滤器(eventFilter)来拦截和处理其他对象的事件。

        事件处理函数有一个参数用于传递事件对象,还是以 QAbstractButton 类为例,下图是该类重写的事件处理函数:

        可以看到这一堆事件处理函数中的参数有 QEvent *、QFocusEvent *、QKeyEvent *、QMouseEvent * 等,它们都是QEvent及其子类的指针。根据该事件对象参数,我们可以获取事件的详细信息,并根据需要进行处理。

比如QMouseEvent *表示鼠标事件对象,它包含了鼠标的位置、按钮、状态等信息:

int QMouseEvent::x() const        //鼠标指针相对于控件左上角的x坐标
int QMouseEvent::y() const        //鼠标指针相对于控件左上角的y坐标

int QMouseEvent::globalX() const  //鼠标指针相对于屏幕左上角的x坐标
int QMouseEvent::globalY() const  //鼠标指针相对于屏幕左上角的y坐标

Qt::MouseButton QMouseEvent::button() const    //返回引起该事件的鼠标按键
Qt::MouseButtons QMouseEvent::buttons() const  //返回事件生成时鼠标按键的状态。按钮状态是Qt::LeftButton、Qt::RightButton、Qt::MidButton使用或(|)运算符的组合。对于鼠标移动事件,这是所有按下的按钮;对于鼠标按下并双击事件,这包括引起该事件的按钮;对于鼠标释放事件,这是排除引起该事件之外的按钮

QKeyEvent *表示键盘事件对象,它包含了按下的键、修饰符、文本等信息:

int QKeyEvent::key() const    //返回按下或释放的那个按键编码(编码详见Qt::Key)

Qt::KeyboardModifiers QKeyEvent::modifiers() const    //返回事件发生后立即存在的键盘修饰符标志
quint32 QKeyEvent::nativeModifiers() const            //返回按键事件的本机修饰符。如果按键事件不包含此数据,则返回0

QString QKeyEvent::text() const    //返回该按键生成的Unicode文本

(更多内容可以参考这些事件类的Qt官方文档)

2、「Qt」信号 与 事件 的区别:

  • 事件来源于系统消息的转换,信号(大多)来源于对象中的事件处理函数。
  • 事件由具体的QObject对象进行处理,信号由连接的槽函数进行处理。
  • 事件更底层,它是封装系统消息形成的,信号更偏上层一点。
  • 改写事件处理函数可能导致程序行为改变,单独的信号不对程序行为造成影响。
  • 事件处理函数(如果有)返回值是有意义的,它表示事件是否已经被识别并处理,并决定了是否传递给基类或父类做进一步处理;而信号和槽是没有返回值的。

        事件(event)是由系统或者Qt应用程序自身产生的一种消息,通常是由用户的操作(如鼠标点击、键盘输入等)或者底层的软硬件信息触发的。事件是被动的,需要被对象接收和处理。
信号(signal)是由Qt对象自身发出的一种通知,通常是在对象的状态发生变化或者执行某些操作时发出的。信号是主动的,可以被其他对象连接和响应。

        事件和信号之间有一定的联系,但也有本质的区别。事件是Qt中最基础的通信机制,信号是Qt中特有的扩展机制。事件可以被过滤、转发、忽略、接受,信号只能被连接、断开、发射、阻塞。事件是同步或异步的,信号总是异步的。事件在对象中有特定的处理函数,信号在对象中没有处理函数,只有槽函数。

        事件和信号之间也有一些区别,例如事件是按照层次结构从子对象向父对象传递的,而信号是按照连接顺序调用槽函数的;事件使用了一个事件队列来维护,而信号处理是立即回调的;事件更底层,它是与平台相关的,而信号更上层,它是与平台无关的。

3、Qt事件处理机制模型:

(并非Qt官方提供的模型,而是笔者依照自己对Qt事件的理解和归纳画出来的,仅供参考)

        对于该事件处理机制模型,事件的传递顺序为:

事件传递顺序

        下面是Qt事件从产生到处理完成的全过程阐述(由New Bing提供支持):

4、事件分发 —— event()函数:

        我们在查阅 Qt 官方文档时,可能会发现几乎所有QObject的派生类都重写了 event() 函数,这个函数到底有什么来头?其实,event() 函数源自于QObject类的一个公共虚函数,主要用于事件的分发。

        当事件发生时,Qt 将创建一个事件对象。Qt 中所有事件类都继承于QEvent。在事件对象创建完毕后,Qt 将这个事件对象传递给QObject的 event() 函数。event() 函数并不直接处理事件,而是将这些事件对象按照它们不同的类型,分发给不同的事件处理器(event handler,即事件响应函数)。

        事件处理函数和事件对象之间有两种方式相互通信:

  1. 一种是通过返回bool值来表示是否已处理,这种方式用于 QApplication::notify(), QObject::eventFilter(), QObject::event() 等函数;
  2. 另一种是通过调用 QEvent::accept()QEvent::ignore() 对事件进行标识,这种方式只用于 event() 函数和特定事件处理函数之间的沟通,而且只有用在某些类别事件上是有意义的,这些事件就是会被转发的事件,包括:鼠标,滚轮,按键等事件;

一个小例子:

        还是用上面测试信号与事件时用的那个例子,窗口很简单长这样:

带有一个按钮的小窗口
  • 该例的控件层次结构很明了:窗口是顶层控件(顶层控件即没有父控件的控件),按钮是底层控件(即没有子控件的控件);
  • QApplication::notify() 在派发事件时会优先选择最具体的控件来处理事件。在这个例子中,如果有一个子控件能够接收鼠标按下事件,就不会再向上查找父控件了:
    • 也就是说当我们在按钮区域外、窗口区域内按下鼠标时,窗口会处理我们的鼠标按下事件;
    • 当我们在按钮区域内按下鼠标时,窗口就不会处理我们的鼠标按下事件了;

  • 对于例子中的鼠标按下事件,QApplication::notify() 函数会根据鼠标的位置和控件的几何形状来判断这个事件应该派发给哪个控件接收。它会从最顶层的控件开始,沿着控件的层次结构往下查找,直到找到能够接收该鼠标事件的最底层的控件,然后调用它的event() 函数来处理鼠标事件;
  • 如果按钮的鼠标按下事件处理函数忽略了该事件(调用 event->ignore() 函数),这样就会告诉 QApplication::notify() 函数沿着控件的层次结构继续向上查找父控件来继续处理该鼠标事件;

  • 当有多个子控件发生堆叠时,会根据谁显示在上层来派发事件;
    两个测试按钮在控件层次结构中属于同一层次,但在显示层面会存在谁显示在谁的上层的情况,两者并不冲突。谁显示在上层,就由谁的鼠标释放事件处理函数来发出clicked信号

        event() 函数有一个bool返回值,如果返回true,表示事件已经被处理,不需要再向下分发或者调用默认的事件处理器。如果返回false,表示事件没有被处理,需要继续分发或者调用默认的事件处理器。

        在事件响应函数中,我们可以调用 QEvent::accept() 或 QEvent::ignore() 函数来控制事件的传递,决定事件是否被当前对象处理或传递给其他对象进行处理;而在 event() 函数中,调用事件对象的 accept()ignore() 函数是没有作用的,不会影响到事件的传播。

5、事件过滤器 —— eventFilter()函数:

        事件过滤器是一种可以监视其他对象事件的机制,它可以在事件到达目标对象之前对其进行检查和处理。事件过滤器需要调用 installEventFilter() 函数安装在目标对象上,然后重写eventFilter() 函数。事件过滤器的返回值表示是否已经处理了事件,如果返回true,表示事件已经被过滤器处理,不会再发送给目标对象。如果返回false,表示事件未被过滤器处理,会继续发送给目标对象或其他过滤器。

        Qt事件处理器和事件过滤器的区别是,事件处理器是用来处理特定类型的事件,比如mousePressEvent() 用来处理鼠标按下事件,keyPressEvent() 用来处理键盘按下事件等。事件过滤器是用来拦截目标对象的所有事件,可以根据需要检查和丢弃事件。事件过滤器可以安装多个,而事件处理器只能重写一个。

        我们需要事件过滤器的原因是,有些时候我们想要拦截或修改某些对象的事件,但是又不想继承或重写该对象的事件处理函数。比如,我们想要在一个对话框中使用空格键切换输入焦点,但是我们不想继承QLineEdit重写 keyPressEvent() 函数。这时候,我们可以使用事件过滤器来实现这个功能。

下面是一个使用事件过滤器解决问题的实例:Qt自定义控件 —— 子控件与父控件的鼠标事件问题_YMGogre的博客-CSDN博客https://blog.csdn.net/YMGogre/article/details/129357734

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值