簡體   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