
Qt -信号与槽
Qt信号与槽,connect调用链,qobject_p_p.h相关结构体简介,信号与槽的模拟实现(利用了TMP进行类型检查)。
博客主页:【夜泉_ly】
本文专栏:【暂无】
欢迎点赞👍收藏⭐关注❤️
目录
前言
面向对象,
这个词从开始学 C++ 我们就知道了,
但我们或许仍然不能真正理解它。
而本篇的信号与槽,
或许多多少少能加深我们对面向对象的认识。
引入
信号与槽,
本质解决的是对象之间的通信问题。
很简单的一个例子:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* btn = new QPushButton(this);
btn->setText("关闭窗口");
connect(btn, &QPushButton::clicked, this, &QWidget::close); // 针对不同对象。点击按钮,关闭窗口
}
在这里,
connect
将一个按钮和一个控件建立了连接。
点击按钮,
按钮会告诉控件:
你该关闭了。
很明显按钮和控件是两个不同的对象,
而它们能够通信,
借助的就是信号与槽。
connect调用链
模板类型的connect
为了加深理解,
下面我们来简单看看Qt中对刚刚的 connect
的处理。
首先,当我们写下:
connect(btn, &QPushButton::clicked, this, &QWidget::close);
会调用 qobject.h
的模板类型的 connect
(227行左右)
//connect with context
template <typename Func1, typename Func2>
static inline QMetaObject::Connection
connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
const typename QtPrivate::ContextTypeForFunctor<Func2>::ContextType *context, Func2 &&slot,
Qt::ConnectionType type = Qt::AutoConnection)
之后会进行一系列的检查,
用了大量 TMP
的知识,
我暂时看不懂。
总之,大概检查了信号和槽的各种类型后,
调用了 connectImpl
,
impl
是 implementation
(实现)的缩写,
所以这里才是连接信号和槽的地方。
(那 connect
的几十行代码全用来检查了?恐怖如斯)
connect
函数末尾:
QObject::connectImpl
这个 connectImpl
也有很多地方实现了,
不过根据参数类型,
我觉得它调的是这个 QObject::connectImpl
:
qobject.cpp
5324行左右
QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signal,
const QObject *receiver, void **slot,
QtPrivate::QSlotObjectBase *slotObjRaw, Qt::ConnectionType type,
const int *types, const QMetaObject *senderMetaObject)
查了查,
Qt 的信号是由 moc 工具生成的元数据,
依赖于 QObject
和 QMetaObject
。
而这个 QObject::connectImpl
的作用就是:
使用 QMetaObject
的元信息查找信号的索引 signal_index
。
然后在函数末尾调用了 QObjectPrivate::connectImpl
:
return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj.release(), type, types, senderMetaObject);
QObjectPrivate::connectImpl
qobject.cpp
5370行左右
QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,
const QObject *receiver, void **slot,
QtPrivate::QSlotObjectBase *slotObjRaw, int type,
const int *types, const QMetaObject *senderMetaObject)
这下才是真的来到核心实现了🤣。
我觉得最重要的两句话:
std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
QObjectPrivate::get(s)->addConnection(signal_index, c.get());
这里创建了一个 QObjectPrivate::Connection
对象,
在里面保存信号和槽的连接信息。
又用 QObjectPrivate::get(s)
即指向信号发送者( sender
)的 QObjectPrivate
指针,
调用 addConnection()
而 addConnection。。。
qobject_p_p.h
我们还是先看看 QObjectPrivate
中的几个结构体吧:
struct Connection;
struct ConnectionData;
struct ConnectionList;
struct ConnectionOrSignalVector;
struct SignalVector;
struct Sender;
struct TaggedSignalVector;
定义在 qobject_p_p.h
中,
共同构成了 Qt 信号与槽机制的底层实现。
Connection的关键字段(我认为的):
struct QObjectPrivate::Connection : public ConnectionOrSignalVector
{
QObject *sender;
QAtomicPointer<QObject> receiver;
union {
StaticMetaCallFunction callFunction;
QtPrivate::QSlotObjectBase *slotObj;
};
signed int signal_index : 27; // In signal range (see QObjectPrivate::signalIndex())
ushort connectionType : 2; // 0 == auto, 1 == direct, 2 == queued, 3 == blocking
};
Connection
表示信号与槽之间的一个具体连接,
是个双链表的节点(指针被我省略了,因为我看不懂)。
sender
发出信号的对象receiver
接收信号的对象union
中,提供两种方式去调用函数
且因为是union
,所以同时只存在一个方法
Qt会根据你传的槽去选择:callFunction
对应 成员函数slotObj
对应lambda
表达式、仿函数等复杂点的对象
ConnectionData
的关键字段(我认为的):
struct QObjectPrivate::ConnectionData
{
QAtomicPointer<SignalVector> signalVector;
Connection *senders = nullptr;
Sender *currentSender = nullptr;
};
-
signalVector
存的与对象相关的所有信号的连接信息。
每个元素对应一个ConnecionList
,
存储了与该信号相关的所有槽的连接信息。
-
senders
存的连接到当前对象槽的信号的连接信息。
从类型Connection
看出,这是个双链表。
-
currentSender
指的是当前被激活的信号发送者,
当有信号被激活,会有个Sender
对象被创建,并连接到这里。
具体的细节。。嘶,又要跳文件吗 !?
好像激活和qobjectdefs.h
的QMetaObject::activate
有关,
暂时不看了。
但Sender
的构造可以看看,
这里体现了连接到currentSender
的过程:Sender(QObject *receiver, QObject *sender, int signal, ConnectionData *receiverConnections) : receiver(receiver), sender(sender), signal(signal) { if (receiverConnections) { previous = receiverConnections->currentSender; receiverConnections->currentSender = this; } }
connect作用总结
那么看到这里,
似乎 addConnection
不太需要看了,
Qt的 SignalVector
使用了非常规的方法表示数组,
主要利用的是指针的偏移,
所以相关的代码都涉及大量的指针操作,
提高了性能,
但降低了我这种fw的阅读体验。
总结一下 QObjectPrivate::connectImpl
的主要作用吧:
创建连接信息,用的 Connection
结构体。
将连接信息添加到发送者的 signalVector
中
将连接信息添加到接收者的 senders
中
最后回到开头的例子:
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QPushButton* btn = new QPushButton(this);
btn->setText("关闭窗口");
connect(btn, &QPushButton::clicked, this, &QWidget::close); // 针对不同对象。点击按钮,关闭窗口
}
发送者就是 btn
,是个按钮。
接收者就是 this
, 是个控件。
通过 connect
:
btn
的 singalVector
的 clicked
信号的 ConnecionList
多了一个 Connection
,
this
的 senders
双链表中 也多了一个 Connection
,
而这两个地方指向的是同一个 Connection
结构体,
而这个 Connection
的内容:
sender
就是 btn,即按钮
receiver
就是 this,即控件
callFunction
就是 QWidget::close
。(因为 QWidget::close
是个成员函数,所以union
中是 callFunction
,而不是 slotObj
)
signal_index
就是 QPushButton::clicked
信号对应的下标。
这就是建立连接的过程了。
至于调用。。力竭了,不想看了。。
ai对信号与槽的模拟实现
最后,看看ai的模拟实现吧:
类型检查虽然非必须,
但 TMP 真的帅啊。。。
#include <iostream>
#include <vector>
#include <functional>
#include <tuple>
// 检查每一对参数类型是否兼容的类型特征
template <typename SignalArgsTuple, typename SlotArgsTuple>
struct CheckCompatibleArguments;
// 基本情况:参数列表为空
template <>
struct CheckCompatibleArguments<std::tuple<>, std::tuple<>> {
static constexpr bool value = true;
};
// 递归情况:逐一检查所有参数对
template <typename SignalArg, typename... SignalRest,
typename SlotArg, typename... SlotRest>
struct CheckCompatibleArguments<
std::tuple<SignalArg, SignalRest...>, std::tuple<SlotArg, SlotRest...>
> {
static constexpr bool value =
std::is_convertible<SignalArg, SlotArg>::value&&
CheckCompatibleArguments<std::tuple<SignalRest...>, std::tuple<SlotRest...>>::value;
};
// Signal类定义,用于表示信号,管理连接的槽
template <typename... Args>
class Signal {
std::vector<std::function<void(Args...)>> slots;
public:
// 连接槽函数到当前信号
template <typename F>
void connect(F&& f) {
slots.emplace_back(std::forward<F>(f));
}
// 触发信号,传播参数至所有已连接的槽
void emit(Args... args) {
for (auto& slot : slots) {
slot(args...);
}
}
};
// 全局connect函数,用于连接信号和成员函数槽
template <typename Receiver, typename... SignalArgs, typename... SlotArgs>
void connect(
Signal<SignalArgs...>& signal,
Receiver* receiver,
void (Receiver::* slot)(SlotArgs...)
) {
// 确保信号和槽参数个数匹配
static_assert(
sizeof...(SignalArgs) == sizeof...(SlotArgs),
"Signal and slot have different number of arguments"
);
// 确保参数类型兼容
static_assert(
CheckCompatibleArguments<
std::tuple<SignalArgs...>,
std::tuple<SlotArgs...>
>::value,
"Signal and slot arguments are not compatible"
);
signal.connect([receiver, slot](SignalArgs... args) {
(receiver->*slot)(args...);
});
}
// 示例发送者类:按钮
class Button {
public:
Signal<int> clicked; // 带整数参数(点击次数)的信号
void press() {
static int count = 0;
clicked.emit(++count); // 发出信号
}
};
// 示例接收者类:标签
class Label {
public:
void showCount(int num) {
std::cout << "点击次数:" << num << std::endl;
}
};
int main() {
// 测试 1: 基本测试:按钮点击信号和标签的显示槽连接
Button button;
Label label;
// 连接按钮的点击信号到标签的显示槽
connect(button.clicked, &label, &Label::showCount);
// 模拟用户点击按钮
button.press(); // 输出:点击次数:1
button.press(); // 输出:点击次数:2
std::cout << "-------------------" << std::endl;
// 测试 2: 多个槽连接到同一个信号
Button button2;
Label label2;
Label label3;
// 连接按钮的点击信号到多个槽
connect(button2.clicked, &label2, &Label::showCount);
connect(button2.clicked, &label3, &Label::showCount);
// 模拟用户点击按钮
button2.press(); // 输出:点击次数:1
button2.press(); // 输出:点击次数:2
std::cout << "-------------------" << std::endl;
// 测试 3: 使用不同类型的信号和槽
class StringLabel {
public:
void showString(const std::string& str) {
std::cout << "显示字符串:" << str << std::endl;
}
};
Signal<std::string> stringSignal;
StringLabel stringLabel;
// 连接信号和槽
connect(stringSignal, &stringLabel, &StringLabel::showString);
// 发射一个字符串信号
stringSignal.emit("Hello, world!"); // 输出:显示字符串:Hello, world!
std::cout << "-------------------" << std::endl;
// 测试 4: 测试无参信号
Signal<> noArgSignal;
class NoArgLabel {
public:
void notify() {
std::cout << "信号发射了!" << std::endl;
}
};
NoArgLabel noArgLabel;
// 连接无参信号和槽
connect(noArgSignal, &noArgLabel, &NoArgLabel::notify);
// 发射无参信号
noArgSignal.emit(); // 输出:信号发射了!
std::cout << "-------------------" << std::endl;
// 测试 5: 参数类型不兼容的错误(编译时错误)
// Uncommenting the code below will result in a compilation error.
// Signal<double> doubleSignal;
// connect(doubleSignal, &label, &Label::showCount); // 编译错误:类型不兼容
return 0;
}
运行结果:
希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!
更多推荐
所有评论(0)