Qt信号槽原理

news2024/10/6 4:11:28

Qt之信号槽原理

一.概述

所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt 的信号槽使用了额外的处理来实现 ,并不是 GoF 经典的观察者模式的实现方式。)

信号和槽是Qt特有的信息传输机制,是Qt设计程序的重要基础,它可以让互不干扰的对象建立一种联系 Qt信号槽有如下优点:

1.类型安全。需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,编译器会报错。

2.松散耦合。信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是那个对象的那个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和那个对象接受了。Qt就保证了适当的槽得到了调用,即使关联的对象在运行时被删除。程序也不会奔溃。

3.灵活性。一个信号可以关联多个槽,或多个信号关联同一个槽

二.信号槽的实现

众所周知,C++语言的编译过程为预处理->编译->汇编->链接。

a.驱动程序首先运行C预处理器(cpp),将源程序翻译成一个ASCII码的中间文件main.i,
​
b.驱动程序运行C编译器,将main.i翻译成一个ASCII汇编文件main.s
​
c.驱动程序运行汇编器(as),将main.s翻译成一个可重定位目标文件main.o
​
d.运行链接器,将main.o及其一些必要的系统目标文件组合在一起,创建一个可执行的目标文件

但是在qt中,首先会有一个Moc预处理器对源代码进行处理,经过Moc预处理器处理后的代码才是标准的C++代码,此后就可以执行正常的C++编译流程了。正是因为Moc预处理器,Qt才实现了信号槽的功能,下面我们通过用纯C++代码实现一个简洁的信号槽功能来对信号槽深入了解。

使用C++语言模拟信号槽实现:

首先看一下类图:

 

QObject有一个静态元对象QMetaObject,静态元对象存放着信号槽名称的字符信息以及一个根据信号发送者和信号index调用对应槽的函数;

QObject有一个容器connections,此容器是一个map,key是信号index,value是一个Connection,维护者信号槽的对应关系;

QObject有一个静态方法connect,此方法将信号的index作为key,创建一个value插入到对象维护的连接列表容器中;

  • 程序运行时,connect借助两个字符串,即可将信号与槽的关联建立起来,那么,它是如果做到的呢?C++的经验可以告诉我们:

    • 类中应该保存有信号和槽的字符串信息

    • 字符串和信号槽函数要关联

引入元对象系统

定义信号和槽,为了和普通成员进行区分以使得预处理器可以提取信息,定义几个关键字。

#define slots  
#define signals protected  
#define emit  
  • 通过预处理器,将信息提取取来,放置到一个单独的文件中(比如moc_QObject.cpp):

  • 规则很简单,将信号和槽的名字提取出来,放到字符串中。可以有多个信号或槽,按顺序"sig1/nsig2/n"

    static const char sig_names[] = "sig1/nsing2/n";
    static const char slts_names[] = "slot1/nslot2/n";

利用这些信号槽的信息,建立连接;

定义一个结构体,存放信息

struct QMetaObject
{
    const char * sig_names;
    const char * slts_names;
};

然后将它作为QObject的一个静态对象,这个就是Qt中的元对象。

class QObject
{
    static QMetaObject staticMetaObject;
    ...

利用预处理器生成的moc_Object.cpp:

#include "object.h"
​
static const char sig_names[] = "sig1/n";
static const char slts_names[] = "slot1/n";
QMetaObject QObject::staticMetaObject = {sig_names, slts_names};

建立信号槽连接

利用moc预处理器保存的信息,通过 connect 将信号和槽的对应关系保存到一个 mutlimap中。

struct Connection
{
    Object * receiver;
    int method;
};
​
class QObject
{
public:
...
    static void connect(QObject*, const char*, QObject*, const char*);
...
private:
    std::multimap<int, Connection> connections;

connect函数:

void QObject::connect(QObject* sender, const char* sig, QObject* receiver, const char* slt)
{
    int sig_idx = find_string(sender->meta.sig_names, sig);
    int slt_idx = find_string(receiver->meta.slts_names, slt);
    if (sig_idx == -1 || slt_idx == -1) {
        perror("signal or slot not found!");
    } else {
        Connection c = {receiver, slt_idx};
        sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
    }
}

首先从元对象信息中查找信号和槽的名字是否存在,如果存在,则将信号的索引和接收者的信息存入信号发送者的一个map中。

信号的激活:

在Qt中我们都是使用emit来激活一个信号,这是emit的本来面目。

#define emit

在Qt中我们必须要在类里增加一个Q_OBJECT的宏,发送信号使用emit,定义槽和信号,使用signals,public slots等。其实这些都是一些宏替换,在qobjects.h文件里可以看到这些宏的本来面目。

我们这里使用emit来激活信号。我们在定义信号的时候只写了信号的声明,信号的实现并未给出。而槽函数的实现是开发者给出的。其实信号的实现是Moc编译器帮助我们实现的。

void QObject::sig1()
{
    QMetaObject::active(this, 0);
}

信号的调用工作由QMetaObject类来完成

class QObject;
struct QMetaObject
{
    const char * sig_names;
    const char * slts_names;

    static void active(QObject * sender, int idx);

};

这个函数该怎么写呢:思路很简单

从前面的保存连接的map中,找出与该信号关联的对象和槽 调用该对象这个槽

typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;

void QMetaObject::active(QObject* sender, int idx)
{
    ConnectionMapIt it;
    std::pair<ConnectionMapIt, ConnectionMapIt> ret;
    ret = sender->connections.equal_range(idx);
    for (it=ret.first; it!=ret.second; ++it) {
        Connection c = (*it).second;
        //c.receiver->metacall(c.method);
    }
}

槽的调用:

这个最后一个关键问题了,槽函数如何根据一个索引值进行调用。

  • 直接调用槽函数我们都知道了,就一个普通函数

  • 可现在通过索引调用了,那么我们必须定义一个接口函数

class QObject
{
    void metacall(int idx);
...

该函数如何实现呢?这个又回到我们的元对象预处理过程中了,因为在预处理的过程,我们能将槽的索引和槽的调用关联起来。

所以,在预处理生成的文件(moc_QObject.cpp)中,我们很容易生成其定义:

void QObject::qt_static_metacall(int idx)
{
	switch (idx)
	{
	case 0:
	{
		slot1();
		break;
	}
	case 1:
	{
		slot2();
		break;
	}
	};
}

总结:moc通过元对象系统保存了信号和槽的字符信息,然后为每一个信号和槽分配编号。当我们调用connect函数时,把信号槽的对应关系保存在对象的一个map容器中。当发送信号时(也就是调用信号函数时)通过刚才保存在map容器中的信号槽对应关系找到对应的接收对象和槽函数。

完整代码:

#include <string.h>  
#include "QObject.h"  
#include <iostream>

static int find_string(const char * str, const char * substr)
{
	if (strlen(str) < strlen(substr))
		return -1;
	int idx = 0;
	int len = strlen(substr);
	bool start = true;
	const char * pos = str;
	char cEnd = '\n';
	while (*pos) {
		if (start && !strncmp(pos, substr, len) && pos[len] == '\n')
			return idx;
		start = false;
		if (*pos == cEnd) {
			idx++;
			start = true;
		}
		pos++;
	}
	return -1;
}

void QObject::connect(QObject* sender, const char* sig, QObject* receiver, const char* slt)
{
	int sig_idx = find_string(sender->staticMetaObject.sig_names, sig);
	int slt_idx = find_string(receiver->staticMetaObject.slts_names, slt);
	if (sig_idx == -1 || slt_idx == -1) {
		perror("signal or slot not found!");
	}
	else {
		Connection c = { receiver, slt_idx };
		sender->connections.insert(std::pair<int, Connection>(sig_idx, c));
	}
}

void QObject::slot1()
{
	std::cout << "into slot1" << std::endl;
}

void QObject::slot2()
{
	std::cout << "into slot2" << std::endl;
}
/*
从前面的保存连接的map中,找出与
该信号关联的对象和槽调用该对象这个槽
*/
void QMetaObject::active(QObject* sender, int idx)
{
	ConnectionMapIt it;
	std::pair<ConnectionMapIt, ConnectionMapIt> ret;
	ret = sender->connections.equal_range(idx);
	for (it = ret.first; it != ret.second; ++it) {
		Connection c = (*it).second;
		c.receiver->qt_static_metacall(c.method);
	}
}

void QObject::testSignal()
{
	emit sig2();
	emit sig1();
}
#pragma  once

#ifndef Q_OBJECT_H  
#define Q_OBJECT_H
#include <map>  

# define slots  
# define signals protected  
# define emit  

# define Q_OBJECT static QMetaObject staticMetaObject;void qt_static_metacall(int idx);


class QObject;
/*元对象*/
struct QMetaObject
{
	const char * sig_names;
	const char * slts_names;
	/*信号的发送者和信号的索引*/
	static void active(QObject * sender, int idx);
};

struct Connection
{
	QObject * receiver;
	int method;
};

typedef std::multimap<int, Connection> ConnectionMap;
typedef std::multimap<int, Connection>::iterator ConnectionMapIt;

class QObject
{
	static QMetaObject staticMetaObject; 
	void qt_static_metacall(int idx);

public:
	static void connect(QObject*, const char*, QObject*, const char*);
	void testSignal();

signals:
	void sig1();
	void sig2();

public slots:
	void slot1();
	void slot2();
	friend struct QMetaObject;

private:
	ConnectionMap connections;
};
#endif  
/*此文件手动生成,其实应该是Moc预编译器生成的*/
#include "QObject.h"  

static const char sig_names[] = "sig1\nsig2\n";

static const char slts_names[] = "slot1\nslot2\n";

QMetaObject QObject::staticMetaObject = { sig_names, slts_names };

void QObject::sig1()
{
	QMetaObject::active(this, 0);
}


void QObject::sig2()
{
	QMetaObject::active(this, 1);
}

void QObject::qt_static_metacall(int idx)
{
	switch (idx)
	{
	case 0:
	{
		slot1();
		break;
	}
	case 1:
	{
		slot2();
		break;
	}
	};
}
#include <iostream>
#include <string>
#include "QObject.h"

int main()
{
	QObject obj1, obj2;

	QObject::connect(&obj1, "sig1", &obj2, "slot1");
	QObject::connect(&obj1, "sig2", &obj2, "slot2");
	obj1.testSignal();

	return 0;;
}

下面是Qt中的宏替换

QObject::connect(countObj1, SIGNAL(valueChanged()), countObj2, SLOT(slotValueChanged()));
QObject::connect(countObj1, "2""valueChanged()", countObj2, "1""slotValueChanged()");
#define Q_OBJECT \
public: \
    static const QMetaObject staticMetaObject; \
    virtual const QMetaObject *metaObject() const; \
    virtual void *qt_metacast(const char *); \
    virtual int qt_metacall(QMetaObject::Call, int, void **);
private: \
    static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
    struct QPrivateSignal {};
struct Q_CORE_EXPORT QArrayData
{
    QtPrivate::RefCount ref;
    int size;
    uint alloc : 31;
    uint capacityReserved : 1;

    qptrdiff offset; // in bytes from beginning of header

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/461755.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea无效的目标版本和类文件具有错误的版本 61.0, 应为 52.0错误(测试有用,一次性解决问题)

SpringBoot己更新到3后&#xff0c;使用的JAVA版本最低要求JAVA17&#xff0c;如果低于这个版本就是报错&#xff1a; 问题一&#xff1a;类文件具有错误的版本 61.0, 应为 52.0。 解决就只有升级JAVA-sdk&#xff1a; 官方下载地址&#xff1a;JAVA20-17 官方推荐更好的sd…

MySQL 视图、函数和存储过程

MySQL 是一种流行的关系型数据库管理系统&#xff0c;其具有强大的功能和灵活性&#xff0c;使其成为了许多企业和个人喜爱的数据库选择。在 MySQL 中&#xff0c;视图、函数和存储过程是常见的数据库对象&#xff0c;它们都有助于提高数据的处理效率和可重用性。 一、视图 视…

UE5实现Runtime环境下绘制点功能

文章目录 1.实现目标2.实现过程2.1 C++实现2.2 蓝图调用3.参考资料1.实现目标 UE5在Runtime环境下基于PDI绘制点,GIF动态如下: 2.实现过程 UE常用的在运行时环境下绘制点方法主要有两种。一种是基于Mesh,即添加Sphere等StaticMesh来模拟显示绘制点;另一种是基于Primitive的…

用代码实现标签打印的三种方式

最近项目中要实现标签打印的功能&#xff0c;有几个条件 标签模板可以事先生成&#xff0c;用的是CodeSoft软件标签模板里面有二维码标签模板里面有一些变量&#xff0c;要求打印的时候自动填充产线电脑上没有安装CodeSoft&#xff0c;即便安装也不能使用&#xff0c;因为没有…

Java线程池及拒绝策略详解

前文提到线程的使用以及线程间通信方式&#xff0c;通常情况下我们通过new Thread或者new Runnable创建线程&#xff0c;这种情况下&#xff0c;需要开发者手动管理线程的创建和回收&#xff0c;线程对象没有复用&#xff0c;大量的线程对象创建与销毁会引起频繁GC&#xff0c;…

Unity入门(一)

Unity Unity是一套完善体系与编辑器的跨平台游戏开发工具&#xff0c;也可以称之为游戏引擎。游戏引擎是指一些编写好的可以重复利用的代码与开发游戏所用的各功能编辑器。 基于C#编程&#xff0c;易上手&#xff0c;高安全性独特的面向组件游戏开发思想让游戏开发更加简单易…

Maven 如何下载依赖包的源码包

使用Maven下载依赖包的时候&#xff0c; 默认是不会下载源码包的&#xff0c;但是有时候&#xff0c; 需要Debug代码&#xff0c;或是看看依赖项的源码的写法&#xff0c; 就需要下载源码包了。 这里以 Apache 的 commons-text 为例&#xff0c; 在Maven中添加如下依赖配置&am…

pwlink用作USB转TTL,进入HC-05的AT模式

不说废话的文章概括&#xff1a; 直接连接PWLINK与HC-05&#xff0c;无法进入AT模式&#xff0c;因为蓝牙模块的VCC只能接5V&#xff0c;不能接3.3V&#xff0c;而且PWLINK有两个VDD引脚&#xff0c;且两个VDD引脚初始默认输出电压都是3.3V&#xff0c;所以需要将3.3V改为5V的…

【JavaEE】网络通信中的一些基本概念及协议分层

博主简介&#xff1a;想进大厂的打工人博主主页&#xff1a;xyk:所属专栏: JavaEE初阶 互联网是怎么来的&#xff1f;很多先进技术&#xff0c;都是先军用&#xff0c;用了之后太香了才逐渐民用~~ 互联网也是如此&#xff0c;互联网之前&#xff0c;可以通过有线/无线&#xf…

单链表——“数据结构与算法”

各位CSDN的uu们你们好呀&#xff0c;今天&#xff0c;小雅兰的内容终于是我们心心念念的单链表啦&#xff0c;这一块呢&#xff0c;是一个很重要的部分&#xff0c;也是一个对目前的我来说&#xff0c;比较困难的部分&#xff0c;下面&#xff0c;就让我们进入单链表的世界吧 之…

【unity项目实战】3DRPG游戏开发04——导航、人物移动和鼠标指针图片替换

AI导航烘培 还不清楚怎么用的可以看我另一篇文章 零基础带你从小白到超神29——导航系统 将地形调成静态导航的 选中地形,设置为可行区域 点击烘培 可爬坡改为30度,就会发现坑就变为不可行区域了 选择所有的树,为不可行区域,点击烘培 给主角人物添加导航组件

Kafka3.0.0版本——生产者数据可靠性

目录 一、ACK应答原理1.1、应答级别1.1.1、acks 01.1.2、acks 11.1.3、acks -1&#xff08;all&#xff09; 1.2、问题思考 二、数据可靠性2.1、数据可靠性分析2.2、 数据完全可靠条件2.3、ACK应答级别可靠性总结 三、数据可靠性代码示例 一、ACK应答原理 1.1、应答级别 1…

一日一题:第十二题---模拟散列表(三种方法!!)

​作者&#xff1a;小妮无语 专栏&#xff1a;一日一题 &#x1f6b6;‍♀️✌️道阻且长&#xff0c;不要放弃✌️&#x1f3c3;‍♀️ 今天来给大家介绍的是简单的Hash表的应用 目录 关于哈希的知识点 题目描述&#xff08;模拟散列表&#xff09; 代码 1&#xff08;拉链…

基于GPT-4的神仙插件Bito,亲测好用

基于GPT-4的神仙插件&#xff0c;无需魔法,目前免费 一、Bito 简介 最近发现一个可以有效提升coding效率的插件神器&#xff0c;截止当前(20230425)已有65k的下载量了&#xff01; 类似与Cursor一样&#xff0c;可以使用AI辅助写代码&#xff0c;但是又解决Cursor没有语法提…

力扣刷题day35|416分割等和子集

416. 分割等和子集 力扣题目链接 给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集&#xff0c;使得两个子集的元素和相等。 示例 1&#xff1a; 输入&#xff1a;nums [1,5,11,5] 输出&#xff1a;true 解释&#xff1a;数组可以分割…

keil设置程序起始地址及生成bin文件的方法

一.keil设置程序起始地址 1.1FLASH APP 的起始地址设置 随便打开一个之前的实例工程&#xff0c;点击 Options for Target→Target 选项卡 默认的条件下&#xff0c;图中 IROM1 的起始地址&#xff08;Start&#xff09;一般为 0X08000000&#xff0c;大小&#xff08;Size&a…

通用el-table 修改样式

通用el-table 修改样式 el-table实现下图效果: <template><div class"contentbox"><el-table:data"tableData"height"310"style"width: 40%"highlight-current-rowcurrent-change"handleCurrentChange"&g…

利用Floodlight进行DDOS攻击防御实验笔记

Floodlight Floodlight是Apache授权并基于JAVA开发的企业级OpenFlow控制器&#xff0c;当前最新版本是1.2。 Floodlight OpenFlow Controller -ProjectFloodlight&#xff1a;http://www.projectfloodlight.org/floodlight/ 流表 把同一时间&#xff0c;经过同一网络中具有某种…

线程同步方式之二条件变量

Linux线程同步方法之二 条件变量 饥饿状态&#xff1a;由于线程A频繁地申请/释放锁&#xff0c;而导致其他线程无法访问临界资源的情况。 同步synchronized&#xff1a;在保证数据安全的前提下&#xff0c;让线程能够按照某种特定的顺序访问临界资源&#xff0c;从而有效避免…

Spring Security实战(九)—— 使用Spring Security OAuth实现OAuth对接

一、OAuth2.0介绍 OAuth2.0是一种授权协议&#xff0c;允许用户授权第三方应用程序代表他们获取受保护的资源&#xff0c;如个人信息或照片等。它允许用户授权访问他们存储在另一个服务提供商上的资源&#xff0c;而无需将其凭据共享给第三方应用程序。OAuth2.0协议建立在OAuth…