Qt信號與槽機制的實現(xiàn)原理

一直都有用Qt開發(fā),進來找工作面試的時候經(jīng)常被問到知道Qt信號與槽機制是如何實現(xiàn)的,最近看了幾篇博客,寫一個簡單的實現(xiàn)。

GUI程序中,當我們我們點擊一個按鈕時,我們會期待我們自定義的某個函數(shù)被調(diào)用。對此,較老的工具集(toolkits)都是通過回調(diào)函數(shù)(callback)來實現(xiàn)的,Qt的神奇之處就在于,它使用信號(signal)與槽(slot)的技術來取代了回調(diào)。

工具集(toolkits)都是通過回調(diào)函數(shù)(callback)來實現(xiàn)的,Qt的神奇之處就在于,它使用信號(signal)與槽(slot)的技術來取代了回調(diào)。
在繼續(xù)之前,我們先看一眼最最常用的 connnect 函數(shù):

connect(btn, "2clicked()", this, "1onBtnClicked()")

可能你會覺得稍有點眼生,因為為了清楚起見,我沒有直接使用大家很熟悉的SIGNAL和SLOT兩個宏,宏定義如下:

# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a

程序運行時,connect借助兩個字符串,即可將信號與槽的關聯(lián)建立起來,那么,它是如果做到的呢?C++的經(jīng)驗可以告訴我們:

1.類中應該保存有信號和槽的字符串信息
2.字符串和信號槽函數(shù)要關聯(lián)

而這,就是通過神奇的元對象系統(tǒng)所實現(xiàn)的(Qt的元對象系統(tǒng)預處理器叫做moc,對文件預處理之后生成一個moc_xxx.cpp文件,然后和其他文件一塊編譯即可)。

接下來,我們不妨嘗試用純C++來實現(xiàn)自己的元對象系統(tǒng)(我們需要有一個自己的預處理器,本文中用雙手來代替了,預處理生成的文件是db_xxx.cpp)。

繼續(xù)之前,我們可以先看一下我們最終的類定義

class Object    
{    
    DB_OBJECT  
public:    
    Object();    
    virtual ~Object();    
    static void db_connect(Object *, const char *, Object *, const char *);    
    void testSignal();    
db_signals:    
    void sig1();    
public db_slots:    
    void slot1();    
friend class MetaObject;    
private:    
     ConnectionMap connections;    
};    

引入元對象系統(tǒng)

首先定義自己的信號和槽
● 為了和普通成員進行區(qū)別(以使得預處理器可以知道如何提取信息),我們需要創(chuàng)造一些"關鍵字"

db_signals
db_slots

class Object
{
public:
    Object();
    virtual ~Object();
db_signals:
    void sig1();
public db_slots:
    void slot1();
};

● 通過自己的預處理器,將信息提取取來,放置到一個單獨的文件中(比如db_object.cpp):
● 規(guī)則很簡單,將信號和槽的名字提取出來,放到字符串中??梢杂卸鄠€信號或槽,按順序"sig1/nsig2/n"

static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";

● 這些信號和槽的信息,如何才能與類建立關聯(lián),如何被訪問呢?
我們可以定義一個類,來存放信息:

struct MetaObject
{
    const char * sig_names;
    const char * slts_names;
};

然后將其作為一個Object的靜態(tài)成員(注意哦,這就是我們的元對象啦 ):

class Object
{
    static MetaObject meta;
...

這樣一來,我們的預處理器可以生成這樣的 db_object.cpp 文件:

include "object.h"

static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
MetaObject Object::meta = {sig_names, slts_names};

信息提取的問題解決了:可是,還有一個嚴重問題,我們定義的關鍵字 C++ 編譯器不認識啊,怎么辦?通過定義一下宏,問題解決了:

# define db_slots
# define db_signals protected

建立信號槽鏈接

我們的最終目的就是:當信號被觸發(fā)的時候,能找到并觸發(fā)相應的槽。所以有了信號和槽的信息,我們就可以建立信號和槽的連接了。我們通過 db_connect 將信號和槽的對應關系保存到一個 mutlimap 中:

struct Connection
{
    Object * receiver;
    int method;
};

class Object
{
public:
...
    static void db_connect(Object*, const char*, Object*, const char*);
...
private:
    std::multimap<int, Connection> connections;
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
    int sig_idx = find_string(sender->meta.sig_names, sig);
    int slt_idx = find_string(receiver->meta.slts_names, slt);
    if (sig_idx == -1 || slt_idx == -1) {
        perror("signal or slot not found!");
    } else {
        Connection c = {receiver, slt_idx};
        sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
    }
}

首先從元對象信息中查找信號和槽的名字是否存在,如果存在,則將信號的索引和接收者的信息存入信號發(fā)送者的的一個map中。如果信號或槽無效,就什么都不用做了。

我們這兒定義了一個find_string函數(shù),就是個簡單的字符串查找(此處就不列出了)。

信號的激活

連接信息有了,我們看看信號到底是怎么發(fā)出的。
在 Qt 中,我們都知道用 emit 來發(fā)射信號:

class Object
{
public:
    void testSignal()
...
};

void Object::testSignal()
{
    db_emit sig1();
}

C++編譯器不認識 db_emit ?加一行就行了

#define db_emit

從前面我的Object定義中可以看到,所謂的信號或槽,都只是普普通通的C++類的成員函數(shù)。既然是成員函數(shù),就需要函數(shù)定義:

●槽函數(shù):由于它包含我們需要的功能代碼,我們都會想到在 object.cpp 文件中去定義它,不存在問題。
●信號函數(shù):它的函數(shù)體不需要自己編寫。那么它在哪兒呢?這就是本節(jié)的內(nèi)容了
信號函數(shù)由我們的"預處理器"來生成,也就是它要定義在我們的 db_object.cpp 文件中:

void Object::sig1()
{
    MetaObject::active(this, 0);
}

我們預處理源文件時,就知道它是第幾個信號。所以根據(jù)它的索引去調(diào)用和它關聯(lián)的槽即可。具體工作交給了MetaObject類:

class Object;
struct MetaObject
{
    const char * sig_names;
    const char * slts_names;

    static void active(Object * sender, int idx);
};

這個函數(shù)該怎么寫呢:思路很簡單
●從前面的保存連接的map中,找出與該信號關聯(lián)的對象和槽
●調(diào)用該對象這個槽

typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;

void MetaObject::active(Object* sender, int idx)
{
    ConnectionMapIt it;
    std::pair<ConnectionMapIt, ConnectionMapIt> ret;
    ret = sender->connections.equal_range(idx);
    for (it=ret.first; it!=ret.second; ++it) {
        Connection c = (*it).second;
        //c.receiver->metacall(c.method);
    }
}

槽的調(diào)用

這個最后一個關鍵問題了,槽函數(shù)如何根據(jù)一個索引值進行調(diào)用。
●直接調(diào)用槽函數(shù)我們都知道了,就一個普通函數(shù)
●可現(xiàn)在通過索引調(diào)用了,那么我們必須定義一個接口函數(shù)

class Object
{
    void metacall(int idx);
...

該函數(shù)如何實現(xiàn)呢?這個又回到我們的元對象預處理過程中了,因為在預處理的過程,我們能將槽的索引和槽的調(diào)用關聯(lián)起來。

所以,在預處理生成的文件(db_object.cpp)中,我們很容易生成其定義:

void Object::metacall(int idx)
{
    switch (idx) {
        case 0:
            slot1();
            break;
        default:
            break;
    };
}

至此,我們已經(jīng)實現(xiàn)的一個簡化的自己的信號與槽的程序。下面我們總體上看看程序的所有代碼:

#ifndef DB_OBJECT
#define DB_OBJECT
#include <map>
# define db_slots
# define db_signals protected
# define db_emit
class Object;
struct MetaObject
{
    const char * sig_names;
    const char * slts_names;
    static void active(Object * sender, int idx);
};
struct Connection
{
    Object * receiver;
    int method;
};
typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;
class Object
{
    static MetaObject meta;
    void metacall(int idx);
public:
    Object();
    virtual ~Object();
    static void db_connect(Object*, const char*, Object*, const char*);
    void testSignal();
db_signals:
    void sig1();
public db_slots:
    void slot1();
friend class MetaObject;
private:
     ConnectionMap connections;
};
#endif
#include <stdio.h>
#include <string.h>
#include "object.h"
Object::Object()
{
}
Object::~Object()
{
}
static int find_string(const char * str, const char * substr)
{
    if (strlen(str) < strlen(substr))
        return -1;
    int idx = 0;
    int len = strlen(substr);
    bool start = true;
    const char * pos = str;
    while (*pos) {
        if (start && !strncmp(pos, substr, len) && pos[len]=='/n')
            return idx;
        start = false;
        if (*pos == '/n') {
            idx++;
            start = true;
        }
        pos++;
    }
    return -1;
}
void Object::db_connect(Object* sender, const char* sig, Object* receiver, const char* slt)
{
    int sig_idx = find_string(sender->meta.sig_names, sig);
    int slt_idx = find_string(receiver->meta.slts_names, slt);
    if (sig_idx == -1 || slt_idx == -1) {
        perror("signal or slot not found!");
    } else {
        Connection c = {receiver, slt_idx};
        sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
    }
}
void Object::slot1()
{
    printf("hello dbzhang800!");
}
void MetaObject::active(Object* sender, int idx)
{
    ConnectionMapIt it;
    std::pair<ConnectionMapIt, ConnectionMapIt> ret;
    ret = sender->connections.equal_range(idx);
    for (it=ret.first; it!=ret.second; ++it) {
        Connection c = (*it).second;
        c.receiver->metacall(c.method);
    }
}
void Object::testSignal()
{
    db_emit sig1();
}

另一種版本源碼解釋

QMetaObject類:

  /*******************生成元對象需要的輸入?yún)?shù)*****************/  
//類名  
const char * const class_name,  
//父類名  
QMetaObject *superclass,  
//記錄slot 信息  
const QMetaData * const slot_data,   
//記錄槽的個數(shù)  
int n_slots,  
//記錄signal 信息  
const QMetaData * const signal_data,  
//記錄信號的個數(shù)  
int n_signals  
/******************* 元對象類提供的方法**************************/  
int   numSlots( bool super = FALSE ) const;//返回槽的個數(shù)  
int   numSignals( bool super = FALSE ) const;//返回信號的個數(shù)  
int   findSlot( const char *, bool super = FALSE ) const;//查找槽  
int   findSignal( const char *, bool super = FALSE ) const;//查找信號  
 //返回指定位置的槽  
const QMetaData *slot( int index, bool super = FALSE ) const;  
 //返回指定位置的信號  
const QMetaData *signal( int index, bool super = FALSE ) const;  
//所有槽名字的列表  
QStrList  slotNames( bool super = FALSE ) const;  
//所有信號名字的列表  
QStrList  signalNames( bool super = FALSE ) const;  
//槽的起始索引  
int   slotOffset() const;  
//信號的起始索引  
int   signalOffset() const;  
/***********************兩個獲取類的元對象的方法*****************/  
static QMetaObject *metaObject( const char *class_name );  
static bool hasMetaObject( const char *class_name ); 

QMetaData類:

//記錄元對象數(shù)據(jù)for 信號與槽  
struct QMetaData           
                     {                                   
const char *name;  //名稱  
const QUMethod* method; //詳細描述信息  
enum Access { Private, Protected, Public };  
Access access; //訪問權限  
 }; 

二、QObject類實現(xiàn)了信號與槽機制

它利用元對象紀錄的信息,實現(xiàn)了信號與槽機制

(1)信號與槽建立連接的實現(xiàn)

接口函數(shù):

//連接  
//參數(shù)(發(fā)送對象,信號,接收對象,處理信號的信號/槽)  
static bool  connect( const QObject *sender, const char *signal,  
const QObject *receiver, const char *member );  
bool connect(const QObject *sender, const char *signal,  
const char *member ) const;  
static bool  disconnect( const QObject *sender, const char *signal,  
const QObject *receiver, const char *member );  
bool disconnect(const char *signal=0,  
const QObject *receiver=0, const char *member=0 );  
bool disconnect( const QObject *receiver, const char *member=0 );  
//連接的內(nèi)部實現(xiàn)  
//(發(fā)送對象,信號的索引,接收對象,處理信號的類型,處理信號信號/槽的索引)  
static void connectInternal(const QObject *sender, int signal_index,  
const QObject *receiver, int membcode, int member_index );  
static bool disconnectInternal(const QObject *sender, int signal_index,  
const QObject *receiver, int membcode, int member_index ); 

信號與槽連接的實現(xiàn)原理:

①階段

bool QObject::connect( const QObject *sender,//發(fā)送對象        
const char *signal,//信號  
 const QObject *receiver, //接收對象  
const char *member //槽  
                                )  
       {  
 //檢查發(fā)送對象,信號,接收對象,槽不為null  
if ( sender == 0 || receiver == 0 || signal == 0 || member == 0 ) {        
                     return FALSE;  
           }  
//獲取發(fā)送對象的元對象  
QMetaObject *smeta = sender->metaObject();  
//檢查信號  
if ( !check_signal_macro( sender, signal, "connect", "bind" ) )  
return FALSE;     
//獲取信號的索引  
int signal_index = smeta->findSignal( signal, TRUE );  
if ( signal_index < 0 ) {                // normalize and retry  
nw_signal = qt_rmWS( signal-1 ); // remove whitespace  
signal = nw_signal.data()+1;         // skip member type code  
signal_index = smeta->findSignal( signal, TRUE );  
            }  
           //如果信號不存在,則退出  
           if ( signal_index < 0  ) {                    // no such signal  
                     return FALSE;  
           }  
           //獲取信號的元數(shù)據(jù)對象  
const QMetaData *sm = smeta->signal( signal_index, TRUE );  
//獲取信號名字  
signal = sm->name;         
 //獲取處理信號的類型(是信號/槽)  
int membcode = member[0] - '0';        // get member code  
              //發(fā)送信號對象  
QObject *s = (QObject *)sender;        // we need to change them  
          //接收信號對象  
QObject *r = (QObject *)receiver;             //   internally  
           //獲取接收對象的元對象  
           QMetaObject *rrmeta = r->metaObject();  
           int member_index = -1;  
           switch ( membcode ) {                // get receiver member  
case QSLOT_CODE://如果是槽  
//獲取槽索引  
member_index = rmeta->findSlot( member, TRUE );  
if ( member_index < 0 ) {            // normalize and retry  
nw_member = qt_rmWS(member);     // remove whitespace  
 member = nw_member;  
 member_index = rmeta->findSlot( member, TRUE );  
                         }  
                         break;  
                     case QSIGNAL_CODE://如果是信號  
                            //獲取信號索引  
 member_index = rmeta->findSignal( member, TRUE );  
 if ( member_index < 0 ) {           // normalize and retry  
nw_member = qt_rmWS(member);     // remove whitespace  
member = nw_member;  
member_index = rmeta->findSignal( member, TRUE );  
                         }  
                         break;  
           }  
           /如果接收對象不存在相應的信號或槽,則退出  
           if ( member_index < 0  ) {  
                     return FALSE;  
           }  
//檢查連接的參數(shù)(發(fā)送的信號,接收對象,處理信號的槽或信號)  
if ( !s->checkConnectArgs(signal,receiver,member) ) {  
                     return FALSE;  
           } else {  
                //獲取處理信號的元數(shù)據(jù)對象  
const QMetaData *rm = membcode == QSLOT_CODE ?  
rmeta->slot( member_index, TRUE ) :  
rmeta->signal( member_index, TRUE );  
                     if ( rm ) {            
                         //建立連接  
                            //(發(fā)送信號的對象,信號的索引,接收信號的對象,  
                              處理信號的類型,處理信號的索引)  
                        connectInternal( sender, signal_index, receiver, membcode, member_index );  
                   }  
              }  
           return TRUE;  
       }  

②階段

       //建立連接  
       //(發(fā)送信號的對象,信號的索引,接收信號的對象,處理信號的類型,處理信號的索引)  
void QObject::connectInternal( const QObject *sender, int signal_index,   
const QObject *receiver, int membcode, int member_index )  
      {  
       //發(fā)送信號的對象  
    QObject *s = (QObject*)sender;  
       //接收信號的對象  
    QObject *r = (QObject*)receiver;  
    //如果發(fā)送對象的連接查詢表為null,則建立  
    if ( !s->connections ) {                // create connections lookup table  
       s->connections = new QSignalVec( signal_index+1 );  
       Q_CHECK_PTR( s->connections );  
      s->connections->setAutoDelete( TRUE );  
    }  
    //獲取發(fā)送對象的相應信號的連接列表  
 
    QConnectionList *clist = s->connections->at( signal_index );  
 
    if ( !clist ) {                         // create receiver list  
       clist = new QConnectionList;  
       Q_CHECK_PTR( clist );  
       clist->setAutoDelete( TRUE );  
       s->connections->insert( signal_index, clist );  
    }  
    QMetaObject *rrmeta = r->metaObject();  
   const QMetaData *rm = 0;  
    switch ( membcode ) {                // get receiver member  
       case QSLOT_CODE:  
           rm = rmeta->slot( member_index, TRUE );  
           break;  
       case QSIGNAL_CODE:  
           rm = rmeta->signal( member_index, TRUE );  
           break;  
    }  
    //建立連接  
QConnection *c = new QConnection( r, member_index, rm ? rm->name :   
                                                                      "qt_invoke", membcode );  
    Q_CHECK_PTR( c );  
   //把連接添加到發(fā)送對象的連接列表中  
    clist->append( c );  
    //判斷接收對象的發(fā)送對象列表是否為null  
    if ( !r->senderObjects )               // create list of senders  
           {  
          //建立接收對象的發(fā)送對象列表  
       r->senderObjects = new QSenderObjectList;  
          }  
    //把發(fā)送對象添加到發(fā)送對象列表中  
    r->senderObjects->append( s );           // add sender to list  
       } 

(2)信號發(fā)生時激活的操作函數(shù)。 激活slot的方法

接口:

 void QObject::activate_signal( int signal )  
       {  
       #ifndef QT_NO_PRELIMINARY_SIGNAL_SPY  
           if ( qt_preliminary_signal_spy ) {  
                  //信號沒有被阻塞  
                  //信號>=0  
                  //連接列表不為空,或者信號對應的連接存在  
              if ( !signalsBlocked() && signal >= 0 &&  
                ( !connections || !connections->at( signal ) ) ) {  
                 //  
                 QUObject o[1];  
                  qt_spy_signal( this, signal, o );  
                  return;  
              }  
    }  
       #endif  
    if ( !connections || signalsBlocked() || signal < 0 )  
       return;  
    //獲取信號對應的連接列表  
    QConnectionList *clist = connections->at( signal );  
    if ( !clist )  
       return;  
   QUObject o[1];  
    //  
    activate_signal( clist, o );  
}  
 
void QObject::activate_signal( QConnectionList *clist, QUObject *o )  
{  
    if ( !clist )  
       return;  
#ifndef QT_NO_PRELIMINARY_SIGNAL_SPY  
    if ( qt_preliminary_signal_spy )  
       qt_spy_signal( this, connections->findRef( clist), o );  
#endif  
    QObject *object;  
       //發(fā)送對象列表  
    QSenderObjectList* sol;  
       //舊的發(fā)送對象  
    QObject* oldSender = 0;  
       //連接  
    QConnection *c;  
    if ( clist->count() == 1 ) { // save iterator  
           //獲取連接  
       c = clist->first();  
       //  
       object = c->object();  
       //獲取發(fā)送對象列表  
       sol = object->senderObjects;  
       if ( sol ) {  
              //獲取舊的發(fā)送對象  
          oldSender = sol->currentSender;  
              //  
           sol->ref();  
              //設置新的發(fā)送對象  
           sol->currentSender = this;  
       }  
       if ( c->memberType() == QSIGNAL_CODE )//如果是信號,則發(fā)送出去  
           object->qt_emit( c->member(), o );  
       else  
           object->qt_invoke( c->member(), o );//如果是槽,則執(zhí)行  
       //       
       if ( sol ) {  
              //設置恢復為舊的發(fā)送對象  
           sol->currentSender = oldSender;  
           if ( sol->deref() )  
              delete sol;  
       }  
    } else {  
       QConnection *cd = 0;  
       QConnectionListIt it(*clist);  
       while ( (c=it.current()) ) {  
          ++it;  
          if ( c == cd )  
             continue;  
           ccd = c;  
           object = c->object();  
           //操作前設置當前發(fā)送對象  
           sol = object->senderObjects;  
           if ( sol ) {  
              oldSender = sol->currentSender;  
              sol->ref();  
              sol->currentSender = this;  
          }  
           //如果是信號,則發(fā)送出去  
           if ( c->memberType() == QSIGNAL_CODE ){  
              object->qt_emit( c->member(), o );  
           }  
           //如果是槽,則執(zhí)行  
           else{  
              object->qt_invoke( c->member(), o );  
           }  
           //操作后恢復當前發(fā)送對象  
           if (sol ) {  
              sol->currentSender = oldSender;  
              if ( sol->deref() )  
                  delete sol;  
           }  
       }  
    }  
} 
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容