ChatGPT解决这个技术问题 Extra ChatGPT

Qt 事件和信号/槽

在 Qt 世界中,事件和信号/插槽有什么区别?

一个替换另一个吗?事件是信号/槽的抽象吗?


S
Stefan Monov

在 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() 可以互换 - 只是它们是相关的。

所以信号和事件有不同的目的(但它们都让你“订阅”发生某事的通知)。


我明白了。事件是每个类的,一个类的所有实例都会做出相同的反应,它们都会调用相同的 QClassName::event。但是信号是每个对象的,每个对象都可以有其唯一的信号槽连接。
R
Robert Siemer

到目前为止,我不喜欢这些答案。 – 让我集中讨论这部分问题:

事件是信号/槽的抽象吗?

简短的回答:没有。长答案提出了一个“更好”的问题:信号和事件如何相关?

空闲的主循环(例如 Qt 的)通常“卡”在操作系统的 select() 调用中。该调用使应用程序“休眠”,同时它将一堆套接字或文件或任何其他内容传递给内核要求:如果这些发生变化,则让 select() 调用返回。 – 内核,作为世界的主人,知道什么时候会发生。

select() 调用的结果可能是:套接字上的新数据连接到 X11,传入我们监听的 UDP 端口的数据包,等等。这些东西既不是 Qt 信号,也不是 Qt 事件,并且Qt 主循环自行决定是否将新数据转换为一个、另一个或忽略它。

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

这两个概念的一个区别在这里是可见的:一个插槽对于注册到该信号的其他插槽是否会被调用没有投票权。 – 事件更像是一个链,事件处理程序决定是否中断该链。在这方面,信号看起来像一颗星星或一棵树。

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

什么抽象取决于具体情况:clicked() 信号抽象鼠标事件(按钮上下移动而没有太多移动)。键盘事件是来自较低级别的抽象(像 果 或 é 是我系统上的几个键击)。

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


我发现这部分:“一个插槽对注册到该信号的其他插槽是否会被调用没有投票权”对于信号和事件之间的区别非常有启发性。但是,我有一个相关的问题:什么是消息以及消息如何与信号和事件相关? (如果他们是相关的)。消息是信号和事件的抽象吗?它们可以用来描述 QObject(或其他对象)之间的这种交互吗?
@axeoth:在 Qt 中,没有“消息”之类的东西。嗯,有一个消息框,但仅此而已。
@KubaOber:谢谢。我的意思是“事件”。
@axeoth:那么你的问题是无稽之谈。它写道:“什么是事件以及事件与信号和事件的关系如何”。
@KubaOber:一年后我不记得上下文了。无论如何,似乎我问的问题几乎与 OP 相同:事件之间有什么区别,以及事件与信号和插槽的关系如何。那时的情况是什么,IIRC,我正在寻找一种将 Qt 信号/插槽驱动的类包装在某种非 Qt 类中的方法,所以我正在寻找一种从后者内部命令第一个类的方法。我想我正在考虑向他们发送事件,因为这意味着我只需要包含 Qt 标头,而不是强制我的类实现信号/插槽。
H
Harald Scheirich

Qt documentation 可能解释得最好:

在 Qt 中,事件是派生自抽象 QEvent 类的对象,它们表示在应用程序内发生的事情或作为应用程序需要了解的外部活动的结果。事件可以被 QObject 子类的任何实例接收和处理,但它们与小部件特别相关。本文档描述了在典型应用程序中如何传递和处理事件。

所以事件和信号/槽是完成相同事情的两个并行机制。通常,事件将由外部实体(例如,键盘或鼠标滚轮)生成,并将通过 QApplication 中的事件循环传递。通常,除非您设置代码,否则您不会生成事件。您可以通过 QObject::installEventFilter() 过滤它们或通过覆盖适当的函数来处理子类对象中的事件。

信号和槽更容易生成和接收,您可以连接任意两个 QObject 子类。它们是通过元类处理的(请查看 moc_classname.cpp 文件了解更多信息),但您将产生的大多数类间通信可能会使用信号和插槽。信号可以立即传递或通过队列延迟(如果您使用线程)。

可以产生信号。


延迟队列与队列事件循环相同,还是存在两个队列?一个用于延迟信号,一个用于事件?
@Raphael 对你接受这个答案感到羞耻。 – 引用的段落甚至没有包含“信号”或“插槽”这两个词!
@neuronet:该段落被引用为“[它] 解释得最好”,但它没有。一点也不。一点也不是。
@Robert 因此,如果将那条小介绍行更改为“首先让我们考虑一下文档如何解释事件,然后再继续考虑它们与信号的关系”,您会接受答案吗?如果是这样,那么我们可以编辑答案以改进它,因为这是一个小问题,我同意可以更好地措辞。 [编辑这是一个真正的问题:因为我不确定它的其余部分会好得多......但仅仅因为引用的部分没有提到信号,如果它的其余部分实际上是好的,这似乎是一种肤浅的担忧,我就是不假设。]
@neuronet,如果您完全删除引用,它将改善我眼中的答案。 – 如果您删除整个答案,它将改善整个 QA。
P
Peter Mortensen

事件由事件循环分派。每个 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


这句话让我对 Qt 信号槽和事件之间的区别有一个简明的理解: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
感谢您节省我写自己答案的时间。同样主要的区别是事件是通过队列和事件循环传递的,而信号是直接从可观察对象传递给观察者的。一个事件在下一个事件循环周期被传递,而一个信号被立即传递。这也意味着事件执行是异步的和信号同步的。很不一样。
e
eric

有一篇文章详细讨论了事件处理:http://www.packtpub.com/article/events-and-signals

它在这里讨论了事件和信号之间的区别:

事件和信号是用于完成同一件事的两种并行机制。一般来说,信号在使用小部件时很有用,而事件在实现小部件时很有用。例如,当我们使用像 QPushButton 这样的小部件时,我们对它的 clicked() 信号比对导致信号发射的低级鼠标按下或按键事件更感兴趣。但是如果我们在实现 QPushButton 类,我们更感兴趣的是鼠标和按键事件的代码实现。此外,我们通常会处理事件,但会通过信号发射得到通知。

这似乎是一种常见的谈论方式,因为公认的答案使用了一些相同的短语。

请注意,请参阅下面 Kuba Ober 对此答案的有用评论,这让我想知道它是否有点简单化。


我看不出它们如何成为完成同一件事的两种机制——我认为这是一种无用的概括,对任何人都没有帮助。
@KubaOber 不,它有助于避免较低级别的细节。你会说 Python 没有帮助,因为你可以用机器代码做同样的事情吗? :)
我是说这句话的第一句话是泛泛而谈,以至于毫无用处。从技术上讲,这并没有错,只是因为如果您愿意,您可以使用事件传递来完成信号槽机制所做的事情,反之亦然。会很麻烦。所以,不,信号槽与事件不同,它们完成的事情不同,两者的存在对 Qt 的成功至关重要。引用的按钮示例完全忽略了信号槽系统的一般功能。
“事件和信号是两种并行机制,用于在某些 UI 小部件/控件的狭窄范围内完成相同的事情。”粗体部分严重缺失,使引用无济于事。即便如此,人们仍会质疑“同样的事情”到底完成了多少:信号让您无需在小部件上安装事件过滤器(或从小部件派生)就可以对点击事件做出反应。在没有信号的情况下执行此操作要麻烦得多,并且会将客户端代码与控件紧密耦合。那是糟糕的设计。
事件是一个无处不在的概念。 Qt 中的特定实现具体 将它们设置为传递给event 的数据结构。信号和槽具体地是方法,而连接机制是一种数据结构,它允许信号调用一个或多个与其连接的槽。我希望您看到将信号/插槽称为事件的某些“子集”或“变体”是无稽之谈,反之亦然。它们确实是在某些小部件的上下文中发生用于相似目的的不同事物。而已。您概括的越多,您对恕我直言的帮助就越小。
K
Kuba hasn't forgotten Monica

TL;DR:信号和槽是间接方法调用。事件是数据结构。所以它们是完全不同的动物。

它们聚集在一起的唯一时间是在跨线程边界进行槽调用时。槽调用参数被打包在一个数据结构中,并作为一个事件发送到接收线程的事件队列。在接收线程中,QObject::event 方法解包参数,执行调用,如果是阻塞连接,则可能返回结果。

如果我们愿意概括为遗忘,可以将事件视为调用目标对象的 event 方法的一种方式。这是一种间接的方法调用,虽然是一种时尚——但我认为这不是一种有用的思考方式,即使它是一个真实的陈述。


f
francek

'Event processing' Leow Wee Kheng 说:

https://i.stack.imgur.com/JV4K5.png

Jasmine Blanchette 说:

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


j
jkerian

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

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

确实,如果您正在等待某种事件(同样在一般情况下),您的插槽几乎肯定会接受 QEvent 子类作为参数。

话虽如此,信号和槽当然可以在没有 QEvents 的情况下使用,尽管您会发现激活信号的最初动力通常是某种用户交互或其他异步活动。但是,有时,您的代码会达到一个点,即触发某个信号将是正确的做法。例如,在长时间的过程中触发连接到 progress bar 的信号在此之前不涉及 QEvent。


“事件通常在 Qt 中使用信号/插槽处理” - 实际上没有...... QEvent 对象总是通过重载的虚拟对象传递!例如,QWidget 中没有信号“keyDown”——相反,keyDown 是一个虚函数。
同意 stefan,实际上是 Qt 的一个相当混乱的部分
@Stefan:我认为很少有 Qt 应用程序会覆盖 keyDown (而是主要使用诸如 QAbstractButton::clicked 和 QLineEdit::editingFinished 之类的信号)来证明“通常”是合理的。我之前肯定遇到过键盘输入,但这不是处理事件的常用方式。
编辑后(澄清“事件”的含义),您的帖子现在是正确的。不过,我避免重复使用这样的词。如果没有将信号称为“一种事件”,事情就已经够混乱了。
b
bootchk

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


u
user1095108

在我看来,事件是完全多余的,可以被丢弃。除了 Qt 已经按原样设置之外,没有理由不能用事件或事件替换信号。排队的信号由事件包装,事件可以被信号包装,例如:

connect(this, &MyItem::mouseMove, [this](QMouseEvent*){});

将替换 QWidget 中的便捷 mouseMoveEvent() 函数(但不再在 QQuickItem 中),并将处理场景管理器将为项目发出的 mouseMove 信号。信号由某个外部实体代表项目发出这一事实并不重要,并且在 Qt 组件的世界中经常发生,即使它被认为是不允许的(Qt 组件经常规避此规则)。但是 Qt 是许多不同设计决策的集合体,并且由于害怕破坏旧代码(无论如何这种情况经常发生),它几乎是一成不变的。


事件具有在必要时向上传播 QObject 父子层次结构的优势。信号/插槽连接只是在满足特定条件时直接或间接调用函数的承诺。没有与信号和槽相关的处理层次结构。
您可以使用信号实现类似的传播规则。没有规则。不能将某个信号重新发送给另一个对象(子对象)。您可以将 accepted 引用参数添加到信号和处理它的槽,或者您可以将结构作为带有 accepted 字段的引用参数。但是 Qt 做出了它做出的设计选择,现在它们已经一成不变了。
如果您按照建议的方式进行操作,您基本上只是在重新实现事件。
@法比奥A。这就是OP问题的重点。事件和信号可以/可以相互替换。
如果您重新实现事件,那么您不会替换事件。更重要的是:信号是在事件之上实现的。你需要一个“事件系统”来告诉其他一些事件循环,可能在一个不是你的线程中,将来某个时候它必须在某个地方执行给定的函数。