Qt信号和槽知识点总结
更多关于Qt对象模型及其信号与槽的文章参考链接如下:Qt对象模型之一:信号和槽
信号和槽知识点总结
信号和槽函数介绍
作为Qt框架中备受推崇的一种机制之一的是所谓的信号槽这一概念。其中所谓的"信号"本质上即为此类事件的本质属性。当某一事件发生时(例如按钮检测到自己被点击了一下),系统会触发相应的响应机制——从而会发送出一个名为'signal'的消息给所有关注该消息的对象。这种消息发送并没有特定的目的性——类似于计算机中的广播功能。如果存在关注该消息的对象,则这些接收端将通过预先定义好的回调函数来处理此类信息——也就是说,在此消息发送时所有与之相关联的功能都会被调用并执行相应的操作步骤
槽作为类的基本功能单元,在设计上具有高度灵活性和扩展性。它允许接受任何类型的参数,并且在实现方式上与普通C++成员函数几乎无异——既可被视为虚函数也可进行重载操作。同样可设为公有、protected或private属性,并且同样可在其他C++成员函数中被调用。其主要区别在于——当与其相连的信号被触发时,就会自动执行该槽操作。之所以将其定义为槽功能,是因为它们的主要职责就是处理Qt框架所触发的各种信号。而这类功能的所有者也同样是某个对象实例的具体化体现。
在早期阶段,通过 callbacks 机制来进行对象间的通信。其中 callbacks 机制是通过将函数指针传递给处理函数来实现通知传递的方式。当处理函数期望接收到特定事件的通知时,则需将被处理事件对应的 callbacks 函数指针传递给该处理函数。回显的主要缺陷在于:这样的处理方式会导致响应速度变慢,并且增加了系统的开销。
- 它们不是类型安全的。
- 我们缺乏对处理功能的安全保障。
- 回显与处理器之间存在高度相互依存的关系。
- 因为处理器必须识别哪个组件被触发。
自定义信号即需声明而不必实现其功能仅用于数据传输返回值类型为void此类事件机制允许多个实例同时运行支持重载功能且应在类体内预先定义该关键字以便后续触发当开发者希望向系统注入特定行为时可在此基础上进行扩展启动自定义事件机制其本质即为执行对应事件处理逻辑通常被视为一种约定俗成的做法但并非强制要求通过宏定义的方式即可完成这一操作底层实现较为简单只需将_emit_视为一种标识符即可完成相关操作其作用即是将数据传递给相关槽函数每当某实体发起请求时该槽函数会自动接收相应的参数并执行预设的行为流程
自定义槽函数模块:需同时进行声明与实现操作,并规定返回类型为void。该模块支持带有参数的定义且允许重载功能的设置。在早期版本中需指定为Public Slots关键字,在较高级版本则可灵活设置为Public关键字或全局槽函数形式。
请注意:signal的代码会由moc被自动生成,在本项目的C++代码中无法自行进行处理。相比之下,slot则应当由开发人员自行负责。
请注意:signal的代码会由moc被自动生成,在本项目的C++代码中无法自行进行处理。相比之下,slot则应当由开发人员自行负责。
当自定义信号或槽函数发生重命名时,在C++编程中需明确指定指向目标函数地址的函数指针(格式为:返回值数据类型(作用域限定符*变量名)(参数数据类型)),特别提醒需注意作用域限定符不可遗漏以避免潜在错误
信号槽函数无参和有参举例如下:
//连接无参数的信号和槽
void(Teacher::* teacherSignal1)()=&Teacher::hungry;
void(Student::* studentSlot1)()=&Student::treat;//函数指针要明确归属类
connect(teacher,teacherSignal1,student,studentSlot1);//连接老师饿了的信号和学生请吃饭的槽函数
classIsOver1();//利用下课了触发老师饿了的信号,函数内部有emit触发
//连接带参数的信号和槽(信号和槽函数的类型索引必须一一对应)
void(Teacher::* teacherSignal2)(QString)=&Teacher::hungry;
void(Student::* studentSlot2)(QString)=&Student::treat;
connect(teacher,teacherSignal2,student,studentSlot2);
classIsOver2();
为在Qt中实现自定义信号槽功能,则需遵循特定条件与注意事项如下所示:
- 首先需创建一个新的类并使其继承Qt的标准父类之一。
- 该新子类必须继承自QObject或其子类以便支持事件重发功能。
- 在包含该分类头文件时请声明Q_OBJECT宏以便引用相关静态成员函数。
信号和槽的关联—connect函数
在Qt4环境中,信号与槽之间的关联操作通常由QObject类提供的connect()函数来实现
static QMetaObject::Connection QObject::connect(const QObject *sender,
const char *signal,
const QObject *receiver,
const char *member,
Qt::ConnectionType = Qt::AutoConnection);
第一个参数sender标识发送信号的对象;第二个parameter signal表示待发送的信息;第三个parameter receiver标识接收到该信息的对象;第四个parameter slot用于指定接收到该信息后将被调用的槽函数。connect( ) 函数的最后一项指定关联方式,默认采用 Qt::AutoConnection 机制。必须使用预定义的 SIGNAL( ) 与 SLOT( ) 宏来处理相关事件;这些宏可将所有输入转换为 const char* 类型。connect( ) 函数返回一个布尔型变量:当关联成功时返回 true 值;否则返回 false 值(默认情况下会失败)。
在信号与槽的参数配置方面,在这种情况下基本要求是确保信号中的每个类型必须与槽内的类型一致,并且允许在某些特殊情况下(即当一个信号携带了比预期更多的信息)出现多出的部分。需要注意的是,在这种情况下(即当一个槽缺少必要的输入信息),相反的情况则不会发生)。如果一个信号携带了比预期更多的输入信息,则这些多余的输入将被系统完全忽略。
而Qt5中connect()函数新加入的一种重载形式如下:
static QMetaObject::Connection QObject::connect(const QObject *sender,
PointerToMemberFunction signal,
const QObject *receiver,
PointerToMemberFunction member,
Qt::ConnectionType type = Qt::AutoConnection);
与Qt4相比最大的区别在于,在指定一个对象作为信号接收者时无需再强制使用SIGNAL()和 SLOT()宏来绑定两个相关联的对象;此外,在绑定槽时也不再强制要求槽函数必须声明为slots关键字的形式了
connect(this,&Widget::testSignals,this,&Widget::testSolts);
相较于前一种方式而言,在编译阶段就可以检验这些问题:信号与槽的拼写错误、参函数参数数目多于信号参数数目等情况,在编译阶段即可被检测出来;因此,在编写Qt5相关代码时建议采用这种关联方式
信号与槽还有一种自动关联的方式 ,例如,在设计模式直接生成按钮时,默认会绑定其单击事件所对应的槽 。这种命名方式使得绑定到相应信号变得更为直观 。其中名称通常由三部分构成:前后缀名(如 on )与对象名(如 objectName ),以及信号名(如 signalName )之间用下划线连接 。这样形式命名的槽可以直接被相应的事件捕获触发 ,无需再调用 connect 函数 ,不过这种命名方式仍然需要配合其他相关配置完成完整绑定操作 。
断开信号和槽的关联—disconnect函数
可以通过disconnect()函数来断开信号和槽的关联,其原型如下:
static bool QObject::disconnect(const QObject *sender, const char *signal,
const QObject *receiver, const char *member);
1)断开与一个发送对象所有信号的所有关联
disconnect(myObject, nullptr, nullptr, nullptr);
等价于
myObject->disconnect();
2)断开与一个指定信号的所有关联
disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);
等价于
myObject->disconnect(SIGNAL(mySignal()));
3)断开与一个指定接受对象的所有关联
disconnect(myObject, nullptr, myReceiver, nullptr);
等价于
myObject->disconnect(myReceiver);
4)断开一个指定信号和槽的关联
disconnect(myObject, SIGNAL(mySignal()), myReceiver, SLOT(mySlot()));
等价于
myObject->disconnect(SIGNAL(mySignal()), myReceiver, SLOT(mySlot()));
也等价于
disconnect(myConnection); //myConnection是进行关联时connect()的返回值
其用法类似,在使用上与其相似的情况下(即),其信号与槽参数必须采用函数指针 &MyObject::mySignal() 以及 &MyReceiver::mySlot() 等特定的形式。该函数无法切断该信号与常规函数或lambda表达式之间的联系。若需实现此功能,则可借助connect()方法返回值来实现切断。值得注意的是,在对象被delete后(即),所有相关联的连接都将失效;QT会自动去除与此对象相关的所有连接。
信号和槽实现原理
信号与槽的实现原理主要依据Qt的核心技术体系——其主要由 Qt 的 元对象系统 来支撑。具体而言,在构建应用程序的过程中, Qt 的 **元对象编译器 MOC(Meta-Object Customization)负责解析类文件并提取相关的信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构信息数据结构)。随后,在这个过程中, MOC 会动态地创建与应用逻辑相关的 **抽象语法树 AST(Abstract Syntax Tree)以及相应的中间表示 IMB(Internal Model Binding)。这些中间表示会被进一步优化并最终转化为可执行代码, 其核心作用相当于一个预处理工具。
元对象系统由以下三点构成:
- QObject 类作为所有元对象系统的基类,在具备信号与槽功能时必须继承自该类。
- 在某个类的私有部分声明 Q_OBJECT 宏后即可使用其特性(如信号与槽)。
- MOC 通过提供必要的代码段来实现元对象系统的属性功能。
信号和槽的底层设计模式是基于观察者设计模式。
信号的参数类型
当同一线程内同时存在信号与槽时,在值传输方式下创建的对象会被复制(例如,在测试过程中比较被传输前后内存地址会有明显差异),而引用传输方式则不会进行对象复制(如在测试期间比较被传输前后内存地址会发现两者一致)。
当信号与槽不在同一线程运行时请考虑以下两种情况进行分析。首先,在通过AutoConnection进行连接(跨线程默认采用QueuedConnection模式)的情况下,请注意值传输方式与引用传输方式并无区别,并且都会导致目标对象被复制(通过测试发现,在传输前后目标对象的内存地址均会发生变化)。其次,在通过DirectConnection建立连接的情况下,请观察其效果是否与在同一线程环境中创建连接所获得的效果一致
当执行第二次发射信号操作时,请务必谨慎考虑引用实体的情况。因为此时已不再存在相关的实体对象。因此,在设计每次执行发射信号操作时的操作参数时,请避免将引用或指针作为操作参数。相反地,请采用值传递的方式进行数据传输。这样每次执行发射信号操作时的操作参数都能被正确地记录下来。
信号的参数对自定义类型的传递
两个方案中第一个方案是通过QVariant实现封装的方式 第二个方案则是通过自定义数据类型的注册方法
采用QVariant进行封装的方法
1)声明自定义数据类型 用于我们想要传递的一个结构体类型。
typedef struct _TEST
{
int a;
int b;
bool c;
}TEST;
Q_DECLARE_METATYPE(TEST);
请注意,在此处需要用Q_DECLARE_METATYPE()宏来声明相应的新类型。
2)为相应的signal与slot做出定义。
signals:
void sgnParams(QVariant var);
//other classes
slots:
void slotParams(QVariant var);
这里的变量是QVariant类型的,在编写代码时我们需要将新定义的数据格式打包成QVariant类型的形式以便后续处理。为了实现数据的有效传递,在相应的函数中我们需要将变量指定为QVariant类型的参数,并确保在调用函数时能够正确解析这些数据形式。
TEST test;
QVariant var;
test.a = 300;
test.b = 200;
test.c = false;
var.setValue(test);
Q_EMIT sgnParams(var);
在对应的funnel函数中,在遵循QVariant数据类型的规范下实施读取操作的具体实现细节需进一步说明。具体实施时可遵循以下步骤:
TEST test;
if (var.canConvert<TEST>()) {
test = var.value<TEST>();
//process
}
注册自定义数据类型的方法
1)创建自定义数据类型 在此处指定要传递的自定义数据类型,请具体说明是哪种类型的变量。
typedef struct _TEST
{
int a;
int b;
bool c;
}TEST;
2)构造函数中注册新类型
#include <QMetaType>
qRegisterMetaType<TEST>("TEST");
请注意:如果涉及引用传递,则需单独注册相关类型信息 ,例如通过调用函数qRegisterMetaType并将其参数设置为字符串形式' TEST& '进行操作。具体定义相应的signal和slot变量以完成相关的类型注册流程。
signals:
void sgnParams(TEST test);
//other classes
slots:
void slotParams(TEST test);
在本场景中, 形参直接采用了自定义数据类型; 这是因为我们已经成功地将该数据类型进行了注册.
TEST test;
test.a = 300;
test.b = 200;
test.c = false;
Q_EMIT sgnParams(test);
发送直接赋值即可。
5)接收
接收直接获取形参值,和标准类型操作一样的。
qDebug() << test.a << test.b << test.c;
信号和槽的五种连接类型
1)Qt::AutoConnection:默认值为该值时,在信号发送时会自动决定连接方式 。若接收者与发送者位于同一线程内,则采用Qt::DirectConnection类型;若接收者与发送者位于不同线程内,则采用Qt::QueuedConnection类型 。
2)Qt::DirectConnection:槽函数会在信号发送时立即被调用 ,且运行于发送信号者的线程中 。这看起来像是直接在信号发送位置执行槽函数 ,但在多线程环境中可能带来安全隐患 ,可能导致程序崩溃 。
3)Qt::QueuedConnection:槽函数会在接收者返回事件循环后被调用 ,运行于接收者所在的线程中 。信号发送后 ,槽函数不会立即被触发 ,直到接收者的当前操作完成后进入事件循环才会被调用 。通常在多线程环境下使用此连接类型更为合适 。
4)Qt::BlockingQueuedConnection:其槽函数调用时机与Qt::QueuedConnection一致 ,但 sender在发送信号后会阻塞其所在的线程 ,直至槽函数执行完毕 。。如果 sender和receiver处于同一线程内会导致程序出现死锁现象 。。因此,在需要多线程环境下的同步操作中可能需要用到此连接类型 。。
5)Qt::UniqueConnection:此标志可通过按位或运算与其他四个标志组合使用 。。当此标志设置时 ,如果已有信号与槽进行了连接 ,再次尝试建立相同连接会导致失败 。。从而避免重复连接的发生 。。
信号和槽连接函数connect注意事项
- 该函数针对信号处理动作进行了注册。
- 当使用
connect函数时,在sender对象发出信号之前调用其方法将不会发生作用。 - 槽函数本质上属于回调机制,在特定条件下会被触发。
- 在
connect操作中引用的sender和receiver两个指针必须得到实例化才能完成任务。 connect()操作通常放置在窗口构造函数中,在程序运行初期完成预先配置步骤;一旦窗口创建完成并开始处理事件时,在Qt框架下会自动绑定相关接收者及其对应的槽功能够触发;如果在此过程中未发生相应的信号产生,则槽功能够触发;如果没有信号产生,则槽功能够触发;如果没有相应的信号产生,则槽功能够触发。
