繁体   English   中英

Qt 事件和信号/插槽

[英]Qt events and signal/slots

Qt世界中,事件和信号/槽的区别是什么?

一个会取代另一个吗? 事件是信号/槽的抽象吗?

在 Qt 中,信号和事件都是观察者模式的实现 它们用于不同的情况,因为它们具有不同的优点和缺点。

首先,让我们准确定义“Qt 事件”的含义:Qt 类中的虚函数,如果您想处理该事件,则需要在您的基类中重新实现它。 它与模板方法模式有关

请注意我是如何使用“句柄”这个词的。 事实上,这是信号和事件意图之间的基本区别:

  • 你“处理”事件
  • 你“得到通知”信号发射

不同之处在于,当您“处理”事件时,您有责任以在类之外有用的行为“响应”。 例如,考虑一个应用程序,上面有一个带有数字的按钮。 该应用程序需要让用户聚焦按钮并通过按“向上”和“向下”键盘键来更改数字。 否则按钮应该像普通的QPushButton (它可以被点击等)。 在 Qt 中,这是通过创建您自己的QPushButton重用“组件”( 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)

看到了吗? 这段代码提出了一个新的抽象:一个像按钮一样的小部件,但有一些额外的功能。 我们非常方便地添加了此功能:

  • 由于我们重新实现了一个 virtual,我们的实现会自动封装在我们的类中。 如果 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()是可以互换的——只是它们是相关的。

所以信号和事件有不同的目的(但两者都可以让你“订阅”正在发生的事情的通知)。

我不喜欢到目前为止的答案。 – 让我专注于问题的这一部分:

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

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

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

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

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

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

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

什么抽象取决于情况:clicked()-signal 抽象鼠标事件(一个按钮在没有太多移动的情况下再次上下移动)。 键盘事件是来自较低级别的抽象(例如 果 或 é 是我系统上的几个击键)。

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

Qt 文档可能解释得最好:

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

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

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

可以产生信号。

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

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

操作系统和其他应用程序发送到您的程序的消息作为事件分派。

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

事件应该有一个接收器,它应该调度它。 没有其他人应该得到那个事件。

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

您不应该将信号视为事件,因为您可以在 Qt 文档中阅读:

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

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

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

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

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

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


请注意,请参阅下面关于 Kuba Ober 的这个答案的有用评论,这让我想知道它是否可能有点简单。

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

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

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

Leow Wee Kheng 的“事件处理”说:

在此处输入图片说明

茉莉花布兰切特说:

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

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

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

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

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

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

在我看来,事件是完全多余的,可以扔掉。 没有理由不能用事件代替信号或用信号代替事件,除非 Qt 已经按原样设置。 排队的信号被事件包裹,而事件可以被信号包裹,例如:

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM