使用一个按钮按下时,我们可能想要窗口的 close()
函数被调用。这个操作可以通过设置回调函数实现。但回调函数不够直观,而且容易出现参数类型错误等问题。Qt中使用的代替方案是信号和槽机制。
信号和槽
当特定的事件出现时,一个信号被发出。槽函数作为信号的响应被调用。Qt的控件包含许多预定义的信号和槽函数。
信号槽机制是类型安全的:信号和槽函数的签名必须是匹配的。实际上,槽函数的参数可以比信号少,槽函数会忽略多出来的参数。发出信号的类不知道也不关心哪些槽函数会收到信号。信号槽机制保证信号发出来时,连接到该信号的槽函数会在正确的时间被调用。信号和槽可以带有任意数量和任意类型的参数。
所有继承自QObject
或其子类的类都可以包含信号和槽。当对象的(可能被其他对象感兴趣的)状态改变时,信号可以被对象发出。
槽可以来接受信号,但同时也是普通的成员函数。正如信号不知道会被哪些槽函数接受一样,槽函数也不知道有有哪些信号连接到这个槽。
信号也可以连接到另一个信号,当前者发出后,后者会立即发出。
信号
信号实际上是一个公共访问的函数,可以在任何地方发出,但建议只从定义这个信号的类或其子类中发出。
当信号发出时,连接到信号的槽函数通常会立即被调用,就像普通函数调用一样。emit
声明之后的代码会在所有的槽函数都返回之后继续执行。不同的情况是使用队列连接(queued connection)的信号槽,这种情况下,emit
声明之后的代码会继续执行,而槽函数则会在稍后执行。
多个槽函数连接到同一个信号时,调用顺序为他们连接到这个信号的顺序。
信号由元对象系统自动生成,禁止在.cpp
文件中实现,且返回值必须是void
。
经验告诉我们:信号和槽在不使用特殊类型时有更好的可复用性。即使用基本数据类型更好。
槽
槽是可以被正常调用的C++函数,唯一的特殊功能是可以被信号连接。当直接调用槽函数时,遵循正常的C++规则。但通过信号槽的连接,槽函数可以被任意对象调用,无视槽的访问等级。也就是说来自无关的类发出的信号也可以导致被声明为私有的(private)槽被调用。
槽可以被声明为virtual
的,这在实践中被发现很有用。
与回调函数相比,信号槽由于提高了灵活性而略慢,尽管在实际应用程序中的差异微不足道。通常,发射一个连接到槽函数的信号比直接调用接受者的非虚函数要慢10倍。因为需要定位连接的对象,检查接收者是否已经被删除,处理参数等。10倍听起来有点多,但这比任何new
或delete
操作要少得多。
带默认参数的信号和槽
信号和槽可以包含参数,参数可以有默认值。如QObject::destroyed
:
void destroyed(QObject* = nullptr);
当QObject对象被delete时,这个对象发出QObject::destroyed()
信号。我们希望在对这个对象有引用的地方捕捉这个信号,然后清理对它的引用。适当的槽函数签名为:
void objectDestroyed(QObject* obj = nullptr);
我们使用QObject::connect
()连接信号和槽。多种方法可以连接信号和槽。
第一种是使用函数指针:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
使用函数指针的好处是,允许编辑器检查信号和槽的签名是否兼容;在需要的时候,参数可以被编译器隐式地转换。
也可以连接信号到lambda:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });
上面两个例子都提供this
作为调用connect
的上下文。上下文对象提供在哪个线程执行接收者代码的信息。即接收者的代码在上下文线程中执行。
当发送者或接收者被销毁时,lambda也被断开连接。使用者需要保证在lambda函数体内使用的对象在信号触发时仍然是存活的。
第二种是使用SIGNAL
和SLOT
宏定义。
如果参数具有默认值,则传给 SIGNAL 宏的签名的参数不能比 SLOT 的少。
正确:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
错误:
// 编译可以通过,但运行时错误
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
总结
- 信号和槽通过 QObject::connect() 函数连接。
- 信号和槽相互都不关心对方。
- 槽触发的顺序为连接到信号的顺序。
- 信号可以连接到信号。
- 信号和槽可以带参数,且信号的参数不能比槽的参数少。
- 槽函数是普通的 c++ 函数,除了承担接受信号的功能外与普通函数一样。
- 槽函数可以是 lambda。
- 信号槽机制比普通的函数调用慢。
- 槽函数的执行线程在接收者所在线程。
参考:
Signals & Slots