一、信號和槽函數機制簡介
(注1:下文中的槽與槽函數表示一個意思)
(注2:閱讀本文可能有點枯燥,但文中有關于信號和槽的重要知識,這些知識甚至在開發中經常被忽略。請君繼續下看)
信號和槽用于多個對象之間的通信。信號和槽機制是Qt的核心特性,也是Qt與其他框架最大的不同之處。Qt的元對象系統是信號和槽實現的基礎。
在GUI編程中,當更改一個小部件時,通常希望另一個小部件得到通知。希望任何類型的對象之間都能夠相互通信。例如,如果用戶單擊關閉按鈕,可能希望調用窗口的Close()函數。
其他軟件工具包或框架可能使用回調機制實現這種通信機制。一個回調函數是一個指向一個函數的指針,所以如果想讓一個處理函數通知一些事件,可以向處理函數傳遞一個指向另一個函數(回調函數)的指針,然后處理函數在適當的時候調用回調函數。雖然使用這種方法的成功框架確實存在,但回調可能不太直觀,在確保回調參數類型的正確性上可能會存在問題。
在Qt中,有一種回調技術的替代方法:那就是信號和槽機制。當特定事件發生時,會發出一個信號。Qt的小部件中有許多預定義的信號,但我們可以將小部件子類化,向它們添加自定義的信號。槽是響應特定信號的函數。Qt的小部件有許多預定義的槽函數,但是通常是子類化小部件并添加自己的槽函數,這樣就可以處理與之相關聯的信號了。如下圖所示:
信號和槽機制是類型安全的:信號的參數必須與槽函數的參數相匹配。(實際上,槽的參數可以比它接收到的信號參數更少,因為槽可以忽略額外的參數)由于參數是兼容的,所以在使用基于函數指針語法的信號與槽關聯機制時,編譯器可以幫助檢測類型是否匹配,從而可以檢測出在開發中信號和槽函數關聯時出現的問題。
信號和槽函數是松耦合的:當一個對象發出信號,該對象不知道也不關心哪個對象的槽函數會接收這個信號。Qt的信號和槽函數機制確保:如果將一個信號連接到一個槽函數上,該槽函數將在正確的時間被調用。信號和槽函數可以接受任意數量的任意類型的參數。它們完全是類型安全的。所有從QObject或它的一個子類(例如,QWidget)繼承的類都可以使用信號和槽槽函數機制。當對象改變其狀態時,可能就會發出信號(這一點由開發人員和父類確定其關聯的信號什么時候發出)。
槽函數用來接收信號,但也是普通的成員函數。就像對象不知道是否有東西接收到它的信號一樣,槽函數也不知道是否有信號連接到它,因此可以創建獨立的軟件組件。當需要使用該獨立組件時,確定其組件類中預定義的信號和槽函數,然后關聯信號和槽函數即可。
可以將多個信號連接到一個槽函數上(即【多對一】),而一個信號也可以連接到多個槽函數上【即一對多】。
也可以將一個信號直接連接到另一個信號。(當第一個信號發出時,它將立即發出第二個信號。)
綜上,在Qt中,信號和槽函數共同構成了一個功能強大的組件編程機制。
二、信號
(2-1)信號的發出
由于某種條件到達可能引起了對象改變,其內部狀態將發生改變,這時候對象就會發出信號。信號是公共訪問函數,可以從任何地方發出,但是建議:【只從定義該信號的類及其子類發出信號】。
在Qt框架下,信號發出分為兩種:
1、【每個類預定義的信號】:這些信號何時發出可以通過查看官方文檔獲知。
2、【自定義的信號】:這些信號的發出由開發人員自行定義。
(2-2)信號的處理
當一個信號發出時,連接到它的槽函數通常會立即執行,就像一個普通函數調用一樣。在這種情況下,信號和槽函數機制是完全獨立于GUI事件循環的,也并不會干擾GUI的事件循環。emit語句之后的代碼將在所有槽函數都返回之后才執行。如果使用排隊連接(queued connections),情況略有不同,在這種情況下,emit關鍵字后面的代碼將立即繼續,槽函數將在后續執行。
如果幾個槽函數連接到同一個信號上,當信號發出時,這些槽函數將按照它們連接時的順序依次執行【這一點很重要】。
信號是由moc工具自動生成,不能在.cpp文件中實現,所以信號永遠不能有返回類型(必須使用void關鍵字定義信號)。
關于信號和槽參數的注意事項:經驗表明,如果信號和槽函數不使用特殊類型,那么代碼具有極強的可重用性。
下表是使用connect()創建信號和槽函數連接時,可以指定5種不同的連接類型:
序號 | 類型 | 含義 |
---|---|---|
1 | Qt::AutoConnection | 如果接收者生活在發出信號的線程中,Qt::DirectConnection被使用。否則,使用Qt::QueuedConnection。連接類型是在信號發出時確定。【這是Qt創建信號和槽函數時的默認連接方式】 |
2 | Qt::DirectConnection | 當信號發出時,槽函數立即被調用。槽函數在發送信號的線程中執行。 |
3 | Qt::QueuedConnection | 當控制返回到接收方線程的事件循環時,將調用槽函數。槽函數在接收方的線程中執行。 |
4 | Qt::BlockingQueuedConnection | 與Qt::QueuedConnection相同,只是在槽函數返回之前線程會阻塞。如果接收方存在于發送信號的線程中,則不能使用此連接,否則應用程序將會死鎖。 |
5 | Qt::UniqueConnection | 這是一個標志,可以使用按位OR與上述的連接類型進行組合。當Qt::UniqueConnection被設置時,如果連接已經存在,QObject::connect()將失敗(例如,如果相同的信號已經連接到同一對對象的相同槽位)。注:這個標志在Qt 4.6中引入。 |
三、槽函數
當一個連接到槽函數的信號被發射時,槽函數將被調用。槽函數是普通的C++函數,在實際開發中也可以正常調用;它們唯一的特點是:【信號可以與它們相連接】。
由于槽是普通的成員函數,所以它們在直接調用時遵循普通的C++規則。但是,作為槽函數時,任何組件都可以通過信號連接從而調用它們。
還可以將槽函數定義為虛擬的,這在開發中非常有用。
與回調機制相比,信號和槽函數機制的速度稍微慢一些,這一點對于實際應用程序來說,這種差別并不顯著。一般來說,發送一個連接到某些槽函數的信號,比直接調用非虛函數要慢大約10倍。這是定位連接對象、安全地遍歷所有連接(即檢查后續接收方在發射過程中沒有被銷毀)以及以函數調用增加的開銷。雖然10個非虛函數調用聽起來很多,但是它比new操作或delete操作的開銷要小得多。一旦在后臺執行一個需要new或delete的字符串、向量或列表操作,信號和槽函數的開銷只占整個函數調用開銷的很小一部分。在槽函數中執行系統調用時也是如此(或間接調用超過十個函數)。因此信號和槽函數機制的簡單性和靈活性是值得的,這些開銷在實際應用場景下甚至不會注意到。
注意,當與基于Qt的應用程序一起編譯時,定義為信號或槽的變量的第三方庫可能會導致編譯器出現警告和錯誤。要解決這個問題,使用#undef來定義出錯的預處理器符號即可。
(3-1)帶有默認參數的信號和槽函數
信號和槽可以包含參數,參數可以有默認值。例如:QObject::destroyed():
voiddestroyed(QObject*=nullptr);
當QObject被刪除時,它會發出這個QObject::destroyed()信號。無論我們在哪里有一個懸空引用指向已刪除的QObject,都希望捕捉到這個信號,這樣就可以清除它。合適的槽參數可以是:
voidobjectDestroyed(QObject*obj=nullptr);
(3-2)使用QObject::connect()將信號連接到槽函數的三種方法
1、第一種方法:使用函數指針
connect(sender,&QObject::destroyed,this,&MyObject::objectDestroyed);
將QObject::connect()與函數指針一起使用有幾個優點。它允許編譯器檢查信號的參數是否與槽的參數兼容。當然,編譯器還可以隱式地轉換參數。
2、第二種方法:連接到C++ 11的lambdas
connect(sender,&QObject::destroyed,this,[=](){this->m_objects.remove(sender);});
在這種情況下,我們在connect()調用中提供這個上下文。上下文對象提供關于應該在哪個線程中執行接收器的信息。
當發送方或上下文被銷毀時,lambda將斷開連接。注意:當信號發出時,函數內部使用的所有對象依然是激活的。
3、第三種方法:使用QObject::connect()以及信號和槽聲明宏。
在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()關聯信號和槽函數時,編譯器不會自動檢查信號和槽函數的參數之間是否匹配。
綜上:使用第一種方法 創建信號和槽 在開發中較為常用,也較為合適。
(3-3)信號和槽函數的一些高級用法
當需要獲取信號發送方的信息時,使用Qt提供QObject::sender()函數,該函數返回一個指向發送信號對象的指針。
Lambda表達式是傳遞自定義參數到槽的一種方便方式:
connect(action,&QAction::triggered,engine,[=](){engine->processAction(action->text());});
四、使用disconnect斷開信號/槽連接
disconnect()用于斷開對象發送器中的信號與對象接收器中的方法的連接。如果連接成功斷開,則返回true;否則返回false。
當對信號/槽關聯的兩方中的任何一個對象進行銷毀時,信號/槽連接將被移除。
disconnect()有三種使用方法,如下示例所示:
1、斷開所有與對象相連的信號/槽:
disconnect(myObject,nullptr,nullptr,nullptr);
相當于非靜態重載函數:
myObject->disconnect();
2、斷開所有與特定信號相連的對象:
disconnect(myObject,SIGNAL(mySignal()),nullptr,nullptr);
相當于非靜態重載函數:
myObject->disconnect(SIGNAL(mySignal()));
3、斷開特定接收對象的連接:
disconnect(myObject,nullptr,myReceiver,nullptr);
相當于非靜態重載函數:
myObject->disconnect(myReceiver);
nullptr可以用作通配符,分別表示“任何信號”、“任何接收對象”或“接收對象中的任何槽”。
如下格式的使用示例:
disconnect(發送對象,信號,接收對象,方法)
發送對象不會是nullptr。
如果信號為nullptr,將斷開接收對象和槽函數與所有信號的連接。否則只斷開指定的信號。
如果接收對象是nullptr,它斷開所有關聯該信號的連接。否則,只斷開與接收對象的槽函數連接。
如果方法是nullptr,它會斷開任何連接到接收對象的連接。如果不是,只有命名為方法的槽函數連接將被斷開。如果沒有接收對象,方法必須為nullptr。即:
disconnect(發送對象,信號,nullptr,nullptr)
五、使用Qt與第三方信號和槽函數
當第三方庫中也有信號/槽函數機制時,這時候又需要使用Qt的信號和槽函數機制。對于這種開發場景,Qt可以在同一個項目中使用這兩種機制。需將下面一行添加到qmake項目(.pro)工程配置文件中:
CONFIG+=no_keywords
該配置將告訴Qt不要定義moc關鍵字信號、槽函數和emit,因為這些名稱將被第三方庫使用(例如Boost)。如果要在使用no_keywords標志下繼續使用Qt信號和槽機制,需將源文件中所有的Qt moc關鍵字替換為對應的Qt宏:Q_SIGNALS(或Q_SIGNAL)、Q_SLOT(或Q_SLOT)和Q_EMIT。
六、總結
本文站在開發的角度,描述了Qt的信號和槽函數機制。
-
信號
+關注
關注
11文章
2852瀏覽量
78265 -
函數
+關注
關注
3文章
4380瀏覽量
64845 -
Qt
+關注
關注
2文章
314瀏覽量
39074 -
GUI
+關注
關注
3文章
679瀏覽量
41215 -
回調函數
+關注
關注
0文章
88瀏覽量
11890
原文標題:Qt信號和槽函數機制,此篇足矣
文章出處:【微信號:嵌入式小生,微信公眾號:嵌入式小生】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
Qt之信號與槽例子(二)
怎樣通過Qt界面控制步進電機的啟停
信號與槽是如何實現連接的呢
簡單概述一下窗口之間傳遞參數的機制
Qt5位置相關函數及圖形與圖片的詳細資料免費下載

評論