Qt 信号和槽机制

2023-10-27

这篇文章篇幅很长,阅读可能需要10分钟以上,如果你是Qt的初学者,前面的6个章节就已经够用了,至少能够让你在一些普通的场面撑得起场子。 但如果你想了解的更深一点,最后一个章节是必不可少的内容。

Qt提供了很多我们学习的文档,甚至是源码。阅读源码在我们需要对它原生的控件做功能扩展的时候会显得尤为重要。

就像曾经有人问我,Qt在不同对象之间使用信号槽通信的,那么什么是信号槽,信号槽的本质知道吗?他们之间是怎么通信的,既然已经有了回调,为什么还要有信号槽?信号槽的第五个参数决定了他们的关联方式,队列关联和阻塞队列关联知道吗?阻塞是怎么阻塞的。能不能详细说说。

希望读完这篇文章,对你会有所帮助。

1、什么是信号槽?

Qt帮助文档里面这么说:

Signals and slots are used for communication between objects. The signals and slots mechanism is a central feature of Qt and probably the part that differs most from the features provided by other frameworks. Signals and slots are made possible by Qt’s meta-object system.

也就是说,信号和槽被用来两个对象之间的通讯。是Qt独有的核心机制。这也是区分于其他框架的特性,Qt的元对象系统使信号槽成为可能。信号和槽也让我们在Qt中有了回调技术的可替代方案。

上面提到了,Qt的元对象使得Qt的信号槽成为可能。那么元对象是什么

大家都知道,Qt是一个利用标准C++编写的跨平台类库,对标准C++做了一些扩展。引入了一些类似信号槽、元对象、动态属性等特有的东西。元对象的引入是为了解决即使编译器不支持RTTI机制,也可以用过元对象系统(meta-object system )机制来获取运行时的类型信息。

在Qt帮助文档中有这样的定义,meta-object依赖下面三件事:

  1. The QObject class provides a base class for objects that can take advantage of the meta-object system.(QObject类可以利用meta-object对象的类提供一个基类)
  2. The Q_OBJECT macro inside the private section of the class declaration is used to enable meta-object features, such as dynamic properties, signals, and slots.(在类的生命中加入Q_OBJECT宏可使用meta-object系统的功能,例如动态属性、信号、槽等)
  3. The Meta-Object Compiler (moc) supplies each QObject subclass with the necessary code to implement meta-object features.(元对象编译器(moc)为每个QObject子类提供元对象所需的代码)

总的理解一下就是,元对象可以做为QObject的一个基类,在类的声明中添加Q_OBJECT宏便可使用Qt独有的特性,比如动态属性、信号槽等。同时,moc编译器也实现了我们元对象系统的代码支持。

奇怪的是,我们已经有了C++的编译器,为什么Qt还要有自己的Meta-Object Compiler(moc)编译器呢?

这主要是因为,前面我们已经说了,Qt是对标准C++做了一些扩展,这些扩展对于C++的编译器来说并不认识呀。也就有了特定的Meta-Object Compiler(moc)编译器来进行一次转换,以便我们编写的代码能够被C++编译器正常编译。

如果我们需要编译一个Qt程序,他的步骤大致是这样的:

  1. 首先检查类中是否包含了Q_OBJECT宏,如果包含了,Meta-Object Compiler(moc)编译器会将该类首先进行处理,生成一个moc_xxx.cpp的文件。这个过程主要是为了对包含了Q_OBJECT宏中的一些关键字,比如:signals、slots、emit等转换为C++编译器可以识别的代码。
  2. 再进行一次C++编译器的编译。

我们后面会看到moc_xxx.cpp这个文件中的具体内容。

上面我们从Qt的信号槽机制,引伸到了meta-object system。我们是否知道了信号槽究竟是什么?

信号槽其实就是在Qt中对实例对象之间的通信做了一些替代。标准C++中我们一般使用回调做实例对象之间的通讯。但其实信号槽的本质也是回调。信号槽方便了我们在一对多实例对象之间的通信,免除了我们在这个过程中维护对象列表的过程。

2、信号槽的关联方式

从Qt帮助文档中找到了connect 函数的原型,我挑了其中的三个。

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
[static] QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);
[static] QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor); //支持Lambda表达式

信号槽的关联在Qt4中我们一般写成下面这样:

connect(sender, SIGNAL(signal_xxx(int, const QString&)), reciever, SLOT(slot_xxx(int, const QString&)));

调用的是上面的第一个connect函数。

上面的这种方式存在一些问题,因为你在书写的时候一定要注意参数列表,数量、类型要保持一致才能正常关联。在Qt5之后,对这种关联方式做了一些优化。

connect(sender, &Object::signal_xxx, reciever, &Object::slot_xxx);

调用的是上面的第二个connect函数。

是不是方便了很多,至少我们不用再特意的关注信号和槽的参数列表要一一对应。但是这种方式会有一个艰难的选择,就是如果有多个同名但参数列表不一样的信号,如果我们直接这样写,编译器就会报错,需要指定具体是哪一个信息被关联。我们也有相应的解决方法:

connect(sender, static_cast<void(QObject::*)(const QString&)>(&Object::signal_xxx), reciever, &Object::slot_xxx);

举个例子:

QComboBoxcurrentIndexChanged 信号有两个。

void currentIndexChanged(int index);
void currentIndexChanged(const QString &text);

如果我们直接使用上面取地址的写法。编译器是会报错的。

connect(ui->cmb, &QComboBox::currentIndexChanged, this, &XXXWidget::slot_storageChanged);

为什么呢?

因为currentIndexChanged有两个不同参数列表的信号。编译的时候,编译器会无法确定需要哪个重载函数。所以我们需要告诉编译器需要调用哪个重载函数。

connect(ui->cmb, static_cast<void(QComboBox::*)(const QString&)>(&QComboBox::currentIndexChanged), this, &XXXWidget::slot_storageChanged);

这样我们就指定了这个关联需要调用哪个信号函数。

3、信号槽的使用

  1. 一个信号连接多个槽,每个槽都会被一一执行,但是如果在多线程情况下,执行的顺序是不确定的,因为主要依赖事件循环,并不一定能够确定多线程的顺序。
  2. 一个信号对应一个槽,这种方式是我们最常见的。
  3. 多个信号连接到一个槽,这种方式下,任何一个信号被 emit,槽函数都会被执行。
  4. 一个信号连接另一个信号,这种方式是被允许的,当信号被 emit后,相连的另一个信号会被emit
  5. 在Qt5之后,槽函数可以是 lambda 表达式。
  6. 信号可以通过引用的方式,返回出参。
  7. 信号可以继承,子类中可以直接 emit 父类的信号。
  8. 信号和槽既然能被 connect,同样也可以被 disconnect
disconnect(sender, 0, 0, 0)

最简单的取消关联的方法,在这行代码中,0 maybe any signal, any receiver, any slot。

4、信号槽的第五个参数,多线程情况下,信号槽在什么线程中执行,怎么控制?

或许你已经发现了,我们在上面看到了connect函数的最后一个函数Qt::ConnectionType type给了一个默认的值,Qt::AutoConnection。而这也是我们这个章节的主角。其实Qt connect函数的第五个参数表示了信号和槽的关联方式,目前是有4种方式。

Qt::AutoConnection :自动选择方式,也是默认的方式,这种方式主要看 槽函数是否在 和 emit 信号线程中,如果是,则Qt::DirectConnection方式被调用,否则,Qt::QueuedConnection方式被使用。

Qt::DirectConnection :直接关联,也就是槽函数在 emit 信号的线程中执行。这种方式和在 emit 信号的地方调用槽函数是同样的效果。

Qt::QueuedConnection : 这种方式在 emit 信号的时候,会调用 poseEvent函数给事件循环队列中发送一个事件,当Qt的事件循环调用接收方的事件循环式,槽函数被调用。槽函数在接收方所在的线程中被执行。

Qt::BlockingQueuedConnection :这种方式跟 Qt::QueuedConnection 是一样的,区别就是在槽函数执行返回之前,emit 信号的线程会被阻塞,所以这种方式不能用在 emit 信号 和 槽函数的执行在同一个线程里面的情况。因为会造成线程死锁。信号被 emit 后,线程被阻塞,等待槽函数指向线程的返回,但由于在同一线程中,并且现场被阻塞了,槽函数也就永远不会被执行。

Qt::UniqueConnection : 这是一个标志位,可以与上面的几种方式按位或。目的主要是为了防止重复关联。(当该标志位被设置时,如果重复关联相同的信号和槽,则会返回失败)。

实际使用情况下,我们还是要根据实际的情况选择不同的关联方式。

值得注意的是,如果是使用的队列关联方式,必须要使用Qt Meta-object system 中已知的参数类型,是因为使用队列的时候,Qt需要复制参数到事件循环中。如果我们不小心使用了自定义的参数类型,比如有个参数是自定义的枚举。编译器是会报错给我们的。

QObject::connect: Cannot queue arguments of type 'MyType'

当然这种错误我们也是有方法去解决的,我们使用注册机制将自定义的类型注册到Qt中。

qRegisterMetaType<LogType>("LogType");
connect(worker, &LoadWorker::signal_logMessage, this, &W3DimensionWidget::slot_logMessage);

看完了信号槽的第五个参数之后,我们先抛出一个问题,上面的第四种方式,阻塞队列方式,是怎么阻塞的?

5、信号槽的优缺点

我们都知道,信号槽在本质上其实是回调,那么为什么要多此一举在回调的基础上增加这一步呢?相比较回调,信号槽又有什么优越性呢?

回调函数的本质是你想让别人的代码执行你的代码,但别人的代码你又不能动。回调函数是函数指针的一种用法,如果多个类都关注某个类的状态变化,则需要维护一个列表,用来存放回调函数的地址。并且每个关注的类都要实现自己的代码来相应被关注类的状态变化。

相比较回调函数,信号槽主要体现了下面的两个优点:

  1. 松散耦合 : 信号槽降低了Qt对象之间的耦合度。Qt中 emit 信号的对象并不需要关心哪个对象的哪个槽会被执行。同样的,槽函数也并不关心是哪个信号被 emit了。他们各自就像课题分离一样各司其职。
  2. 类型安全 : 需要关联的信号和槽的签名必须是一致的。

信号和槽虽然增加了对象之间通信的灵活性,但就跟热力学第二定律一样,同时,也会损耗一些东西做为补偿。同回调函数相比,信号和槽的运行比回调慢10倍。原因如下:

  1. 信号和槽的关联,当信号被 emit 之后,Qt需要从队列里面遍历,找到需要执行的槽函数。这点在后面的源码分析中会有详细的介绍。
  2. 安全的遍历所有的关联,也就是当一个信号关联多个槽函数后,需要对所有的槽函数遍历并执行。
  3. 需要编解组传递的参数。
  4. 多线程的时候,槽函数的执行可能需要等待。多线程情况下,事件循环。

6、信号的屏蔽

虽然信号槽方便了我们很多的操作,但是信号槽不正当的使用也会给我们造成一些不必要的麻烦。比如,我们在某个类的对象中对QComboBox信号进行了关联。在类被使用过的过程中会频繁的对该QComboBoxitem进行修改。这也就意味着信号会被频繁的触发,槽函数会被频繁的调用。如果槽函数执行了比较耗时的操作,这也许会有意料之外的事情发生。

那么有没有什么方法能够让我们可以自由的控制信号被 emit,控制槽函数的执行呢?

Qt中已经提供给我们这种解决方法。blockSignals 函数会屏蔽掉信号和槽函数的执行。

bool QObject::blockSignals(bool block) Q_DECL_NOTHROW
{
    Q_D(QObject);
    bool previous = d->blockSig;
    d->blockSig = block;
    return previous;
}

这个函数返回上一次的状态,并设置新状态, Qt中也是通过这个状态去屏蔽的,但他并不能屏蔽掉信号被 emit,而是阻止了槽函数的执行。具体是怎么屏蔽的,我们后面在详细说。

举个例子:

ui->cmb->blockSignals(true);
ui->cmb->addItems(...);
ui->cmb->blockSignals(false);

7、信号槽的运行机制

我们从一个例子看起。

class MenuButton : public QPushButton
{
     Q_OBJECT
 public:
     using QPushButton::QPushButton;

 signals:
     void signal_btnClicked(const QString&, int, bool&);
     void signal_btnClicked(const QString&, int);
public slots:
	void slot_btnClicked(const QString&, int);
 private:
     void btnClick();
 };

上面的这个例子中,我们声明了一个信号 。

void signal_btnClicked(const QString&, int, bool&);

这个信号有三个参数,前面两个是比较常见的,最后一个通过引用的方式,其实跟我们日常调用函数是一样的,信号也可以用这样的方式来返回一个参数。

我们今天的目的不是去说明Qt 信号槽怎么定义和使用的。我们主要是想看看我们使用的幕后究竟发生了什么事情。比如,connect函数是怎么关联的,emit 信号之后到底是怎么执行槽函数的。

前面已经提到过,The Meta-Object Compiler (moc) 编译器会将C++的代码转换为C++编译器可以识别的标准C++源代码,并且以 moc_xxx.cpp为名。我们首先看下这个文件中有什么东西。这个文件里面有很多东西,比如Qt的一些动态属性,信号槽,或者类名等一些变量,其他的我们目前不关心,主要关注下信号槽方面的东西。由于代码篇幅过多,下面的代码对与信号槽无关的代码中做了一些精简。

void MenuButton::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a)
{
    if (_c == QMetaObject::InvokeMetaMethod) {
        auto *_t = static_cast<MenuButton *>(_o);
        Q_UNUSED(_t)
        switch (_id) {
        case 0: _t->signal_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2])),(*reinterpret_cast< bool(*)>(_a[3]))); break;
        case 1: _t->signal_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        case 2: _t->slot_btnClicked((*reinterpret_cast< const QString(*)>(_a[1])),(*reinterpret_cast< int(*)>(_a[2]))); break;
        default: ;
        }
    } else if (_c == QMetaObject::IndexOfMethod) {
        int *result = reinterpret_cast<int *>(_a[0]);
        {
            using _t = void (MenuButton::*)(const QString & , int , bool & );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MenuButton::signal_btnClicked)) {
                *result = 0;
                return;
            }
        }
        {
            using _t = void (MenuButton::*)(const QString & , int );
            if (*reinterpret_cast<_t *>(_a[1]) == static_cast<_t>(&MenuButton::signal_btnClicked)) {
                *result = 1;
                return;
            }
        }
    }
    //...
}

int MenuButton::qt_metacall(QMetaObject::Call _c, int _id, void **_a)
{
    _id = QPushButton::qt_metacall(_c, _id, _a);
    if (_id < 0)
        return _id;
    if (_c == QMetaObject::InvokeMetaMethod) {
        if (_id < 3)
            qt_static_metacall(this, _c, _id, _a);
        _id -= 3;
    } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) {
        if (_id < 3)
            *reinterpret_cast<int*>(_a[0]) = -1;
        _id -= 3;
    }

    //...

    return _id;
}

// SIGNAL 0
void MenuButton::signal_btnClicked(const QString & _t1, int _t2, bool & _t3)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)), const_cast<void*>(reinterpret_cast<const void*>(&_t3)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void MenuButton::signal_btnClicked(const QString & _t1, int _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

上面最后的这两个函数刚好对应我们在前面对信号的声明。并且按照我们在类定义中声明的信号的顺序进行了标注。这两个函数在什么时候运行呢?其实在emit 信号的时候,实质上也就是调用了这两个函数。

1、connect函数的实现

我们都知道,信号槽要起作用,首先要用QObject::connect 函数进行关联。并且因为Qt4 和Qt5调用的函数是不一样的,但两个函数的大致逻辑是一样的,在日常使用过程中,推荐使用Qt5的关联方式,所以下面我们主要针对Qt5的方式看一写其中的逻辑。

template <typename Func1, typename Func2>
static inline QMetaObject::Connection connect(const typename QtPrivate::FunctionPointer<Func1>::Object *sender, Func1 signal,
                                const typename QtPrivate::FunctionPointer<Func2>::Object *receiver, Func2 slot,
                                Qt::ConnectionType type = Qt::AutoConnection)
{
    typedef QtPrivate::FunctionPointer<Func1> SignalType;
    typedef QtPrivate::FunctionPointer<Func2> SlotType;

    //...

    const int *types = nullptr;
    if (type == Qt::QueuedConnection || type == Qt::BlockingQueuedConnection)
        types = QtPrivate::ConnectionTypes<typename SignalType::Arguments>::types();

    return connectImpl(sender, reinterpret_cast<void **>(&signal),
                    receiver, reinterpret_cast<void **>(&slot),
                    new QtPrivate::QSlotObject<Func2, typename QtPrivate::List_Left<typename SignalType::Arguments, SlotType::ArgumentCount>::Value,
                                    typename SignalType::ReturnType>(slot),
                        type, types, &SignalType::Object::staticMetaObject);
}

从上面这个函数看,在调用了connect 函数之后,除了进行一些必要的防错判断被我用 //…代替了之外,还对关联方式是队列方式的type进行了判断。最后调用了connectImpl函数。

QMetaObject::Connection QObject::connectImpl(const QObject *sender, void **signal,
                                     const QObject *receiver, void **slot,
                                     QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                     const int *types, const QMetaObject *senderMetaObject)
{
    if (!signal) {
        qWarning("QObject::connect: invalid null parameter");
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

    int signal_index = -1;
    void *args[] = { &signal_index, signal };
    for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
        senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
        if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
            break;
    }
    if (!senderMetaObject) {
        qWarning("QObject::connect: signal not found in %s", sender->metaObject()->className());
        slotObj->destroyIfLastRef();
        return QMetaObject::Connection(0);
    }
    signal_index += QMetaObjectPrivate::signalOffset(senderMetaObject);
    return QObjectPrivate::connectImpl(sender, signal_index, receiver, slot, slotObj, type, types, senderMetaObject);
}

QObject::connectImpl 函数就比较有意思了。他首先对入参进行的检查,紧接着用了一个循环查询了信号ID和参数列表。

 int signal_index = -1;
void *args[] = { &signal_index, signal };
for (; senderMetaObject && signal_index < 0; senderMetaObject = senderMetaObject->superClass()) {
    senderMetaObject->static_metacall(QMetaObject::IndexOfMethod, 0, args);
    if (signal_index >= 0 && signal_index < QMetaObjectPrivate::get(senderMetaObject)->signalCount)
        break;
}

循环体中调用的函数 static_metacall好像在哪见过,往前翻一翻,我们在说moc_xxx.cpp文件中的内容的时候好像见过。这个循环的目的主要是为了找到信号在该类中的index。然后计算得出信号在该类中的偏移量,从前面的moc_xxx.cpp文件中我们可以看出,信号这些,在该文件中其实是被写成了一些数字(偏移量)。

紧接着调用了QObjectPrivate::connectImpl函数。

QMetaObject::Connection QObjectPrivate::connectImpl(const QObject *sender, int signal_index,
                                             const QObject *receiver, void **slot,
                                             QtPrivate::QSlotObjectBase *slotObj, Qt::ConnectionType type,
                                             const int *types, const QMetaObject *senderMetaObject)
{
    if (!sender || !receiver || !slotObj || !senderMetaObject) {
        const char *senderString = sender ? sender->metaObject()->className()
                                        : senderMetaObject ? senderMetaObject->className()	
                                        : "Unknown";
        const char *receiverString = receiver ? receiver->metaObject()->className()
                                            : "Unknown";
        qWarning("QObject::connect(%s, %s): invalid null parameter", senderString, receiverString);
        if (slotObj)
            slotObj->destroyIfLastRef();
        return QMetaObject::Connection();
    }

    QObject *s = const_cast<QObject *>(sender);
    QObject *r = const_cast<QObject *>(receiver);

    QOrderedMutexLocker locker(signalSlotLock(sender),
                            signalSlotLock(receiver));

    if (type & Qt::UniqueConnection && slot) {
        QObjectConnectionListVector *connectionLists = QObjectPrivate::get(s)->connectionLists;
        if (connectionLists && connectionLists->count() > signal_index) {
            const QObjectPrivate::Connection *c2 =
                (*connectionLists)[signal_index].first;

            while (c2) {
                if (c2->receiver == receiver && c2->isSlotObject && c2->slotObj->compare(slot)) {
                    slotObj->destroyIfLastRef();
                    return QMetaObject::Connection();
                }
                c2 = c2->nextConnectionList;
            }
        }
        type = static_cast<Qt::ConnectionType>(type ^ Qt::UniqueConnection);
    }

    QScopedPointer<QObjectPrivate::Connection> c(new QObjectPrivate::Connection);
    c->sender = s;
    c->signal_index = signal_index;
    c->receiver = r;
    c->slotObj = slotObj;
    c->connectionType = type;
    c->isSlotObject = true;
    if (types) {
        c->argumentTypes.store(types);
        c->ownArgumentTypes = false;
    }

    QObjectPrivate::get(s)->addConnection(signal_index, c.data());
    QMetaObject::Connection ret(c.take());
    locker.unlock();

    QMetaMethod method = QMetaObjectPrivate::signal(senderMetaObject, signal_index);
    Q_ASSERT(method.isValid());
    s->connectNotify(method);

    return ret;
}

上面的这个函数其实是很简单的,前面进行了参数有效性校验。中间有个if条件判断,主要是判断关联方式中是否有 Qt::UniqueConnection 标志位,如果有这个标志位,则遍历关联列表,判断是否已经存在了相同的关联,如果存在,则返回失败,否则将本次关联的 Qt::UniqueConnection 标志位设为有效。

后面构建了一个 QObjectPrivate::Connection 的对象,并将该对象放到关联列表中。

到此,connect关联信号和槽的部分就已经结束了。

2、emit 信号后,怎么调用槽函数

前面在看moc_xxx.cpp文件中的内容的时候,我们已经提到过,emit 信号后,实际上是调用了该文件中的对象的函数。

// SIGNAL 0
void MenuButton::signal_btnClicked(const QString & _t1, int _t2, bool & _t3)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)), const_cast<void*>(reinterpret_cast<const void*>(&_t3)) };
    QMetaObject::activate(this, &staticMetaObject, 0, _a);
}

// SIGNAL 1
void MenuButton::signal_btnClicked(const QString & _t1, int _t2)
{
    void *_a[] = { nullptr, const_cast<void*>(reinterpret_cast<const void*>(&_t1)), const_cast<void*>(reinterpret_cast<const void*>(&_t2)) };
    QMetaObject::activate(this, &staticMetaObject, 1, _a);
}

而上面的这两个函数,最终都调用了 QMetaObject::activate 这个函数,因此,搞明白这个函数是怎么运行的,也就成了我们搞明白槽函数是怎么执行的关键。

很多人可能在源码中找过这个函数,我相信很多人可能是没有找到的,说实话这个函数藏得是有点深。 这个函数其实是在源码的 qobject.cpp中的。同名函数有三个,最终都调用到了下面这个函数中。

/*!
    \internal
*/
void QMetaObject::activate(QObject *sender, int signalOffset, int local_signal_index, void **argv)
{
    int signal_index = signalOffset + local_signal_index;

    if (sender->d_func()->blockSig)
        return;

    Q_TRACE_SCOPE(QMetaObject_activate, sender, signal_index);

    if (sender->d_func()->isDeclarativeSignalConnected(signal_index)
            && QAbstractDeclarativeData::signalEmitted) {
        Q_TRACE_SCOPE(QMetaObject_activate_declarative_signal, sender, signal_index);
        QAbstractDeclarativeData::signalEmitted(sender->d_func()->declarativeData, sender,
                                                signal_index, argv);
    }

    if (!sender->d_func()->isSignalConnected(signal_index, /*checkDeclarative =*/ false)
        && !qt_signal_spy_callback_set.signal_begin_callback
        && !qt_signal_spy_callback_set.signal_end_callback) {
        // The possible declarative connection is done, and nothing else is connected, so:
        return;
    }

    void *empty_argv[] = { 0 };
    if (qt_signal_spy_callback_set.signal_begin_callback != 0) {
        qt_signal_spy_callback_set.signal_begin_callback(sender, signal_index,
                                                        argv ? argv : empty_argv);
    }

    {
    QMutexLocker locker(signalSlotLock(sender));
    struct ConnectionListsRef {
        QObjectConnectionListVector *connectionLists;
        ConnectionListsRef(QObjectConnectionListVector *connectionLists) : connectionLists(connectionLists)
        {
            if (connectionLists)
                ++connectionLists->inUse;
        }
        ~ConnectionListsRef()
        {
            if (!connectionLists)
                return;

            --connectionLists->inUse;
            Q_ASSERT(connectionLists->inUse >= 0);
            if (connectionLists->orphaned) {
                if (!connectionLists->inUse)
                    delete connectionLists;
            }
        }

        QObjectConnectionListVector *operator->() const { return connectionLists; }
    };
    ConnectionListsRef connectionLists = sender->d_func()->connectionLists;
    if (!connectionLists.connectionLists) {
        locker.unlock();
        if (qt_signal_spy_callback_set.signal_end_callback != 0)
            qt_signal_spy_callback_set.signal_end_callback(sender, signal_index);
        return;
    }

    const QObjectPrivate::ConnectionList *list;
    if (signal_index < connectionLists->count())
        list = &connectionLists->at(signal_index);
    else
        list = &connectionLists->allsignals;

    Qt::HANDLE currentThreadId = QThread::currentThreadId();

    do {
        QObjectPrivate::Connection *c = list->first;
        if (!c) continue;
        // We need to check against last here to ensure that signals added
        // during the signal emission are not emitted in this emission.
        QObjectPrivate::Connection *last = list->last;

        do {
            if (!c->receiver)
                continue;

            QObject * const receiver = c->receiver;
            const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId.load();

            // determine if this connection should be sent immediately or
            // put into the event queue
            if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
                || (c->connectionType == Qt::QueuedConnection)) {
                queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
                continue;
#if QT_CONFIG(thread)
            } else if (c->connectionType == Qt::BlockingQueuedConnection) {
                if (receiverInSameThread) {
                    qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
                    "Sender is %s(%p), receiver is %s(%p)",
                    sender->metaObject()->className(), sender,
                    receiver->metaObject()->className(), receiver);
                }
                QSemaphore semaphore;
                QMetaCallEvent *ev = c->isSlotObject ?
                    new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) :
                    new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore);
                QCoreApplication::postEvent(receiver, ev);
                locker.unlock();
                semaphore.acquire();
                locker.relock();
                continue;
#endif
            }

            QConnectionSenderSwitcher sw;

            if (receiverInSameThread) {
                sw.switchSender(receiver, sender, signal_index);
            }
            if (c->isSlotObject) {
                c->slotObj->ref();
                QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj);
                locker.unlock();

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.data());
                    obj->call(receiver, argv ? argv : empty_argv);
                }

                // Make sure the slot object gets destroyed before the mutex is locked again, as the
                // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool,
                // and that would deadlock if the pool happens to return the same mutex.
                obj.reset();

                locker.relock();
            } else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
                //we compare the vtable to make sure we are not in the destructor of the object.
                const int methodIndex = c->method();
                const int method_relative = c->method_relative;
                const auto callFunction = c->callFunction;
                locker.unlock();
                if (qt_signal_spy_callback_set.slot_begin_callback != 0)
                    qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
                    callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
                }

                if (qt_signal_spy_callback_set.slot_end_callback != 0)
                    qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex);
                locker.relock();
            } else {
                const int method = c->method_relative + c->method_offset;
                locker.unlock();

                if (qt_signal_spy_callback_set.slot_begin_callback != 0) {
                    qt_signal_spy_callback_set.slot_begin_callback(receiver,
                                                                method,
                                                                argv ? argv : empty_argv);
                }

                {
                    Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
                    metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
                }

                if (qt_signal_spy_callback_set.slot_end_callback != 0)
                    qt_signal_spy_callback_set.slot_end_callback(receiver, method);

                locker.relock();
            }

            if (connectionLists->orphaned)
                break;
        } while (c != last && (c = c->nextConnectionList) != 0);

        if (connectionLists->orphaned)
            break;
    } while (list != &connectionLists->allsignals &&
        //start over for all signals;
        ((list = &connectionLists->allsignals), true));

    }

    if (qt_signal_spy_callback_set.signal_end_callback != 0)
        qt_signal_spy_callback_set.signal_end_callback(sender, signal_index);
}

这个函数有180行,看着会比较吓人,其实如果你去掉一些参数校验或者其他一些无关痛痒的代码,剩下的也就没有多少了。就像在第一个 do{...}while()循环开始之前,前面的都是一些参数的校验和数据的准备,比如从类中拿到关联列表,比如获取获取当前线程ID等等。

上面这个函数有两个嵌套的 do{...}while()循环。第一个循环是在遍历关联列表,这个其实我们能已经在前面说过了,信号槽的缺点就是因为要遍历关联列表,所以他的效率比回调要慢10倍。

 do {
    QObjectPrivate::Connection *c = list->first;
    if (!c) continue;
    // We need to check against last here to ensure that signals added
    // during the signal emission are not emitted in this emission.
    QObjectPrivate::Connection *last = list->last;

    //...
} while (list != &connectionLists->allsignals &&
    //start over for all signals;
    ((list = &connectionLists->allsignals), true));

第二个 do{...}while() 中才对每个关联进行了单独的处理。

 if (!c->receiver)
        continue;

QObject * const receiver = c->receiver;
const bool receiverInSameThread = currentThreadId == receiver->d_func()->threadData->threadId.load();

// determine if this connection should be sent immediately or
// put into the event queue
if ((c->connectionType == Qt::AutoConnection && !receiverInSameThread)
    || (c->connectionType == Qt::QueuedConnection)) {
    queued_activate(sender, signal_index, c, argv ? argv : empty_argv, locker);
    continue;
} 

首先判断关联的方式,如果是自动关联并且 receiver 和 sender 不在同一个线程或者关联方式是队列关联,则直接调用 queued_activate 函数进行处理,该函数中的处理部分暂时先不看了,最终目的是创建一个事件,并且使用 QCoreApplication::postEvent 方法将该事件post 给 receiver 的事件队列中去。queued_activate 函数的部分代码如下:

static void queued_activate(QObject *sender, int signal, QObjectPrivate::Connection *c, void **argv, QMutexLocker &locker)
{
    //....
    
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal, nargs, types, args) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal, nargs, types, args);
    QCoreApplication::postEvent(c->receiver, ev);
}

接下来,如果关联方式是 Qt::BlockingQueuedConnection 方式,首先会判断 receiver 和 sender 是不是在同一个线程。

如果是在同一个线程中,则直接报错线程死锁。

如果不在一个线程中,则创建事件并将该事件post到receiver的事件队列中。

跟上面的队列关联是差不多的,区别是这种方式会阻塞 sender 线程一直到 receiver 线程槽函数执行完成。

else if (c->connectionType == Qt::BlockingQueuedConnection) {
    if (receiverInSameThread) {
        qWarning("Qt: Dead lock detected while activating a BlockingQueuedConnection: "
        "Sender is %s(%p), receiver is %s(%p)",
        sender->metaObject()->className(), sender,
        receiver->metaObject()->className(), receiver);
    }
    QSemaphore semaphore;
    QMetaCallEvent *ev = c->isSlotObject ?
        new QMetaCallEvent(c->slotObj, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore) :
        new QMetaCallEvent(c->method_offset, c->method_relative, c->callFunction, sender, signal_index, 0, 0, argv ? argv : empty_argv, &semaphore);
    QCoreApplication::postEvent(receiver, ev);
    locker.unlock();
    semaphore.acquire();
    locker.relock();
    continue;
}

我们从代码中可以看到,创建了一个 QSemaphore 的对象 semaphore,在 QCoreApplication::postEvent 函数调用之后,调用了 semaphore.acquire(); 函数。这样做的目的是什么? 其实 QSemaphore 是Qt中的信号量,功能类似于 C++中 mutex。

这样也就实现了线程的同步问题。同时也回答了我们在前面第四部分提出的问题。

接下来的代码,我们说完了自动关联和队列关联的方式,也就剩下了直接关联的方式,即 sender 和 receiver 在同一个线程中,过程也无非就是判断槽函数的类型,并且根据不同的类型选择合适的调用方式调用。

如果槽函数是槽函数对象,则找到槽函数的对象,并且调用他的 call 函数。

 if (c->isSlotObject) {
    c->slotObj->ref();
    QScopedPointer<QtPrivate::QSlotObjectBase, QSlotObjectBaseDeleter> obj(c->slotObj);
    locker.unlock();

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot_functor, obj.data());
        obj->call(receiver, argv ? argv : empty_argv);
    }

    // Make sure the slot object gets destroyed before the mutex is locked again, as the
    // destructor of the slot object might also lock a mutex from the signalSlotLock() mutex pool,
    // and that would deadlock if the pool happens to return the same mutex.
    obj.reset();

    locker.relock();
} 

如果是回调函数,并且满足方法的偏移在 接收者的方法偏移之内。则直接调用其回调函数。

else if (c->callFunction && c->method_offset <= receiver->metaObject()->methodOffset()) {
   //we compare the vtable to make sure we are not in the destructor of the object.
    const int methodIndex = c->method();
    const int method_relative = c->method_relative;
    const auto callFunction = c->callFunction;
    locker.unlock();
    if (qt_signal_spy_callback_set.slot_begin_callback != 0)
        qt_signal_spy_callback_set.slot_begin_callback(receiver, methodIndex, argv ? argv : empty_argv);

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, methodIndex);
        callFunction(receiver, QMetaObject::InvokeMetaMethod, method_relative, argv ? argv : empty_argv);
    }

    if (qt_signal_spy_callback_set.slot_end_callback != 0)
        qt_signal_spy_callback_set.slot_end_callback(receiver, methodIndex);
    locker.relock();
}
else {
   const int method = c->method_relative + c->method_offset;
    locker.unlock();

    if (qt_signal_spy_callback_set.slot_begin_callback != 0) {
        qt_signal_spy_callback_set.slot_begin_callback(receiver,
                                                    method,
                                                    argv ? argv : empty_argv);
    }

    {
        Q_TRACE_SCOPE(QMetaObject_activate_slot, receiver, method);
        metacall(receiver, QMetaObject::InvokeMetaMethod, method, argv ? argv : empty_argv);
    }

    if (qt_signal_spy_callback_set.slot_end_callback != 0)
        qt_signal_spy_callback_set.slot_end_callback(receiver, method);

    locker.relock();
}

虽然这些操作有不一样的调用方法,最终还是会调到 moc_xxx.cpp文件中的 int Object::qt_metacall(QMetaObject::Call _c, int _id, void **_a) 函数。

3、对象信号的屏蔽

前面我们已经看了信号的屏蔽,并且知道了屏蔽的其实是槽函数的执行,我们从上面的activate 函数中也能找到答案。在该函数的前几行有个 if 条件的判断。如果读取sender对象的 blockSig 状态为true。则直接返回,退出activate函数的执行。

if (sender->d_func()->blockSig)
    return;

中间可能有一些理解不对的地方,欢迎指出交流。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Qt 信号和槽机制 的相关文章

  • 属性对象什么时候创建?

    由于属性实际上只是附加到程序集的元数据 这是否意味着属性对象仅根据请求创建 例如当您调用 GetCustomAttributes 时 或者它们是在创建对象时创建的 或者 前两个的组合 在由于 CLR 的属性扫描而创建对象时创建 从 CLR
  • 自动从 C# 代码进行调试过程并读取寄存器值

    我正在寻找一种方法来读取某个地址的 edx 注册表 就像这个问题中所问的那样 读取eax寄存器 https stackoverflow com questions 16490906 read eax register 虽然我的解决方案需要用
  • 如何在 Unity 中从 RenderTexture 访问原始数据

    问题的简短版本 我正在尝试访问 Unity 中 RenderTexture 的内容 我一直在使用 Graphics Blit 使用自己的材质进行绘制 Graphics Blit null renderTexture material 我的材
  • 跨多个控件共享事件处理程序

    在我用 C 编写的 Windows 窗体应用程序中 我有一堆按钮 当用户的鼠标悬停在按钮上时 我希望按钮的边框发生变化 目前我有以下多个实例 每个按钮一个副本 private void btnStopServer MouseEnter ob
  • 将字符串从非托管代码传递到托管

    我在将字符串从非托管代码传递到托管代码时遇到问题 在我的非托管类中 非托管类 cpp 我有一个来自托管代码的函数指针 TESTCALLBACK FUNCTION testCbFunc TESTCALLBACK FUNCTION 接受一个字符
  • 如何在 WPF RichTextBox 中跟踪 TextPointer?

    我正在尝试了解 WPF RichTextBox 中的 TextPointer 类 我希望能够跟踪它们 以便我可以将信息与文本中的区域相关联 我目前正在使用一个非常简单的示例来尝试弄清楚发生了什么 在 PreviewKeyDown 事件中 我
  • 使用 C# 在 WinRT 中获取可用磁盘空间

    DllImport kernel32 dll SetLastError true static extern bool GetDiskFreeSpaceEx string lpDirectoryName out ulong lpFreeBy
  • c 中的错误:声明隐藏了全局范围内的变量

    当我尝试编译以下代码时 我收到此错误消息 错误 声明隐藏了全局范围内的变量 无效迭代器 节点 根 我不明白我到底在哪里隐藏或隐藏了之前声明的全局变量 我怎样才能解决这个问题 typedef node typedef struct node
  • 使用向量的 merge_sort 在少于 9 个输入的情况下效果很好

    不知何故 我使用向量实现了合并排序 问题是 它可以在少于 9 个输入的情况下正常工作 但在有 9 个或更多输入的情况下 它会执行一些我不明白的操作 如下所示 Input 5 4 3 2 1 6 5 4 3 2 1 9 8 7 6 5 4 3
  • 使用安全函数在 C 中将字符串添加到字符串

    我想将文件名复制到字符串并附加 cpt 但我无法使用安全函数 strcat s 来做到这一点 错误 字符串不是空终止的 我确实设置了 0 如何使用安全函数修复此问题 size strlen locatie size nieuw char m
  • 是否有比 lex/flex 更好(更现代)的工具来生成 C++ 分词器?

    我最近将源文件解析添加到现有工具中 该工具从复杂的命令行参数生成输出文件 命令行参数变得如此复杂 以至于我们开始允许它们作为一个文件提供 该文件被解析为一个非常大的命令行 但语法仍然很尴尬 因此我添加了使用更合理的语法解析源文件的功能 我使
  • .NET 选项将视频文件流式传输为网络摄像头图像

    我有兴趣开发一个应用程序 它允许我从 xml 构建视频列表 包含视频标题 持续时间等 并将该列表作为我的网络摄像头流播放 这意味着 如果我要访问 ustream tv 或在实时通讯软件上激活我的网络摄像头 我的视频播放列表将注册为我的活动网
  • 可空属性与可空局部变量

    我对以下行为感到困惑Nullable types class TestClass public int value 0 TestClass test new TestClass Now Nullable GetUnderlyingType
  • 什么是 C 语言的高效工作流程? - Makefile + bash脚本

    我正在开发我的第一个项目 该项目将跨越多个 C 文件 对于我的前几个练习程序 我只是在中编写了我的代码main c并使用编译gcc main c o main 当我学习时 这对我有用 现在 我正在独自开展一个更大的项目 我想继续自己进行编译
  • 在 URL 中发送之前对特殊字符进行百分比编码

    我需要传递特殊字符 如 等 Facebook Twitter 和此类社交网站的 URL 为此 我将这些字符替换为 URL 转义码 return valToEncode Replace 21 Replace 23 Replace 24 Rep
  • 将日期参数传递给对 MVC 操作的 ajax 调用的安全方法

    我有一个 MVC 操作 它的参数之一是DateTime如果我通过 17 07 2012 它会抛出一个异常 指出参数为空但不能有空值 但如果我通过01 07 2012它被解析为Jan 07 2012 我将日期传递给 ajax 调用DD MM
  • ListDictionary 类是否有通用替代方案?

    我正在查看一些示例代码 其中他们使用了ListDictionary对象来存储少量数据 大约 5 10 个对象左右 但这个数字可能会随着时间的推移而改变 我使用此类的唯一问题是 与我所做的其他所有事情不同 它不是通用的 这意味着 如果我在这里
  • Bing 地图运行时错误 Windows 8.1

    当我运行带有 Bing Map 集成的 Windows 8 1 应用程序时 出现以下错误 Windows UI Xaml Markup XamlParseException 类型的异常 发生在 DistanceApp exe 中 但未在用户
  • 如何使用 ReactiveList 以便在添加新项目时更新 UI

    我正在创建一个带有列表的 Xamarin Forms 应用程序 itemSource 是一个reactiveList 但是 向列表添加新项目不会更新 UI 这样做的正确方法是什么 列表定义 listView new ListView var
  • 不同类型的指针可以互相分配吗?

    考虑到 T1 p1 T2 p2 我们可以将 p1 分配给 p2 或反之亦然吗 如果是这样 是否可以不使用强制转换来完成 或者我们必须使用强制转换 首先 让我们考虑不进行强制转换的分配 C 2018 6 5 16 1 1 列出了简单赋值的约束

随机推荐

  • Ubuntu 系统中安装htpasswd

    htpasswd是Apache附带的程序 htpasswd生成包含用户名和密码的文本文件 每行内容格式为 用户名 密码 用于用户文件的基本身份认证 当用户浏览某些网页的时候 浏览器会提示输入用户名和密码 比如awstats的日志报表 你肯定
  • LightGBM 直方图优化算法

    给出下面这个广泛使用 直方图优化算法的ppt 本文是对该张ppt的解释 直方图优化算法需要在训练前预先把特征值转化为bin 也就是对每个特征的取值做个分段函数 将所有样本在该特征上的取值划分到某一段 bin 中 最终把特征取值从连续值转化成
  • 两种类型的变量

    在java中你会像下面那样声明变量 String s Hello int i 42 Person p new Person hello 每个变量声明都包含了类型 相比之下 在scala中有两种类型的变量 val创建一个不可变的变量 跟jav
  • 【计算机网络】湖科大微课堂笔记 p33-35 MAC地址、IP地址以及ARP协议

    MAC地址 为什么要有MAC地址 原因如图 MAC地址与帧 MAC地址也被称为物理地址 硬件地址 因为它被固化在网卡上 总览 IEEE 802局域网的MAC地址格式 MAC地址发送顺序 举例 单播 广播 多播 单播 主机B想给C发一个单播帧
  • hive get_json_object json_tuple json解析详解

    1 hive中处理json的两个函数 json是常见的数据接口形式 实际中使用也很广泛 下面我们看看怎么在hive中解析json格式 hive中常用的解析json格式的函数有两个 先看看get json object gt desc fun
  • 【Golang】数据结构-slice类型

    slice底层数据结构 一个指针 指向内存地址 len存储当前的内存使用量 cap存储预留的内存最大容量 type slice struct array unsafe Pointer len int cap int 新建slice make
  • Hugging Face 中文预训练模型使用介绍及情感分析项目实战

    Hugging Face 中文预训练模型使用介绍及情感分析项目实战 Hugging Face 一直致力于自然语言处理NLP技术的平民化 democratize 希望每个人都能用上最先进 SOTA state of the art 的NLP技
  • Eigen库中的Map类到底是做什么的?

    转自 https www zhihu com question 43571898 answer 95934049Map类用于通过C 中普通的连续指针或者数组 raw C C arrays 来构造Eigen里的Matrix类 这就好比Eige
  • LLM大模型推理加速 vLLM;Qwen vLLM使用案例;模型生成速度吞吐量计算

    参考 https github com vllm project vllm https zhuanlan zhihu com p 645732302 https vllm readthedocs io en latest getting s
  • 信号与系统(10)- 周期性信号的频谱

    周期性函数可以在傅里叶级数中展开 也就是说 如果给定了各个频率分量的幅度和相位 则可以确定原信号 频谱是信号的一种图形表示方法 它将各个频率分量上的系数关系用图形的方法表示出来 用来说明信号的特性 并且后续可以给信号处理带来很多便利 频谱图
  • VUE table 列宽度百分比自适应

    由于原来项目中 前台列表页面 使用具体数值的列的宽度 有的电脑分辨率问题造成最右侧的需要拉滚动条 考虑到使用更简单 合理 改成百分比 原代码
  • 人体2D关键点检测 文章+代码(2020-2016)

    目录 2020 HigherHRNet CVPR2020 MSRA微软亚洲研究院 bottom up DarkPose CVPR2020 2nd place entry of COCO Keypoints Challenge ICCV 20
  • STRIDE威胁建模(面向安全应用程序开发的威胁分析框架)

    STRIDE 威胁建模 STRIDE 威胁模型由Microsoft安全研究人员于 1999 年创建 是一种以开发人员为中心的威胁建模方法 通过此方法可识别可能影响应用程序的威胁 攻击 漏洞 进而设计对应的缓解对策 以降低安全风险并满足公司的
  • Uncaught TypeError: Cannot read property 'offsetTop' of null

    在获取内容区块高度 无滚动 时使用offsetTop报如下错误 Uncaught TypeError Cannot read property offsetTop of null 实现效果如图 主要js代码 内容可视区域的高度 client
  • nacos怎么开启账号密码登录

    Nacos 默认是不启用账号密码登录的 但你可以通过修改配置来启用账号密码登录以增强安全性 以下是在 Nacos 中启用账号密码登录的步骤 打开 Nacos 配置文件 nacos conf application properties 在文
  • 如何使VMware虚拟机下的虚拟机可以相互连接又可以访问外网

    由于要练习搭建Hadoop集群 所以不得不在自己的电脑上搭建几台虚拟机试试 这里博主打算搭建一台有界面的Ubuntu系统 麒麟系统 以及几台服务器版的Ubuntu系统 通过带界面的系统进行ssh远程控制去操作其他的几台虚拟机 带界面的是国产
  • ajax post 请求 一直提示 404 not found textStatus error

    2019独角兽企业重金招聘Python工程师标准 gt gt gt ajax post 请求 一直提示 404 not found textStatus error var dataParams schoolSupplierIds scho
  • 虚拟机的防火墙设置指令

    查看防火墙服务状态 systemctl status firewalld 开启防火墙 service firewalld start 关闭防火墙 service firewalld stop 重启防火墙 service firewalld
  • 权限管理02-前台左侧菜单栏实现(根据用户获取菜单树)

    实现技术 vue element ui 1 后台管理主页布局
  • Qt 信号和槽机制

    这篇文章篇幅很长 阅读可能需要10分钟以上 如果你是Qt的初学者 前面的6个章节就已经够用了 至少能够让你在一些普通的场面撑得起场子 但如果你想了解的更深一点 最后一个章节是必不可少的内容 Qt提供了很多我们学习的文档 甚至是源码 阅读源码