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