信号槽
前提:如果没有消息循环,那么Qt的信号和槽无法完全使用。最开始的Qt消息循环开始于QCoreApplication::exec。在绝大部分GUI程序中,GetMessage, DispatchMessage是写在一个死循环中的,除非程序退出,否则会一直处理各种事件。
所谓信号槽(观察者模式),信号本质是事件。
信号:
signals:
void startGetDataThread();
void sendPointer(MainWindow*);
1.声明一个信号要使用signals关键字。
2.在signals前面不能使用public、private和protected等限定符,因为只有定义该信号的类及其子类才可以发射该信号。(使用emit函数发射信号)
3.信号只用声明,不需要也不能对它进行定义实现。
4.信号没有返回值,只能是void类型的。
5.只有QObject类及其子类派生的类才能使用信号和槽机制,使用信号和槽,还必须在类声明的最开始处添加Q_OBJECT宏。
6.使用emit 强行发射信号
emit sendPointer(this);
Qt之所以使用# define emit,是因为编译器并不认识emit,所以把它定义成一个空的宏就可以通过编译。
7. 信号是一个函数, 类的成员函数。所以可以是虚函数的重写。
即在基类定义一个纯虚函数,在子类的重写该虚函数,并且声明为信号。
8、信号可以支持重载。
9.在同一个线程中,当一个信号被emit发出时,会立即执行其槽函数,等槽函数执行完毕后,才会执行emit后面的代码。如果一个信号链接了多个槽,那么会等所有的槽函数执行完毕后才执行后面的代码,槽函数的执行顺序是按照它们链接时的顺序执行的。如果信号和槽不是在同一线程,默认情况下,是异步执行,不会阻塞。
10.获取信号发送者
当多个信号连接一个槽时,有时需要判断是哪个对象发来的,那么可以调用sender()函数获取对象指针,返回为QObject指针。
QObject* sender() ;
槽函数
public slots:
void GetDataFromRTDB();
void getPointer(MainWindow *pMainWindow);
1.声明一个槽需要使用slots关键字。
2.一个槽可以是private、public或者protected类型的,
3.槽也可以被声明为虚函数,静态函数、全局函数,这与普通的成员函数是一样的,也可以像调用一个普通函数一样来调用槽。本质就是回调函数。
4.发送者和接受者都需要是QObject的子类(当然,槽函数是全局函数,Lambda表达式等无需接收者的时候除外。
connect():
connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
sender:发出信号的对象
signal:sender对象的信号
receiver:信号接收者
method:receiver对象的槽函数,当检测到 sender 信号,receiver 对象调用 method 方法
ConnectionType: 连接类型,一般不填,为默认值。
1、Qt::AutoConnection: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
2、Qt::DirectConnection:槽函数会在信号发送的时候直接被调用,槽函数和信号发送者在同一线程。效果看上去就像是直接在信号发送位置调用了槽函数,效果上看起来像函数调用,同步执行。
特点:emit语句后面的代码将在与信号关联的所有槽函数执行完毕后才被执行。
3、Qt::QueuedConnection:信号发出后,信号会暂时被放到一个消息队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,然后执行和信号关联的槽函数,这种方式既可以在同一线程内传递消息也可以跨线程操作。
特点:emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕
4、Qt::BlockingQueuedConnection:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。而且接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
5、Qt::UniqueConnection:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是为了避免重复连接。
两种表达方式:
1.connect(this,SIGNAL(sendPointer(MainWindow*)),m_pGetDataThreadObj,SLOT(getPointer(MainWindow*)));
2.connect(this,&MainWindow::startGetDataThread,m_pGetDataThreadObj,&DataThreadObject::GetDataFromRTDB);
信号槽的特点:
1.一个信号可以和多个槽相连。
特点:槽函数的执行顺序和信号槽连接的顺序一致。
2.多个信号可以连接到一个槽。
槽函数中判断信号的发出者的办法:首先利用 QObject::setObjectName(const QString&) 方法设置信号发出者的对象名称,
然后在槽函数中利用 QObject::sender()->objectName() 方法获取信号发出者的对象名称。
3.一个信号可以连接到另外的一个信号。
4.信号的参数类型可以与槽的参数类型对应,信号的参数可以比槽的参数多,但不可以少, 否则连接将失败。
5.槽可以被取消连接;这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。可使用disconnect()进行解绑定,其写法和connect一样。
6.信号和槽也不能携带模板类参数。
如果将信号、槽声明为模板类参数的话,即使moc工具不报告错误,也不可能得到预期的结果,也可以取巧,用typedef
typedef pair IntPair;
public slots:
void setLocation (IntPair location);
7.嵌套的类不能位于信号或槽区域内,也不能有信号或槽。即类b嵌套在类a内,想在类b中声明信号与槽是不行的。
8.友元声明不能位于信号或槽声明区内。相反,他们应该在普通C++的private、protected或public区内进行声明
信号与槽的具体流程
可通过moc编译后,得到moc_xxx.cpp文件查看。如下
1.moc查找头文件中的signals,slots,标记出信号和槽。
2.将信号槽信息存储到类静态变量元对象staticMetaObject中,并且按声明顺序进行存放,建立索引。
注解:QObject类有一个静态成员static const QMetaObject staticQtMetaObject;
3.当发现有connect连接时,将信号槽的索引信息放到一个map中,彼此配对。
注解:QObject有一个容器connections,此容器是一个map,key是信号index,value是一个Connection,维护者信号槽的对应关系。
4.当调用emit时,调用信号函数,并且传递发送信号的对象指针,元对象指针,信号索引,参数列表到QMetaObject(元对象)的active函数。
5.通过active函数找到在map中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数。
以上,便是信号槽的整个流程,总的来说就是一个“注册-索引”机制。
static void activate(QObject *sender, const QMetaObject *, int local_signal_index, void **argv);
如ColorMaker 有2个信号
signals:
void valueChanged(int value);
void testChanged(CTest test);
在Widget类进行关联
ColorMaker *cm = new ColorMaker();
QObject *object = cm;
connect(cm,&ColorMaker::valueChanged,this,&Widget::recv);
connect(cm,&ColorMaker::testChanged,this,&Widget::recvtest);
经过moc编译后,得到moc_ColorMaker.cpp
// SIGNAL 0
void ColorMaker::valueChanged(int _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 0, _a);
}
// SIGNAL 1
void ColorMaker::testChanged(CTest _t1)
{
void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(std::addressof(_t1))) };
QMetaObject::activate(this, &staticMetaObject, 1, _a);
}
经过moc编译后,得到moc_Widget.cpp
void Widget::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
if (_c == QMetaObject::InvokeMetaMethod) {
auto *_t = static_cast<Widget *>(_o);
Q_UNUSED(_t)
switch (_id) {
case 0: _t->recv((*reinterpret_cast< int(*)>(_a[1]))); break;
case 1: _t->recvtest((*reinterpret_cast< CTest(*)>(_a[1]))); break;
case 2: _t->test(); break;
default: ;
}
} else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
switch (_id) {
default: *reinterpret_cast<int*>(_a[0]) = -1; break;
case 1:
switch (*reinterpret_cast<int*>(_a[1])) {
default: *reinterpret_cast<int*>(_a[0]) = -1; break;
case 0:
*reinterpret_cast<int*>(_a[0]) = qRegisterMetaType< CTest >(); break;
}
break;
}
}
}
信号槽机制的优缺点
优点:
Qt信号与槽机制降低了Qt对象的耦合度。
观察者模式,激发信号的Qt对象无须知道是哪个对象的哪个槽函数需要接收它发出的信号,它只需要做的是在适当的时间发送适当的信号就可以了,而不需要知道也不关心它的信号有没有被接收到,更不需要知道哪个对象的哪个槽接收了信号。
缺点:
信号槽机制,同回调函数相比,信号和槽机制运行速度有些慢。遍历,通过传递一个信号来调用槽函数将会比直接调用非虚函数运行速度慢10倍。原因如下:- 需要定位接收信号的对象;- 安全地遍历所有的关联(一个信号关联多个槽的情况);- 编组/解组传递的参数;- 多线程的时候,信号可能需要排队等待。
在没有信号槽机制的时代,C++对象间的交互一般使用回调函数来实现。使用某对象时,用指针指向另一个对象的函数,这个函数就称为回调函数。使用回调函数有个弊端,当某个对象被多个对象通信时,需要一个容器来存放多个对象的回调函数。维护这个容器使得代码编写效率低、扩展性弱。
qt信号槽的本质就是回调函数。
文章评论