目錄
1.?信號和槽概述
1.1 事件和控件
1.2 信號的本質
1.3 槽的本質
2. 信號和槽的使用
2.1 connect 連接信號和槽
2.2 查看內置信號和槽
2.3 Qt Creator 生成信號槽代碼
3. 自定義信號和槽
3.1 不帶參數的信號和槽
3.2 帶參數的信號和槽
4. 信號與槽的連接方式
4.1 一對一
4.2 一對多
4.3 多對一
4.4 多對多
5. 信號與槽的斷開
6. Qt4 版本信號與槽的連接
7. Lambda 定義槽函數
7.1 局部變量引入方式 [ ]?
7.2 函數參數 ( )
7.3 選項 Opt
7.4 返回值類型?->
7.5 函數體?{ }
7.6 槽函數使用 Lambda
8. 信號與槽的優缺點
本篇完。
1.?信號和槽概述
1.1 事件和控件
????????在 Qt 中,用戶和控件的每次交互過程稱為一個事件。比如 “用戶點擊按鈕”?是一個事件,“用戶關閉窗口”?也是一個事件。每個事件都會發出一個信號,例如用戶點擊按鈕會發出 “按鈕被點擊”?的信號,用戶關閉窗口會發出 “窗口被關閉”?的信號。
????????Qt 中的所有控件都具有接收信號的能力,一個控件還可以接收多個不同的信號。對于接收到的每個信號,控件都會做出相應的響應動作。例如,按鈕所在的窗口接收到 “按鈕被點擊” 的信號后,會做出 “關閉自己” 的響應動作;再比如輸入框自己接收到 “輸入框被點擊” 的信號后,會做出 “顯示閃爍的光標,等待用戶輸入數據” 的響應動作。在 Qt 中,對信號做出的響應動作就稱之為槽。
????????信號和槽是 Qt 特有的消息傳輸機制,可以通過 connect 這樣的函數,把一個信號和一個槽這種相互獨立的控件關聯起來。比如,“按鈕” 和 “窗口” 本身是兩個獨立的控件,點擊 “按鈕” 并不會對 “窗口” 造成任何影響。通過信號和槽機制,可以將 “按鈕” 和 “窗口” 關聯起來,實現 “點擊按鈕會使窗口關閉” 的效果。
1.2 信號的本質
????????信號是由于用戶對窗口或控件進行了某些操作,導致窗口或控件產生了某個特定事件,這時 Qt 對應的窗口類會發出某個信號,以此對用戶的操作做出反應。因此,信號的本質就是事件。
- 按鈕單擊、雙擊
- 窗口刷新
- 鼠標移動、鼠標按下、鼠標釋放
- 鍵盤輸入
那么在 Qt 中信號是通過什么形式呈現給使用者的呢?
- 我們對哪個窗口進行操作, 哪個窗口就可以捕捉到這些被觸發的事件。
-
對于使用者來說觸發了一個事件,我們就可以得到 Qt 框架給我們發出的某個特定信號。
- 信號的呈現形式就是函數, 也就是說某個事件產生了, Qt 框架就會調用某個對應的信號函數,通知使用者。
在 Qt 中信號的發出者是某個實例化的類對象。
1.3 槽的本質
????????槽(Slot)就是對信號響應的函數。槽就是一個函數,與一般的 C++ 函數是?樣的,可以定義在類的任何位置(public、protected 或 private),可以具有任何參數,可以被重載,也可以被直接調用(但是不能有默認參數)。
????????槽函數與一般的函數不同的是:槽函數可以與一個信號關聯,當信號被發射時,關聯的槽函數被自動執行。
所謂的 “槽函數” 本質上也是一種 “回調函數”(callback)。
C 語言階段 - 函數指針:
- 實現轉移表,降低代碼的 “圈復雜度”
- 實現回調函數效果(qsort)
C++ 階段:
- STL 中,函數對象 / 仿函數
- lambda 表達式
Linux 階段:
- 信號處理函數
- 線程的入口函數
- epoll 基于回調的機制
信號和槽機制底層是通過函數間的相互調用實現的
????????每個信號都可以用函數來表示,稱為信號函數;每個槽也可以用函數表示,稱為槽函數。????????例如:“按鈕被按下” 這個信號可以用 clicked() 函數表示,“窗口關閉” 這個槽可以用 close() 函數表示,假如使用信號和槽機制 - 實現:“點擊按鈕會關閉窗口” 的功能,其實就是 clicked() 函數調用 close() 函數的效果。
信號函數和槽函數通常位于某個類中
和普通的成員函數相比,它們的特別之處:
- 信號函數用 signals 關鍵字修飾,槽函數用 public slots、protected slots 或者 private slots 修飾。signals 和 slots 是 Qt 在 C++ 的基礎上擴展的關鍵字,專門用來指明信號函數和槽函數。
- 信號函數只需要聲明,不需要定義(實現),而槽函數需要定義(實現)。
????????信號函數的定義是 Qt 自動在編譯程序之前生成的,我們編寫 Qt 應用程序無需關注這個,這種自動生成代碼的機制稱為 “?元編程?”(Meta Programming),這種操作在很多場景中都能見到。
2. 信號和槽的使用
2.1 connect 連接信號和槽
????????在 Qt 中,QObject 類提供了一個靜態成員函數 connect() ,該函數專門用來關聯指定的信號函數和槽函數。
關于 QObject:
????????QObject 是 Qt 內置的父類,Qt 中提供的很多類都是直接或者間接繼承自 QObject。(在 Java 中也存在相似的設定)
connect() 函數原型:
connect (const QObject *sender, //描述當前信號是哪個控件發出的const char * signal , //信號類型const QObject * receiver , //信號如何處理(哪個對象處理)const char * method , //信號如何處理(這個對象該怎么處理——要處理信號的對象提供的成員函數)Qt::ConnectionType type = Qt::AutoConnection ) //暫時不考慮,很少使用
參數說明:
-
sender:信號的發送者。
-
signal:發送的信號(信號函數)。
-
receiver:信號的接收者。
-
method:接收信號的槽函數。
-
type:用于指定關聯方式,默認的關聯方式為 Qt::AutoConnection,通常不需要手動設定。
代碼示例:
在窗口中設置一個按鈕,當點擊 “按鈕”?時關閉 “窗口”:
2.2 查看內置信號和槽
打開幫助文檔有三種方式,實際編程中使用哪種都可以。
(1)光標放到要查詢的類名 / 方法名上,直接按 F1
(2)Qt Creator 左側邊欄中直接用鼠標單擊 “幫助”?按鈕。
(3)找到 Qt Creator 的安裝路徑,在 "bin" 文件夾下找到 assistant.exe,雙擊打開
????????系統自帶的信號和槽通常是通過 “Qt 幫助文檔”?來查詢的。如上述示例,要查詢 “按鈕”?的信號,在幫助文檔中輸入:QPushButton
????????首先可以在 "Contents" 中尋找關鍵字?signals,如果沒有找到,繼續去父類中查找,因此我們去他的父類?QAbstractButton?中繼續查找關鍵字?signals:
這里的 clicked() 就是我們要找的信號。槽函數的尋找方式和信號一樣,只不過它的關鍵字是 slot。
2.3 Qt Creator 生成信號槽代碼
Qt Creator 可以快速幫助我們生成信號槽相關的代碼。
代碼示例:在窗口中設置一個按鈕,當點擊 “按鈕”?時關閉 “窗口
(1)新建項目,如下圖為新建完成之后所包含的所有文件
注意:創建時要生成 UI 設計文件。
(2)雙擊?widget.ui?文件,進入?UI 設計界面
(3)在 UI 設計窗口中拖入一個 “按鈕”,并且修改 “按鈕”?的名稱及字體大小等
(4)可視化生成槽函數
????????當鼠標單擊 “轉到槽...”?之后,出現如下界面:對于按鈕來說,當點擊時發送的信號是:clicked(),所以此處選擇:clicked()
這個窗口列出了 QPushButton 給我們提供的所有信號(還包含了 QPushButton 父類的信號)。
????????對于普通按鈕來說,使用?clicked 信號即可。clicked(bool) 是沒有意義的,具有特殊狀態的按鈕(比如:復選按鈕)才會用到 clicked(bool)。
(5)自動生成槽函數原型框架
發現在 "widget.h" 頭文件中自動添加了槽函數的聲明
自動生成槽函數的名稱有一定的規則,槽函數的命名規則為:on_XXX_SSS,其中:
- 以 "on" 開頭,中間使用下劃線連接起來。
- "XXX" 表示的是對象名(控件的?objectName?屬性)。
- "SSS" 表示的是對應的信號。
????????如:"on_pushButton_clicked() ",pushButton 代表的是對象名,clicked 是對應的信號。按照這種命名風格定義的槽函數,就會被 Qt 自動的和對應的信號進行連接。但是我們日常寫代碼的時候,除非是 IDE 自動生成,否則最好還是不要去依賴命名規則,而是顯式使用 connect 更好。
- 一方面,顯式 connect 可以更清晰直觀的描述信號和槽的連接關系。
- 另一方面,也是防止信號或者槽的名字拼寫錯誤導致連接失效。
????????當然,是配置大于約定,還是約定大于配置,哪種更好。這樣的話題業界尚存在爭議,這里還是更建議優先考慮顯式 connect。
發現在 "widget.cpp" 中自動生成槽函數定義??
(6)在槽函數函數定義中添加要實現的功能,實現關閉窗口的效果
3. 自定義信號和槽
3.1 不帶參數的信號和槽
????????在 Qt 中,允許自定義信號的發送方以及接收方,即可以自定義信號函數和槽函數。但是對于自定義的信號函數和槽函數有一定的書寫規范。
????????Qt 中如果要讓某個類能夠使用信號槽(可以在類中定義信號和槽函數),則必須要在類最開始的地方寫下 Q_OBJECT 宏(這個宏可以展開很多代碼)。如果不加上這個宏,那么編譯時就會報錯。這個宏不需要我們自己手動添加,一般 Qt Creator 會幫我們自動生成。
自定義信號函數書寫規范:
自定義信號在實際開發中很少需要用到,Qt 內置的信號就足以應付大部分的開發場景了。
????????所謂 Qt 的信號,本質上也就是一個 “函數”,在 Qt 5 以及更高版本中,槽函數和普通的成員函數之間沒有什么差別。但是,信號是一類非常特殊的函數,我們只要寫出函數聲明,并告訴 Qt 這是一個 “信號” 即可。這個函數的定義是 Qt 在編譯過程中自動生成的。
- 自定義信號函數必須寫到 "signals" 下。
- 返回值為?void,只需要聲明,不需要實現。
- 可以有參數,也可以發生重載。
????????此處的 signals?是 Qt 自己擴展的關鍵字。qmake 構建 Qt 項目時,就會調用一些代碼的分析 / 生成工具,掃描到類中包含 signals 這個關鍵字時,就會自動的把下面的函數聲明認為是信號,并且給這些信號函數自動的生成函數定義。
自定義槽函數書寫規范:
所謂的自定義一個槽函數,其操作規程和自定義一個普通成員函數沒有什么區別。
- 早期的 Qt 版本要求槽函數必須寫到 "public slots" 下,但是現在高級版本的 Qt 不做要求。
- 返回值為 void,需要聲明,也需要實現。
- 可以有參數,可以發生重載。
槽聲明的兩種寫法:?
????????此處的 slots 是 Qt 自己擴展的關鍵字,不是 C++ 標準中的語法。qmake 構建 Qt 項目時,就會調用專門的掃描器,掃描代碼中特定的關鍵字(比如:slot),基于關鍵字自動生成一大堆相關代碼。
發送信號 :
????????使用 "emit" 關鍵字發送信號 。"emit" 是一個空的宏。"emit" 其實是可選的,沒有什么含義,只是為了提醒開發人員。
使用示例:
在 widget.h 中聲明自定義的信號和槽
在 widget.cpp 中實現槽函數,并且關聯信號和槽
????????首先關聯信號和槽,一旦檢測到信號發射之后就會立刻執行關聯的槽函數。反之,若先發射信號,此時還沒有關聯槽函數,當信號發射之后槽函數就不會響應。
在 widget.cpp 中實現槽函數,并且關聯信號和槽
????????首先關聯信號和槽,?旦檢測到信號發射之后就會立刻執行關聯的槽函數。反之,若先發射信號,此時還沒有關聯槽函數,當信號發射之后槽函數就不會響應。
3.2 帶參數的信號和槽
????????Qt 的信號和槽也支持帶有參數,同時也可以支持重載。要求:信號函數的參數列表要和對應連接的槽函數參數列表一致。(一致主要是要求類型,個數如果不一致也可以,但要求信號的參數個數比槽的參數個數多)
????????此時信號觸發,調用到槽函數的時候,信號函數中的實參就能夠被傳遞到槽函數的形參當中。通過這樣的機制就可以讓信號給槽傳遞數據了。
(1)重載信號槽示例
在 "widget.h" 頭文件中聲明重載的信號函數以及重載的槽函數
在 "Widget.cpp" 文件實現重載槽函數以及連接信號和槽
執行結果:
(2)信號槽參數列表匹配規則示例
在 "widget.h" 頭文件中聲明信號和槽函數
在 "widget.cpp" 文件中實現槽函數以及連接信號和槽
????????其實信號的參數個數可以多于槽函數的參數個數,但是槽的參數個數不能多于信號參數個數。但是實際開發中最好還是保持參數個數也能匹配?致。
(3)信號的參數個數多于槽函數的參數個數
在 "widget.h" 頭文件中聲明信號和槽函數
在 "widget.cpp" 文件中實現槽函數以及連接信號和槽
4. 信號與槽的連接方式
4.1 一對一
主要有兩種形式,分別是:
- ?個信號連接?個槽
- ?個信號連接?個信號
一個信號連接一個槽
在 "widget.h" 中聲明信號和槽以及信號發射函數:
在 "widget.cpp" 中實現槽函數,信號發射函數以及連接信號和槽:
一個信號連接另一個信號
在上一段代碼的基礎上,在 "widget.cpp" 文件中添加如下代碼:
4.2 一對多
一個信號連接多個槽:
在 "widget.h" 頭文件中聲明一個信號和三個槽:
4.3 多對一
多個信號連接一個槽函數:
在 "widget.h" 頭文件中聲明兩個信號以及一個槽:
在 "widget.cpp" 文件中實現槽函數以及連接信號和槽:
4.4 多對多
多個信號連接多個槽函數:
在 "widget.h" 頭文件中聲明三個信號以及三個槽:
在 "widget.cpp" 文件中實現槽函數以及連接信號和槽:
????????實際上,隨著程序員經驗越來越多,在 GUI 開發的過程中,“多對多” 其實是個 “偽需求”,實際開發很少會用到,絕大部分情況來說,“一對一” 就夠用了。
信號槽存在的意義:
- 解耦合(寫代碼追求 “高內聚,低耦合”)
- 多對多(非常類似于 MySQL 中的 “多對多”)
5. 信號與槽的斷開
使用?disconnect?即可完成斷開,disconnect 的用法和 connect 基本一致。
????????實際上大部分情況下,把信號和槽連接上之后就不必管了,所以 disconnect 使用的比較少。主動斷開往往是把信號重新綁定到另一個槽函數上。
6. Qt4 版本信號與槽的連接
????????Qt4 中的 connect 用法和 Qt5 相比更復雜,需要搭配?SIGNAL?和?SLOT?宏來完成,而且缺少必要的函數類型的檢查,使代碼更容易出錯。
在 "widget.h" 頭文件中聲明信號和槽:
在 "widget.cpp" 文件中實現槽函數以及連接信號與槽:
Qt4 版本信號與槽連接的優缺點:
- 優點:參數直觀。
- 缺點:參數類型不做檢測。
7. Lambda 定義槽函數
????????Qt5 在 Qt4 的基礎上提高了信號與槽的靈活性,允許使用任意函數作為槽函數。但如果想方便的編寫槽函數,比如在編寫函數時連函數名都不想定義,則可以通過 Lambda 表達式來達到這個目的。
????????Lambda 表達式是?C++11?增加的特性。C++11 中的 Lambda 表達式用于定義并創建匿名的函數對象,以簡化編程工作。
Lambda 表達式的語法格式如下:
[ capture ] ( params ) opt -> ret { Function body;
};
說明:
7.1 局部變量引入方式 [ ]?
[ ] : 標識一個? Lambda 表達式的開始。不可省略。
說明:
- 由于使用引用方式捕獲對象會有局部變量釋放了而 Lambda 函數還沒有被調用的情況。如果執行?Lambda函數,那么引用傳遞方式捕獲進來的局部變量的值不可預知。所以絕大多數場合使用的形式為:[=] () { }
- 早期版本的 Qt,若要使用 Lambda 表達式,要在 ".pro" 文件中添加:?CONFIG += C++11?因為 Lambda 表達式是 C++11 標準提出的。Qt5 以上的版本無需手動添加,在新建項目時會自動添加。
Lambda 表達式的使用:
以 [=] 方式傳遞,外部的所有變量在 Lambda 表達式中都可以使用:
以 [a] 方式傳遞,在 Lambda 表達式中只能使用傳遞進來的 a:
????????Lambda 表達式除了可以按照值得方式來捕獲變量 [=],還可以按照引用得方式來捕獲 [&](但 Qt 中很少這樣寫),捕獲到的變量一般就是各自控件得指針。指針變量按照值傳遞或者引用來傳遞都無所謂。如果選擇按照引用來傳遞,還得更關注這個引用得變量本身的生命周期。
7.2 函數參數 ( )
????????(params) 表示 Lambda函數對象接收的參數,類似于函數定義中的小括號表示函數接收的參數類型和個數。參數可以通過按值(如:(int a,int b))和按引用(如:(int &a,int &b))兩種方式進行傳遞。函數參數部分可以省略,省略后相當于無參的函數。
7.3 選項 Opt
Opt?部分是可選項,最常用的是?mutable?聲明,這部分可以省略。
????????Lambda 表達式外部的局部變量通過值傳遞進來時,其默認是?const,所以不能修改這個局部變量的拷貝,加上 mutable 就可以修改。
7.4 返回值類型?->
????????可以指定 Lambda表達式返回值類型。如果不指定返回值類型,則編譯器會根據代碼實現為函數推導一個返回類型。如果沒有返回值,則可忽略此部分。
7.5 函數體?{ }
????????Lambda 表達式的函數體部分與普通函數體一致。用?{ }?標識函數的實現,不能省略,但函數體可以為空。
7.6 槽函數使用 Lambda
點擊按鈕關閉窗口:
當?"connect" 函數第三個參數為 "this" 時,第四個參數使用?Lambda表達式時,可以省略掉 "this"
8. 信號與槽的優缺點
優點:松散耦合
????????信號發送者不需要知道發出的信號被哪個對象的槽函數接收,槽函數也不需要知道哪些信號關聯了自己,Qt 的信號槽機制保證了信號與槽函數的調用。支持信號槽機制的類或者父類必須繼承于 QObject 類。
缺點:效率較低
????????與回調函數相比,信號和槽稍微慢一些,因為它們提供了更高的靈活性,盡管在實際應用程序中差別不大。通過信號調用的槽函數比直接調用的速度慢約 10 倍(這是定位信號的接收對象所需的開銷;遍歷所有關聯;編組 / 解組傳遞的參數;多線程時,信號可能需要排隊),這種調用速度對性能要求不是非常高的場景是可以忽略的,是可以滿足絕大部分場景。
? ? ? ? 一個客戶端程序中,最慢的環節往往是 “人”。假設本身基于回調的方式是 10us,使用信號槽的方式是 100us。對于使用程序的人來說,是感知不到的。
本篇完。
下一篇:Qt開發④Qt常用控件_上_QWdget屬性+按鈕類控件。