信号(signals)和槽(slots) 精讲
2010-11-01 22:54

信号(signals)和槽(slots)

信号和信号槽被用于对象(object)之间的通信。信号和槽机制是QT的重要特征并且也许是QT与其他框架最不相同的部分。

前言

在GUI程序设计中,通常我们希望当对一个窗口部件(widget)进行改变时能告知另一个对此改变感兴趣的窗口部件。更一般的,我们希望任何一类的对象(object)都能和其他对象进行通信。例如,如果用户单击一个关闭按钮,我们可能就希望窗口的 close() 函数被调用。

早期的工具包用回调(backcalls)的方式实现上面所提到的对象间的通信。回调是指一个函数的指针,因此如果你希望一个处理函数通知你一些事情,你可以传递另一个函数(回调函数)指针给这个处理函数。这个处理函数就会在适当的时候调用回调函数。回调有两个重要的缺陷:首先,它们不是类型安全的。我们无法确定处理函数是用正确的参数调用这个回调函数。其次,回调与处理函数紧密的联系在一起以致处理函数必须知道调用哪个回调。

消息和槽

在QT中,我们使用一种可替代回调的技术:信号和槽机制。当一个特别的事件产生时则发出一个信号。QT的窗口部件有很多已经预定义好的信号,我们也可以通过继承,给窗口部件的子类添加他们自己信号。槽就是一个可以被调用处理特定信号的函数。QT的窗口部件有很多预定义好的槽,但是通常的做法是给子类窗口部件添加自己的信号,这样就可以操纵自己加入的信号了。

  




上面这个图一定要好好理解,每个signal和Slot都是一个Object的属性,不同Object的signal可以对应不用的Object的Slot。


信号和槽机制是类型安全的:一个信号的签名必须和该信号接受槽的签名相匹配。(事实上以一个槽的签名可以比他可接受的信号的签名少,因为它可以忽略一些签名)。因此签名是一致的,编译器就可以帮助我们检测类型匹配。信号和槽是松耦合的:一个类不知道也不关心哪个槽接受了它所发出的信号。QT的信号和槽机制确保他们自生的正确连接,槽会在正确的时间使用信号参数而被调用。信号和槽可以使用任何数量、任何类型的参数。他们完全是类型安全的。

所有继承至QObject或是其子类(如 QWidget)的类都可包含信号和槽。当对象改变它们自身状态的时候,信号被发送,从某种意义上讲,它们也许对外面的世界感兴趣。这就是所有对象在通讯时所做的一切。它不知道也不关心有没有其他的东西接受它发出的信号。这就是真正的消息封装,并且确保对象可用作一个软件组件。

槽被用于接收信号,但是他们也是正常的成员函数。正如一个对象不知道是否有东西接受了他信号,一个槽也不知道它是否被某个信号连接。这就确保QT能创建真正独立的组件。

你可以将任意个信号连接到你想连接的信号槽,并且在需要时可将一个信号连接到多个槽。将信号直接连接到另一个信号也是可能的(这样无论何时当第一个信号被发出后会立即发出第二个)。

总体来看,信号和槽构成了一个强有力的组件编程机制。

简单示例

一个极小的 C++ 类 声明如下:

class Counter
{
public:
Counter() {m_value = 0;}

int value() const {return m_value;}
void setValue(int Value);
private:
int m_value;
};


一个小型的 QObject 子类声明为:

#include <QObject>

class Counter : public QObject
{
Q_OBJECT

public:
Counter() {m_value = 0;}

int value() const {return m_value;}

public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);

private:
int m_value;
};

QObject版本的类与前一个C++类有着相同的域,并且提供公有函数接受这个域,但是它还增加了对信号和槽(signals-slots)组件编程的支持。这个类可以通过valueChanged()发送信号告诉外部世界他的域发生了改变,并且它有一个可以接受来自其他对象发出信号的槽。

所有包含信号和槽的类都必须在他们声明中的最开始提到Q_OBJECT。并且他们必须继承至(直接或间接)QObject。

槽可以由应用程序的编写者来实现。这里是Counter::setVaule()的一个可能的实现:

void Counter::setValue(int value)
{
if(value != m_value)
{
m_value = value;
emit valueChanged(value);
}
}

emit所在的这一行从对象发出valueChanged信号,并使用新值做为参数。

在下面的代码片段中,我们创建两个Counter对象并且使用QObject::connect()函数将第一个对象的valueChanged()信号连接到第二个对象的setValue()槽。

Counter a, b;
QObject::connect (&a, SIGNAL(valueChanged(int)),
&b, SLOT(setValue(int)));
a.setValue(12);    // a.value() == 12, b.value() == 12
b.setValue(48);    // a.value() == 12, b.value() == 48

函数a.setValue(12)的调用导致信号valueChange(12)被发出,对象b的setValue()槽接受该信号,即函数setValue()被调用。然后b同样发出信号valueChange(),但是由于没有槽连接到b到valueChange()信号,所以该信号被忽略。

注意,只有当 value != m_value 时,函数 setValue() 才会设置新值并发出信号。这样就避免了在循环连接的情况下(比如b.valueChanged() 和a.setValue()连接在一起)出现无休止的循环的情况。

信号将被发送给任何你建立了连接的槽;如果重复连接,将会发送两个信号。总是可以使用QObject::disconnect()函数断开一个连接。

这个例子说明了对象之间可以不需要知道相互间的任何信息而系协同工作。为了实现这一目的,只需要将对象通过函数QObject::connect()的调用相连接(connect),或者利用uic的automatic connections的特性。

编译这个示例

C++预编译器会改变或去除关键字signals,slots,和emit,这样就可以使用标准的C++编译器。

在一个定义有信号和槽的类上运行moc,这样就会生成一个可以和其它对象文件编译和连接成应用程序的C++源文件。如果使用qmake工具,将会在你的makefile文件里加入自动调用moc的规则。

信号

当对象的内部状态发生改变,信号就被发射,在某些方面对于对象代理或者所有者也许是很有趣的。只有定义了信号的对象或其子对象才能发射该信号。

当一个信号被发出,被连接的槽通常会立刻运行,就像执行一个普通的函数调用。当这一切发生时,信号和槽机制是完全独立于任何GUI事件循环之外的。槽会在emit域下定义的代码执行完后返回。当使用队列连接(queued connections)时会有一些不同;这种情况下,关键字emit后的代码会继续执行,而槽在此之后执行。

如果几个槽被连接到一个信号,当信号被发出后,槽会以任意顺序一个接一个的执行。

关于参数需要注意:我们的经验显示如果信号和槽不使用特殊的类型将会变得更具重用性。如果QScrollBar::valueChanged() 使用了一个特殊的类型,比如hypothetical QRangeControl::Range,它就只能被连接到被设计成可以处理QRangeControl的槽。再没有象教程5这样简单的例子。



当一个信号被发出时连接他的槽被调用。槽是一个普通的C++函数并按普通方式调用;他的特点仅仅是可以被信号连接。

由于槽只是普通的成员函数,当调用时直接遵循C++规则。然而,对于槽,他们可以被任何组件通过一个信号-槽连接(signal-slot connection)调用,而不管其访问权限。也就是说,一个从任意的类的实例发出的信号可导致一个不与此类相关的另一个类的实例的私有槽被调用。

你还可以定义一个虚拟槽,在实践中被发现也是非常有用的。

由于增加来灵活性,与回调相比,信号和槽稍微慢一些,尽管这对真实的应用程序来说是可以忽略掉的。通常,发出一连接了某个槽的信号,比直接调用那些非虚拟调用的接受器要慢十倍。这是定位连接对象所需的开销,可以安全地重复所有地连接(例如在发射期间检查并发接收器是否被破坏)并且可以按一般的方式安排任何参数。当十个非虚函数调用听起来很多时,实际上他比任何new和delete操作的开销都少,例如,当你执行一个字符串、矢量或列表操作时,就需要用到new和delete,而信号和槽的开销只是全部函数调用花费的一小部分。

无论何时你用槽进行一个系统调用和间接的调用超过10个以上的函数时间都是一样的。在i586-500机器上,每秒钟你可以发送超过2,000,000个信号给一个接受者,或者每秒发送1,200,000个信号给两个接受者。相对于信号和槽机制的简洁性和灵活性,他的时间开销是完全值得的,你的用户甚至察觉不出来。

注意:若其他的库将变量定义为signals和slots,可能导致编译器在连接基于QT的应用程序时出错或警告。为了解决这个问题,请使用#undef预处理符号。

元对象信息

元对象编译器(moc)解析一个C++文件中的类声明并且生成初始化元对象的C++代码。元对象包括信号和槽的名字,和指向这些函数的指针。

if (widget->inherits("QAbstractButton")) {
QAbstractButton *button = static_cast<QAbstractButton *>(widget);
button->toggle();
}

元对象信息的使用也可以是qobject_cast<T>(), 他和QObject::inherits() 相似,但更不容易出错。

if (QAbstractButton *button = qobject_cast<QAbstractButton *>(widget))
button->toggle();

查看Meta-Object系统可获取更多信息。

一个实例

这是一个注释过的简单的例子(代码片断选自qlcdnumber.h)。

#ifndef LCDNUMBER_H
#define LCDNUMBER_H

#include <QFrame>

class LcdNumber : public QFrame
{
Q_OBJECT

LcdNumber通过QFrame和QWiget继承至QObject,它包含了大部分signal-slot知识。这是有点类似于内置的QLCDNumber部件。

Q_OBJECT宏由预处理器展开,用来声明由moc实现的机个成员函数;如果你的编译器出现错误如下"undefined reference to vtable for LcdNumber", 你可能忘了运行moc或者没有用连接命令包含moc输出。

public:
LcdNumber(QWidget *parent = 0);

LcdNumber并不明显的与moc相关,但是如果你继承了QWidege,那么可以几乎肯定在你的构造函数中有父对象的变量,并且希望把它传给基类的构造函数。

析构函数和一些成员函数在这里省略;moc会忽视成员函数。

signals:
void overflow();

当LcdNumbe被要求显示一个不可能的值时,便发出信号。

如果你没有留意溢出,或者你知道溢出不会出现,你可以忽略overflow()信号,比如不将其连接到任何槽。

如果另一方面,当有数字溢出时你想调用两个不同的错误处理函数,可以将这个信号简单的连接到两个不同的槽。QT将调用两个函数(无序的)。

public slots:
void display(int num);
void display(double num);
void display(const QString &str);
void setHexMode();
void setDecMode();
void setOctMode();
void setBinMode();
void setSmallDecimalPoint(bool point);
};

#endif

一个槽是一个接受函数,用于获得其他窗口部件的信息变化。LcdNumber使用它,就像上面的代码一样,来设置显示的数字。因为display()是这个类和程序的其它的部分的一个接口,所以这个槽是公有的。

几个例程把QScrollBar的valueChanged()信号连接到display()槽,所以LCD数字可以继续显示滚动条的值。

请注意display()被重载了,当将一个信号连接到槽时QT将选择一个最适合的一个。而对于回调,你会发现五个不同的名字并且自己来跟踪类型。

一个不相干的成员函数在例子中被忽略。

高级信号和槽的使用

在当你需要信号发送者的信息时,QT提供了一个函数QObject::sender(),他返回指向一个信号发送对象的指针。

当有几个信号被连接到同一槽上,并且槽需要处理每个不同的信号,可使用 QSignalMapper类。

假设你用三个按钮来决定打开哪个文件:Tax File", "Accounts File", or "Report File"。

为了能打开真确的文件,你需要分别将它们的信号 QPushButton::clicked()连接到 readFile()。然后用QSignalMapper 的 setMapping()来映射所有 clicked()信号到一个 QSignalMapper对象。

signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));

connect(taxFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(accountFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));
connect(reportFileButton, SIGNAL(clicked()),
signalMapper, SLOT (map()));

然后,连接信号 mapped()到 readFile() ,根据被按下的按钮,就可以打开不同的文件。

connect(signalMapper, SIGNAL(mapped(const QString &)),
this, SLOT(readFile(const QString &)));

在QT中使用第三方signals slots

在QT中使用第三方signals slots是可能的。你甚至可以在同一类中使用两种机制。仅仅需要在你的qmake工程文件(.pro)中加入下面语句:

CONFIG += no_keywords

它告诉QT不要定义moc关键字signals,slots和emit,因为这些名字可能将被用于第三方库,例如Boost。你只需简单的用QT宏将他们替换为 Q_SIGNALS, Q_SLOTS,和 Q_EMIT,就可以继续使用信号和槽了。

Qt源码分析之信号和槽机制
2009/09/17 13:21

Qt的信号和槽机制是Qt的一大特点,实际上这是和MFC中的消息映射机制相似的东西,要完成的事情也差不多,就是发送一个消息然后让其它窗口响应,当然,这里的消息是广义的
说法,简单点说就是如何在一个类的一个函数中触发另一个类的另一个函数调用,而且还要把相关的参数传递过去.好像这和回调函数也有点关系,但是消息机制可比回调函数有用
多了,也复杂多了

MFC中的消息机制没有采用C++中的虚函数机制,原因是消息太多,虚函数开销太大.在Qt中也没有采用C++中的虚函数机制,原因与此相同.其实这里还有更深层次上的原因,大体说来,
多态的底层实现机制只有两种,一种是按照名称查表,一种是按照位置查表,两种方式各有利弊,而C++的虚函数机制无条件的采用了后者,导致的问题就是在子类很少重载基类实现
的时候开销太大,再加上象界面编程这样子类众多的情况,基本上C++的虚函数机制就废掉了,于是各家库的编写者就只好自谋生路了,说到底,这确实是C++语言本身的缺陷

示例代码:
#include <QApplication>
#include <QPushButton>
#include <QPointer>

int main(int argc, char *argv[])
{
QApplication app(argc, argv);

    QPushButton quit("Quit");
quit.resize(100, 30);
quit.show();
QObject::connect(&quit, SIGNAL(clicked()), &app, SLOT(quit()));

return app.exec();
}

这里主要是看QPushButton的clicked()信号和app的quit()槽如何连接?又是如何响应?
前面已经说过了,Qt的信号槽机制其实就是按照名称查表,因此这里的首要问题是如何构造这个表?
和C++虚函数表机制类似的,在Qt中,这个表就是元数据表,Qt中的元数据表最大的作用就是支持信号槽机制,当然,也可以在此基础上扩展出更多的功能,因为
元数据是我们可以直接访问到的,不再是象虚函数表那样被编译器遮遮掩掩的藏了起来,不过Qt似乎还没有完全发挥元数据的能力,动态属性,反射之类的机制好像还没有

任何从QObject派生的类都包含了自己的元数据模型,一般是通过宏Q_OBJECT定义的
#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private:

首先声明了一个QMetaObject类型的静态成员变量,这就是元数据的数据结构

struct Q_CORE_EXPORT QMetaObject
{
...
struct { // private data
const QMetaObject *superdata;
const char *stringdata;
const uint *data;
const QMetaObject **extradata;
} d;
}
QMetaObject中有一个嵌套类封装了所有的数据
const QMetaObject *superdata;//这是元数据代表的类的基类的元数据
const char *stringdata;//这是元数据的签名标记
const uint *data;//这是元数据的索引数组的指针
const QMetaObject **extradata;//这是扩展元数据表的指针,一般是不用的       

这里的三个虚函数metaObject,qt_metacast,qt_metacall是在moc文件中定义的
metaObject的作用是得到元数据表指针
qt_metacast的作用是根据签名得到相关结构的指针,注意它返回的可是void*指针
qt_metacall的作用是查表然后调用调用相关的函数

宏QT_TR_FUNCTIONS是和翻译相关的
#  define QT_TR_FUNCTIONS \
static inline QString tr(const char *s, const char *c = 0) \
{ return staticMetaObject.tr(s, c); }

好了,看看实际的例子吧:

QPushButton的元数据表如下:
static const uint qt_meta_data_QPushButton[] = {

 // content:
1,       // revision
0,       // classname
0,    0, // classinfo
2,   10, // methods
3,   20, // properties
0,    0, // enums/sets

 // slots: signature, parameters, type, tag, flags
13,   12,   12,   12, 0x0a,
24,   12,   12,   12, 0x08,

 // properties: name, type, flags
44,   39, 0x01095103,
56,   39, 0x01095103,
64,   39, 0x01095103,

       0        // eod
};

static const char qt_meta_stringdata_QPushButton[] = {
"QPushButton\0\0showMenu()\0popupPressed()\0bool\0autoDefault\0default\0"
"flat\0"
};

const QMetaObject QPushButton::staticMetaObject = {
{ &QAbstractButton::staticMetaObject, qt_meta_stringdata_QPushButton,
qt_meta_data_QPushButton, 0 }
};

在这里我们看到了静态成员staticMetaObject被填充了
const QMetaObject *superdata;//这是元数据代表的类的基类的元数据,被填充为基类的元数据指针&QAbstractButton::staticMetaObject
const char *stringdata;//这是元数据的签名标记,被填充为qt_meta_stringdata_QPushButton
const uint *data;//这是元数据的索引数组的指针,被填充为qt_meta_data_QPushButton
const QMetaObject **extradata;//这是扩展元数据表的指针,一般是不用的,被填充为0

首先应该看qt_meta_data_QPushButton,因为这里是元数据的主要数据,它被填充为一个整数数组,正因为这里只有整数,不能有任何字符串存在,因此才有
qt_meta_stringdata_QPushButton发挥作用的机会,可以说真正的元数据应该是qt_meta_data_QPushButton加上qt_meta_stringdata_QPushButton,那么为什么
不把这两个东西合在一起呢?估计是两者都不是定长的结构,合在一起反而麻烦吧

qt_meta_data_QPushButton实际上是以以下结构开头的

struct QMetaObjectPrivate
{
int revision;
int className;
int classInfoCount, classInfoData;
int methodCount, methodData;
int propertyCount, propertyData;
int enumeratorCount, enumeratorData;
};

一般使用中是直接使用以下函数做个转换
static inline const QMetaObjectPrivate *priv(const uint* data)
{ return reinterpret_cast<const QMetaObjectPrivate*>(data); }

这种转换怎么看都有些黑客的味道,这确实是十足的C风格

再结合实际的数据看一看
static const uint qt_meta_data_QPushButton[] = {

 // content:
1,       // revision  版本号是1
0,       // classname 类名存储在qt_meta_stringdata_QPushButton中,索引是0,因此就是QPushButton了
0,    0, // classinfo  类信息数量为0,数据也是0
2,   10, // methods  QPushButton有2个自定义方法,方法数据存储在qt_meta_data_QPushButton中,索引是10,就是下面的slots:开始的地方
3,   20, // properties QPushButton有3个自定义属性,属性数据存储在qt_meta_data_QPushButton中,索引是20,就是下面的properties:开始的地方
0,    0, // enums/sets QPushButton没有自定义的枚举

 // slots: signature, parameters, type, tag, flags
13,   12,   12,   12, 0x0a,
第一个自定义方法的签名存储在qt_meta_data_QPushButton中,索引是13,就是showMenu()了
24,   12,   12,   12, 0x08,
第二个自定义方法的签名存储在qt_meta_data_QPushButton中,索引是24,popupPressed()了

 // properties: name, type, flags
44,   39, 0x01095103,  
第一个自定义属性的签名存储在qt_meta_data_QPushButton中,索引是44,就是autoDefault了
第一个自定义属性的类型存储在qt_meta_data_QPushButton中,索引是39,就是bool
56,   39, 0x01095103,  
第二个自定义属性的签名存储在qt_meta_data_QPushButton中,索引是56,就是default了
第二个自定义属性的类型存储在qt_meta_data_QPushButton中,索引是39,就是bool
64,   39, 0x01095103,  
第三个自定义属性的签名存储在qt_meta_data_QPushButton中,索引是64,就是flat了
第三个自定义属性的类型存储在qt_meta_data_QPushButton中,索引是39,就是bool

       0        // eod 元数据的结束标记
};

static const char qt_meta_stringdata_QPushButton[] = {
"QPushButton\0\0showMenu()\0popupPressed()\0bool\0autoDefault\0default\0"
"flat\0"
};

QPushButton\\showMenu()\popupPressed()\bool\autoDefault\default\flat\
这里把\0直接替换为\是为了数数的方便

当然我们还可以看看QPushButton的基类QAbstractButton的元数据
static const uint qt_meta_data_QAbstractButton[] = {

 // content:
1,       // revision
0,       // classname
0,    0, // classinfo
12,   10, // methods
9,   70, // properties
0,    0, // enums/sets

 // signals: signature, parameters, type, tag, flags
17,   16,   16,   16, 0x05,
27,   16,   16,   16, 0x05,
46,   38,   16,   16, 0x05,
60,   16,   16,   16, 0x25,
70,   38,   16,   16, 0x05,

 // slots: signature, parameters, type, tag, flags
89,   84,   16,   16, 0x0a,
113,  108,   16,   16, 0x0a,
131,   16,   16,   16, 0x2a,
146,   16,   16,   16, 0x0a,
154,   16,   16,   16, 0x0a,
163,   16,   16,   16, 0x0a,
182,  180,   16,   16, 0x1a,

 // properties: name, type, flags
202,  194, 0x0a095103,
213,  207, 0x45095103,
224,  218, 0x15095103,
246,  233, 0x4c095103,
260,  255, 0x01095103,
38,  255, 0x01195103,
270,  255, 0x01095103,
281,  255, 0x01095103,
295,  255, 0x01094103,

       0        // eod
};

static const char qt_meta_stringdata_QAbstractButton[] = {
"QAbstractButton\0\0pressed()\0released()\0checked\0clicked(bool)\0"
"clicked()\0toggled(bool)\0size\0setIconSize(QSize)\0msec\0"
"animateClick(int)\0animateClick()\0click()\0toggle()\0setChecked(bool)\0"
"b\0setOn(bool)\0QString\0text\0QIcon\0icon\0QSize\0iconSize\0"
"QKeySequence\0shortcut\0bool\0checkable\0autoRepeat\0autoExclusive\0"
"down\0"
};

QAbstractButton00pressed()0released()0checked0clicked(bool)0clicked()0toggled(bool)0size0setIconSize(QSize)0msec0animateClick(int)0animateClick()0click()0toggle()0setChecked(bool)0b0setOn(bool)0QString0text0QIcon0icon0QSize0iconSize0QKeySequence0shortcut0bool0checkable0autoRepeat0autoExclusive0down0

基本上都是大同小异的

  QObject::connect(&quit, SIGNAL(clicked()), &app, SLOT(quit()));
下面开始看信号和槽连接的源码了

// connect的源码
connect函数是连接信号和槽的桥梁,非常关键
bool QObject::connect(const QObject *sender, const char *signal,
const QObject *receiver, const char *method,
Qt::ConnectionType type)
{
#ifndef QT_NO_DEBUG
bool warnCompat = true;
#endif
if (type == Qt::AutoCompatConnection) {
type = Qt::AutoConnection;
#ifndef QT_NO_DEBUG
warnCompat = false;
#endif
}

 // 不允许空输入
if (sender == 0 || receiver == 0 || signal == 0 || method == 0) {
#ifndef QT_NO_DEBUG
qWarning("Object::connect: Cannot connect %s::%s to %s::%s",
sender ? sender->metaObject()->className() : "(null)",
signal ? signal+1 : "(null)",
receiver ? receiver->metaObject()->className() : "(null)",
method ? method+1 : "(null)");
#endif
return false;
}
QByteArray tmp_signal_name;

#ifndef QT_NO_DEBUG
// 检查是否是信号标记
if (!check_signal_macro(sender, signal, "connect", "bind"))
return false;
#endif
// 得到元数据类
const QMetaObject *smeta = sender->metaObject();
++signal; //skip code跳过信号标记,直接得到信号标识
// 得到信号的索引
int signal_index = smeta->indexOfSignal(signal);
if (signal_index < 0) {
// check for normalized signatures
tmp_signal_name = QMetaObject::normalizedSignature(signal).prepend(*(signal - 1));
signal = tmp_signal_name.constData() + 1;
signal_index = smeta->indexOfSignal(signal);
if (signal_index < 0) {
#ifndef QT_NO_DEBUG
err_method_notfound(QSIGNAL_CODE, sender, signal, "connect");
err_info_about_objects("connect", sender, receiver);
#endif
return false;
}
}

    QByteArray tmp_method_name;
int membcode = method[0] - '0';

#ifndef QT_NO_DEBUG
// 检查是否是槽,用QSLOT_CODE 1标记
if (!check_method_code(membcode, receiver, method, "connect"))
return false;
#endif
++method; // skip code

  // 得到元数据类
const QMetaObject *rmeta = receiver->metaObject();
int method_index = -1;
// 这里是一个case,信号即可以和信号连接也可以和槽连接
switch (membcode) {
case QSLOT_CODE:
// 得到槽的索引
method_index = rmeta->indexOfSlot(method);
break;
case QSIGNAL_CODE:
// 得到信号的索引
method_index = rmeta->indexOfSignal(method);
break;
}
if (method_index < 0) {
// check for normalized methods
tmp_method_name = QMetaObject::normalizedSignature(method);
method = tmp_method_name.constData();
switch (membcode) {
case QSLOT_CODE:
method_index = rmeta->indexOfSlot(method);
break;
case QSIGNAL_CODE:
method_index = rmeta->indexOfSignal(method);
break;
}
}

    if (method_index < 0) {
#ifndef QT_NO_DEBUG
err_method_notfound(membcode, receiver, method, "connect");
err_info_about_objects("connect", sender, receiver);
#endif
return false;
}
#ifndef QT_NO_DEBUG
// 检查参数,信号和槽的参数必须一致,槽的参数也可以小于信号的参数
if (!QMetaObject::checkConnectArgs(signal, method)) {
qWarning("Object::connect: Incompatible sender/receiver arguments"
"\n\t%s::%s --> %s::%s",
sender->metaObject()->className(), signal,
receiver->metaObject()->className(), method);
return false;
}
#endif

    int *types = 0;
if (type == Qt::QueuedConnection
&& !(types = ::queuedConnectionTypes(smeta->method(signal_index).parameterTypes())))
return false;

#ifndef QT_NO_DEBUG
{
// 得到方法的元数据
QMetaMethod smethod = smeta->method(signal_index);
QMetaMethod rmethod = rmeta->method(method_index);
if (warnCompat) {
if(smethod.attributes() & QMetaMethod::Compatibility) {
if (!(rmethod.attributes() & QMetaMethod::Compatibility))
qWarning("Object::connect: Connecting from COMPAT signal (%s::%s).", smeta->className(), signal);
} else if(rmethod.attributes() & QMetaMethod::Compatibility && membcode != QSIGNAL_CODE) {
qWarning("Object::connect: Connecting from %s::%s to COMPAT slot (%s::%s).",
smeta->className(), signal, rmeta->className(), method);
}
}
}
#endif
// 调用元数据类的连接
QMetaObject::connect(sender, signal_index, receiver, method_index, type, types);
// 发送连接的通知,现在的实现是空的
const_cast<QObject*>(sender)->connectNotify(signal - 1);
return true;
}

检查信号标记其实比较简单,就是用signal的第一个字符和用QSIGNAL_CODE=2的标记比较而已
static bool check_signal_macro(const QObject *sender, const char *signal,
const char *func, const char *op)
{
int sigcode = (int)(*signal) - '0';
if (sigcode != QSIGNAL_CODE) {
if (sigcode == QSLOT_CODE)
qWarning("Object::%s: Attempt to %s non-signal %s::%s",
func, op, sender->metaObject()->className(), signal+1);
else
qWarning("Object::%s: Use the SIGNAL macro to %s %s::%s",
func, op, sender->metaObject()->className(), signal);
return false;
}
return true;
}

得到信号的索引实际上要依次找每个基类的元数据,得到的偏移也是所有元数据表加在一起后的一个索引
int QMetaObject::indexOfSignal(const char *signal) const
{
int i = -1;
const QMetaObject *m = this;
while (m && i < 0) {
// 根据方法的数目倒序的查找
for (i = priv(m->d.data)->methodCount-1; i >= 0; --i)
// 得到该方法的类型
if ((m->d.data[priv(m->d.data)->methodData + 5*i + 4] & MethodTypeMask) == MethodSignal
&& strcmp(signal, m->d.stringdata
// 得到方法名称的偏移
+ m->d.data[priv(m->d.data)->methodData + 5*i]) == 0) {
//如果找到了正确的方法,再增加所有基类的方法偏移量
i += m->methodOffset();
break;
}
// 在父类中继续找
m = m->d.superdata;
}
#ifndef QT_NO_DEBUG
// 判断是否于基类中的冲突
if (i >= 0 && m && m->d.superdata) {
int conflict = m->d.superdata->indexOfMethod(signal);
if (conflict >= 0)
qWarning("QMetaObject::indexOfSignal:%s: Conflict with %s::%s",
m->d.stringdata, m->d.superdata->d.stringdata, signal);
}
#endif
return i;
}

// 这里是所有基类的方法偏移量算法,就是累加基类所有的方法数目
int QMetaObject::methodOffset() const
{
int offset = 0;
const QMetaObject *m = d.superdata;
while (m) {
offset += priv(m->d.data)->methodCount;
m = m->d.superdata;
}
return offset;
}

// 得到方法的元数据
QMetaMethod QMetaObject::method(int index) const
{
int i = index;
// 要减去基类的偏移
i -= methodOffset();
// 如果本类找不到,就到基类中去找
if (i < 0 && d.superdata)
return d.superdata->method(index);

 // 如果找到了,就填充QMetaMethod结构
QMetaMethod result;
if (i >= 0 && i < priv(d.data)->methodCount) {
// 这里是类的元数据
result.mobj = this;
// 这里是方法相关数据在data数组中的偏移量
result.handle = priv(d.data)->methodData + 5*i;
}
return result;
}

bool QMetaObject::connect(const QObject *sender, int signal_index,
const QObject *receiver, int method_index, int type, int *types)
{
// 得到全局的连接列表
QConnectionList *list = ::connectionList();
if (!list)
return false;
QWriteLocker locker(&list->lock);
// 增加一个连接
list->addConnection(const_cast<QObject *>(sender), signal_index,
const_cast<QObject *>(receiver), method_index, type, types);
return true;
}

void QConnectionList::addConnection(QObject *sender, int signal,
QObject *receiver, int method,
int type, int *types)
{
// 构造一个连接
QConnection c = { sender, signal, receiver, method, 0, 0, types };
c.type = type; // don't warn on VC++6
int at = -1;
// 如果有中间被删除的连接,可以重用这个空间
for (int i = 0; i < unusedConnections.size(); ++i) {
if (!connections.at(unusedConnections.at(i)).inUse) {
// reuse an unused connection
at = unusedConnections.takeAt(i);
connections[at] = c;
break;
}
}
if (at == -1) {
// append new connection
at = connections.size();
// 加入一个连接
connections << c;
}
// 构造sender,receiver连接的哈希表,加速搜索速度
sendersHash.insert(sender, at);
receiversHash.insert(receiver, at);
}

通过connect函数,我们建立了信号和槽的连接,并且把信号类+信号索引+槽类,槽索引作为记录写到了全局的connect列表中

一旦我们发送了信号,就应该调用相关槽中的方法了,这个过程其实就是查找全局的connect列表的过程,当然还要注意其中要对相关的参数打包和解包

// emit是发送信号的代码
void Foo::setValue(int v)
{
if (v != val)
{
val = v;
emit valueChanged(v);
}
}

// 发送信号的真正实现在moc里面
// SIGNAL 0
void Foo::valueChanged(int _t1)
{
// 首先把参数打包
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
// 调用元数据类的激活
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

void QMetaObject::activate(QObject *sender, const QMetaObject *m, int local_signal_index,
void **argv)
{
// 增加一个基类偏移量
int offset = m->methodOffset();
activate(sender, offset + local_signal_index, offset + local_signal_index, argv);
}

void QMetaObject::activate(QObject *sender, int from_signal_index, int to_signal_index, void **argv)
{
// 这里得到的是QObject的数据,首先判断是否为阻塞设置
if (sender->d_func()->blockSig)
return;

 // 得到全局链表
QConnectionList * const list = ::connectionList();
if (!list)
return;

    QReadLocker locker(&list->lock);

    void *empty_argv[] = { 0 };
if (qt_signal_spy_callback_set.signal_begin_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_begin_callback(sender, from_signal_index,
argv ? argv : empty_argv);
locker.relock();
}

 // 在sender的哈希表中得到sender的连接
QConnectionList::Hash::const_iterator it = list->sendersHash.find(sender);
const QConnectionList::Hash::const_iterator end = list->sendersHash.constEnd();

    if (it == end) {
if (qt_signal_spy_callback_set.signal_end_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_end_callback(sender, from_signal_index);
locker.relock();
}
return;
}

    QThread * const currentThread = QThread::currentThread();
const int currentQThreadId = currentThread ? QThreadData::get(currentThread)->id : -1;

 // 记录sender连接的索引
QVarLengthArray<int> connections;
for (; it != end && it.key() == sender; ++it) {
connections.append(it.value());
// 打上使用标记,因为可能是放在队列中
list->connections[it.value()].inUse = 1;
}

    for (int i = 0; i < connections.size(); ++i) {
const int at = connections.constData()[connections.size() - (i + 1)];
QConnectionList * const list = ::connectionList();
// 得到连接
QConnection &c = list->connections[at];
c.inUse = 0;
if (!c.receiver || (c.signal < from_signal_index || c.signal > to_signal_index))
continue;

  // 判断是否放到队列中
// determine if this connection should be sent immediately or
// put into the event queue
if ((c.type == Qt::AutoConnection
&& (currentQThreadId != sender->d_func()->thread
|| c.receiver->d_func()->thread != sender->d_func()->thread))
|| (c.type == Qt::QueuedConnection)) {
::queued_activate(sender, c, argv);
continue;
}

  // 为receiver设置当前发送者
const int method = c.method;
QObject * const previousSender = c.receiver->d_func()->currentSender;
c.receiver->d_func()->currentSender = sender;
list->lock.unlock();

        if (qt_signal_spy_callback_set.slot_begin_callback != 0)
qt_signal_spy_callback_set.slot_begin_callback(c.receiver, method, argv ? argv : empty_argv);

#if defined(QT_NO_EXCEPTIONS)
c.receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
#else
try {
// 调用receiver的方法
c.receiver->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
} catch (...) {
list->lock.lockForRead();
if (c.receiver)
c.receiver->d_func()->currentSender = previousSender;
throw;
}
#endif

        if (qt_signal_spy_callback_set.slot_end_callback != 0)
qt_signal_spy_callback_set.slot_end_callback(c.receiver, method);

        list->lock.lockForRead();
if (c.receiver)
c.receiver->d_func()->currentSender = previousSender;
}

    if (qt_signal_spy_callback_set.signal_end_callback != 0) {
locker.unlock();
qt_signal_spy_callback_set.signal_end_callback(sender, from_signal_index);
locker.relock();
}
}

// 响应信号也是在moc里实现的
int Foo::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
// 首先在基类中调用方法,返回的id已经变成当前类的方法id了
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
// 这里就是真正的调用方法了,注意参数的解包用法
case 0: valueChanged(*reinterpret_cast< int(*)>(_a[1])); break;
case 1: setValue(*reinterpret_cast< int(*)>(_a[1])); break;
}
_id -= 2;
}
return _id;
}