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