Qt事件和信号/槽

110
在 Qt 中,事件和信号/槽的区别是什么?它们是否可以相互替换?事件是否是信号/槽的一种抽象形式?

很高兴看到我不是唯一一个对此感到困惑的人 - undefined
10个回答

165
在Qt中,信号和事件都是观察者模式的实现。它们在不同情况下使用,因为它们有不同的优缺点。
首先让我们定义一下什么是"Qt事件":它是一个Qt类中的虚函数,如果你想要处理该事件,就需要在自己的基类中重新实现它。它与模板方法模式有关。
请注意我使用了"handle"这个词。实际上,信号和事件之间的意图有一个基本的区别:
  • 你"handle"事件
  • 你"get notified of"信号发射
区别在于当你"handle"事件时,你承担了用一个在类外有用的行为来"响应"的责任。例如,考虑一个带有数字按钮的应用程序。该应用程序需要允许用户通过按"上"和"下"键来聚焦按钮并更改数字。否则,该按钮应像正常的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”messagebox的小部件仅在一个特定情况下有用-因此它完全不可重用。同样,我们别无选择,只能通过外部连接来做正确的事情。
  • 我们可能希望将几个插槽连接到clicked(),或将几个信号连接到sayHello()。使用信号没有麻烦。通过子类化,您将不得不坐下来考虑一些类图,直到您决定适当的设计。

请注意,QPushButton发出clicked()的位置之一是在其mousePressEvent()实现中。这并不意味着clicked()mousePressEvent()可以互换-只是它们相关。

因此,信号和事件具有不同的目的(但是相关的是两者都让您“订阅”某些事情发生的通知)。


2
我明白了。事件是针对类的,该类的所有实例都会做出相同的反应,它们都将调用相同的 QClassName::event。但信号是针对对象的,每个对象都可以有其独特的信号槽连接。 - 炸鱼薯条德里克

44

到目前为止,我不喜欢这些答案。-让我集中在问题的这部分上:

事件是否是信号/插槽的抽象?

简短回答:不是。长答引出了一个更好的问题:信号和事件有什么关系?

一个空闲的主循环(例如Qt)通常会在操作系统的select()调用中“卡住”。当应用程序传输一堆套接字、文件或其他东西给内核并要求它们:如果其中的某些内容发生变化,请让select()调用返回。当内核知道那个变化发生时,应用程序就会“睡眠”。

select()调用的结果可能是:与X11连接的套接字上有新数据,我们监听的UDP端口收到了一个包等等。- 那些东西既不是Qt信号,也不是Qt事件,而且Qt主循环自己决定是否将新鲜数据转换为其中之一或忽略它。

Qt可以调用类似于keyPressEvent()的方法(或多个方法),从而将其转换为Qt事件。或者Qt发出一个信号,该信号实际上查找为该信号注册的所有函数,并依次调用它们。

这两个概念的一个区别在于:插槽对于是否调用注册到该信号的其他插槽没有任何投票权。-事件更像是一个链,事件处理程序决定是否中断该链或不中断。这方面,信号看起来像星形或树状结构。

事件可以触发或完全转换为信号(只需发出一个信号,而不调用“super()”)。信号可以转换为事件(调用事件处理程序)。

抽象什么取决于情况:clicked() -signal抽象了鼠标事件(按钮按下并松开而没有太多移动)。键盘事件是从较低级别的抽象(类似果或é的东西在我的系统上需要几个按键)。

也许focusInEvent()是相反的例子:它可以使用(和因此抽象)clicked()信号,但我不知道它实际上是否这样做。


4
我发现这句话:“一个槽没有投票权来决定是否调用注册到该信号的其他槽”对于信号和事件之间的区别非常有启发性。然而,我有一个相关的问题:消息是什么,它们与信号和事件有什么关系?(如果它们有关系)。消息是信号和事件的抽象吗?它们可以用来描述QObjects(或其他对象)之间的这种交互吗? - user1284631
@axeoth:在Qt中,没有“消息”这样的东西。嗯,有一个消息框,但也仅此而已。 - Kuba hasn't forgotten Monica
@KubaOber:谢谢。我是指“事件”。 - user1284631
3
@axeoth:那么你的问题就毫无意义了。它的意思是:“事件是什么,以及事件如何与信号和事件相关。” - Kuba hasn't forgotten Monica
@KubaOber: 一年后我已经记不得那个上下文了。不管怎样,貌似我当时问的和 OP 差不多:events 有什么区别,以及 events 与 signals 和 slots 的关系。当时实际情况是我在寻找一种方法,在一些非 Qt 类中包装 Qt signal/slot 驱动的类,所以我当时在考虑通过某种方式从后者命令前者。我想象当时我是在考虑向它们发送事件,因为那样做只需包含 Qt headers 即可,而不用强制让我的类去实现 signal/slots。 - user1284631

31
Qt文档可能是最好的解释:
在Qt中,事件是从抽象的QEvent类派生的对象,它们表示在应用程序内发生的事情或由应用程序需要知道的外部活动的结果。事件可以被任何QObject子类的实例接收和处理,但对于小部件来说尤为重要。本文档描述了事件在典型应用程序中如何传递和处理。
因此,事件和信号/槽是实现相同功能的两个并行机制。一般而言,事件将由外部实体(例如键盘或鼠标滚轮)生成,并通过QApplication中的事件循环传递。一般而言,除非您设置了代码,否则不会生成事件。您可以通过installEventFilter()过滤它们,或通过覆盖适当的函数在子类化对象中处理事件。
信号和槽更容易生成和接收,您可以连接任何两个QObject子类。它们通过元类进行处理(请查看您的moc_classname.cpp文件了解更多信息),但您可能会使用信号和槽产生的大部分类间通信。信号可以立即传递或通过队列(如果您正在使用线程)延迟传递。
信号可以被生成。

延迟队列与事件循环队列是相同的,还是存在两个队列?一个用于延迟信号,另一个用于事件? - Guillaume Paris
32
@Raphael,你接受这个答案真让人失望。引用的段落中甚至没有包含“signal”或“slot”这两个词! - Robert Siemer
1
@neuronet:这段话被引用为“[它]最好地解释了它”,但实际上并没有。一点也没有。甚至一点都不。 - Robert Siemer
@Robert,如果将那个简短的介绍改为“首先让我们考虑文档如何解释事件,然后再考虑它们与信号的关系”,你对答案满意吗?如果是这样,我们可以编辑答案以改进它,因为这只是一个小问题,我同意可以用更好的措辞表达。[编辑:这是一个真正的问题:我不确定其余部分是否更好...但是,如果其余部分实际上很好,那么似乎忽略引用部分未提到信号是一个肤浅的担忧如果,我并不是在假设。] - eric
5
如果你把引用部分全部删除,那么在我看来会改善这个答案。- 如果你把整个回答都删除,就会改善整个问答环节。 - Robert Siemer

19

事件由事件循环触发。每个GUI程序都需要一个事件循环,无论您是在Windows还是Linux上编写它,使用Qt、Win32或任何其他GUI库。同时,每个线程都有自己的事件循环。在Qt中,“GUI事件循环”(是所有Qt应用程序的主循环)是隐藏的,但您可以通过调用以下方式来启动它:

QApplication a(argc, argv);
return a.exec();

操作系统和其他应用程序发送到您的程序的消息被视为事件并进行调度。

信号和槽是Qt机制。在使用moc(元对象编译器)进行编译的过程中,它们会被转换为回调函数。

每个事件应该有一个接收者,应该由该接收者进行分派,其他人不应该获取该事件。

与发出的信号连接的所有槽将被执行。

您不应将信号视为事件,因为正如您可以在Qt文档中读到的那样:

当信号被发射时,与其连接的槽通常会立即执行,就像普通的函数调用一样。 当这种情况发生时,信号和槽机制完全独立于任何GUI事件循环。

当您发送事件时,必须等待一段时间,直到事件循环调度所有先前到达的事件。因此,发送事件或信号后的代码执行方式不同。发送事件后的代码将立即运行。对于信号和槽机制,它取决于连接类型。通常,它会在所有槽之后执行。使用Qt :: QueuedConnection,它将立即执行,就像事件一样。请查看Qt文档中的所有连接类型


1
这句话让我简洁地了解了Qt信号槽和事件之间的区别:当您发送事件时,它必须等待事件循环调度所有先前到达的事件的时间。因此,发送事件或信号后的代码执行是不同的。 - swdev
谢谢您帮我省下了编写答案的时间。再次强调,主要区别在于事件是通过队列和事件循环传递的,而信号直接从可观察者传递给观察者。事件会在下一个事件循环周期内被传递,而信号会立即传递。这也意味着事件执行是异步的,而信号是同步的。这是相当大的区别。 - Dimitrios Menounos

7
有一篇文章详细讨论了事件处理:http://www.packtpub.com/article/events-and-signals 它在这里讨论了事件和信号的区别:
事件和信号是用于完成相同任务的两种并行机制。作为一般性差异,当使用小部件时,信号很有用,而当实现小部件时,事件很有用。例如,当我们使用QPushButton之类的小部件时,我们更关心其clicked()信号,而不是导致信号发射的低级鼠标按下或键盘按下事件。但是,如果我们正在实现QPushButton类,我们更关心鼠标和键盘事件的代码实现。此外,我们通常处理事件,但通过信号发射进行通知。
这似乎是谈论它的常见方式,因为接受的答案使用了一些相同的短语。
请注意,下面有关于Kuba Ober答案的有用评论,使我想知道它是否有点简单化了。

我看不出来会有两种机制用于完成同一件事情 - 我认为这是一个无用的概括,对任何人都没有帮助。 - Kuba hasn't forgotten Monica
1
@KubaOber 不,它有助于避免低级细节。你会说Python没有帮助吗,因为你可以用机器码编写相同的东西吗? :) - eric
2
我想说的是,引用中的第一句话过于概括,以至于毫无用处。从技术上讲,它并不是不正确,但只是因为如果你愿意,可以使用事件传递来完成信号槽机制所做的事情,反之亦然。这将非常麻烦。因此,信号槽与事件不同,它们不能完成相同的事情,而两者都存在是Qt成功的关键。所引用的按钮示例完全忽略了信号槽系统的一般功能。 - Kuba hasn't forgotten Monica
1
“事件和信号是用于在某些UI小部件/控件的狭窄范围内完成相同任务的两种并行机制。” 粗体部分非常重要,没有它引用就不太有用。即使如此,也可以质疑是否真正实现了多少“相同任务”:信号允许您对单击事件做出反应,而无需在小部件上安装事件过滤器(或派生自小部件)。没有信号来做这件事情会更加麻烦,并且将客户端代码与控件紧密耦合。这是不好的设计。 - Kuba hasn't forgotten Monica
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Kuba hasn't forgotten Monica
显示剩余4条评论

5
TL;DR: 信号和槽是间接方法调用,事件是数据结构,它们是完全不同的东西。
只有在跨线程边界进行槽调用时,它们才会一起出现。槽调用参数被打包成数据结构,并作为事件发送到接收线程的事件队列中。在接收线程中,QObject::event 方法解包参数,执行调用,并在阻塞连接时可能返回结果。
如果我们愿意将其泛化到遗忘的程度,可以将事件视为调用目标对象的event方法的一种方式。这是一种间接的方法调用,但我认为这并不是一种有帮助的思考方式,即使它是一个真实的陈述。

5

'事件处理',作者Leow Wee Kheng说:

enter image description here

Jasmine Blanchette说:

使用事件而不是标准函数调用或信号和插槽的主要原因是,事件可以同步和异步地使用(取决于您调用sendEvent()还是postEvents()),而调用函数或调用插槽始终是同步的。事件的另一个优点是它们可以被过滤。


2

在Qt中,事件(以用户/网络交互的一般意义为例)通常使用信号/槽来处理,但信号/槽还可以做很多其他事情。

QEvent及其子类基本上只是用于框架与您的代码进行通信的小型标准化数据包。如果您想以某种方式关注鼠标,则只需查看QMouseEvent API,库设计人员无需每次需要弄清楚Qt API某个角落中鼠标所做的事情时都重新发明轮子。

确实,如果您正在等待某种类型的事件(再次以一般情况为例),则您的槽几乎肯定会接受一个QEvent子类作为参数。

话虽如此,信号和槽当然可以在没有QEvents的情况下使用,尽管您会发现激活信号的原始动机通常是某种用户交互或其他异步活动。但有时,您的代码将仅达到触发某个信号的正确时机。例如,在长时间进程期间触发连接到进度条的信号不涉及到那个点的QEvent。


2
在Qt中,事件通常通过信号/槽来处理——实际上不是这样的... QEvent对象总是通过重载的虚函数传递!例如,在QWidget中没有"keyDown"信号——相反,keyDown是一个虚函数。 - Stefan Monov
同意Stefan的观点,这确实是Qt中相当令人困惑的一部分。 - Harald Scheirich
1
@Stefan:我认为,很少有Qt应用程序覆盖keyDown(而是大多使用诸如QAbstractButton :: clicked和QLineEdit :: editingFinished之类的信号),以证明“通常”是合理的。 我肯定曾经抓住过键盘输入,但这不是处理事件的常规方式。 - jkerian
在您编辑后(澄清您所说的“事件”是什么意思),您的帖子现在是正确的。虽然事情已经够混乱了,但我还是避免重复使用那样的词语,不过称信号为“事件类型”确实会让事情更加混乱。 - Stefan Monov

1
另一个小的实用考虑:发射或接收信号需要继承 QObject,而任何继承的对象都可以发布或发送事件(因为您调用 QCoreApplication.sendEvent()postEvent() )。这通常不是问题,但是使用信号时,PyQt 奇怪地要求 QObject 必须是第一个超类,而您可能不想重新排列继承顺序以便能够发送信号。

-1
在我看来,事件是完全冗余的,可以被抛弃。没有理由不能用信号替换事件或用事件替换信号,除了Qt已经按照现有方式设置好了。排队信号被事件包装,事件也可以被信号包装,例如:
connect(this, &MyItem::mouseMove, [this](QMouseEvent*){});

将替换在QWidget中找到的方便的mouseMoveEvent()函数(但不再在QQuickItem中),并处理场景管理器为该项发出的mouseMove信号。信号代表某个外部实体代表该项发出并不重要,在Qt组件世界中经常发生,尽管据说不允许(Qt组件经常规避此规则)。但是,Qt是许多不同设计决策的综合体,并且基本上是铸成石头的,以免破坏旧代码(无论如何都经常发生)。


事件具有在必要时向上传播QObject父子层次结构的优势。信号/槽连接只是承诺在满足某些条件时直接或间接调用函数。信号和槽没有相关的处理层次结构。 - jonspaceharper
你可以使用信号来实现类似的传播规则。并没有规定某个信号不能被重新发出到另一个对象(子对象)。你可以为信号添加一个“accepted”引用参数以及处理它的插槽,或者你可以将一个带有“accepted”字段的结构作为引用参数。但是Qt做出了它所做的设计选择,并且现在已经铸成了定局。 - user1095108
1
如果按照你的建议去做,基本上就是重新实现事件。 - Fabio A.
@FabioA。这就是OP问题的关键所在。事件和信号可以/可能互相替代。 - user1095108
如果你重新实现事件,那么你并没有替换事件。更进一步说:信号是在事件之上实现的。为了告诉其他事件循环(可能是不属于你的线程),在将来的某个时刻它必须在某个地方执行给定函数,你需要一种“事件系统”。 - Fabio A.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接