在 Qt 中,信号和事件都是 Observer pattern 的实现。它们用于不同的情况,因为它们具有不同的优点和缺点。
首先,让我们准确定义“Qt 事件”的含义:Qt 类中的虚函数,如果您想处理该事件,您需要在您的基类中重新实现它。它与 Template Method pattern 有关。
注意我是如何使用“句柄”这个词的。实际上,信号和事件的意图之间存在基本区别:
你“处理”事件
您“得到通知”信号发射
不同之处在于,当您“处理”事件时,您有责任用在课堂外有用的行为来“响应”。例如,考虑一个应用程序,其上有一个带有数字的按钮。该应用程序需要让用户通过按“向上”和“向下”键盘键来聚焦按钮并更改数字。否则,按钮应该像普通的 QPushButton
一样工作(可以点击等)。在 Qt 中,这是通过创建自己的小可重用“组件”(QPushButton
的子类)来完成的,它重新实现了 QWidget::keyPressEvent
。伪代码:
class NumericButton extends QPushButton
private void addToNumber(int value):
// ...
reimplement base.keyPressEvent(QKeyEvent event):
if(event.key == up)
this.addToNumber(1)
else if(event.key == down)
this.addToNumber(-1)
else
base.keyPressEvent(event)
看?这段代码提出了一个新的抽象:一个像按钮一样的小部件,但具有一些额外的功能。我们非常方便地添加了这个功能:
由于我们重新实现了虚拟,我们的实现自动封装在我们的类中。如果 Qt 的设计者将 keyPressEvent 设为信号,我们将需要决定是继承 QPushButton 还是只是从外部连接到信号。但这将是愚蠢的,因为在 Qt 中,您总是希望在编写具有自定义行为的小部件时继承(有充分的理由 - 可重用性/模块化)。因此,通过使 keyPressEvent 成为事件,他们传达了他们的意图,即 keyPressEvent 只是功能的基本构建块。如果它是一个信号,那么它看起来就像一个面向用户的东西,而它本来不是这样的。
由于函数的基类实现是可用的,我们通过处理我们的特殊情况(向上和向下键)并将其余部分留给基类来轻松实现责任链模式。您可以看到,如果 keyPressEvent 是一个信号,这几乎是不可能的。
Qt 的设计是经过深思熟虑的——它们使我们容易做正确的事情而很难做错误的事情(通过使 keyPressEvent 成为事件),从而使我们陷入了成功的陷阱。
另一方面,考虑 QPushButton
的最简单用法 - 只是实例化它并在点击它时收到通知:
button = new QPushButton(this)
connect(button, SIGNAL(clicked()), SLOT(sayHello())
这显然是由该类的用户完成的:
如果我们每次想要某个按钮通知我们点击时都必须继承 QPushButton 子类,那将需要很多子类,没有充分的理由!单击时始终显示“Hello world”消息框的小部件仅在一种情况下有用-因此它完全不可重用。同样,我们别无选择,只能做正确的事——通过外部连接。
我们可能想将多个插槽连接到 clicked() - 或将多个信号连接到 sayHello()。有了信号就不会大惊小怪了。使用子类化,您将不得不坐下来思考一些类图,直到您决定合适的设计。
请注意,QPushButton
发出 clicked()
的位置之一是在其 mousePressEvent()
实现中。这并不意味着 clicked()
和 mousePressEvent()
可以互换 - 只是它们是相关的。
所以信号和事件有不同的目的(但它们都让你“订阅”发生某事的通知)。
到目前为止,我不喜欢这些答案。 – 让我集中讨论这部分问题:
事件是信号/槽的抽象吗?
简短的回答:没有。长答案提出了一个“更好”的问题:信号和事件如何相关?
空闲的主循环(例如 Qt 的)通常“卡”在操作系统的 select() 调用中。该调用使应用程序“休眠”,同时它将一堆套接字或文件或任何其他内容传递给内核要求:如果这些发生变化,则让 select() 调用返回。 – 内核,作为世界的主人,知道什么时候会发生。
select() 调用的结果可能是:套接字上的新数据连接到 X11,传入我们监听的 UDP 端口的数据包,等等。这些东西既不是 Qt 信号,也不是 Qt 事件,并且Qt 主循环自行决定是否将新数据转换为一个、另一个或忽略它。
Qt 可以调用一个(或多个)方法,如 keyPressEvent(),有效地将其转换为 Qt 事件。或者 Qt 发出一个信号,它实际上查找为该信号注册的所有函数,并一个接一个地调用它们。
这两个概念的一个区别在这里是可见的:一个插槽对于注册到该信号的其他插槽是否会被调用没有投票权。 – 事件更像是一个链,事件处理程序决定是否中断该链。在这方面,信号看起来像一颗星星或一棵树。
一个事件可以触发或完全变成一个信号(只发出一个信号,不要调用“super()”)。信号可以转换为事件(调用事件处理程序)。
什么抽象取决于具体情况:clicked() 信号抽象鼠标事件(按钮上下移动而没有太多移动)。键盘事件是来自较低级别的抽象(像 果 或 é 是我系统上的几个键击)。
也许 focusInEvent() 是一个相反的例子:它可以使用(并因此抽象)clicked() 信号,但我不知道它是否真的这样做。
Qt documentation 可能解释得最好:
在 Qt 中,事件是派生自抽象 QEvent 类的对象,它们表示在应用程序内发生的事情或作为应用程序需要了解的外部活动的结果。事件可以被 QObject 子类的任何实例接收和处理,但它们与小部件特别相关。本文档描述了在典型应用程序中如何传递和处理事件。
所以事件和信号/槽是完成相同事情的两个并行机制。通常,事件将由外部实体(例如,键盘或鼠标滚轮)生成,并将通过 QApplication
中的事件循环传递。通常,除非您设置代码,否则您不会生成事件。您可以通过 QObject::installEventFilter()
过滤它们或通过覆盖适当的函数来处理子类对象中的事件。
信号和槽更容易生成和接收,您可以连接任意两个 QObject
子类。它们是通过元类处理的(请查看 moc_classname.cpp 文件了解更多信息),但您将产生的大多数类间通信可能会使用信号和插槽。信号可以立即传递或通过队列延迟(如果您使用线程)。
可以产生信号。
事件由事件循环分派。每个 GUI 程序都需要一个事件循环,无论您使用 Qt、Win32 还是任何其他 GUI 库编写 Windows 或 Linux 程序。同样,每个线程都有自己的事件循环。在 Qt 中,“GUI 事件循环”(这是所有 Qt 应用程序的主循环)是隐藏的,但是您开始调用它:
QApplication a(argc, argv);
return a.exec();
操作系统和其他应用程序发送到您的程序的消息作为事件分派。
信号和槽是 Qt 机制。在使用 moc(元对象编译器)进行编译的过程中,它们被更改为回调函数。
事件应该有一个接收者,它应该分派它。没有其他人应该得到那个事件。
所有连接到发射信号的槽都将被执行。
您不应该将信号视为事件,因为您可以在 Qt 文档中阅读:
当一个信号发出时,连接到它的槽通常会立即执行,就像一个普通的函数调用一样。发生这种情况时,信号和槽机制完全独立于任何 GUI 事件循环。
当您发送一个事件时,它必须等待一段时间,直到事件循环调度所有较早发生的事件。因此,发送事件或信号后代码的执行是不同的。发送事件后的代码将立即运行。对于信号和槽机制,它取决于连接类型。通常它将在所有插槽之后执行。使用 Qt::QueuedConnection 会立即执行,就像事件一样。检查all connection types in the Qt documentation。
When you send an event, it must wait for time when event loop dispatch all events that came earlier. Because of this, execution of the cod after sending event or signal is different
有一篇文章详细讨论了事件处理:http://www.packtpub.com/article/events-and-signals
它在这里讨论了事件和信号之间的区别:
事件和信号是用于完成同一件事的两种并行机制。一般来说,信号在使用小部件时很有用,而事件在实现小部件时很有用。例如,当我们使用像 QPushButton 这样的小部件时,我们对它的 clicked() 信号比对导致信号发射的低级鼠标按下或按键事件更感兴趣。但是如果我们在实现 QPushButton 类,我们更感兴趣的是鼠标和按键事件的代码实现。此外,我们通常会处理事件,但会通过信号发射得到通知。
这似乎是一种常见的谈论方式,因为公认的答案使用了一些相同的短语。
请注意,请参阅下面 Kuba Ober 对此答案的有用评论,这让我想知道它是否有点简单化。
event
的数据结构。信号和槽具体地是方法,而连接机制是一种数据结构,它允许信号调用一个或多个与其连接的槽。我希望您看到将信号/插槽称为事件的某些“子集”或“变体”是无稽之谈,反之亦然。它们确实是在某些小部件的上下文中发生用于相似目的的不同事物。而已。您概括的越多,您对恕我直言的帮助就越小。
TL;DR:信号和槽是间接方法调用。事件是数据结构。所以它们是完全不同的动物。
它们聚集在一起的唯一时间是在跨线程边界进行槽调用时。槽调用参数被打包在一个数据结构中,并作为一个事件发送到接收线程的事件队列。在接收线程中,QObject::event
方法解包参数,执行调用,如果是阻塞连接,则可能返回结果。
如果我们愿意概括为遗忘,可以将事件视为调用目标对象的 event
方法的一种方式。这是一种间接的方法调用,虽然是一种时尚——但我认为这不是一种有用的思考方式,即使它是一个真实的陈述。
'Event processing' Leow Wee Kheng 说:
https://i.stack.imgur.com/JV4K5.png
您将使用事件而不是标准函数调用或信号和槽的主要原因是事件可以同步和异步使用(取决于您是调用 sendEvent() 还是 postEvents()),而调用函数或调用插槽始终是同步的。事件的另一个优点是它们可以被过滤。
事件(一般意义上的用户/网络交互)通常在 Qt 中用信号/槽处理,但信号/槽可以做很多其他事情。
QEvent 及其子类基本上只是用于框架与您的代码进行通信的小型标准化数据包。如果你想以某种方式关注鼠标,你只需要查看 QMouseEvent API,库设计者不必每次需要弄清楚鼠标在某个角落做了什么时都重新发明轮子Qt API。
确实,如果您正在等待某种事件(同样在一般情况下),您的插槽几乎肯定会接受 QEvent 子类作为参数。
话虽如此,信号和槽当然可以在没有 QEvents 的情况下使用,尽管您会发现激活信号的最初动力通常是某种用户交互或其他异步活动。但是,有时,您的代码会达到一个点,即触发某个信号将是正确的做法。例如,在长时间的过程中触发连接到 progress bar 的信号在此之前不涉及 QEvent。
另一个小的实用考虑:发出或接收信号需要继承 QObject
而任何继承的对象都可以发布或发送事件(因为您调用 QCoreApplication.sendEvent()
或 postEvent()
)这通常不是问题,但是:使用信号 PyQt奇怪的是要求 QObject
是第一个超类,并且您可能不想为了能够发送信号而重新排列继承顺序。)
在我看来,事件是完全多余的,可以被丢弃。除了 Qt 已经按原样设置之外,没有理由不能用事件或事件替换信号。排队的信号由事件包装,事件可以被信号包装,例如:
connect(this, &MyItem::mouseMove, [this](QMouseEvent*){});
将替换 QWidget
中的便捷 mouseMoveEvent()
函数(但不再在 QQuickItem
中),并将处理场景管理器将为项目发出的 mouseMove
信号。信号由某个外部实体代表项目发出这一事实并不重要,并且在 Qt 组件的世界中经常发生,即使它被认为是不允许的(Qt 组件经常规避此规则)。但是 Qt 是许多不同设计决策的集合体,并且由于害怕破坏旧代码(无论如何这种情况经常发生),它几乎是一成不变的。
QObject
父子层次结构的优势。信号/插槽连接只是在满足特定条件时直接或间接调用函数的承诺。没有与信号和槽相关的处理层次结构。
accepted
引用参数添加到信号和处理它的槽,或者您可以将结构作为带有 accepted
字段的引用参数。但是 Qt 做出了它做出的设计选择,现在它们已经一成不变了。