1. 程式人生 > >Qt Signal and Slot

Qt Signal and Slot

Qt4中的訊號槽

Qt4中的訊號槽是通過SIGNAL,SLOT兩個巨集,將引數轉換成字串.Qt編譯前,會從原始碼的標頭檔案中提取由signalslot宣告的訊號和槽的函式,
將其組成一張訊號和槽對應的字串表.connect函式的作用是,將訊號關聯的槽字串,同這張表的資訊進行對比.這樣訊號發出的時候,就可以知道呼叫哪一個槽函數了.

Qt4訊號槽的不足

  1. 沒有編譯期的檢查:Qt4中的訊號槽會被巨集轉化成字串處理,而字串的比較機制是在程式執行的時候檢測的.而且,轉換成字串後,訊號槽的引數資料型別就會丟失.這就導致,有的時候,訊號槽在編譯的時候沒有問題,在執行的時候,反而出錯.
  2. 無法使用相容型別的引數
    :因為訊號槽的機制使用的是字串的匹配的方法,所以,槽函式的引數型別的名字,必須和訊號引數型別的名字一致,同時,還必須和標頭檔案中宣告的型別名字一致,也就是字串意義上的嚴格相同.如果使用了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中的問題,而且還有一些擴充.

  1. 支援編譯期的檢查:拼寫錯誤,槽函式引數個數大於訊號引數的個數等;
  2. 支援相容型別的自動轉換;
  3. 槽允許連線到任意的函式: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類,進行訊號和槽的對映。 民間也有很多其他的好方法;

  1. 每一個槽在觸發的時候,都可以獲得對應的sender物件,可以根據不同的物件,進行不同的操作。
void slotFun(){
    QObject* obj = sender();
}
  1. 或者使用lambda表示式. connect(m_button, &QPushButton::clicked,[m,this]()->void{HandleButton(m);});

訊號和槽的注意

  1. Strongly advise against deleting the signal object in the slot function.

參考