信号槽
信號槽是 Qt 框架引以為豪的機制之一。熟練使用和理解信號槽,能夠設計出解耦的非常漂亮的程序,有利于增強我們的技術設計能力。
所謂信號槽,實際就是觀察者模式。當某個事件發生之后,比如,按鈕檢測到自己被點擊了一下,它就會發出一個信號(signal)。這種發出是沒有目的的,類似廣播。如果有對象對這個信號感興趣,它就會使用連接(connect)函數,意思是,用自己的一個函數(成為槽(slot))來處理這個信號。也就是說,當信號發出時,被連接的槽函數會自動被回調。這就類似觀察者模式:當發生了感興趣的事件,某一個操作就會被自動觸發。(這里提一句,Qt 的信號槽使用了額外的處理來實現,并不是 GoF 經典的觀察者模式的實現方式。)
為了體驗一下信號槽的使用,我們以一段簡單的代碼說明:
| 1234567891011121314 | // !!! Qt 5#include <QApplication>#include <QPushButton>int main(int argc, char *argv[]){????QApplication app(argc, argv);????QPushButton button("Quit");????QObject::connect(&button, &QPushButton::clicked, &QApplication::quit);????button.show();????return app.exec();} |
這里再次強調,我們的代碼是以 Qt 5 為主線,這意味著,有的代碼放在 Qt 4 上是不能編譯的。因此,豆子會在每一段代碼的第一行添加注釋,用以表明該段代碼是使用 Qt 5 還是 Qt 4 進行編譯。讀者在測試代碼的時候,需要自行選擇相應的 Qt 版本。
我們按照前面文章中介紹的在 Qt Creator 中創建工程的方法創建好工程,然后將main()函數修改為上面的代碼。點擊運行,我們會看到一個按鈕,上面有“Quit”字樣。點擊按鈕,程序退出。
按鈕在 Qt 中被稱為QPushButton。對它的創建和顯示,同前文類似,這里不做過多的講解。我們這里要仔細分析QObject::connect()這個函數。
在 Qt 5 中,QObject::connect()有五個重載:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | QMetaObject::Connection connect(const QObject *, const char *, ????????????????????????????????const QObject *, const char *, ????????????????????????????????Qt::ConnectionType); QMetaObject::Connection connect(const QObject *, const QMetaMethod &, ????????????????????????????????const QObject *, const QMetaMethod &, ????????????????????????????????Qt::ConnectionType); QMetaObject::Connection connect(const QObject *, const char *, ????????????????????????????????const char *, ????????????????????????????????Qt::ConnectionType) const; QMetaObject::Connection connect(const QObject *, PointerToMemberFunction, ????????????????????????????????const QObject *, PointerToMemberFunction, ????????????????????????????????Qt::ConnectionType) QMetaObject::Connection connect(const QObject *, PointerToMemberFunction, ????????????????????????????????Functor); |
這五個重載的返回值都是QMetaObject::Connection,現在我們不去關心這個返回值。下面我們先來看看connect()函數最常用的一般形式:
| 123 | // !!! Qt 5connect(sender,?? signal,????????receiver, slot); |
這是我們最常用的形式。connect()一般會使用前面四個參數,第一個是發出信號的對象,第二個是發送對象發出的信號,第三個是接收信號的對象,第四個是接收對象在接收到信號之后所需要調用的函數。也就是說,當 sender 發出了 signal 信號之后,會自動調用 receiver 的 slot 函數。
這是最常用的形式,我們可以套用這個形式去分析上面給出的五個重載。第一個,sender 類型是const QObject *,signal 的類型是const char *,receiver 類型是const QObject *,slot 類型是const char *。這個函數將 signal 和 slot 作為字符串處理。第二個,sender 和 receiver 同樣是const QObject *,但是 signal 和 slot 都是const QMetaMethod &。我們可以將每個函數看做是QMetaMethod的子類。因此,這種寫法可以使用QMetaMethod進行類型比對。第三個,sender 同樣是const QObject *,signal 和 slot 同樣是const char *,但是卻缺少了 receiver。這個函數其實是將 this 指針作為 receiver。第四個,sender 和 receiver 也都存在,都是const QObject *,但是 signal 和 slot 類型則是PointerToMemberFunction。看這個名字就應該知道,這是指向成員函數的指針。第五個,前面兩個參數沒有什么不同,最后一個參數是Functor類型。這個類型可以接受 static 函數、全局函數以及 Lambda 表達式。
由此我們可以看出,connect()函數,sender 和 receiver 沒有什么區別,都是QObject指針;主要是 signal 和 slot 形式的區別。具體到我們的示例,我們的connect()函數顯然是使用的第五個重載,最后一個參數是QApplication的 static 函數quit()。也就是說,當我們的 button 發出了clicked()信號時,會調用QApplication的quit()函數,使程序退出。
信號槽要求信號和槽的參數一致,所謂一致,是參數類型一致。如果不一致,允許的情況是,槽函數的參數可以比信號的少,即便如此,槽函數存在的那些參數的順序也必須和信號的前面幾個一致起來。這是因為,你可以在槽函數中選擇忽略信號傳來的數據(也就是槽函數的參數比信號的少),但是不能說信號根本沒有這個數據,你就要在槽函數中使用(就是槽函數的參數比信號的多,這是不允許的)。
如果信號槽不符合,或者根本找不到這個信號或者槽函數的話,比如我們改成:
| 1 | QObject::connect(&button, &QPushButton::clicked, &QApplication::quit2); |
由于 QApplication 沒有 quit2 這樣的函數的,因此在編譯時,會有編譯錯誤:
| 1 | 'quit2' is not a member of QApplication |
這樣,使用成員函數指針,我們就不會擔心在編寫信號槽的時候會出現函數錯誤。
借助 Qt 5 的信號槽語法,我們可以將一個對象的信號連接到 Lambda 表達式,例如:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // !!! Qt 5 #include <QApplication> #include <QPushButton> #include <QDebug> int main(int argc, char *argv[]) { ????QApplication app(argc, argv); ????QPushButton button("Quit"); ????QObject::connect(&button, &QPushButton::clicked, [](bool) { ????????qDebug() << "You clicked me!"; ????}); ????button.show(); ????return app.exec(); } |
注意這里的 Lambda 表達式接收一個 bool 參數,這是因為QPushButton的clicked()信號實際上是有一個參數的。Lambda 表達式中的qDebug()類似于cout,將后面的字符串打印到標準輸出。如果要編譯上面的代碼,你需要在 pro 文件中添加這么一句:
| 1 | QMAKE_CXXFLAGS += -std=c++0x |
然后正常編譯即可。
Qt 4 的信號槽同 Qt 5 類似。在 Qt 4 的 QObject 中,有三個不同的connect()重載:
C++| 1 2 3 4 5 6 7 8 9 10 11 | bool connect(const QObject *, const char *, ???????????? const QObject *, const char *, ???????????? Qt::ConnectionType); bool connect(const QObject *, const QMetaMethod &, ???????????? const QObject *, const QMetaMethod &, ???????????? Qt::ConnectionType); bool connect(const QObject *, const char *, ???????????? const char *, ???????????? Qt::ConnectionType) const |
除了返回值,Qt 4 的connect()函數與 Qt 5 最大的區別在于,Qt 4 的 signal 和 slot 只有const char *這么一種形式。如果我們將上面的代碼修改為 Qt 4 的,則應該是這樣的:
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // !!! Qt 4 #include <QApplication> #include <QPushButton> int main(int argc, char *argv[]) { ????QApplication app(argc, argv); ????QPushButton button("Quit"); ????QObject::connect(&button, SIGNAL(clicked()), ???????????????????? &app,????SLOT(quit())); ????button.show(); ????return app.exec(); } |
我們使用了SIGNAL和SLOT這兩個宏,將兩個函數名轉換成了字符串。注意,即使quit()是QApplication的 static 函數,也必須傳入一個對象指針。這也是 Qt 4 的信號槽語法的局限之處。另外,注意到connect()函數的 signal 和 slot 都是接受字符串,因此,不能將全局函數或者 Lambda 表達式傳入connect()。一旦出現連接不成功的情況,Qt 4 是沒有編譯錯誤的(因為一切都是字符串,編譯期是不檢查字符串是否匹配),而是在運行時給出錯誤。這無疑會增加程序的不穩定性。
信號槽機制是 Qt 的最大特性之一。這次我們只是初略了解了信號槽,知道了如何使用connect()函數進行信號槽的連接。在后面的內容中,我們將進一步介紹信號槽,了解如何設計自己的信號槽等等。
總結
- 上一篇: QT 信号与槽 最简单例子
- 下一篇: 入门Qt——hello, world