軍工物聯(lián)網(wǎng)技術(shù):C++模擬實(shí)現(xiàn)Qt的信號(hào)與槽機(jī)制
對(duì)于大多學(xué)習(xí)Qt的朋友,心中都有種好奇——那就是Qt最核心的信號(hào)與槽是如何實(shí)現(xiàn)的,對(duì)于小編自己也是一樣,當(dāng)然大家肯定都會(huì)去查閱相關(guān)資料,但大部分時(shí)候也只是一知半解,如果說(shuō)要自己實(shí)現(xiàn)就會(huì)又摸不著頭腦了;所以小編決定自己親自用C++實(shí)現(xiàn)一個(gè)簡(jiǎn)單版的信號(hào)槽,來(lái)理解Qt的實(shí)現(xiàn)原理。于是小編就在翻閱各牛人朋友的博客和反復(fù)研究Qt源碼自己重新寫(xiě)了一下以便交流學(xué)習(xí)。
我們先還是簡(jiǎn)單的梳理一下Qt信號(hào)與槽的實(shí)現(xiàn)機(jī)理:在Qt中實(shí)現(xiàn)信號(hào)與槽最重要的就是通過(guò)元對(duì)象系統(tǒng)(MOS)的元對(duì)象編譯器(MOC)將我們定義的需要使用到信號(hào)與槽的類(lèi)中的信號(hào)及信號(hào)調(diào)用槽函數(shù)的方法進(jìn)行定義(這一步就會(huì)生成與源文件對(duì)應(yīng)的moc_xx.cpp文件),然后通過(guò)系統(tǒng)提供的關(guān)聯(lián)方法(connect)將信號(hào)與槽建立一一對(duì)應(yīng)關(guān)系,當(dāng)發(fā)射信號(hào)(其實(shí)就是調(diào)用信號(hào)函數(shù))時(shí)就會(huì)通過(guò)信號(hào)與槽的對(duì)應(yīng)關(guān)系找到對(duì)應(yīng)槽函數(shù)進(jìn)行調(diào)用。這樣的好處就是對(duì)于使用者而言不必去關(guān)心函數(shù)指針回調(diào)函數(shù)這些對(duì)于初學(xué)者比較不太容易搞清晰的東西,簡(jiǎn)化了使用者的操作。當(dāng)然就像我們?cè)谙硎苄腋I畹臅r(shí)候,就一定有人在我們背后默默付出砥礪前行!這里也一樣,對(duì)于我們使用者簡(jiǎn)化了操作,那為了實(shí)現(xiàn)這樣的效果就需要在后臺(tái)提供更多的支持。接下來(lái)我們就通過(guò)代碼再來(lái)梳理一遍。
首先我們使用信號(hào)與槽肯定就會(huì)有信號(hào)的發(fā)送者與接收者,所以我們就先去定義這兩個(gè)類(lèi)對(duì)象:
- sender.h
- #pragma once
- #include "object.h"
- class Sender : public Object
- {
- X_OBJECT
- public:
- Sender(int n = 0) : m_num(n){
- }
- void sendSig();
- signals:
- void holdClass(int n);
- int m_num;
- };
- sender.cpp
- #include "sender.h"
- void Sender::sendSig()
- {
- std::cout << "發(fā)送信號(hào):holdClass" << std::endl;
- emit holdClass(m_num);
- }
在Qt中需要使用信號(hào)槽的對(duì)象都需要直接或間接繼承一個(gè)類(lèi)QObject,并且需要添加一個(gè)私有宏定義Q_OBJECT,這里就用Object和X_OBJECT代替,signals是Qt中用于聲明信號(hào)函數(shù)的關(guān)鍵字,emit是Qt中用于發(fā)送信號(hào)定義的關(guān)鍵字,這里我們先假設(shè)已經(jīng)有這些類(lèi)和宏定義,注意信號(hào)函數(shù)是不需要我們定義的,他是在MOC預(yù)處理生成的moc_xx.cpp中自動(dòng)生成定義的,所以這里的cpp很簡(jiǎn)單只有一個(gè)普通函數(shù)sendSig()的定義。同理我們?cè)僮约憾x一個(gè)信號(hào)的接收者對(duì)象和其對(duì)應(yīng)的槽函數(shù)。
- receiver.h
- #pragma once
- #include "object.h"
- class Receiver : public Object
- {
- X_OBJECT
- public:
- Receiver() {
- }
- public slots:
- void attendClass(int n);
- };
- receiver.cpp
- #include "receiver.h"
- void Receiver::attendClass(int n)
- {
- std::cout << "執(zhí)行槽函數(shù)attendClass:cur class " << n << std::endl;
- }
這里的slots就是Qt中用于標(biāo)識(shí)槽函數(shù)聲明的關(guān)鍵字,槽函數(shù)是需要用戶(hù)自己定義的。
然后我們就需要再將發(fā)送者信號(hào)與接收者槽關(guān)聯(lián)起來(lái),我們這就提供一個(gè)主函數(shù)來(lái)模擬關(guān)聯(lián)信號(hào)與槽,讓發(fā)送者產(chǎn)生信號(hào):
- main.cpp
- #include "sender.h"
- #include "receiver.h"
- int main()
- {
- Sender xuedao(9527);
- Receiver rjc;
- Object::connect(&xuedao, SIGNAL(holdClass(int)), &rjc, SLOT(attendClass(int)));
- xuedao.sendSig();
- return 0;
- }
這里的SIGNAL與SLOT在Qt中就是兩個(gè)轉(zhuǎn)換字符串的宏定義,connect是QObject的一個(gè)靜態(tài)函數(shù)方法。
我們要想這個(gè)程序能正常運(yùn)行起來(lái),接下來(lái)我們就需要去定義一個(gè)類(lèi)似QObject的Object類(lèi)和上面需要用到的關(guān)鍵字與宏定義,以及模擬MOC預(yù)處理產(chǎn)生對(duì)應(yīng)的moc_xx.cpp,里面細(xì)節(jié)的地方為了方便理解我都通過(guò)代碼注釋解釋說(shuō)明了
- object.h
- #pragma once
- #include
- #include
- #include
- #include
- #define signals protected
- #define slots
- #define emit
- #define SLOT(slt) "1"#slt // 1用于標(biāo)識(shí)槽函數(shù)
- #define SIGNAL(sig) "2"#sig //2用于標(biāo)識(shí)信號(hào)
- class Object;
- struct MetaObject
- {
- //每個(gè)對(duì)象可能會(huì)有多個(gè)信號(hào)與槽函數(shù),這里就用兩個(gè)vector分別保存信號(hào)與槽函數(shù)信息操作起來(lái)方便點(diǎn)
- std::vector sigs;
- std::vector slts;
- //activate的功能是通過(guò)信號(hào)發(fā)送者即信號(hào)索引找到關(guān)聯(lián)接收者和方法索引并調(diào)用對(duì)應(yīng)方法
- static void activate(Object *sender, int idx, void **argv); //void **argv對(duì)應(yīng)信號(hào)傳遞的參數(shù)
- struct Connection //用于打包信號(hào)接收者與方法的索引(對(duì)應(yīng)上面定義的vector中的信號(hào)槽的索引)
- {
- Object *m_receiver;
- int method;
- };
- };
- //Q_OBJECT宏中定義的比較多這里只選擇了我們需使用的幾個(gè)
- //static MetaObject meta用于保存使用該宏定義對(duì)象中的信號(hào)與槽信號(hào)與槽的相關(guān)信息
- //getMetaObject()用于返回發(fā)送者或接收者對(duì)象中的static MetaObject meta對(duì)象
- #define X_OBJECT static MetaObject meta; \
- virtual MetaObject *getMetaObject(); \
- virtual void metaCall(int idx, void **argv); //idx為對(duì)應(yīng)槽函數(shù)的索引,void**argv用于接收信號(hào)傳遞的參數(shù)
- class Object //需要使用信號(hào)槽對(duì)象的公共接口對(duì)象
- {
- X_OBJECT
- public:
- virtual ~Object() {}
- //connect用于建立信號(hào)與槽的關(guān)聯(lián)信息
- static void connect(Object *sender, const char *s1, Object *receiver, const char *s2);
- private:
- friend class MetaObject; //用于方便meta對(duì)象訪問(wèn)下面的信號(hào)槽map
- std::multimap mp; //用于保存信號(hào)索引與接收者對(duì)象即索引的對(duì)應(yīng)關(guān)系
- //由于一個(gè)信號(hào)可以對(duì)應(yīng)多個(gè)槽,同樣多個(gè)信號(hào)也可以對(duì)應(yīng)一個(gè)槽,所以這里選用了multimap容器做對(duì)應(yīng)關(guān)系映射
- };
- object.cpp
- #include "object.h"
- #include //調(diào)用strcmp函數(shù)需要包含
- void MetaObject::activate(Object *sender, int idx, void **argv)
- {
- //在信號(hào)槽對(duì)應(yīng)關(guān)系的mp中找到發(fā)送者idx索引信號(hào)對(duì)應(yīng)的接收者及關(guān)聯(lián)方法的調(diào)用
- auto ptr = sender->mp.equal_range(idx);
- for(auto it = ptr.first; it != ptr.second; it++) {
- MetaObject::Connection con = it->second;
- con.m_receiver->metaCall(con.method, argv); //調(diào)用接收者與發(fā)送者信號(hào)關(guān)聯(lián)的方法,并傳遞需要的參數(shù)
- }
- }
- void Object::connect(Object *sender, const char *s1, Object *receiver, const char *s2)
- {
- int sig_idx = -1, slt_idx = -1;
- MetaObject *senderMeta = sender->getMetaObject(); //獲取發(fā)送者中保存的meta對(duì)象
- MetaObject *receiverMeta = receiver->getMetaObject(); //獲取接收中保存的meta對(duì)象
- //比對(duì)信號(hào)名稱(chēng)找到對(duì)應(yīng)的信號(hào)索引
- for(int i = 0; i < senderMeta->sigs.size(); i++) {
- if(0 == strcmp(s1+1, senderMeta->sigs[i].c_str())) {
- sig_idx = i;
- }
- }
- //這里確認(rèn)是槽函數(shù),并找到對(duì)應(yīng)的槽函數(shù)索引
- //如果有信號(hào)與信號(hào)關(guān)聯(lián)的情況這里就需要去查找接收者對(duì)應(yīng)的信號(hào)索引,這里省略了
- if('1' == *s2) {
- for(int i = 0; i < receiverMeta->slts.size(); i++) {
- if(0 == strcmp(s2+1, receiverMeta->slts[i].c_str())) {
- slt_idx = i;
- }
- }
- }
- if(-1 == sig_idx || -1 == slt_idx) {
- std::cout << "no match sig or slt" << std::endl;
- }
- //利用multimap建立信號(hào)索引與接收者和方法索引的對(duì)應(yīng)關(guān)系
- MetaObject::Connection con = {receiver, slt_idx};
- sender->mp.insert(std::make_pair(sig_idx, con));
- }
- //下面的主要是預(yù)留的方便父類(lèi)調(diào)用子類(lèi)重寫(xiě)方法的接口這里簡(jiǎn)單定義即可
- void Object::metaCall(int idx, void **ag)
- {
- }
- MetaObject Object::meta;
- MetaObject *Object::getMetaObject()
- {
- return &meta;
- }
下面就輪到MOC生成的moc_xx.cpp,這些文件在Qt中是自動(dòng)生成的不需要我們實(shí)現(xiàn),我這里只能手動(dòng)模擬簡(jiǎn)單的實(shí)現(xiàn)發(fā)送者的moc_sender.cpp與接收者的moc_receiver.cpp最終我們編譯程序是需要將這兩個(gè)文件一起編譯才能通過(guò)的。
- moc_sender.cpp
- #include "sender.h"
- //根據(jù)定義的信號(hào)槽順序?qū)⑿盘?hào)與槽函數(shù)名稱(chēng)進(jìn)行保存,Qt中會(huì)將函數(shù)名稱(chēng)參數(shù)分開(kāi)保存處理,這里簡(jiǎn)單模擬以下就好
- static const char *sigs_name[] = {"holdClass(int)"};
- static const char *slts_name[] = {nullptr}; //空表示當(dāng)前沒(méi)有定義對(duì)應(yīng)的函數(shù)
- static std::vector sigs(sigs_name, sigs_name+1);
- static std::vector slts;
- MetaObject Sender::meta = {sigs, slts};
- //Sender的信號(hào)定義
- void Sender::holdClass(int n)
- {
- void *arg[] = {(void *)&n};
- //調(diào)用MetaObject的靜態(tài)方法activate傳遞當(dāng)前的信號(hào)發(fā)送者對(duì)象、信號(hào)索引及參數(shù)
- MetaObject::activate(this, 0, arg); //0表示當(dāng)前信號(hào)函數(shù)在sigs_name[]中的索引
- }
- MetaObject *Sender::getMetaObject()
- {
- return &meta; //返回Sender的meta對(duì)象
- }
- void Sender::metaCall(int idx, void **arg)
- {
- // 我們這里Sender 中沒(méi)有槽函數(shù)所以這里沒(méi)任何操作
- }
- moc_receiver.cpp
- #include "receiver.h"
- static const char *sigs_name[] = {nullptr};
- static const char *slts_name[] = {"attendClass(int)"};
- static std::vector sigs;
- static std::vector slts(slts_name, slts_name+1);
- MetaObject Receiver::meta = {sigs, slts};
- MetaObject *Receiver::getMetaObject()
- {
- return &meta; //返回Receiver的meta對(duì)象
- }
- void Receiver::metaCall(int idx, void **arg)
- {
- //這里根據(jù)slts_name[]中的索引值調(diào)用對(duì)應(yīng)的槽函數(shù)
- if(0 == idx) {
- int n = *((int *)arg[0]);
- attendClass(n);
- }
- }
有了上面這些文件最后我們只需要將所有的.cpp文件一起編譯運(yùn)行就可以實(shí)現(xiàn)Qt中信號(hào)與槽的效果了:
- g++ object.cpp sender.cpp receiver.cpp moc_sender.cpp moc_receiver.cpp main.cpp -o xuedao
也可用其他可使用的編譯器編譯進(jìn)行編譯,這里直接用的g++。
另外如果某個(gè)對(duì)象修改或增刪了信號(hào)或槽就需要去手動(dòng)修改對(duì)應(yīng)的moc_xx.cpp文件即可,Qt中實(shí)現(xiàn)考慮的實(shí)際問(wèn)題會(huì)更多,這里只是把整個(gè)信號(hào)槽關(guān)聯(lián)及調(diào)用流程框架進(jìn)行了梳理,具體的大家可以參考Qt源碼做深入學(xué)習(xí)。