基于字符串与基于函数连接之间的差异

Qt提供了两种在C++中编写信号和槽连接的方法:基于字符串的连接语法和基于函数对象的连接语法。这两种语法各有利弊。下表总结了它们之间的差异。

基于字符串基于函数对象
类型检查是在...运行时编译时
可以执行隐式类型转换
可以将信号连接到lambda表达式
可以将信号连接到比信号参数更多的槽(使用默认参数)
可以将C++函数连接到QML函数

以下各节将详细解释这些差异,并演示如何使用每个连接语法的独特功能。

类型检查和隐式类型转换

基于字符串的连接在运行时通过比较字符串进行类型检查。这种方法存在三个局限性

  1. 连接错误只能在程序开始运行之后检测到。
  2. 无法在信号和槽之间进行隐式转换。
  3. 无法解析typedef和命名空间。

局限性2和3存在,因为字符串比较器无法访问C++类型信息,所以它依赖于精确的字符串匹配。

相比之下,基于函数对象的连接由编译器检查。编译器在编译时捕获错误,在兼容的类型之间启用隐式转换,并识别同一类型的不同名称。

例如,只有基于函数对象的语法可以用来将一个传递int类型的信号连接到一个接收double类型的槽。一个QSlider保留一个int值,而一个QDoubleSpinBox保留一个double值。以下代码片段显示了如何使它们保持同步

    auto slider = new QSlider(this);
    auto doubleSpinBox = new QDoubleSpinBox(this);

    // OK: The compiler can convert an int into a double
    connect(slider, &QSlider::valueChanged,
            doubleSpinBox, &QDoubleSpinBox::setValue);

    // ERROR: The string table doesn't contain conversion information
    connect(slider, SIGNAL(valueChanged(int)),
            doubleSpinBox, SLOT(setValue(double)));

以下示例说明缺少名称解析。QAudioInput::stateChanged()的参数声明为"QAudio::State"类型。因此,基于字符串的连接还必须指定"QAudio::State",即使"State"已经可见。这个问题不适用于基于函数对象的连接,因为参数类型不是连接的一部分。

    auto audioInput = new QAudioInput(QAudioFormat(), this);
    auto widget = new QWidget(this);

    // OK
    connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
            widget, SLOT(show()));

    // ERROR: The strings "State" and "QAudio::State" don't match
    using namespace QAudio;
    connect(audioInput, SIGNAL(stateChanged(State)),
            widget, SLOT(show()));

    // ...

将连接到Lambda表达式

基于函数对象的连接语法可以将信号连接到C++11 lambda表达式,这实际上是内联槽。这个特性在基于字符串的语法中是不可用的。

在以下示例中,TextSender类在用户点击按钮时发出一个携带QString参数的textCompleted()信号。这里是类声明:

class TextSender : public QWidget {
    Q_OBJECT

    QLineEdit *lineEdit;
    QPushButton *button;

signals:
    void textCompleted(const QString& text) const;

public:
    TextSender(QWidget *parent = nullptr);
};

以下是发出TextSender::textcompleted()的连接,当用户点击按钮时

TextSender::TextSender(QWidget *parent) : QWidget(parent) {
    lineEdit = new QLineEdit(this);
    button = new QPushButton("Send", this);

    connect(button, &QPushButton::clicked, [=] {
        emit textCompleted(lineEdit->text());
    });

    // ...
}

在这个例子中,即使QPushButton::clicked()和TextSender::textCompleted()具有不兼容的参数,lambda函数仍然使连接变得简单。相比之下,基于字符串的实现将需要更多的样板代码。

注意:基于函数对象的连接语法接受指向所有函数的指针,包括独立的函数和常规成员函数。然而,为了可读性,应仅将信号连接到槽、lambda表达式和其他信号。

连接C++对象到QML对象

基于字符串的语法可以将C++对象连接到QML对象,但不能使用基于函数对象的语法。这是因为QML类型在运行时解析,因此它们不能在C++编译器中使用。

以下示例中,点击QML对象将使C++对象打印一条消息,反之亦然。以下是QML类型(在QmlGui.qml中)

Rectangle {
    width: 100; height: 100

    signal qmlSignal(string sentMsg)
    function qmlSlot(receivedMsg) {
        console.log("QML received: " + receivedMsg)
    }

    MouseArea {
        anchors.fill: parent
        onClicked: qmlSignal("Hello from QML!")
    }
}

以下是C++类

class CppGui : public QWidget {
    Q_OBJECT

    QPushButton *button;

signals:
    void cppSignal(const QVariant& sentMsg) const;

public slots:
    void cppSlot(const QString& receivedMsg) const {
        qDebug() << "C++ received:" << receivedMsg;
    }

public:
    CppGui(QWidget *parent = nullptr) : QWidget(parent) {
        button = new QPushButton("Click Me!", this);
        connect(button, &QPushButton::clicked, [=] {
            emit cppSignal("Hello from C++!");
        });
    }
};

以下是实现信号-槽连接的代码

    auto cppObj = new CppGui(this);
    auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
    auto qmlObj = quickWidget->rootObject();

    // Connect QML signal to C++ slot
    connect(qmlObj, SIGNAL(qmlSignal(QString)),
            cppObj, SLOT(cppSlot(QString)));

    // Connect C++ signal to QML slot
    connect(cppObj, SIGNAL(cppSignal(QVariant)),
            qmlObj, SLOT(qmlSlot(QVariant)));

注意:QML中的所有JavaScript函数都接受var类型的参数,这映射到C++中的QVariant类型,除非它们使用类型注解。有关详细信息,请参阅调用QML方法

QPushButton被点击时,控制台打印“QML接收:'从C++发出的Hello!'”。同样,当矩形被点击时,控制台打印“C++接收:'从QML发出的Hello!'”

有关从C++对象与QML对象交互的其他方法,请参阅从C++与QML对象交互

在槽中使用默认参数来连接参数较少的信号

通常,如果槽具有与信号相同的参数数量(或更少),并且所有参数类型兼容,则可以建立连接。

基于字符串的连接语法为此规则提供了一个解决方案:如果槽具有默认参数,则可以省略信号中的这些参数。当信号发出参数数量少于槽时,Qt使用默认参数值运行槽。

基于函数对象的连接不支持此功能。

假设有一个名为DemoWidget的类,它有一个具有默认参数的槽printNumber()

public slots:
    void printNumber(int number = 42) {
        qDebug() << "Lucky number" << number;
    }

使用基于字符串的连接,即使QApplication::aboutToQuit没有参数,也可以将DemoWidget::printNumber()连接到它。使用基于函数对象的连接将产生编译时错误

DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) {

    // OK: printNumber() will be called with a default value of 42
    connect(qApp, SIGNAL(aboutToQuit()),
            this, SLOT(printNumber()));

    // ERROR: Compiler requires compatible arguments
    connect(qApp, &QCoreApplication::aboutToQuit,
            this, &DemoWidget::printNumber);
}

为了使用基于函数对象的语法解决这个问题,将信号连接到调用槽的lambda函数。请参阅上面的部分,将连接添加到lambda表达式

选择重载的信号和槽

使用基于字符串的语法,显式指定参数类型。因此,重载信号或槽的预期实例是无歧义的。

相比之下,使用基于函数对象的语法,必须进行类型转换以告知编译器使用哪个实例。

例如,QLCDNumber有三个版本的display()

  1. QLCDNumber::display(int)
  2. QLCDNumber::display(double)
  3. QLCDNumber::display(QString)

要连接到int版本的QSlider::valueChanged,两种语法都是

    auto slider = new QSlider(this);
    auto lcd = new QLCDNumber(this);

    // String-based syntax
    connect(slider, SIGNAL(valueChanged(int)),
            lcd, SLOT(display(int)));

    // Functor-based syntax
    connect(slider, &QSlider::valueChanged,
            lcd, qOverload<int>(&QLCDNumber::display));

另请参阅:qOverload

© 2024 Qt公司有限公司。本文件中包含的文档贡献均为各自所有者的版权。提供的文档受由自由软件基金会发布的GNU自由文档许可证版本1.3条款许可。Qt及其相关标志为芬兰和/或其他国家的Qt公司商标。所有其他商标均为各自所有者的财产。