QT信号槽原理(一)connect函数

2个类型和1个签名

Qt::ConnectionType

信号的连接类型

  • Qt::DirectConnection
    直接连接,相当于在emit信号的地方直接调用接收者的槽函数,多见于在同一线程中去连接信号和槽,是一个同步的调用过程。
  • Qt::QueuedConnection
    队列连接,在emit信号时,会往接收者所在线程的消息队列post一个QMetaCallEvent,接收者线程在未来某个时刻处理消息队列中的该事件时就会调用接收者的槽函数,是一个异步的调用过程。这种方式下,如果信号的参数是个原始指针,那需要确保原始指针的生命周期直到事件被处理时仍然有效;其他类型(包括引用)的参数,qt会自动去克隆。还要注意的是,该类型下,信号的参数会放到event中,所以参数类型比如是QMetatype,如果不是,需要使用qRegisterMetaType()去注册。
  • Qt::BlockingQueuedConnection
    也是队列连接,不同于QueuedConnection在post事件之后立马返回,BlockingQueuedConnection在post事件之后,会一直阻塞直到该事件被处理。所以要注意,当发送者和接收者在同一线程时,千万布套使用这种方式,不然就会导致线程死锁。另外,虽然这是队列连接,但是由于是阻塞的,所以可以看成是同步的,也就不存在参数的生命周期问题。
  • Qt::AutoConnection
    自动连接,如果发送者和接收者在同一线程,那么就是DirectConnection,否则就使用QueuedConnection。
  • Qt::UniqueConnection
  • 严格来说,这并不是一种连接方式,而仅仅是一个掩码,表示唯一连接,它可以与上面四种连接方式以 | 的方式组合,比如 Qt::DirectConnection|Qt::UniqueConnection。如果发送者的信号已于接收者的槽连接过,再进行连接时就会失败。

QMetaMethod::MethodType

类中方法的类型

  • QMetaMethod::Method
    是用invoke修饰的成员函数
  • QMetaMethod::Signal
    信号
  • QMetaMethod::Slot
    槽函数
  • QMetaMethod::Constructor
    构造函数

方法签名

QMetaMethod的methodSignature()返回方法的签名,签名是个字符串。
下面是QMetaMethod获取方法签名的源码:

QByteArray QMetaMethodPrivate::signature() const
{
    QByteArray result;
    result.reserve(256);
    result += name();
    result += '(';
    QList<QByteArray> argTypes = parameterTypes();
    for (int i = 0; i < argTypes.size(); ++i) {
        if (i)
            result += ',';
        result += argTypes.at(i);
    }
    result += ')';
    return result;
}

可以看出签名实际是方法声明去掉返回值和参数名称后的字符串。
例如:
信号声明:
void clicked(bool state)
那么信号的签名就是clicked(bool)

connect函数源码解析

函数原型:

QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type)
五个参数:发送者,信号,接收者,槽函数或信号,连接类型

第一步:参数检查

if (sender == nullptr
        || receiver == nullptr
        || signal.methodType() != QMetaMethod::Signal
        || method.methodType() == QMetaMethod::Constructor) {
    qWarning("QObject::connect: Cannot connect %s::%s to %s::%s",
             sender ? sender->metaObject()->className() : "(nullptr)",
             signal.methodSignature().constData(),
             receiver ? receiver->metaObject()->className() : "(nullptr)",
             method.methodSignature().constData() );
    return QMetaObject::Connection(0);
}

发送者和接收者不能为空这是基本条件,另外,第二个参数必须是信号,第四个参数不能是构造函数(从第一节可以看出,除了构造函数外,还有信号、槽函数、普通成员函数三种类型)。

第二步:获取信号和槽的index

int signal_index;
int method_index;
{
    int dummy;
    QMetaObjectPrivate::memberIndexes(sender, signal, &signal_index, &dummy);
    QMetaObjectPrivate::memberIndexes(receiver, method, &dummy, &method_index);
}

const QMetaObject *smeta = sender->metaObject();
const QMetaObject *rmeta = receiver->metaObject();
if (signal_index == -1) {
    qWarning("QObject::connect: Can't find signal %s on instance of class %s",
                 signal.methodSignature().constData(), smeta->className());
        return QMetaObject::Connection(0);
}
if (method_index == -1) {
    qWarning("QObject::connect: Can't find method %s on instance of class %s",
             method.methodSignature().constData(), rmeta->className());
    return QMetaObject::Connection(0);
}

通过QMetaObjectPrivate::memberIndexes获取信号或槽的index,然后再去检查index。
从moc后的代码来看,信号和槽的index,从信号开始,按声明顺序从0开始依次递增的排列,然后接着是槽声明的顺序,接着信号最后一个序号排列。
例如下面的类声明:

class MocTest : public QObject {
    Q_OBJECT
public:
    explicit MocTest(QObject* parent = nullptr);

signals:
    void testSignal(const QString& name, int age);

public slots:
    void testSlot(const QString& name, int age);
};

信号testSignal的index为0, 槽函数testSlot的index为1。

第三步:检查信号和槽的参数是否匹配

if (!QMetaObject::checkConnectArgs(signal.methodSignature().constData(), method.methodSignature().constData())) {
    qWarning("QObject::connect: Incompatible sender/receiver arguments"
             "\n        %s::%s --> %s::%s",
             smeta->className(), signal.methodSignature().constData(),
             rmeta->className(), method.methodSignature().constData())     
   return QMetaObject::Connection(0);
}

这里的比较基于这样一个要求:槽函数参数个数≤信号参数个数,且参数类型和顺序必须一致。想想也很容易明白,槽函数参数少,那信号多出来的参数可以不传下去,但是如果槽函数参数多的话,那多出来的参数应该传什么值呢。
在第一节我们说过方法签名的组成:方法名称(参数类型1,参数类型2,…),下面看checkConnectArgs源码:

bool QMetaObject::checkConnectArgs(const char *signal, const char *method)
{
    const char *s1 = signal;
    const char *s2 = method;
    while (*s1++ != '(') { }                        // scan to first '('
    while (*s2++ != '(') { }
    if (*s2 == ')' || qstrcmp(s1,s2) == 0)        // method has no args or
        return true;                                //   exact match
    int s1len = qstrlen(s1);
    int s2len = qstrlen(s2);
    if (s2len < s1len && strncmp(s1,s2,s2len-1)==0 && s1[s2len-1]==',')
        return true;                                // method has less args
    return false;
}

按上面提到的比较要求,比较参数列表的话,就是看括号里的参数列表字符串,如果槽函数参数列表字符串是信号参数列表字符串的子串或者相同,那ok,否则fail。

第四步:检查信号参数类型

int *types = 0;
if ((type == Qt::QueuedConnection)
        && !(types = queuedConnectionTypes(signal.parameterTypes())))
    return QMetaObject::Connection(0);

在前面讲连接类型我们提到如果是队列连接,参数类型比如是QMetaType或者是注册到QMetaType。这一步就是检查参数类型是否是QMetaType。
那BlockingQueuedConnection也是队列连接,为啥不需要检查参数类型呢?
那是因为虽然BlockingQueuedConnection是队列连接,但是通过阻塞变成了同步调用,所以不存在参数生命周期问题,所以不会去克隆信号参数,所以也就不需要是QMetaType。

第五步:创建连接对象

QMetaObject::Connection handle = QMetaObject::Connection(QMetaObjectPrivate::connect(
    sender, signal_index, signal.enclosingMetaObject(), receiver, method_index, 0, type, types));
return handle;

所有的检查都通过后,就通过QMetaObjectPrivate::connect来创建连接对象。
这个函数主要分3小步:

检查Qt::UniqueConnection

    QObjectPrivate::ConnectionData *scd  = QObjectPrivate::get(s)->connections.loadRelaxed();
    if (type & Qt::UniqueConnection && scd) {
        if (scd->signalVectorCount() > signal_index) {
            const QObjectPrivate::Connection *c2 = scd->signalVector.loadRelaxed()->at(signal_index).first.loadRelaxed();

            int method_index_absolute = method_index + method_offset;

            while (c2) {
                if (!c2->isSlotObject && c2->receiver.loadRelaxed() == receiver && c2->method() == method_index_absolute)
                    return nullptr;
                c2 = c2->nextConnectionList.loadRelaxed();
            }
        }
        type &= Qt::UniqueConnection - 1;
    }

如果连接类型带了UniqueConnection掩码,则检查当前是否符合UniqueConnection。

创建connection对象,并填充各个数据

    std::unique_ptr<QObjectPrivate::Connection> c{new QObjectPrivate::Connection};
    c->sender = s;
    c->signal_index = signal_index;
    c->receiver.storeRelaxed(r);
    QThreadData *td = r->d_func()->threadData;
    td->ref();
    c->receiverThreadData.storeRelaxed(td);
    c->method_relative = method_index;
    c->method_offset = method_offset;
    c->connectionType = type;
    c->isSlotObject = false;
    c->argumentTypes.storeRelaxed(types);
    c->callFunction = callFunction;

创建Connection对象,填充各个数据成员。

将connection对象添加到sender中

QObjectPrivate::get(s)->addConnection(signal_index, c.get());

将上一步创建的connection对象添加到sender的连接列表中。
到此,一个完整的connect执行流程就分析完成了。

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Qt中,信号是一种用于对象间通信的机制。信号是对象发出的事件,而是对这些事件进行响应的函数。当一个信号被触发时,与之相连接的函数将会被调用。 在Qt中,信号函数可以具有不同的参数类型。当信号函数的参数类型不匹配时,Qt会自动进行类型转换。但是,如果你需要传递额外的参数给函数,你可以使用信号函数指针参数。 具体来说,你可以在信号函数的定义中使用指针参数。当信号被触发时,可以通过指针参数传递额外的数据给函数。这样,函数就可以根据传递的数据进行相应的处理。 下面是一个示例代码,演示了如何在Qt中使用信号函数指针参数: ```cpp class MyObject : public QObject { Q_OBJECT public: MyObject(QObject *parent = nullptr) : QObject(parent) {} signals: void mySignal(int value, QString text); public slots: void mySlot(int value, QString text) { // 处理传递过来的参数 qDebug() << "Received value:" << value; qDebug() << "Received text:" << text; } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); MyObject obj; // 连接信号函数 QObject::connect(&obj, &MyObject::mySignal, &obj, &MyObject::mySlot); // 触发信号,并传递参数 emit obj.mySignal(42, "Hello, world!"); return app.exec(); } ``` 在上面的示例中,`mySignal`信号带有两个参数:一个整数和一个字符串。`mySlot`函数也带有相同的参数。通过使用信号函数指针参数,我们可以将信号的参数传递给函数进行处理。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值