Qt4中的信號槽
Qt4中的信號槽是通過SIGNAL,SLOT
兩個宏,將參數轉換成字符串.Qt編譯前,會從源碼的頭文件中提取由signal
和slot
聲明的信號和槽的函數,
將其組成一張信號和槽對應的字符串表.connect
函數的作用是,將信號關聯的槽字符串,同這張表的信息進行對比.這樣信號發出的時候,就可以知道調用哪一個槽函數了.
Qt4信號槽的不足
- 沒有編譯期的檢查:Qt4中的信號槽會被宏轉化成字符串處理,而字符串的比較機制是在程序運行的時候檢測的.而且,轉換成字符串后,信號槽的參數數據類型就會丟失.這就導致,有的時候,信號槽在編譯的時候沒有問題,在運行的時候,反而出錯.
- 無法使用相容類型的參數:因為信號槽的機制使用的是字符串的匹配的方法,所以,槽函數的參數類型的名字,必須和信號參數類型的名字一致,同時,還必須和頭文件中聲明的類型名字一致,也就是字符串意義上的嚴格相同.如果使用了
typdef
或者namespace
這樣的類型,雖然實際的類型是一樣的,但是由於字符串的名字不一樣,所以Qt4中是會有錯誤的.如下偽代碼示例(實際類型都是int,但因為按照字符串處理,所以Qt4中,編譯前不能通過.)
//head.h file
typedef int MyInt;
typedef int BigInt;
//head.cpp file
connect(Sender,SIGNAL(sigFun(MyInt)),Receiver,SLOT(sltFun(BigInt)));
Qt5中的信號槽
Qt5中不僅解決了上述Qt4中的問題,而且還有一些擴充.
- 支持編譯期的檢查:拼寫錯誤,槽函數參數個數大於信號參數的個數等;
- 支持相容類型的自動轉換;
- 槽允許連接到任意的函數:Qt5中,因為槽使用的是函數指針,所以槽的調用,可以是任意的成員函數,靜態函數,還可以是C++11 的lambda表達式;Qt4中槽的聲明一般是
private slots
,private
是私有限制,只有把槽函數當作普通函數使用的時候,才會體現私有的性質.而SLOT
,把槽函數轉化成了字符串,此時private
是不起作用的.Qt5中,因為使用的是函數指針,所以在類的外部,connect
是無法關聯一個類的私有槽的,否則,編譯的時候就會報錯.
總之,Qt5中,增加了信號槽的靈活性,加強了信號槽的檢測性.
Qt5信號槽的語法例子
常用用法
//ClassA.h
signal:
void sigClassA(int num);
void sigStringChanged(QString str);
//ClassB.h
void sltClassB(int num);//任意的成員函數,靜態函數都闊以
void sltStringChanged(QVariant str);
//ClassB.cpp
connect(Sender, &ClassA::sigClassA, this, &ClassB::sltClassB);//函數指針關聯的時候,不需要指明參數,而且this可以省略[**Update:2016_11_20,注意在這種情況下,可以省略, 在其他的情況就一定了.省略會有錯誤危險的.**]
connect(Sender, &ClassA::sigClassA, &ClassB::quit);//省略了this,同時靜態函數quit作為槽
//QString 可以轉化成 QVariant
connect(Sender, &ClassA::sigStringChanged, &ClassB::sltStringChanged);//信號槽的參數類型可以發生隱士類型轉化即可
信號槽的重載
解決方法:
- 使用Qt4的方法(不再介紹)
- Qt5顯示轉換函數指針
//信號的重載和槽的重載都是一樣的解決機制
//ClassA.h
signal:
void sigClassA();
void sigClassA(int num);
//ClassB.h
void sltClassB();
void sltClassB(int num);
//ClassB.cpp
connect(Sender, static_cast<void(ClassA::\*)()>(&ClassA::sigClassA),//注意\*為markdown轉義
this,static_cast<void(ClassA::\*)()>(&ClassB::sltClassB) );
connect(Sender, static_cast<void(ClassA::\*)(int)>(&ClassA::sigClassA),
this,static_cast<void(ClassA::\*)()>(&ClassB::sltClassB) );
connect(Sender, static_cast<void(ClassA::\*)(int)>(&ClassA::sigClassA),
this,static_cast<void(ClassA::\*)(int)>(&ClassB::sltClassB) );
帶默認數值的槽函數
解決方法:
- 進一步的封裝函數(不做介紹)
- 采用Qt5的C++11 lambda表達式(表達式規則暫且不做詳細介紹)
//ClassA.h
signal:
void sigClassA();
void sigClassA(int num);
//ClassB.h
void sltClassB(int num = 10);
//ClassB.cpp
connect(Sender, static_cast<void(ClassA::\*)(int)>(&ClassA::sigClassA),//注意\*為markdown轉義
this,static_cast<void(ClassA::\*)(int)>(&ClassB::sltClassB) );//信號和槽的參數個數對應,是可以的
connect(Sender, static_cast<void(ClassA::\*)()>(&ClassA::sigClassA),
this,static_cast<void(ClassA::\*)()>(&ClassB::sltClassB) );//槽的參數,比信號多,這個會報錯誤的
//函數參數的默認數值,只有在函數調用的時候,才會有效,取函數地址的時候,是看不到參數的默認數值的.函數指針並不包含默認數值.又因為槽包含默認數值,所以信號可以不提供參數.那么,這就和信號的參數個必須大於槽的參數的個數產生了矛盾.
connect(Sender, static_cast<void(ClassA::\*)()>(&ClassA::sigClassA),
this,[=](int num = 10){//使用了lambda表達式
//...函數體
}] );
[update:2017_10_13]
信號和槽函數使用自定義數據類型
對於自定義的數據類型,使用前去要使用qRegisterMetaType<NEW_TYPE>("NEW_TYPE");
進行一下類型的注冊。
[update:2016_11_20]
思考this的省略?
前面提到過connect函數的第三個參數this指針是可以省略的.但是在某些情況下this是絕不可以省略的.甚至我建議大家為了避免不必要的錯誤, this指針最好不要省略, 還是帶上比較好.connect函數基本是如下的原型:
connect( 發送者, 發送者信號, 接收者, 接收者處理方法 ); ///< 一般的四個參數
connect( 發送者, 發送者信號, **發送者**處理方法 ); ///< 如果省略this, 是三個參數, 那么最后一個參數的意義就發生了變化. 此時調用的方法則是發送者自己的方法.
///< 試想一下, 如果此時發送者和接收者又剛好擁有同樣的函數名字, 但是內部的方法不同, 那么最后的結果就會讓人莫名奇妙的詭異起來.
所以,一定要明確的區分每個參數的具體意義, 馬馬虎虎最終還是自己填坑.
你也看到connect是可以使用C++的匿名函數的, 也是可以省略this的,但是, 這一步一定要小心了. 尤其是當你在使用線程的時候, 在接收線程信號的時候, 一萬個小心.比如:
connect(pThread, &QThread::finished, [=]() { myFun(); } ); ///< 當線程執行完后, 你會驚奇的接收到應用程序的崩潰. 基本的提示內容, 就是, 你的某個線程出了問題.
///< 就上面的問題,你可以在多個地方, 把線程的ID打印出來就知道了.
qDebug() << "ThreadId1:";
connect(pThread, &QThread::finished, [=]()
{
qDebug() << "ThreadId2:";
myFun();
} );
///< 打印出來以后, 你會發現lambda表達式函數里面ID和線程run函數里面的id是一樣的. 雖然說, 代碼在不同的類里面, 不同的文件里面. 可是運行環境, 運行的線程卻是可以在一起的. 解決方法, 加個this, 就可以了. 你懂的.
信號槽的高級應用
Help manual 說的是高級應用,也不是什么特別深奧的東西,面對一下特殊的需求,解決方法有很多種。
特殊需求:
大部分信號和槽的使用,都是少數對象的信號和少數槽的鏈接,寫幾個connect就可以了。如果有很多對象,同類的或者不同類的,每個對象對應不同的信號槽,那么寫起來就繁瑣一些了。這時候官方提供的是使用QSignalMapper
類,進行信號和槽的映射。 民間也有很多其他的好方法;
- 借助 QSignalMapper
- 每一個槽在觸發的時候,都可以獲得對應的sender對象,可以根據不同的對象,進行不同的操作。
void slotFun(){
QObject* obj = sender();
}
- 或者使用lambda表達式.
connect(m_button, &QPushButton::clicked,[m,this]()->void{HandleButton(m);});
信號和槽的注意
- Strongly advise against deleting the signal object in the slot function.
參考
《Qt 5編程入門》
Qt Signal and Slots
QSignalMapper
QSignalSpy
QSignalBlocker
QSignalTransition
Qt Forum