QT信號和槽


信號和槽(Signals and Slots)

(原創)本文翻譯自Qt 5.10.1 參考文檔,(翻譯的本意是為了更好學習,如有錯誤請批評指正)

信號和槽用於對象之間的通信。信號和槽機制是Qt的核心特性,可能是與其他框架提供的特性最大不同的部分。信號和槽是由QT的元對象系統實現的。

引言

在GUI編程中,當我們更改一個小部件時,我們通常希望通知另一個小部件。更一般地,我們希望任何類型的對象都能相互通信。例如,如果用戶單擊“關閉”按鈕,我們可能希望調用窗口的CLOSE()函數。

其他工具包使用回調實現這種通信。回調是函數的指針,因此,如果希望處理函數通知您某個事件,則將指向另一個函數(回調)的指針傳遞給處理函數。然后,處理函數在適當的時候調用回調。雖然使用這種方法的成功框架確實存在,但回調可能是不直觀的,並且在確保回調參數的類型正確性方面可能會遇到問題。

信號和槽(Signals and Slots

在Qt中,我們有一個替代回調技術的方法:我們使用信號和槽。在特定事件發生時發出信號。Qt的部件有許多預定義的信號,但是我們總是可以通過將部件子類化來添加我們自己的信號到它們中去。槽是響應特定信號而被調用的函數。Qt的部件有許多預定義的槽,但是通常的做法是子類化部件並添加您自己的插槽,這樣您就可以處理你感興趣的信號了。

信號和槽機制是類型安全的:信號的簽名必須與接收槽的簽名相匹配。(事實上,槽的簽名可能比它所接收信號的簽名短,因為它可以忽略額外的參數。)由於簽名是兼容的,編譯器可以幫助我們在使用基於函數指針的語法時檢測類型不匹配。基於字符串的信號和槽語法將在運行時檢測類型不匹配。信號和槽是松散耦合的:發出信號的類,不知道也不關心哪個槽接收該信號。如果您將一個信號連接到一個槽,Qt的信號和槽機制能確保在適當的時候用信號的參數調用該槽。信號和插槽可以接受任意數量任何類型的參數。它們絕對是類型安全的。

所有從QObject或其子類(例如QWidget)繼承的類都可以包含信號和槽。當對象以其他對象可能感興趣的方式改變其狀態時,它們就會發出信號。這就是通信時對象所做的全部。它不知道也不關心是否有誰正在接收它發出的信號。這就是真正的信息封裝,並確保對象可以用作軟件組件。

 槽可用來接收信號,但它們也是正常的成員函數。就像一個對象不知道是否有誰接收它的信號一樣,一個槽也不知道是否有什么信號連接到它。這確保了可以使用Qt創建真正獨立的組件。

 您可以將任意多的信號連接到單個槽,也可根據需要將一個信號連接到任意多個插槽。甚至可以將一個信號直接連接到另一個信號。(每當發出第一個信號時,將立即發出第二個信號。)

信號和槽一起構成了強大的組件編程機制。

信號(Signals)

當對象的內部狀態以某種對象的客戶或所有者感興趣的方式發生改變時,該對象發出的信號。信號是公共訪問函數,可以從任何地方發出,但我們建議只從定義信號的類及其子類中發出。

當信號發出時,連接到它的槽通常會立即執行,就像一般的函數調用一樣。這種情況下,信號和插槽機制完全獨立於任何GUI事件循環。一旦所有槽返回,就會執行發出信號語句后面的代碼。在使用隊列連接時,情況略有不同;在這種情況下,Emit關鍵字后面的代碼將立即執行,而槽將在稍后執行。

如果多個槽連接到一個信號,則當信號被發射時,槽將按照它們連接的順序一個接一個地執行。

信號由MOC自動生成,不能在.cpp文件中實現。它們永遠不能有返回類型(即使用void)。

關於參數的說明:我們的經驗表明,如果信號和插槽不使用特殊類型,它們就方便重用。試想QScrollBar::lue eChanged()使用一種特別類型,如假定QScrollBar::Rnge,則只能將其連接到專門為QScrollBar設計的插槽。將不同的輸入部件連接在一起是不可能的。

槽(Slots

當槽連接的信號被發射時,槽被調用。槽是標准的C++函數,可以正常調用;它們唯一特別的是信號可以連接到它們。

由於槽是標准成員函數,因此它們在直接調用時遵循標准的C++規則。但是,作為槽時,任何組件都可以通過信號-槽連接調用它們,而不管其訪問級別如何。這意味着從任意類的實例中發出的信號可以導致一個不相關類實例中的私有槽被調用。

你還可以將槽定義為virtual,這在實踐中非常有用。

因為它們提供了更大的靈活性,與回調相比信號和槽稍慢一點,盡管如此在實際應用程序中的差別不大。一般來說,發出一個連接到某些槽的信號,比直接呼叫接收器的非虛擬函數調用慢大約十倍。這是定位連接對象、安全地迭代所有連接(即檢查隨后的接收器在發射過程中未被銷毀)和以一般方式序列化所有參數所需的開銷。雖然10次非虛擬函數調用聽起來可能很大,但它的開銷比任何new或delete操作都要小得多。一旦在執行字符串、向量或列表操作時需要new或delete的場景下,信號和槽開銷只占整個函數調用成本的一小部分。當你在槽中執行系統調用時,或者間接調用超過10個函數時,情況也是如此。信號和槽機制的簡單性和靈活性是非常超值的,你的用戶甚至根本不會注意。

請注意,定義名為signals或slots的變量的其他庫在與基於Qt的應用程序一起編譯時可能會導致編譯器警告和錯誤。為了解決這個問題,#undef沖突的預處理器符號。

 小例子

可讀的短小的C++類聲明:

 1 class Counter
 2   {
 3   public:
 4       Counter() { m_value = 0; }
 5 
 6       int value() const { return m_value; }
 7       void setValue(int value);
 8 
 9   private:
10       int m_value;
11   };

基於QObject的類:

 1   #include <QObject>
 2   class Counter : public QObject
 3   {
 4       Q_OBJECT
 5   public:
 6       Counter() { m_value = 0; }
 7 
 8       int value() const { return m_value; }
 9 
10   public slots:
11       void setValue(int value);
12 
13   signals:
14       void valueChanged(int newValue);
15 
16   private:
17       int m_value;
18   };

基於QObject的版本具有相同的內部狀態,並提供訪問狀態的公共方法,但除此之外,它還支持使用信號和槽進行組件編程。這個類可以通過發出信號valueChanged()來告訴外部世界它的狀態已經改變了,並且它有一個其他對象可以發送信號到達的槽。
所有包含信號或插槽的類都必須在聲明的頂部提到Q_Object。它們還必須(直接或間接)從QObject派生出來。
槽由應用程序員實現。下面是Counter::setValue() 槽的一個可能實現:

1 void Counter::setValue(int value)
2   {
3       if (value != m_value) {
4           m_value = value;
5           emit valueChanged(value);
6       }
7   }

發出行從對象發出信號value eChanged(),以新值作為參數。
在下面的代碼片段中,我們創建了兩個計數器對象,並使用QObject::connect()將第一個對象的value eChanged()信號連接到第二個對象的setValue()槽:

1       Counter a, b;
2       QObject::connect(&a, &Counter::valueChanged,
3                        &b, &Counter::setValue);
4 
5       a.setValue(12);     // a.value() == 12, b.value() == 12
6       b.setValue(48);     // a.value() == 12, b.value() == 48

調用a.setValue(12)使a發出一個value eChanged(12)信號,b將在其setValue()槽中接收該信號,即調用b.setValue(12)。然后b發出相同的value eChanged()信號,但是由於沒有連接到b的value eChanged()信號的槽,因此忽略了該信號。

注意,setValue()函數設置值,只有當value!=m_value時才發出信號。這防止了循環連接情況下的無限循環(例如,如果b.value eChanged()連接到a.setValue())。

默認情況下,對於所建立的每個連接,都會發出一個信號;對於重復的連接,會發出兩個信號。您可以通過一個disconnect()調用來斷開所有這些連接。如果您傳遞了Qt::UniqueConnection類型,則只有當它不是重復時才會進行連接。如果已經存在重復(相同的信號指向同一對象上的完全相同的槽),則連接將失敗,連接將返回false。

這個例子說明對象可以一起工作,而不需要知道關於彼此的任何信息。為了實現這一點,對象只需要連接在一起,而這可以通過一些簡單的QObject::connect()函數調用來實現,或者使用UIC的自動連接功能來實現。

 一個具體的例子

下面是一個部件的簡單注釋示例。
 #ifndef LCDNUMBER_H
 #define LCDNUMBER_H
#include <QFrame> class LcdNumber : public QFrame { Q_OBJECT

 LcdNumber通過QFrame和QWidget繼承了擁有大部分信號-槽知識的QObject。它有點類似於內置的QLCDNumber小部件。

預處理器對Q_Object宏進行擴展,以聲明由MOC實現的幾個成員函數;如果得到編譯器錯誤“undefined reference to vtable for LcdNumber”,則可能忘記運行moc或在未在鏈接命令中包含moc輸出。 

public:
      LcdNumber(QWidget *parent = 0);

它顯然與moc無關,但是如果繼承QWidget,您幾乎肯定希望在構造函數中包含父參數,並將其傳遞給基類的構造函數。
這里省略了一些析構函數和成員函數;moc忽略成員函數。

 signals:
      void overflow();

當要求LcdNumber顯示一個不可能的值時,LcdNumber會發出一個信號。
如果不關心溢出,或者知道溢出不會發生,你可以忽略overflow()信號,即不要將它連接到任何槽。
另一方面,如果您想在數字溢出時調用兩個不同的錯誤函數,只需將信號連接到兩個不同的槽。Qt將調用這兩個(按它們連接的順序)。

 public slots:
      void display(int num);
      void display(double num);
      void display(const QString &str);
      void setHexMode();
      void setDecMode();
      void setOctMode();
      void setBinMode();
      void setSmallDecimalPoint(bool point);
  };

  #endif

槽是一個接收函數,用於獲取有關其他小部件中狀態變化的信息。LcdNumber使用它(如上面的代碼所示)來設置顯示的數字。因為Display()是類接口的一部分是與程序其余部分一起的,所以槽是public。
幾個示例程序將QScrollBar的valueChanged()信號連接到display()槽,因此LCDnumber連續顯示滾動條的值。
注意,display()已經重載;當您將信號連接到槽時,Qt將選擇適當的版本。使用回調,您必須找到五個不同的名稱,並自己跟蹤類型。
本例中省略了一些不相關的成員函數。

帶有默認參數的信號和槽

信號和槽的簽名可能包含參數,參數可以具有默認值。思考QObject::destroyed():

  void destroyed(QObject* = 0); 

當一個QObject被刪除時,它會發出qObject::destroyed()信號。我們想要捕捉到這個信號,在任何地方我們可能有一個被刪除的QObject的懸空引用,所以我們可以清理它。合適的槽簽名可能是:

   void objectDestroyed(QObject* obj = 0); 

要將信號連接到槽,我們使用QObject::connect()。有幾種連接信號和插槽的方法。第一種方法是使用函數指針:

 connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);  

使用QObject::connect()和函數指針有幾個優點。首先,它允許編譯器檢查信號的參數是否與槽的參數兼容。如果需要,編譯器也可以隱式轉換參數。

您還可以連接到仿函數或C++11的lambdas:

  connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); }); 

將信號連接到槽的另一種方法是使用QObject::connect()和SIGNAL和SLOT宏。如果參數具有默認值,則是否在SIGNAL()和SLOT()宏中包含參數的規則是,傳遞給SIGNAL()宏的簽名不能比傳遞給SLOT()宏的簽名參數少。

所有這些都有效:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

但這個不行:

 connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));  

...因為槽將期待一個QObject,而信號卻不會發送。此連接將報告運行時錯誤。
注意,在使用此QObject::connect()重載時,編譯器不會檢查信號和槽參數。

高級信號和插槽的使用

對於可能需要有關信號發送方的信息的情況,Qt提供QObject::sender()函數,該函數返回指向發送信號的對象的指針。

Lambda表達式是將自定義參數傳遞給槽的方便方法:

connect(action, &QAction::triggered, engine,
       [=]() { engine->processAction(action->text()); });

使用Qt與第三方信號和插槽

使用Qt與第三方信號/槽機制是可能的。您甚至可以在同一個項目中使用這兩種機制。只需向qmake項目(.pro)文件中添加以下行即可。 

 CONFIG += no_keywords   

它告訴QT不要定義moc關鍵字signals、slots和emit,因為這些名稱將被第三方庫使用,例如Boost。然后,要繼續使用帶有NO_關鍵字標志的Qt信號和槽,只需將源代碼中Qt moc關鍵字的所有用法替換為相應的Qt宏Q_SIGNALS(或Q_SIGNAL)、Q_SLOTS(或Q_SLOT)和Q_EMIT。

還請參閱Meta-Object SystemQt's Property System


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM