Qt信号和槽的实现揭秘

Qt信号和槽的实现揭秘

对于刚开始学习Qt的同学,对信号和槽的运行机制是非常难于理解的,这篇Blog 的目的就是解析掀开信号和槽的神秘面纱。
支持信号与槽机制的类必须派生于QObject,并且在类的声明中必须包涵Q_OBJECT宏。
这里用到的就如下的几行代码,非常简单,一个按钮单击退出。

#include <QtGui/QApplication>
#include <QtGui/QPushButton>

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton button;
QObject::connect(&button,SIGNAL(clicked()),qApp,SLOT(quit()));
button.setText("hello world!");
button.show();
return a.exec();
}
//这里main是qMain的宏替换,有兴趣的可以到qtmain_win.cpp中看看
在开始之前先解释几个宏:
#define slots  
#define signals protected
#define emit  

# define SLOT(a) qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a) qFlagLocation("2"#a QLOCATION)
# define QLOCATION "\0"__FILE__":"QTOSTRING(__LINE__)

__FILE__ __LINE__ //调试宏不用理会

const char *qFlagLocation(const char *method)
{
static int idx = 0;
flagged_locations[idx] = method;
idx = (idx+1) % flagged_locations_count;
return method;
}//qobject.cpp : 2229

这几个宏是非常简单的,signals和slots并不是C++的关键字,是Qt定义的宏为moc的关键字,signals,slots和emit的宏定义只是为了在编译时不会报错,emit表示发射信号的意义,其实不写也没关系,因为它的定义就是空。
SLOT(a)与SIGNAL(a)只是将a替换成加”1”或者”2”的字符串,在加入connectionLists链表时表示这是信号与槽连接还是信号与信号连接。

一. 开始调试:
将断点打到第八行:QObject::connect...与第十行:return a.exec();。
先从connect开始,connect函数的功能是以信号为索引记录对应的槽函数:
1. 将信号与槽函数转换成对应的字符串,传入connect函数,在connect函数中用一个指针数组cbdata保存传入的数据。//qobject.cpp
2. 跳过一些安全检查到第2510行
const QMetaObject *smeta = sender->metaObject();
const char *signal_arg = signal;
++signal; //skip code
得到QPushButton的元对象smeta,signal_arg保存”2clicked()”,++signal后,signal为”clicked()”;
既然出现了QMetaObject类,就在这里介绍一下吧,信号与槽的支持完全是QMetaObject在后台工作的结果,在我们使用Qt时完全用不到这个类,所以看不懂也没关系:
struct Q_CORE_EXPORT QMetaObject
{ ...//函数的定义,可以到qobjectdefs.h查看
struct { // private data
const QMetaObject *superdata;
const char *stringdata;
const uint *data;
const void *extradata;
} d;//唯一一个数据的定义
};
每个支持信号与槽机制的类必须派生于QObject,并且在类的声明中必须包涵Q_OBJECT宏。在blog的最后有Q_OBJECT宏的展开,其中有一项是:
static const QMetaObject staticMetaObject;
详细的解释可以参考第三部分:moc元对象编译器
3. 计算信号索引
int signal_index = QMetaObjectPrivate::indexOfSignalRelative(&smeta, signal);
... ... //一系列计算信号索引的方法,利用staticMetaObject 因为在继承链中支持信号和槽的类 都可以用staticMetaObject 对信号和槽的个数进行计数
signal_index += signalOffset;
4. 对槽函数进行操作
int membcode = extract_code(method);
//membcode得到添加到信号或者槽前面的字符’1’或者’2’减去’0’的值
表示这是一个槽还是信号。这里是1
const char *method_arg = method;
++method; //skip code
const QMetaObject *rmeta = receiver->metaObject();
int method_index = -1;
switch (membcode) {//1
case QSLOT_CODE:
method_index = rmeta->indexOfSlot(method);//获得在整个继承链中的索引
break;
case QSIGNAL_CODE:
method_index = rmeta->indexOfSignal(method);//获得在整个继承链中的索引
break;
}
IndexOfSlot(method)//这里method是”quit()”字符串
在函数内部会从子类就是QApplication的staticMetaObject开始对slots函数名的字符串判断,如果找到返回对应的索引如果没找到就到父类的staticMetaObject继续查找。
5. 通过一系列的判断,并对signal_index,和method_index的赋值后终于到了连接的语句,为了看到连接过程这里摘录了比较多的语句
QMetaObjectPrivate::connect(sender, signal_index, receiver, method_index, type, types)
{//connect内部
QObject *s = const_cast<QObject *>(sender);
QObject *r = const_cast<QObject *>(receiver);
QObjectPrivate::Connection *c = new QObjectPrivate::Connection;
//对Connection赋值
c->sender = s;
c->receiver = r;
c->method = method_index;
c->connectionType = type;
c->argumentTypes = types;
c->nextConnectionList = 0;
//将Connection加入到链表,函数很简单就不做解释了
QObjectPrivate::get(s)->addConnection(signal_index, c);
{//addConnection内部
if (!connectionLists)
connectionLists = new QObjectConnectionListVector();
if (signal >= connectionLists->count())
connectionLists->resize(signal + 1);

ConnectionList &connectionList =
(*connectionLists)[signal];
if (connectionList.last) {
connectionList.last->nextConnectionList = c;
} else {
connectionList.first = c;
}
connectionList.last = c;
cleanConnectionLists();
}//addConnection
...
}//connect

二. 至此信号和槽函数的连接完成,下面是信号调用槽函数的过程:
当button窗口正常显示时,跟踪clicked()信号产生,及如何调用槽函数。
1.window 系统下,每个窗口都有自己的窗口处理函数WndProc,Qt所有的窗口部件共用一个窗口处理函数 QtWndProc(qapplication_win.cpp:1465),这个窗口处理函数在窗口创建的过程中注册到系统,关于窗口的所有消息均由 QtWndProc处理。左键单击是由WM_LBUTTONDOWN和WM_LBUTTONUP两个消息构成。
2.以下是WM_LBUTTONUP处理过程的函数调用关系,因为WM_LBUTTONDOWN与WM_LBUTTONUP的处理过程类似,就以WM_LBUTTONUP为例:
QtWndProc()
QETWidget::translateMouseEvent()
QApplicationPrivate::sendMouseEvent()
QCoreApplication::sendSpontaneousEvent()
QCoreApplication::notifyInternal()
QApplication::notify()
QApplicationPrivate::notify_helper()
QPushButton::event()
QAbstractButton::event()
QWidget::event()
QAbstractButton::mouseReleaseEvent()
QAbstractButtonPrivate::click()
QAbstractButtonPrivate::emitClicked()
QAbstractButton::clicked()
QMetaObject::activate()

窗口获得消息,将其包装成对应的事件,这里是鼠标事件,一层一层的传递到发生事件的对象,对事件传递的过程上面的函数调用已经列举的很清楚了,就从QAbstractButton::mouseReleaseEvent()开始,会对鼠标状态进行判断:
if (e->button() != Qt::LeftButton)...//是否是鼠标左键
if (!d->down)...//按键是否被按下过
if (hitButton(e->pos()))...//按键是否在窗口范围内
如果都成立则调用click()函数,click()调用emitClicked()函数,在emitClicked()中:emit q->clicked(checked);
绕了一大圈终于看到发射信号了,我们将其过程简单的总结一下,首先将消息WM_LBUTTONDOWN和WM_LBUTTONUP包装成鼠标click事件,再将事件转换成信号就是在click()函数中调用emitClicked()函数发射信号,这只是信号到槽的路由的开始。但在开始之前还要解释一下 emit q->clicked(checked);中的语法问题,这里q是QAbstractButton对象的指针,是通过 Q_Q(QAbstractButton)得到的,因为信号是signals:声明的,而signals就是protected,在类外是无法访问 protected成员的,但是在QAbstractButtonPrivate类中有这样的声明:
Q_DECLARE_PUBLIC(QAbstractButton)
宏的定义如下:
#define Q_DECLARE_PUBLIC(Class)\
inline Class* q_func() { return static_cast<Class *>(q_ptr); } \
inline const Class* q_func() const { return static_cast<const Class *>(q_ptr); } \
friend class Class;
这样作为友元类就可以调用clicked()函数了。emit可以不写,但是写上做为信号调用的标识是个不错的习惯。
3.下面的调用进入到信号函数体,在我们写代码的时候,信号只写了声明,信号的实现是由moc来实现的。moc会在文件最后稍作解释。

void QAbstractButton::clicked(bool _t1)
{
void *_a[] = { 0, const_cast<void*>(reinterpret_cast<const void*>(&_t1)) };
QMetaObject::activate(this, &staticMetaObject, 2, _a);
}
_a是保存所有参数地址的数组。
QMetaObject::activate(this, &staticMetaObject, 2, _a);
this是发送者QPushButton,staticMetaObject是QAbstractButton类的元对象,因为有QAbstractButton::作用域,2是clicked()在这个moc文件的索引,_a是参数数组地址。
{//activate
//首先计算通过staticMetaObject得到信号和槽的偏移量
int signalOffset;
int methodOffset;
computeOffsets(metaObject, &signalOffset, &methodOffset);
//从QAbstractButton的父类一直到QObject的所有信号和槽的总数就是偏移量
//加上在QAbstractButton类内的偏移量,就是clicked()信号的索引
int signal_index = signalOffset + local_signal_index;
int signal_absolute_index = methodOffset + local_signal_index;
//一些线程安全的操作,不做解释略过
QObjectConnectionListVector *connectionLists =
sender->d_func()->connectionLists;
// connectionLists在connect时见过的
++connectionLists->inUse;
QObjectPrivate::Connection *c =
connectionLists->at(signal_index).first;
QObjectPrivate::Connection *last =
connectionLists->at(signal_index).last;
QObject * const receiver = c->receiver;//Connection保存起来的receiver,就是qApp;
const int method = c->method;
QObjectPrivate::Sender currentSender;
const bool receiverInSameThread = currentThreadData == receiver->d_func()->threadData;//判断sender和receiver是否在相同的线程
QObjectPrivate::Sender *previousSender = 0;
if (receiverInSameThread) {//如果在同一个线程
currentSender.sender = sender;
currentSender.signal = signal_absolute_index;
currentSender.ref = 1;
previousSender =
QObjectPrivate::setCurrentSender(receiver, ¤tSender);
}//if end
metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
{//metacall
receiver ->qt_metacall(QMetaObject::InvokeMetaMethod, method, argv);
}//metacall end
}//activate end
4.receiver->qt_metacall(...)即是qApp->qt_metacall(...)这个函数是由moc元编译器生成的,原型如下:
int QApplication::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{//qt_metacall
//_c是元方法标识,_id是槽函数索引,_a是参数列表地址
_id = QCoreApplication::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
switch (_id) {
...//这里有10个case:槽函数
}
_id -= 10;
}
return _id;
}//end qt_metacall
槽函数的寻找方式:从QObject的moc文件开始到最终子类的moc文件结束,如果没找到则减去该类中对应的槽函数个数,向下一层查找,重复这个过程直到找到或_id<0为止。
含有参数的槽函数调用,将_a数组内对应元素的转换一下,_a内的元素只能等于或多于槽函数的参数,不能少于。

三. moc元对象编译器
moc 是为了支持Qt的C++扩展特性而设计的,这里只介绍有关信号和槽的部分。元对象编译器moc根据类声明中的宏Q_OBJECT,以及signals及 slots部分的定义生成附加的C++代码,moc编译器生成moc_*.cpp附加文件,这个附加源文件是参与编译的。
signals和slots是moc的关键字,对signals会自动添加信号函数的实现部分,会自动完成Q_OBJECT宏替换出的qt_metacall函数;并填写staticMetaObject如下:
const QMetaObject ClassName::staticMetaObject = {
{
&ParentName::staticMetaObject, //父类的元对象
qt_meta_stringdata_ClassName, //元对象的字符串数据
qt_meta_data_ClassName,//填写了一些建立连接时用到的信息
0//结束标志
}
};
在计算信号和槽的偏移量时,便是利用保存在staticMetaObject中的qt_meta_data_ClassName和qt_meta_stringdata_ClassName数组的数据。
下面举个简单的例子,如下代码,在moc编译后会生成的文件是什么样的:
#ifndef __MAIN_H
#define __MAIN_H

#include <QObject>
class MyClass : public QObject
{
Q_OBJECT

public:
MyClass(QObject *parent = 0);
~MyClass();

signals:
void Signal_MyClass();

public slots:
void Slot_MyClass();
};

#endif

moc生成的文件:
/****************************************************************************
** Meta object code from reading C++ file 'main.h'
**
** Created: ??? ?? 16 09:52:39 2012
** by: The Qt Meta Object Compiler version 63 (Qt 4.6.3)
**
** WARNING! All changes made in this file will be lost!
*****************************************************************************/

#include "main.h"
#if !defined(Q_MOC_OUTPUT_REVISION)
#error "The header file 'main.h' doesn't include <QObject>."
#elif Q_MOC_OUTPUT_REVISION != 63
#error "This file was generated using the moc from 4.6.3. It"
#error "cannot be used with the include files from this version of Qt."
#error "(The moc has changed too much.)"
#endif

QT_BEGIN_MOC_NAMESPACE
static const uint qt_meta_data_MyClass[] = {

// content:
6, // revision
0, // classname
0, 0, // classinfo
2, 14, // methods
0, 0, // properties
0, 0, // enums/sets
0, 0, // constructors
0, // flags
1, // signalCount
//以上与QMetaObjectPrivate结构体中的数据成员的第一对应
// signals: signature, parameters, type, tag, flags
9, 8, 8, 8, 0x05,

// slots: signature, parameters, type, tag, flags
26, 8, 8, 8, 0x0a,

0 // eod
};

static const char qt_meta_stringdata_MyClass[] = {
"MyClass\0\0Signal_MyClass()\0Slot_MyClass()\0"
};//用字符串记录元对象信息

void MyClass::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
Q_ASSERT(staticMetaObject.cast(_o));
MyClass *_t = static_cast<MyClass *>(_o);
switch (_id) {
case 0: _t->Signal_MyClass(); break;
case 1: _t->Slot_MyClass(); break;
default: ;
}
}
Q_UNUSED(_a);
}

const QMetaObjectExtraData MyClass::staticMetaObjectExtraData = {
0, qt_static_metacall
};

const QMetaObject MyClass::staticMetaObject = {
{ &QObject::staticMetaObject, qt_meta_stringdata_MyClass,
qt_meta_data_MyClass, &staticMetaObjectExtraData }
};// staticMetaObject的初始化

#ifdef Q_NO_DATA_RELOCATION
const QMetaObject &MyClass::getStaticMetaObject() { return staticMetaObject; }
#endif //Q_NO_DATA_RELOCATION

const QMetaObject *MyClass::metaObject() const
{
return QObject::d_ptr->metaObject ? QObject::d_ptr->metaObject : &staticMetaObject;
}

void *MyClass::qt_metacast(const char *_clname)
{
if (!_clname) return 0;
if (!strcmp(_clname, qt_meta_stringdata_MyClass))
return static_cast<void*>(const_cast< MyClass*>(this));
return QObject::qt_metacast(_clname);
}

int MyClass::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
_id = QObject::qt_metacall(_c, _id, _a);
if (_id < 0)
return _id;
if (_c == QMetaObject::InvokeMetaMethod) {
if (_id < 2)
qt_static_metacall(this, _c, _id, _a);
_id -= 2;
}
return _id;
}

// SIGNAL 0
void MyClass::Signal_MyClass()
{
QMetaObject::activate(this, &staticMetaObject, 0, 0);
}//自定义信号的实现,没有参数所以只有一句
QT_END_MOC_NAMESPACE
Qt 的元对象系统不依赖于窗口的消息,只依赖与QMetaObject类,只要将信号与槽正确的连接。上层的应用类将得到的消息,事件等转换为信号,信号再通过元对象调用槽函数响应。信号和槽可以很好的将两个对象联系起来,就像两人见面打招呼一样,一人发出信号,另一人做出相应。
如果想要类的对象可以主动发出信号,在类的声明前加上如下几句,就可以像普通函数一样使用信号了:
#if defined signals
#undef signals
#define signals public
#endif

下面是Q_OBJECT宏的定义:
# define QT_TR_FUNCTIONS \ 用来支持字符转换
static inline QString tr(const char *s, const char *c = 0) \
{ return staticMetaObject.tr(s, c); } \
static inline QString tr(const char *s,const char *c,int n) \
{ return staticMetaObject.tr(s, c, n); }

#define Q_OBJECT \
public: \
static const QMetaObject staticMetaObject; \
static const QMetaObject &getStaticMetaObject(); \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
QT_TR_FUNCTIONS \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
private:



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值