Qt Signal and Slot
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
類,進行訊號和槽的對映。 民間也有很多其他的好方法;
- 每一個槽在觸發的時候,都可以獲得對應的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.