【QT八股文】系列之篇章2 | QT的信號與槽機制及通訊流程
- 前言
- 2. 信號與槽
- 信號與槽機制介紹/本質/原理,什么是Qt信號與槽機制?如何在Qt中使用?
- 信號與槽機制原理,解析流程
- Qt信號槽的調用流程
- 信號與槽機制的優缺點
- 信號與槽機制需要注意的問題
- 信號的注意點
- 信號與槽與回調函數區別
- Qt信號與槽的多種用法
- PYQT5 connect 函數
- Qt connect 函數的連接方式
- PYQT5信號槽的鏈接方式
- 信號槽同步與異步/多線程下,信號槽分別在什么線程中執行,如何控制——`Qt connect 函數的連接方式`來控制
- 3. 通訊流程
- QT的TCP通訊流程
- QT的UDP通訊流程
- 下一章筆記
- 說明
前言
第一篇章主要是基礎定義及QT中重要的事件機制
筆記鏈接:【QT八股文】系列之篇章1 | QT的基礎知識及事件/機制
這里我們在了解了QT的大概后,我們將來了解QT中的核心機制:信號與槽
因為介紹到信號與槽,所以筆者我會講通訊流程提前在前面來介紹
原創文章,未經同意請勿轉載
2. 信號與槽
信號與槽機制介紹/本質/原理,什么是Qt信號與槽機制?如何在Qt中使用?
-
定義
Qt信號與槽機制是一種基于事件機制的編程模型,用于對象之間的通信。信號是由發送方對象發射的事件,而槽是接收方對象用于處理這些事件的函數。在Qt中,我們可以使用QObject類中的信號和槽機制來實現對象間的通信。通過定義信號和槽函數,在信號發射時,會自動調用對應的槽函數進行處理。 -
使用
PyQt的內置信號是自動定義的,使用PyQt5.QtCore.pyqtSignal函數可以為QObject對象創建一個信號,使用pyqtSignal函數可以把信號定義為類的屬性。使用connect函數可以將信號綁定到槽函數上,使用disconnect函數可以解除信號與槽函數的綁定,使用emit函數可以發射信號。 -
本質(就是回調函數)
在事件的處理方面,信號槽相比回調函數,具有類型安全、松耦合、任意參數的優勢,但執行效率會有一點損失。信號相當于傳遞參數(指實參,用于傳遞值/動作變化),槽函數像用于傳遞函數體(形參/函數體,用于接收值/根據動作變化來做出對應操作。) -
原理
- Qt 中的信號與槽機制是一種事件處理機制,它允許程序在接收到特定事件時執行特定的操作。在 Qt 中,信號與槽機制被廣泛應用于組件之間的通信和事件處理。
- 具體來說,Qt 中的信號與槽機制是基于 QObject 類的。任何一個 QObject 對象都可以作為一個信號源,它可以通過 emit() 方法發出信號。同時,任何一個 QObject 對象都可以作為一個槽,它可以接受并處理來自信號源的信號。當一個信號源發出信號時,它會連接到相應的槽。這些槽可以是與信號源同一個對象,也可以是其他 QObject 對象。當信號源接收到信號時,它會將信號傳遞給所有已經連接到該槽的對象。這些對象會在接收到信號時執行相應的操作。
信號與槽機制原理,解析流程
-
原理
- Qt 中的信號與槽機制是一種事件處理機制,它允許程序在接收到特定事件時執行特定的操作。在 Qt 中,信號與槽機制被廣泛應用于組件之間的通信和事件處理。
- 具體來說,Qt 中的信號與槽機制是基于 QObject 類的。任何一個 QObject 對象都可以作為一個信號源,它可以通過 emit() 方法發出信號。同時,任何一個 QObject 對象都可以作為一個槽,它可以接受并處理來自信號源的信號。當一個信號源發出信號時,它會連接到相應的槽。這些槽可以是與信號源同一個對象,也可以是其他 QObject 對象。當信號源接收到信號時,它會將信號傳遞給所有已經連接到該槽的對象。這些對象會在接收到信號時執行相應的操作。
-
解析流程
- moc查找頭文件中的signals,slots,標記出信號和槽。
- 將信號槽信息存儲到類靜態變量staticMetaObject中,并且按聲明順序進行存放,建立索引。
- 當發現有connect連接時,將信號槽的索引信息放到一個map中,彼此配對。
- 當調用emit時,調用信號函數,并且傳遞發送信號的對象指針,元對象指針,信號索引,參數列表到active函數
- 通過active函數找到在map中找到所有與信號對應的槽索引
- 根據槽索引找到槽函數,執行槽函數。
Qt信號槽的調用流程
注意:信號槽的實現:元對象編譯器MOC,MOC的本質就是反射器
- MOC(元對象編譯器)查找頭文件中的signal與slots,標記出信號槽。將信號槽信息儲存到類靜態變量staticMetaObject中,并按照聲明的順序進行存放,建立索引。
- connect鏈接,將信號槽的索引信息放到一個雙向鏈表中,彼此配對。
- emit被調用,調用信號函數,且傳遞發送信號的對象指針,元對象指針,信號索引,參數列表到active函數。
- active函數在雙向鏈表中找到所有與信號對應的槽索引,根據槽索引找到槽函數,執行槽函數。
信號與槽機制的優缺點
- 優點:
-
類型安全。需要關聯的信號槽的簽名必須是等同的。即信號的參數類型和參數個數同接受該信號的槽的參數類型和參數個數相同。若信號和槽簽名不一致,編譯器會報錯。
信號的參數可以多于槽,槽參數數量不能大于于信號
。💡 槽函數的參數是否可以比信號的參數多?
也可以。唯一的情況就是槽函數參數帶有默認參數,除去默認參數外,槽函數的參數必須小于等于信號的參數。 -
松散耦合。QT的信號槽的建立和解除綁定十分自由。信號和槽機制減弱了Qt對象的耦合度。激發信號的Qt對象無需知道是那個對象的那個信號槽接收它發出的信號,它只需在適當的時間發送適當的信號即可,而不需要關心是否被接受和那個對象接受了。Qt就保證了適當的槽得到了調用,即使關聯的對象在運行時被刪除。程序也不會奔潰。
💡 信號重載了,如何確定連接哪個信號?
采用函數指針確定連接哪個信號。 -
靈活性。一個信號可以關聯多個槽,或多個信號關聯同一個槽。
-
- 不足:
- 速度較慢。與回調函數相比,信號和槽機制運行速度比直接調用非虛函數慢10倍。信號槽同真正的回調函數比起來時間的耗損還是很大的,所以在嵌入式實時系統中應當慎用。
- 原因:
- ①需要定位接收信號的對象。
- ②安全地遍歷所有關聯槽。
- ③編組、解組傳遞參數。
- ④多線程的時候,信號需要排隊等待。(
然而,與創建對象的new操作及刪除對象的delete操作相比,信號和槽的運行代價只是他們很少的一部分。信號和槽機制導致的這點性能損耗,對實時應用程序是可以忽略的。
)
- 原因:
- 不能出現宏定義。信號槽的參數限定很多例如不能攜帶模板類參數,不能出現宏定義等等。
- 速度較慢。與回調函數相比,信號和槽機制運行速度比直接調用非虛函數慢10倍。信號槽同真正的回調函數比起來時間的耗損還是很大的,所以在嵌入式實時系統中應當慎用。
信號與槽機制需要注意的問題
信號與槽機制是比較靈活的,但有些局限性我們必須了解,這樣在實際的使用過程中才能夠做到有的放矢,避免產生一些錯誤。下面就介紹一下這方面的情況。
- 信號與槽的效率是非常高的,但是同真正的回調函數比較起來,由于增加了靈活 性,因此在速度上還是有所損失,當然這種損失相對來說是比較小的,通過在一臺 i586- 133 的機器上測試是 10 微秒(運行 Linux),可見這種機制所提供的簡潔性、靈活性還是 值得的。但如果我們要追求高效率的話,比如在實時系統中就要盡可能的少用這種機制。
- 信號與槽機制與普通函數的調用一樣,如果使用不當的話,在程序執行時也有可能 產生死循環。因此,在定義槽函數時一定要注意避免間接形成無限循環,即在槽中再次發射 所接收到的同樣信號。
- 如果一個信號與多個槽相關聯的話,那么,當這個信號被發射時,與之相關的槽被 激活的順序將是隨機的,并且我們不能指定該順序。
- 宏定義不能用在 signal 和 slot 的參數中。
- 構造函數不能用在 signals 或者 slots 聲明區域內。
- 函數指針不能作為信號或槽的參數。
- 信號與槽不能有缺省參數。
- 信號與槽也不能攜帶模板類參數。
信號的注意點
- 所有的信號聲明都是公有的,所以Qt規定不能在signals前面加public,private, protected。
- 所有的信號都沒有返回值,所以返回值都用void。
- 所有的信號都不需要定義。
- 必須直接或間接繼承自QOBject類,并且開頭私有聲明包含Q_OBJECT。
- 在同一個線程中,當一個信號被emit發出時,會立即執行其槽函數,等槽函數執行完畢后,才會執行emit后面的代碼,如果一個信號鏈接了多個槽,那么會等所有的槽函數執行完畢后才執行后面的代碼,槽函數的執行順序是按照它們鏈接時的順序執行的。不同線程中(即跨線程時),槽函數的執行順序是隨機的。
- 在鏈接信號和槽時,可以設置鏈接方式為:在發出信號后,不需要等待槽函數執行完,而是直接執行后面的代碼,是通過connect的第5個參數。
- 信號與槽機制要求信號和槽的參數一致,所謂一致,是參數類型一致。如果不一致,允許的情況是,信號的參數可以比槽函數的參數多,即便如此,槽函數存在的那些參數的順序也必須和信號的前面幾個一致起來。這是因為,你可以在槽函數中選擇忽略信號傳來的數據(也就是槽函數的參數比信號的少),但是不能說信號根本沒有這個數據,你就要在槽函數中使用(就是槽函數的參數比信號的多,這是不允許的)。
信號與槽與回調函數區別
-
鏈接的不同
- 回調函數使用函數指針來實現的,如果多個類都關注一個類的動態變化,這樣就會需要寫出一個比較長的列表來管理這些類之間的關系。稍微在編碼方面不那么靈活,稍顯冗余。
- QT使用信號與槽來解決這個連接問題,這種方式比較清晰簡單一些,一個類只需要清楚自己有幾個槽函數有幾個信號,然后將信號與槽進行連接,QT會自己處理函數的調用關系。這樣在軟件設計角度更加的清晰,靈活,不容易出錯。
-
執行順序/時間的不同
- Qt 信號與槽機制中的槽函數在接收到信號時會自動執行,而回調函數通常是在調用時立即執行。Qt 信號與槽機制可以在信號觸發時立即執行槽函數,也可以延遲執行槽函數,而回調函數通常是立即執行的。
- 信號與槽機制中的信號與槽之間的執行順序是不確定的,可以是任意順序,也可以是逆序;而回調函數機制中的回調函數之間的執行順序通常是確定的,按照函數聲明的順序執行。
-
對象綁定
信號與槽機制可以實現對象之間的動態綁定,可以在運行時動態地綁定信號與槽;而回調函數機制通常只能在程序啟動時進行綁定。
-
主要用途不同
信號和槽機制是用于在程序運行時傳遞數據和事件的機制,而回調函數則通常被用于函數或方法的調用。因此,信號和槽機制可以用于模塊之間的通信和交互,而回調函數則通常用于函數或方法的調用。
Qt信號與槽的多種用法
- 一個信號可以和多個槽相連
這時槽的執行順序和在不在同一個線程上有關,同一線程,槽的執行順序和聲明順序有關,跨線程時,執行順序是不確定的。
- 多個信號可以連接到一個槽
只要任意一個信號發出,這個槽就會被調用。 - 一個信號可以連接到另外的一個信號
當第一個信號發出時,第二個信號被發出。除此之外,這種信號-信號的形式和信號-槽的形式沒有什么區別。 - 槽可以被取消鏈接
這種情況并不經常出現,因為當一個對象delete之后,Qt自動取消所有連接到這個對象上面的槽。想主動取消連接就用disconnect()函數中添加任何實現。 - 可以使用Lambda 表達式
在使用 Qt 5 的時候,能夠支持 Qt 5 的編譯器都是支持 Lambda 表達式的。
PYQT5 connect 函數
注:在Qt中第五個參數用于指定信號與槽的匹配規則。而PYQT5是第四個參數
在 PyQt5 中,connect 函數【connect: PyQt5.QtWidgets.QSignalMapper()
】是一個用于連接信號與槽的函數。它通常被用于將對象的信號與槽函數進行連接。
列子:mapper = Qt.QSignalMapper() mapper.setMapping(button, button.clicked.connect(mapper.setCurrentIndex))
第一個參數是一個可選的參數,用于指定要連接的信號源。如果該參數為 None,則表示連接的是系統提供的信號。如果該參數不為 None,則表示要連接自定義信號。
第二個參數是一個可選的參數,用于指定要連接的槽函數。如果該參數為 None,則表示連接的是默認槽函數。如果該參數不為 None,則表示要連接指定的槽函數。
第三個參數是一個字符串,用于指定信號與槽之間的映射關系。該字符串通常由信號名稱和槽函數名稱組成。例如,“clicked” 表示將按鈕的點擊信號與按鈕的 clicked 槽函數進行連接。
第四個參數是一個 PyQt5 中的 QSignalMapper 對象,用于指定信號與槽的匹配規則。該對象應該實現 QSignalMapper 類中的方法,例如 setMapping() 和 currentIndex() 等。
第五個參數是一個可選的參數,用于指定信號中斷連接的函數。如果連接的信號源對象被刪除或重新分配,則連接將被中斷。默認情況下,連接不會自動中斷。
Qt connect 函數的連接方式
-
自動連接
Qt::AutoConnection
默認值,使用這個值則連接類型會在信號發送時決定。如果接收者和發送者在同一個線程,則自動使用
多線程時為隊列連接函數,單線程時為直接連接函數。 -
直接連接
Qt::DirectConnection
== 如果接收者和發送者不在一個線程,則自動使用Qt::QueuedConnection類型。==
Qt::DirectConnection:槽函數會在信號發送的時候直接被調用,槽函數和信號發送者在同一線程。效果看上去就像是直接在信號發送位置調用了槽函數,效果上看起來像函數調用,同步執行。
emit語句后面的代碼將在與信號關聯的所有槽函數執行完畢后才被執行。
信號/槽在信號發出者所在的線程中執行 -
隊列連接
Qt::QueuedConnection
信號發出后,信號會暫時被放到一個消息隊列中,需等到接收對象所屬線程的事件循環取得控制權時才取得該信號,然后執行和信號關聯的槽函數,這種方式既可以在同一線程內傳遞消息也可以跨線程操作。
emit語句后的代碼將在發出信號后立即被執行,無需等待槽函數執行完畢
信號在信號發出者所在的線程中執行,槽函數在信號接收者所在的線程中執行 -
Qt::BlockingQueuedConnection
槽函數的調用時機與Qt::QueuedConnection一致,不過發送完信號后發送者所在線程會阻塞,直到槽函數運行完。而且接收者和發送者絕對不能在一個線程,否則程序會死鎖。在多線程間需要同步的場合可能需要這個。 -
Qt::UniqueConnection:這個flag可以通過按位或(|)與以上四個結合在一起使用。當這個flag設置時,當某個信號和槽已經連接時,再進行重復的連接就會失敗。也就是為了避免重復連接。
PYQT5信號槽的鏈接方式
在 PyQt5 中,信號與槽的連接方式有兩種:1. 使用 connect() 函數;2. 裝飾器@pyqtSlot() 。
@pyqtSlot()
優點是方式書寫比較簡潔。缺點是但函數名稱不能自由定義,在想自定義參數時沒有詳細說明。
connect()
方式優點是理解和學習起來比較簡單,而且函數名稱可以自由定義。缺點是但如果信號比較多時,書寫就比較混亂。
使用信號處理器的優點是可以在信號發生時執行復雜的操作,而缺點是連接信號處理器需要花費更多的內存和時間,并且連接信號處理器需要手動管理連接關系。因此,使用信號處理器僅適用于需要執行復雜操作的情況。
-
裝飾器方法:
@pyqtSlot()
裝飾器@pyqtSlot():修飾關鍵詞,表明下面是完整的信號槽函數
# 需要引入 pyqtSlot 庫函數 from PyQt5.QtCore import pyqtSlot@pyqtSlot() #裝飾器,此函數沒有connect直接通過裝飾器初始化連接槽函數 def on_pushButton_clicked(self)print("我點擊了")
在@pyqtSlot()方式里,函數名稱有特殊要求,如下:
def on_(控件對象名)_信號名(self,內置參數):
@pyqtSlot()控制控件的多信號
@pyqtSlot() def on_lineEdit_returnPressed(self):print('觸發了信號 returnPressed')def on_lineEdit_textChanged(self):print('觸發了信號 textChanged')
注意:
一個控件同時要寫多個信號與槽函數時,只需要寫一遍@pyqtSlot()關鍵詞
,中間可以有其他函數隔開。一定是一個類里面的,一個控件只寫一遍@pyqtSlot(),不是所有控件信號只寫一次@pyqtSlot(),有多少控件的信號還是要寫。 -
connect連接法
使用 connect() 函數將信號與槽函數連接起來。connect() 函數接受兩個參數:要連接的信號和要連接的槽函數。連接成功后,當信號發生時,槽函數將被調用。
# 在初始化函數中信號連接槽函數 self.pushButton.clicked.connect(self.test) # 槽函數 def test(self):print("點擊了一下")
規則:
- 語法規則:
self.控件對象名稱.信號名稱.connect(self.槽函數名稱)
- 有參數時,
槽函數名稱
部分寫成lambda 參數名: 函數名(參數名)
- 沒有參數時,槽函數不用寫括號
()
- 語法規則:
信號槽同步與異步/多線程下,信號槽分別在什么線程中執行,如何控制——Qt connect 函數的連接方式
來控制
可以通過QT的connect函數的第五個參數(PYQT5中是第四個參數)來控制, 信號槽執行時所在的線程。
通常使用的connect,實際上最后一個參數使用的是Qt::AutoConnection類型:Qt支持6種連接方式,其中3中最主要:
-
Qt::AutoConnection(自動方式)
信號槽在信號發出者所在的線程中執行
Qt的默認連接方式,如果信號的發出和接收這個信號的對象同屬一個線程,那個工作方式與直連方式相同(會自動使用Qt::DirectConnection類型);否則工作方式與排隊方式相同(會自動使用Qt::QueuedConnection類型)。
即多線程時為隊列連接函數,單線程時為直接連接函數。
-
Qt::DirectConnection(直連方式)(信號與槽函數關系類似于函數調用,同步執行)
當信號發出后,相應的槽函數將立即被調用。emit語句后的代碼將在所有槽函數執行完畢后被執行。
-
Qt::QueuedConnection(排隊方式)(此時信號被塞到信號隊列里了,信號與槽函數關系類似于消息通信,異步執行)
信號在信號發出者所在的線程中執行,槽函數在信號接收者所在的線程中執行
當信號發出后,排隊到信號隊列中,需等到接收對象所屬線程的事件循環取得控制權時才取得該信號,調用相應的槽函數。emit語句后的代碼將在發出信號后立即被執行,無需等待槽函數執行完畢。
-
Qt::BlockingQueuedConnection(信號和槽必須在不同的線程中,否則就產生死鎖)
這個是完全同步隊列只有槽線程執行完成才會返回,否則發送線程也會一直等待,相當于是
不同的線程可以同步起來執行
。與Qt::QueuedConnection相同,除了信號線程阻塞直到槽返回。如果接收方處于發送信號的線程中,則不能使用此連接,否則應用程序將死鎖。
-
Qt::UniqueConnection
與默認工作方式相同,只是不能重復連接相同的信號和槽,因為如果重復連接就會導致一個信號發出,對應槽函數就會執行多次。
這個flag可以通過按位或(|)與以上四個結合在一起使用。當這個flag設置時,當某個信號和槽已經連接時,再進行重復的連接就會失敗。也就是為了避免重復連接。
-
Qt::AutoCompatConnection
是為了連接Qt4與Qt3的信號槽機制兼容方式,工作方式與Qt::AutoConnection一樣。
3. 通訊流程
QT的TCP通訊流程
QT如果要進行網絡編程首先需要在.pro中添加如下代碼:QT += network
-
服務端:(QTcpServer)
① 創建QTcpServer對象
② 監聽list需要的參數是地址和端口號
③ 當有新的客戶端連接成功回發送newConnect信號
④ 在newConnection信號槽函數中,調用nextPendingConnection函數獲取新連接QTcpSocket對象
⑤ 連接QTcpSocket對象的readRead信號
⑥ 在readRead信號的槽函數使用read接收數據
⑦ 調用write成員函數發送數據 -
服務器端
-
創建用于監聽的套接字
-
給套接字設置監聽
-
如果有連接到來, 監聽的套接字會發出信號newConnected
-
接收連接, 通過nextPendingConnection()函數, 返回一個QTcpSocket類型的套接字對象(用于通信)
-
使用用于通信的套接字對象通信 1>. 發送數據: write 2>. 接收數據: readAll/read
-
代碼
Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this);tcpServer = new QTcpServer;tcpServer->listen(QHostAddress("192.168.0.111"),1234);connect(tcpServer,SIGNAL(newConnection()),this,SLOT(new_connect())); }Widget::~Widget() {delete ui; }void Widget::new_connect() {qDebug("--new connect--");QTcpSocket* tcpSocket = tcpServer->nextPendingConnection();connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(read_data()));socketArr.push_back(tcpSocket);}void Widget::read_data() {for(int i=0; i<socketArr.size(); i++){if(socketArr[i]->bytesAvailable()){char buf[256] = {};socketArr[i]->read(buf,sizeof(buf));qDebug("---read:%s---",buf);}} }
-
-
-
客戶端:(QTcpSocket)
① 創建QTcpSocket對象
② 當對象與Server連接成功時會發送connected 信號
③ 調用成員函數connectToHost連接服務器,需要的參數是地址和端口號
④ connected信號的槽函數開啟發送數據
⑤ 使用write發送數據,read接收數據 -
客戶端:
- 創建用于通信的套接字
- 連接服務器: connectToHost
- 連接成功與服務器通信
1 >. 發送數據: write 2>. 接收數據: readAll/read
-
代碼
Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget) {ui->setupUi(this);tcpSocket = new QTcpSocket;connect(tcpSocket,SIGNAL(connected()),this,SLOT(connect_success()));tcpSocket->connectToHost("172.20.10.3",1234); }Widget::~Widget() {delete ui; }void Widget::on_send_clicked() {std::string msg = ui->msg->text().toStdString();int ret = tcpSocket->write(msg.c_str(),msg.size()+1);qDebug("--send:%d--",ret); }void Widget::connect_success() {ui->send->setEnabled(true); }
QT的UDP通訊流程
UDP(User Datagram Protocol即用戶數據報協議)是一個輕量級的,不可靠的,面向數據報的無連接協議。在網絡質量令人十分不滿意的環境下,UDP協議數據包丟失嚴重。由于UDP的特性:它不屬于連接型協議,因而具有資源消耗小,處理速度快的優點,所以通常音頻、視頻和普通數據在傳送時使用UDP較多,因為它們即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響。所以QQ這種對保密要求并不太高的聊天程序就是使用的UDP協議。
在Qt中提供了QUdpSocket 類來進行UDP數據報(datagrams)的發送和接收。Socket簡單地說,就是一個IP地址加一個port端口 。
QT下UDP通信服務器端和客戶端的關系是對等的, 做的處理也是一樣的:
- 創建套接字對象 2. 如果需要接收數據, 必須綁定端口 3. 發送數據: writeDatagram 4. 接收數據: readDatagram
流程:①創建QUdpSocket套接字對象 ②如果需要接收數據,必須綁定端口 ③發送數據用writeDatagram,接收數據用 readDatagram 。
下一章筆記
下篇筆記鏈接:【QT的多線程以及QThread與QObject】
下篇筆記主要內容:QT的多線程以及QThread與QObject
說明
碼字不易,可能當中存在某些字體錯誤(筆者我沒有發現),如果有錯誤,歡迎大家指正。🤗
另外因為筆記是之前做的,這里我只把我之前做的搬移和重新排版過來,如果有知識上的錯誤也歡迎大家指正。