在Qt世界中,事件和信号/插槽的区别是什么?
有人替换另一个吗?事件是信号/插槽的抽象吗?
在Qt中,信号和事件都是Observer模式的实现.它们用于不同的情况,因为它们具有不同的优点和缺点.
首先让我们确切地定义'Qt事件'的含义:Qt类中的虚函数,如果你想处理事件,你应该在你的基类中重新实现.它与模板方法模式有关.
请注意我是如何使用" 句柄 " 一词的.实际上,这是信号和事件的意图之间的基本区别:
你" 处理 "事件
您" 收到 "信号发射的通知
不同之处在于,当您"处理"事件时,您承担责任"回应"在课堂外有用的行为.例如,考虑一个应用程序,其上有一个带有数字的按钮.应用程序需要让用户关注按钮并通过按"向上"和"向下"键盘键来更改数字.否则按钮应该像正常一样运行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
每次想要一些按钮通知我们点击时都必须进行子类化,那么就没有充分的理由需要很多子类!messagebox
单击时始终显示"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文档可能是最好的解释吧:
在Qt中,事件是从抽象
QEvent
类派生的对象,它们表示在应用程序中发生的事件或者应用程序需要了解的外部活动的结果.事件可以由QObject
子类的任何实例接收和处理,但它们与小部件特别相关.本文档描述了在典型应用程序中如何传递和处理事件.
因此事件和信号/槽是实现相同事物的两种并行机制.通常,事件将由外部实体(例如,键盘或鼠标滚轮)生成,并将通过事件循环传递QApplication
.通常,除非您设置代码,否则您将不会生成事件.您可以QObject::installEventFilter()
通过覆盖相应的函数来过滤它们或处理子类对象中的事件.
信号和插槽更容易生成和接收,您可以连接任何两个QObject
子类.它们通过Metaclass处理(有关更多信息,请查看您的moc_classname.cpp文件),但您将生成的大多数类间通信可能会使用信号和插槽.信号可以立即传递或通过队列延迟(如果您使用线程).
可以生成信号.
事件循环调度事件.每个GUI程序都需要一个事件循环,无论您使用Qt,Win32还是任何其他GUI库编写Windows或Linux.每个线程都有自己的事件循环.在Qt"GUI事件循环"(这是所有Qt应用程序的主循环)是隐藏的,但你启动它美其名曰:
QApplication a(argc, argv); return a.exec();
消息发送到程序的OS和其他应用程序将作为事件发送.
信号和插槽是Qt机制.在使用moc(元对象编译器)的编译过程中,它们被更改为回调函数.
事件应该有一个接收器,应该发送它.没有人应该得到那个事件.
将执行连接到发射信号的所有插槽.
您不应该将Signals视为事件,因为您可以在Qt文档中阅读:
发出信号时,通常会立即执行与其连接的插槽,就像正常的函数调用一样.发生这种情况时,信号和插槽机制完全独立于任何GUI事件循环.
发送事件时,它必须等待一段时间,直到事件循环调度之前发生的所有事件.因此,在发送事件或信号之后执行代码是不同的.发送事件后的代码将立即运行.信号和插槽机制取决于连接类型.通常它将在所有插槽之后执行.使用Qt :: QueuedConnection,它将立即执行,就像事件一样.检查Qt文档中的所有连接类型.
有一篇文章详细讨论了事件处理:http://www.packtpub.com/article/events-and-signals
它讨论了事件和信号之间的区别:
事件和信号是用于完成同样事情的两种并行机制.作为一般差异,信号在使用窗口小部件时很有用,而事件在实现窗口小部件时很有用.例如,当我们使用像QPushButton这样的小部件时,我们对它的clicked()信号比对导致信号发出的低级鼠标按下或按键事件更感兴趣.但是如果我们实现QPushButton类,我们对鼠标和键事件代码的实现更感兴趣.此外,我们通常处理事件,但通过信号发射得到通知.
这似乎是谈论它的常用方式,因为接受的答案使用了一些相同的短语.
请注意,请参阅下面有关Kuba Ober的回答的有用评论,这让我想知道它是否有点过分简单.
TL; DR:信号和插槽是间接方法调用。事件是数据结构。因此它们是完全不同的动物。
它们唯一的在一起就是跨越线程边界进行插槽调用。插槽调用参数打包在数据结构中,并作为事件发送到接收线程的事件队列。在接收线程中,该QObject::event
方法将对参数进行解压缩,执行调用,并可能在阻塞连接的情况下返回结果。
如果我们愿意泛化为遗忘,则可以将事件视为调用目标对象event
方法的一种方式。这是一种间接的方法调用,是一种流行的方式-但我认为这不是思考的有用方法,即使它是真实的声明也是如此。