c++ Qt信號槽原理
1、說明
使用Qt已經(jīng)好幾年了,一直以為自己懂Qt,熟悉Qt,使用起來很是熟練,無論什么項目,都喜歡用Qt編寫。但真正去看Qt的源碼,去理解Qt的思想也就近兩年的事。
本次就著重介紹一下Qt的核心功能--信號槽機制,相信接觸過Qt的人都能很熟悉地使用,甚至,大部分人還能輕松地說出信息槽的幾種用法。但是信號槽的核心可不是簡單說說就能說清楚的。
那么,本次,就從Qt的源碼中講解一下信號槽的機制。
其實,直到寫這篇文章,我也沒有完全看明白相關(guān)的源碼,只是明白了其中的大部分以及使用機制,其中還有很多細(xì)節(jié)的,留待以后整理。
如果錯誤還請大家指正。
2、環(huán)境以及知識點
Qt版本:Qt 5.5.1
系統(tǒng):windows 10
在閱讀本文前,希望你能:
- 熟練使用C++,了解make的編譯方法和過程;
- 熟練使用Qt的信號槽功能,對信號槽的寫法以及4和5的區(qū)別了如指掌;
- QMetaObject元數(shù)據(jù)系統(tǒng);
- 懂一些設(shè)計模式,能理解觀察者模式;
3、信號槽源碼分析
以下將按照SIGNAL/SLOT宏定義連接信號槽的方式做講解
接下來將會從按照以下的步驟來進行分析:
- Qt元數(shù)據(jù)系統(tǒng);
- moc預(yù)編譯;
- Q_OBJECT宏;
- signals和slots關(guān)鍵字以及emit;
- SIGNAL()和SLOT()宏;
- connect 方法;
- 觸發(fā)信號;
3.1、Qt的元數(shù)據(jù)系統(tǒng)
沒看過Qt源碼的同學(xué)可能會對QMetaObject有些陌生,我們打開Qt手冊,查看此類的說明,介紹如下:
The QMetaObject class contains meta-information about Qt objects.
The Qt Meta-Object System in Qt is responsible for the signals and slots inter-object communication mechanism, runtime type information, and the Qt property system. A single QMetaObject instance is created for each QObject subclass that is used in an application, and this instance stores all the meta-information for the QObject subclass. This object is available as QObject::metaObject().
這里是說,QMetaObject包含了Qt的元對象信息。元對象機制類似Java的反射機制。通過繼承QObject,并在定義類是添加一定Qt內(nèi)置宏,能在運行時動態(tài)獲取Qt的信號槽、類型信息以及相關(guān)屬性。
一個簡答的例子
void MainWindow::onClickButton() { qDebug()<<"on click button"; const QMetaObject* metaObject = this->metaObject(); qDebug()<<metaObject->className(); qDebug()<<metaObject->superClass()->className(); int methodIndex = metaObject->indexOfMethod("testFunction()"); qDebug()<<methodIndex; qDebug()<<metaObject->method(methodIndex).name(); metaObject->method(methodIndex).invoke(this); QMetaObject::invokeMethod(this, "testFunction"); }
如上,一個簡單的例子,通過QMetaObject,我們得到了該對象的類名、父類名、方法并調(diào)用了該方法
怎么樣,熟悉Java的小伙伴已經(jīng)發(fā)現(xiàn)了,這不就是Java的反射嗎,誰說C++沒有反射呢
那么,Qt是如何實現(xiàn)”反射“的呢?答案是使用moc預(yù)編譯
3.2、moc編譯
moc全稱Meta-Object Compiler,即元對象編譯器。我們可以在Qt的安裝目錄的bin文件下看到moc工具,moc.exe。Qt的構(gòu)建的時候,會調(diào)用該工具生成moc文件,我們在編譯目錄下看到的moc_xxx.cpp文件就是該工具生成的。
Qt的MinGW版本使用的是qmake進行項目管理,它和cmake功能類似,但沒有后者強大。使用qmake生成Makefile后,我們打開Makefile文件,我們可以狠清楚地看到有一個調(diào)用moc.exe工具的地方,代碼太多,就不列出來了。
此外,我們還發(fā)現(xiàn),并不是所有的代碼都會生成moc_xxx.cpp文件的,只有使用了 Q_OBJECT 宏的類文件,才會生成。沒有錯,moc工具就是根據(jù) Q_OBJECT 宏來生成moc_xxx.cpp文件的,而實現(xiàn)“反射”的元數(shù)據(jù)系統(tǒng)的也是依靠Q_OBJECT的。
到此,我們其實已經(jīng)能夠大概理清qmake項目的構(gòu)建步驟了。步驟和常用的cmake項目類似,區(qū)別就是,qmake生成的Makefile文件種,會寫有調(diào)用moc工具的指令,以達(dá)到moc_xxx.cpp文件的生成。
我們可以使用moc工具手動生成moc_xxx.cpp,使用指令 moc.exe mainwindow.h,即會在控制臺打印moc文件信息,也可以使用 -o 參數(shù)來將生成的內(nèi)容寫入文件,其余參數(shù)可以使用 moc.exe -h 來查看
3.3、Q_OBJECT
我們可以從源代碼中查看 Q_OBJECT 的內(nèi)容,這里調(diào)整一個格式,使用 Q_OBJECT 宏之后,會在類定義的開頭多出以下代碼:
public: Q_OBJECT_CHECK QT_WARNING_PUSH Q_OBJECT_NO_OVERRIDE_WARNING static const QMetaObject staticMetaObject; virtual const QMetaObject *metaObject() const; virtual void *qt_metacast(const char *); virtual int qt_metacall(QMetaObject::Call, int, void **); QT_WARNING_POP QT_TR_FUNCTIONS private: Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); struct QPrivateSignal {};
可以看到,這里多出幾個方法和一些變量
- 屬性staticMetaObject,元數(shù)據(jù)對象,可以從中獲取當(dāng)前類的元數(shù)據(jù);
- 方法metaObject(),獲取元數(shù)據(jù)對象指針,大多數(shù)情況下,返回staticMetaObject指針;
- 方法qt_metacast(),原數(shù)據(jù)對象類型轉(zhuǎn)換,轉(zhuǎn)換成指定的類型,使用時一般傳入父類的名稱字符串;
- 方法qt_metacall(),執(zhí)行函數(shù)的回調(diào),信號觸發(fā);
- 方法qt_static_metacall(),回調(diào)函數(shù),被qt_metacall()調(diào)用,內(nèi)部執(zhí)行槽;
這里的幾個方法都沒有實現(xiàn)體,因為實現(xiàn)部分會有 moc 工具生成,在moc_xxx.cpp 文件中可以查看這些方法的實現(xiàn)體
3.4、signals和slots
signals 用于聲明自定義信號,slots 用于聲明槽函數(shù),emit 用于發(fā)送信號,我們可以從源碼中查看這三個宏定義
define slots define signals public define emit
可以看出,這三個宏幾乎什么都沒有做,signals 就是聲明所謂的信號是public方法,而slots和emit更是為空,標(biāo)準(zhǔn)C++在編譯的時候,根本不受這三個宏的影響,那么它們的用處在哪里呢?在moc工具調(diào)用和connect連接的時候。
打開moc_xxx.cpp文件,對比查看信號
signals: void clickButton(int value); void clickButton2();
// SIGNAL 0 void MainWindow::clickButton(int _t1) { void *_a[] = { Q_NULLPTR, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) }; QMetaObject::activate(this, &staticMetaObject, 0, _a); } // SIGNAL 1 void MainWindow::clickButton2() { QMetaObject::activate(this, &staticMetaObject, 1, Q_NULLPTR); }
其實,信號就是方法,而 emit clickButton() 發(fā)送信號,就是調(diào)用 clickButton() 方法,換言之,觸發(fā)信號,就算不要emit也無妨
3.5、SIGNAL()和SLOT()
查看源碼
# define SLOT(a) qFlagLocation("1"#a QLOCATION) # define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
qFlagLocation() 源碼如下:
const char *qFlagLocation(const char *method) { QThreadData *currentThreadData = QThreadData::current(false); if (currentThreadData != 0) currentThreadData->flaggedSignatures.store(method); return method; }
store() 方法
void store(const char* method) { locations[idx++ % Count] = method; }
所以 SIGNAL(clickButton()) 宏展開為 qFlagLocation("2"clickButton(int) QLOCATION)
SLOT() 同理,這里的1和2,最后會添加到信號槽的前面,其實是為了區(qū)分信號和槽,源碼中還有一個0在 METHOD() 宏
qFlagLocation 方法的作用是將信號槽轉(zhuǎn)換成字符串保存起來,store 方法中,locations是個二維數(shù)組,而 idx 每次都加一,保證信號和槽的不同的方法存儲在不同的數(shù)組中。
我們也可以在代碼中打印出來看下:
qDebug()<<SIGNAL(clickButton(int)); //2clickButton(int) qDebug()<<SLOT(onClickButton()); //1onClickButton()
3.6、connect方法
最后,就是最關(guān)鍵的connect方法,做了一些簡單的注釋
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type) { if (sender == 0 || receiver == 0 || signal == 0 || method == 0) { qWarning("QObject::connect: Cannot connect %s::%s to %s::%s", sender ? sender->metaObject()->className() : "(null)", (signal && *signal) ? signal+1 : "(null)", receiver ? receiver->metaObject()->className() : "(null)", (method && *method) ? method+1 : "(null)"); return QMetaObject::Connection(0); } QByteArray tmp_signal_name; if (!check_signal_macro(sender, signal, "connect", "bind")) return QMetaObject::Connection(0); const QMetaObject *smeta = sender->metaObject();//獲取發(fā)送者的元數(shù)據(jù)對象 const char *signal_arg = signal;//信號 ++signal; //skip code QArgumentTypeArray signalTypes;//信號參數(shù)類型數(shù)組 Q_ASSERT(QMetaObjectPrivate::get(smeta)->revision >= 7); //信號轉(zhuǎn)換為簽名,并得到信號參數(shù)類型數(shù)組 QByteArray signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes); //找到信號索引 int signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); //小于0表示,表示信號索引有問題 if (signal_index < 0) { // check for normalized signatures //將信號重新規(guī)范化,再進行上面的簽名轉(zhuǎn)換,并重新得到索引 tmp_signal_name = QMetaObject::normalizedSignature(signal - 1); signal = tmp_signal_name.constData() + 1; //重新進行簽名轉(zhuǎn)換,并得到參數(shù)類型列表 signalTypes.clear(); signalName = QMetaObjectPrivate::decodeMethodSignature(signal, signalTypes); smeta = sender->metaObject(); signal_index = QMetaObjectPrivate::indexOfSignalRelative( &smeta, signalName, signalTypes.size(), signalTypes.constData()); } //重新獲取的信號索引還是無效,則是頭文件中信號的定義出錯,找不到信號,報錯信號不存在 if (signal_index < 0) { err_method_notfound(sender, signal_arg, "connect"); err_info_about_objects("connect", sender, receiver); return QMetaObject::Connection(0); } //根據(jù)當(dāng)前信號的索引找到最原始的信號的索引,因為信號是可以被繼承,這里找的祖先信號 signal_index = QMetaObjectPrivate::originalClone(smeta, signal_index); signal_index += QMetaObjectPrivate::signalOffset(smeta);//信號的索引再加上信號的偏移量 QByteArray tmp_method_name; //提取槽的編碼,應(yīng)該是QSLOT_CODE或者QSIGNAL_CODE,用于判斷槽是信號還是方法 int membcode = extract_code(method); //檢查槽編碼,槽可以是槽函數(shù)或者信號,初次以為,都無效 if (!check_method_code(membcode, receiver, method, "connect")) return QMetaObject::Connection(0); const char *method_arg = method; ++method; // skip code QArgumentTypeArray methodTypes; //轉(zhuǎn)換槽簽名,并獲取槽的參數(shù)類型列表 QByteArray methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); const QMetaObject *rmeta = receiver->metaObject();//獲取接受者的元數(shù)據(jù)對象 int method_index_relative = -1; Q_ASSERT(QMetaObjectPrivate::get(rmeta)->revision >= 7); switch (membcode) { case QSLOT_CODE://接受者是槽函數(shù) method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; case QSIGNAL_CODE://接受者是信號 method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; } //槽的索引為-1,表示無效 if (method_index_relative < 0) { // check for normalized methods //將槽進行規(guī)范化處理,并重新轉(zhuǎn)換槽簽名 tmp_method_name = QMetaObject::normalizedSignature(method); method = tmp_method_name.constData(); methodTypes.clear(); methodName = QMetaObjectPrivate::decodeMethodSignature(method, methodTypes); // rmeta may have been modified above //接受者元數(shù)據(jù)對象前面可能被修改過,這里重新獲取 rmeta = receiver->metaObject(); //重新獲取槽的索引 switch (membcode) { case QSLOT_CODE: method_index_relative = QMetaObjectPrivate::indexOfSlotRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; case QSIGNAL_CODE: method_index_relative = QMetaObjectPrivate::indexOfSignalRelative( &rmeta, methodName, methodTypes.size(), methodTypes.constData()); break; } } //如果還找不到,則說明槽定義有誤,報錯 if (method_index_relative < 0) { err_method_notfound(receiver, method_arg, "connect"); err_info_about_objects("connect", sender, receiver); return QMetaObject::Connection(0); } //檢查信號和槽的參數(shù) if (!QMetaObjectPrivate::checkConnectArgs(signalTypes.size(), signalTypes.constData(), methodTypes.size(), methodTypes.constData())) { qWarning("QObject::connect: Incompatible sender/receiver arguments" "\n %s::%s --> %s::%s", sender->metaObject()->className(), signal, receiver->metaObject()->className(), method); return QMetaObject::Connection(0); } int *types = 0; //隊列連接檢查,參數(shù)要是基本類型,或者使用元數(shù)據(jù)注冊 if ((type == Qt::QueuedConnection) && !(types = queuedConnectionTypes(signalTypes.constData(), signalTypes.size()))) { return QMetaObject::Connection(0); } #ifndef QT_NO_DEBUG //打印調(diào)試信息 QMetaMethod smethod = QMetaObjectPrivate::signal(smeta, signal_index); QMetaMethod rmethod = rmeta->method(method_index_relative + rmeta->methodOffset()); check_and_warn_compat(smeta, smethod, rmeta, rmethod); #endif QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect( sender, signal_index, smeta, receiver, method_index_relative, rmeta ,type, types)); return handle; } 方法代碼很多很雜,但無非就是檢查信號槽的格式,獲取參數(shù)列表, 最后保存起來 QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection); c->sender = s; //發(fā)送者對象 c->signal_index = signal_index; //信號索引 c->receiver = r; //接受者對象 c->method_relative = method_index; //槽索引 c->method_offset = method_offset; //槽偏移 c->connectionType = type; //連接方式 c->isSlotObject = false; c->argumentTypes.store(types); c->nextConnectionList = 0; c->callFunction = callFunction;//靜態(tài)回調(diào)函數(shù) //在發(fā)送者元數(shù)據(jù)內(nèi)加上連接信息 //信號發(fā)送者的對象內(nèi)存中保存了連接的信息,包括槽的對象,槽地址,連接方式等 QObjectPrivate::get(s)->addConnection(signal_index, c.data());
3.7、觸發(fā)信號
這時候再回過頭來看3.4中的信號觸發(fā),我們知道,emit信號就是調(diào)用moc文件中的方法,方法的核心就是 QMetaObject::activate()
直接看該方法中調(diào)用槽函數(shù)的一段
//因為一個信號可能連接多個槽,這里循環(huán)遍歷鏈表進行調(diào)用 do { QObjectPrivate::Connection *c = list->first; if (!c) continue; // We need to check against last here to ensure that signals added // during the signal emission are not emitted in this emission. QObjectPrivate::Connection *last = list->last; do { if (!c->receiver) continue; QObject * const receiver = c->receiver; const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId; // determine if this connection should be sent immediately or // put into the event queue //直接連接并且發(fā)送和接受不再一個線程中,或者隊列連接,則放入事件隊列中 //可知,直接連接并且發(fā)送和接受不在同一個線程,則效果和隊列連接相同 if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread) || (c->connectionType == Qt::QueuedConnection)) { queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker); continue; #ifndef QT_NO_THREAD //阻塞式隊列連接 } else if (c->connectionType == Qt::BlockingQueuedConnection) { locker.unlock(); //在同一個線程,則報錯 if (receiverInSameThread) { qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: " "Sender is %s(%p), receiver is %s(%p)", sender->metaObject()->className(), sender, receiver->metaObject()->className(), receiver); } QSemaphore semaphore;//資源計數(shù)器,avail為0 QMetaCallEvent *ev = c->isSlotObject ? new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) : new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore); //根據(jù)連接信息構(gòu)造一個事件,并添加到接受者的 事件隊列中 QCoreApplication::postEvent(receiver, ev); //信號發(fā)送者的線程阻塞,acquire資源數(shù)為1,>avail(0),這里阻塞 //當(dāng)槽執(zhí)行玩之后釋放,這里的avail才會增加,阻塞結(jié)束 semaphore.acquire(); locker.relock(); continue; #endif } QConnectionSenderSwitcher sw; if (receiverInSameThread) { sw.switchSender(receiver, sender, signal_index); } const QObjectPrivate::StaticMetaCallFunction callFunction = c->callFunction; const int method_relative = c->method_relative; if (c->isSlotObject) { c->slotObj->ref(); QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj); locker.unlock(); obj->call(receiver, argv ? argv : empty_argv); // Make sure the slot object gets destroyed before the mutex is locked again, as the // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool, // and that would deadlock if the pool happens to return the same mutex. obj.reset(); locker.relock(); } else if (callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) { //we compare the vtable to make sure we are not in the destructor of the object. locker.unlock(); const int methodIndex = c->method(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv); callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex); locker.relock(); } else { const int method = method_relative + c->method_offset; locker.unlock(); if (qt_signal_spy_callback_set.slot_begin_callback != 0) { qt_signal_spy_callback_set.slot_begin_callback(receiver, method, argv ? argv : empty_argv); } metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv); if (qt_signal_spy_callback_set.slot_end_callback != 0) qt_signal_spy_callback_set.slot_end_callback(receiver, method); locker.relock(); } if (connectionLists->orphaned) break; } while (c != last && (c = c->nextConnectionList) != 0); if (connectionLists->orphaned) break; } while (list != &connectionLists->allsignals && //start over for all signals; ((list = &connectionLists->allsignals), true));
由上面代碼,我們大概可以理解信號槽的幾種連接方式:
- 默認(rèn)連接并且信號槽的對象不在同一個線程中,則效果和隊列連接類似;
- 阻塞時隊列連接,信號和槽對象不同處于同一個線程中;
- Qt使用QSemaphore來實現(xiàn)阻塞式的槽函數(shù)調(diào)用;
4、小結(jié)
本次的源碼因為種種原因,看的不是很詳細(xì),但是理解Qt的信號槽機制綽綽有余了
- Qt自帶的元數(shù)據(jù)系統(tǒng)利用C++的宏等特性實現(xiàn)反射機制;
- 利用元數(shù)據(jù)系統(tǒng),在連接信號槽是將槽的信息(接收對象、槽方法、參數(shù)列表、連接方式等)保存在信號的元數(shù)據(jù)中;
- 信號也是方法,方法體有moc工具生成,方法內(nèi)獲取該信號連接的所有槽信息,并依序執(zhí)行;
直到這里,信號槽的邏輯已經(jīng)顯而易見了,它就是一個變種的觀察者模式,槽的信息保存在信號對象中也就是設(shè)置回調(diào)函數(shù),觸發(fā)信號也就是執(zhí)行回調(diào)函數(shù),只是Qt庫將其中的各種操作細(xì)節(jié)封裝起來了,所以,使用起來,不去關(guān)注設(shè)計模式的細(xì)節(jié),也就容易很多了。不得不說,無論是從設(shè)計思路,還是開發(fā)技巧上看,Qt的開發(fā)者真的很牛叉。
5、第三方信號槽庫
信號槽機制是Qt首創(chuàng),但不是其獨有,其他各類C++流行框架也都是互相借鑒,C++標(biāo)準(zhǔn)庫的預(yù)備役的boost中也有信號槽機制的實現(xiàn)。如果平時開發(fā)中需要用到信號槽機制,但是又不想引入這些龐大的類庫,可以使用輕量級別的信號槽庫:http://sigslot.sourceforge.net,該庫不詳細(xì)介紹,有興趣的小伙伴自己學(xué)習(xí)把。
以上就是c++ Qt信號槽原理的詳細(xì)內(nèi)容,更多關(guān)于c++ Qt信號槽的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++ 中將一維數(shù)組轉(zhuǎn)成多維的三種方式示例詳解
這篇文章主要介紹了C++ 中將一維數(shù)組轉(zhuǎn)成多維的三種方式,每種方式結(jié)合實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2023-12-12C++如何獲取當(dāng)前系統(tǒng)時間及格式化輸出
這篇文章主要介紹了C++如何獲取當(dāng)前系統(tǒng)時間及格式化輸出的實例代碼,主要用到time()及strftime()函數(shù),通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02c語言使用fdk_aac實現(xiàn)aac音頻解碼為pcm
這篇文章主要為大家詳細(xì)介紹了c語言如何使用fdk_aac庫實現(xiàn)aac音頻解碼為pcm的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-11-11