Qt信号槽技术小结

 一、connect:String-based和Functor-based写法比较

1.1 概述

从Qt 5.0开始,Qt、C++支持两种信号槽connect写法:string-based、functor-based。

示例代码:

1 ClassA *pClassA = new ClassA();
2 Classb *pClassB = new ClassB();
3 
4 /* string-based */
5 connect(pClassA, SIGNAL(signalA(int)), pClassB, SLOT(slotB(int)));
6
7 /* functor -based*/
8 connect(pClassA, &ClassA::signalA, pClassB, &ClassB::slotB);

下面列表概述了两者的优缺点:

  String-based Functor-based
类型检查时机 运行时 编译时
隐式类型转换   Yes
连接signals和lambda表达式   Yes
连接signals和包含更多参数的slots(使用默认参数) Yes  
连接C++函数和QML函数 Yes  

1.2 类型检查、隐式转换

string-based连接通过运行时比较字符串作类型检查,因此有以下缺点:

  1. 连接错误只能在程序开始运行后提示;
  2. 信号槽之间无法作隐式类型转换;
  3. 无法处理typdefs和namespaces。

1、2是因为字符串比较无法获取C++类型信息,因此string-based依赖于直接用字符串匹配。

与string-based相反,functor-based通过编译时检查,支持可比较类型的隐式转换,并且可以识别同种类型的不同名称。 

示例代码: 

1 auto slider = new QSlider(this);
2 auto doubleSpinBox = new QDoubleSpinBox(this);
3 
4 /* OK: The compiler can convert an int into a double */
5 connect(slider, &QSlider::valueChanged, doubleSpinBox, &QDoubleSpinBox::setValue);
6 
7 /* ERROR: The string table doesn't contain conversion information */
8 connect(slider, SIGNAL(valueChanged(int)), doubleSpinBox, SLOT(setValue(double)));

上述例子中,演示了functor-based连接了参数为int的信号和参数为double的槽函数。

注意:string-based信号槽类型不匹配connect不起效。应用程序输出中可以看到以下错误提示:

QObject::connect: Incompatible sender/receiver arguments

QSlider::valueChanged(int) --> QDoubleSpinBox::setValue(double)

 

下面代码说明string-based连接无法处理同一个类型用不同名称表示的情况。比如,

QAudioInput::stateChanged()声明的时候参数类型是“QAudio::State”,string-based连接要求connect时必须指定“QAudio::State”,而不能是“State”。functor-based连接由于连接时无需指定参数类型,因此不存在这种问题。

示例代码:

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()));
 

1.3 连接lambda表达式

functor-based写法支持C++11的lambda表达式,可以写出高效、内联的槽函数。

string-based写法不支持上述特性。

下面以一个名叫TextSender的类为例。

示例代码:

TextSender.h

 1 class TextSender : public QWidget
 2 {
 3       Q_OBJECT
 4 
 5       QLineEdit *lineEdit;
 6       QPushButton *button;
 7 
 8 signals:
 9       void textCompleted(const QString& text) const;
10 
11 public:
12       TextSender(QWidget *parent = nullptr);
13 };

TextSender.cpp

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表达式就可以相对容易地“connect”两者。

注意:虽然functor-based接收所有指向函数的指针,但是Qt中signals只能connect到slots、lambda表达式和其它signals。

1.4 连接C++对象和QML对象

因为QML类型是运行时处理的,而非在C++编译时,所以无法应用到functor-based连接。

下面演示了点击QML对象(C++对象),使得C++对象(QML对象)打印消息。

示例代码:

QmlGui.qml

 1 Rectangle {
 2       width: 100; height: 100
 3 
 4       signal qmlSignal(string sentMsg)
 5       function qmlSlot(receivedMsg) {
 6           console.log("QML received: " + receivedMsg)
 7       }
 8 
 9       MouseArea {
10           anchors.fill: parent
11           onClicked: qmlSignal("Hello from QML!")
12       }
13   }

.h(C++)

 1 class CppGui : public QWidget {
 2       Q_OBJECT
 3 
 4       QPushButton *button;
 5 
 6   signals:
 7       void cppSignal(const QVariant& sentMsg) const;
 8 
 9   public slots:
10       void cppSlot(const QString& receivedMsg) const {
11           qDebug() << "C++ received:" << receivedMsg;
12       }
13 
14   public:
15       CppGui(QWidget *parent = nullptr) : QWidget(parent) {
16           button = new QPushButton("Click Me!", this);
17           connect(button, &QPushButton::clicked, [=] {
18               emit cppSignal("Hello from C++!");
19           });
20       }
21   };

.cpp

1 auto cppObj = new CppGui(this);
2 auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
3 auto qmlObj = quickWidget->rootObject();
4 
5 /* Connect QML signal to C++ slot */
6 connect(qmlObj, SIGNAL(qmlSignal(QString)), cppObj, SLOT(cppSlot(QString)));
7 
8 /* Connect C++ signal to QML slot */
9 connect(cppObj, SIGNAL(cppSignal(QVariant)), qmlObj, SLOT(qmlSlot(QVariant)));

QML中的所有JavaScript函数的参数类型都是var,对应C++中的QVariant。

1.5 连接signals和包含更多参数的slots(使用默认参数)

通常情况下,connect的slot参数数量小于等于signal,且所有参数类型都得是相容的。

示例代码:

1 /* signal和slot参数数目相同 */
2 connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1, int i1)));
3 
4 /* slot参数比signal少 */
5 connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1)));

注意:slot参数比signal少,必须是后边的参数缺省,不能是前面的或者中间的;string-based要求的参数匹配,必须类型完全一致,若不一致,即使是QVariant也不行,否则都会运行时提示连接错误,不生效。

string-based连接写法支持一种场景:

当slot有默认参数时,signal可以省略这些默认参数;

当emit省略部分参数的signal时,slot会用默认参数代替省略部分。

相反,functor-based写法不支持上述场,不过functor-based可以通过lambda表达式实现相同的效果。

.h

1 public slots:
2 /* 带默认参数的槽函数 */
3 void printNumber(int number = 42)
4 {
5   qDebug() << "Lucky number" << number;
6 }

.cpp

1 DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent)
2 {
3 
4 /* OK: printNumber() 会传入默认参数 42 */
5 connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(printNumber()));
6 
7 /* 编译报错: 编译器需要相容的参数 */
8 connect(qApp, &QCoreApplication::aboutToQuit, this, &DemoWidget::printNumber);
9 }

1.6 连接重载的信号和槽

由于string-based写法要求指明参数类型,因此可以用于连接重载的信号和槽。

例如连接以下信号和槽:

信号:

QSlider::valueChanged()

槽:

QLCDNumber::display(int)
QLCDNumber::display(double)
QLCDNumber::display(QString)

string-based连接写法:

1 auto slider = new QSlider(this);
2 auto lcd = new QLCDNumber(this);
3 
4 /* String-based syntax */
5 connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));

 换作functor-based写法,得这样:

 1 /* 方法一 */
 2 connect(slider, &QSlider::valueChanged, lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));
 3 
 4 /* 方法二 */
 5 void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
 6 connect(slider, &QSlider::valueChanged, lcd, mySlot);
 7 
 8 /* 方法三 */
 9 connect(slider, &QSlider::valueChanged, lcd, QOverload<int>::of(&QLCDNumber::display));
10 
11 /* 方法四 (需要C++14) */
12 connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));

1.7 参考资料

Qt Assistant搜索 Differences between String-Based and Functor-Based Connections

 

posted @ 2020-09-02 07:53  冯耀耀  阅读(542)  评论(0编辑  收藏  举报