Qt信号与槽机制的实现原理
一直都有用Qt开发,进来找工作面试的时候经常被问到知道Qt信号与槽机制是如何实现的,最近看了几篇博客,写一个简单的实现。
GUI程序中,当我们我们点击一个按钮时,我们会期待我们自定义的某个函数被调用。对此,较老的工具集(toolkits)都是通过回调函数(callback)来实现的,Qt的神奇之处就在于,它使用信号(signal)与槽(slot)的技术来取代了回调。
工具集(toolkits)都是通过回调函数(callback)来实现的,Qt的神奇之处就在于,它使用信号(signal)与槽(slot)的技术来取代了回调。
在继续之前,我们先看一眼最最常用的 connnect 函数:
connect(btn, "2clicked()", this, "1onBtnClicked()")
可能你会觉得稍有点眼生,因为为了清楚起见,我没有直接使用大家很熟悉的SIGNAL和SLOT两个宏,宏定义如下:
# define SLOT(a) "1"#a
# define SIGNAL(a) "2"#a
程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:
1.类中应该保存有信号和槽的字符串信息
2.字符串和信号槽函数要关联
而这,就是通过神奇的元对象系统所实现的(Qt的元对象系统预处理器叫做moc,对文件预处理之后生成一个moc_xxx.cpp文件,然后和其他文件一块编译即可)。
接下来,我们不妨尝试用纯C++来实现自己的元对象系统(我们需要有一个自己的预处理器,本文中用双手来代替了,预处理生成的文件是db_xxx.cpp)。
继续之前,我们可以先看一下我们最终的类定义
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;
};
引入元对象系统
首先定义自己的信号和槽
● 为了和普通成员进行区别(以使得预处理器可以知道如何提取信息),我们需要创造一些"关键字"
db_signals
db_slots
class Object
{
public:
Object();
virtual ~Object();
db_signals:
void sig1();
public db_slots:
void slot1();
};
● 通过自己的预处理器,将信息提取取来,放置到一个单独的文件中(比如db_object.cpp):
● 规则很简单,将信号和槽的名字提取出来,放到字符串中。可以有多个信号或槽,按顺序"sig1/nsig2/n"
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
● 这些信号和槽的信息,如何才能与类建立关联,如何被访问呢?
我们可以定义一个类,来存放信息:
struct MetaObject
{
const char * sig_names;
const char * slts_names;
};
然后将其作为一个Object的静态成员(注意哦,这就是我们的元对象啦 ):
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
建立信号槽链接
我们的最终目的就是:当信号被触发的时候,能找到并触发相应的槽。所以有了信号和槽的信息,我们就可以建立信号和槽的连接了。我们通过 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));
}
}
首先从元对象信息中查找信号和槽的名字是否存在,如果存在,则将信号的索引和接收者的信息存入信号发送者的的一个map中。如果信号或槽无效,就什么都不用做了。
我们这儿定义了一个find_string函数,就是个简单的字符串查找(此处就不列出了)。
信号的激活
连接信息有了,我们看看信号到底是怎么发出的。
在 Qt 中,我们都知道用 emit 来发射信号:
class Object
{
public:
void testSignal()
...
};
void Object::testSignal()
{
db_emit sig1();
}
C++编译器不认识 db_emit ?加一行就行了
#define db_emit
从前面我的Object定义中可以看到,所谓的信号或槽,都只是普普通通的C++类的成员函数。既然是成员函数,就需要函数定义:
●槽函数:由于它包含我们需要的功能代码,我们都会想到在 object.cpp 文件中去定义它,不存在问题。
●信号函数:它的函数体不需要自己编写。那么它在哪儿呢?这就是本节的内容了
信号函数由我们的"预处理器"来生成,也就是它要定义在我们的 db_object.cpp 文件中:
void Object::sig1()
{
MetaObject::active(this, 0);
}
我们预处理源文件时,就知道它是第几个信号。所以根据它的索引去调用和它关联的槽即可。具体工作交给了MetaObject类:
class Object;
struct MetaObject
{
const char * sig_names;
const char * slts_names;
static void active(Object * sender, int idx);
};
这个函数该怎么写呢:思路很简单
●从前面的保存连接的map中,找出与该信号关联的对象和槽
●调用该对象这个槽
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);
}
}
槽的调用
这个最后一个关键问题了,槽函数如何根据一个索引值进行调用。
●直接调用槽函数我们都知道了,就一个普通函数
●可现在通过索引调用了,那么我们必须定义一个接口函数
class Object
{
void metacall(int idx);
...
该函数如何实现呢?这个又回到我们的元对象预处理过程中了,因为在预处理的过程,我们能将槽的索引和槽的调用关联起来。
所以,在预处理生成的文件(db_object.cpp)中,我们很容易生成其定义:
void Object::metacall(int idx)
{
switch (idx) {
case 0:
slot1();
break;
default:
break;
};
}
至此,我们已经实现的一个简化的自己的信号与槽的程序。下面我们总体上看看程序的所有代码:
#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类:
/*******************生成元对象需要的输入参数*****************/
//类名
const char * const class_name,
//父类名
QMetaObject *superclass,
//记录slot 信息
const QMetaData * const slot_data,
//记录槽的个数
int n_slots,
//记录signal 信息
const QMetaData * const signal_data,
//记录信号的个数
int n_signals
/******************* 元对象类提供的方法**************************/
int numSlots( bool super = FALSE ) const;//返回槽的个数
int numSignals( bool super = FALSE ) const;//返回信号的个数
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类:
//记录元对象数据for 信号与槽
struct QMetaData
{
const char *name; //名称
const QUMethod* method; //详细描述信息
enum Access { Private, Protected, Public };
Access access; //访问权限
};
二、QObject类实现了信号与槽机制
它利用元对象纪录的信息,实现了信号与槽机制
(1)信号与槽建立连接的实现
接口函数:
//连接
//参数(发送对象,信号,接收对象,处理信号的信号/槽)
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 );
//连接的内部实现
//(发送对象,信号的索引,接收对象,处理信号的类型,处理信号信号/槽的索引)
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 );
信号与槽连接的实现原理:
①阶段
bool QObject::connect( const QObject *sender,//发送对象
const char *signal,//信号
const QObject *receiver, //接收对象
const char *member //槽
)
{
//检查发送对象,信号,接收对象,槽不为null
if ( sender == 0 || receiver == 0 || signal == 0 || member == 0 ) {
return FALSE;
}
//获取发送对象的元对象
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;
}
//获取信号的元数据对象
const QMetaData *sm = smeta->signal( signal_index, TRUE );
//获取信号名字
signal = sm->name;
//获取处理信号的类型(是信号/槽)
int membcode = member[0] - '0'; // get member code
//发送信号对象
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;
}
//检查连接的参数(发送的信号,接收对象,处理信号的槽或信号)
if ( !s->checkConnectArgs(signal,receiver,member) ) {
return FALSE;
} else {
//获取处理信号的元数据对象
const QMetaData *rm = membcode == QSLOT_CODE ?
rmeta->slot( member_index, TRUE ) :
rmeta->signal( member_index, TRUE );
if ( rm ) {
//建立连接
//(发送信号的对象,信号的索引,接收信号的对象,
处理信号的类型,处理信号的索引)
connectInternal( sender, signal_index, receiver, membcode, member_index );
}
}
return TRUE;
}
②阶段
//建立连接
//(发送信号的对象,信号的索引,接收信号的对象,处理信号的类型,处理信号的索引)
void QObject::connectInternal( const QObject *sender, int signal_index,
const QObject *receiver, int membcode, int member_index )
{
//发送信号的对象
QObject *s = (QObject*)sender;
//接收信号的对象
QObject *r = (QObject*)receiver;
//如果发送对象的连接查询表为null,则建立
if ( !s->connections ) { // create connections lookup table
s->connections = new QSignalVec( signal_index+1 );
Q_CHECK_PTR( s->connections );
s->connections->setAutoDelete( TRUE );
}
//获取发送对象的相应信号的连接列表
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 );
//把连接添加到发送对象的连接列表中
clist->append( c );
//判断接收对象的发送对象列表是否为null
if ( !r->senderObjects ) // create list of senders
{
//建立接收对象的发送对象列表
r->senderObjects = new QSenderObjectList;
}
//把发送对象添加到发送对象列表中
r->senderObjects->append( s ); // add sender to list
}
(2)信号发生时激活的操作函数。 激活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;
//发送对象列表
QSenderObjectList* sol;
//旧的发送对象
QObject* oldSender = 0;
//连接
QConnection *c;
if ( clist->count() == 1 ) { // save iterator
//获取连接
c = clist->first();
//
object = c->object();
//获取发送对象列表
sol = object->senderObjects;
if ( sol ) {
//获取旧的发送对象
oldSender = sol->currentSender;
//
sol->ref();
//设置新的发送对象
sol->currentSender = this;
}
if ( c->memberType() == QSIGNAL_CODE )//如果是信号,则发送出去
object->qt_emit( c->member(), o );
else
object->qt_invoke( c->member(), o );//如果是槽,则执行
//
if ( sol ) {
//设置恢复为旧的发送对象
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();
//操作前设置当前发送对象
sol = object->senderObjects;
if ( sol ) {
oldSender = sol->currentSender;
sol->ref();
sol->currentSender = this;
}
//如果是信号,则发送出去
if ( c->memberType() == QSIGNAL_CODE ){
object->qt_emit( c->member(), o );
}
//如果是槽,则执行
else{
object->qt_invoke( c->member(), o );
}
//操作后恢复当前发送对象
if (sol ) {
sol->currentSender = oldSender;
if ( sol->deref() )
delete sol;
}
}
}
}