Qt信号槽connect用法整理

本文整理了一些当前项目中使用的Qt5.6版本信号、槽connect新旧写法的比较、需要注意的问题。

一、connect:string-based和functor-based写法比较

1.1 概述

自Qt 5.0以来,Qt提供了两种C++信号槽connect的写法:string-based和functor-based。
示例代码:

ClassA *pClassA = new ClassA();
Classb *pClassB = new ClassB();

/* functor -based*/
connect(pClassA, &ClassA::signalA, pClassB, &ClassB::slotB);

/* string-based */
connect(pClassA, SIGNAL(signalA(int)), pClassB, SLOT(slotB(int)));

两种connect写法各有优缺点,下表概述了它们的差异:

string-basedfunctor-based
类型检查时机运行时编译时
隐式类型转换Yes
连接signals和lambda表达式Yes
连接signals到更多参数的slots(使用默认参数)Yes
连接C++函数和QML函数Yes
当对象的signal和slot重载时写法简单复杂

1.2 类型检查和隐式转换

string-based的类型检查是在运行时做的,因此有以下限制:

  1. connect错误只有在程序运行起来后才会提示
  2. 无法在signals和slots之间进行类型隐式转换
  3. 无法处理typedefs、namespaces

1、2是因为字符串比较无法获取C++类型信息,因此string-based依赖于直接用字符串匹配。
与string-based相反,functor-based通过编译时检查,支持可比较类型的隐式转换,并且可以识别同种类型的不同名称。
示例代码:

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

上述例子中,演示了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

class TextSender : public QWidget
{
      Q_OBJECT

      QLineEdit *lineEdit;
      QPushButton *button;

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

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

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

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!")
      }
  }

.h(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++!");
          });
      }
  };

.cpp

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。

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

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

示例代码:

/* signal和slot参数数目相同 */
connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1, int i1)));

 /* slot参数比signal少 */
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

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

.cpp

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

/* OK: printNumber() 会传入默认参数 42 */
connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(printNumber()));

/* 编译报错: 编译器需要相容的参数 */
connect(qApp, &QCoreApplication::aboutToQuit, this, &DemoWidget::printNumber);
}

1.6 连接重载的信号和槽

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

例如连接以下信号和槽:

信号:

QSlider::valueChanged()

槽:

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

string-based连接写法

示例代码:

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

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

换作functor-based写法,得这样
示例代码:

/* 方法一 */
connect(slider, &QSlider::valueChanged, lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));

/* 方法二 */
void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
connect(slider, &QSlider::valueChanged, lcd, mySlot);

/* 方法三 */
connect(slider, &QSlider::valueChanged, lcd, QOverload<int>::of(&QLCDNumber::display));

/* 方法四 (需要C++14) */
connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));

1.7 参考资料

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

二、connect:第五个参数

Qt :: ConnectionType 是枚举类型,描述了信号和槽的连接类型。它决定了信号是立即传送到槽,还是排队等待稍后传送。

常量描述
Qt::AutoConnection0(默认)如果信号是从与接收对象不同的线程发出的,则信号将排队,表现为Qt :: QueuedConnection。否则,直接调用槽函数,表现为Qt :: DirectConnection。发射信号时确定连接类型。
Qt::DirectConnection1发出信号时直接调用槽函数,即直接函数调用。
Qt::QueuedConnection2当控制返回到接收者线程的事件循环时,将调用该槽。槽函数在接收者所在的线程中执行。
Qt::BlockingQueuedConnection4与QueuedConnection类似,但会在当前线程阻塞,直到槽返回。此连接类型仅应在发射者和接收者位于不同线程中的情况下使用。

三、信号槽connect应注意的问题

1、有三种情况必须使用 disconnect() 函数

1、断开与某个对象相关联的任何对象。这似乎有点不可理解,事实上,当我们在某个对象中定义了一个或者多个信号,这些信号与另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就可以利用这个方法,非常之简洁。

  disconnect( myObject, 0, 0, 0 )

或者

  myObject->disconnect()

2、断开与某个特定信号的任何关联。

  disconnect( myObject, SIGNAL(mySignal()), 0, 0 )

或者

  myObject->disconnect( SIGNAL(mySignal()) )

3、断开两个对象之间的关联。

  disconnect( myObject, 0, myReceiver, 0 )

或者

  myObject->disconnect(  myReceiver )

在disconnect函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者 sender 不能为 0,其它三个参数的值可以等于 0。

2、宏定义不能用在 signal 和 slot 的参数中

既然 moc 工具不扩展 #define,因此,在 signals 和 slots 中携带参数的宏就不能正确地工作,如果不带参数是可以的。例如,下面的例子中将带有参数的宏 SIGNEDNESS(a) 作为信号的参数是不合语法的:

#ifdef ultrix 
#define SIGNEDNESS(a) unsigned a 
#else 
#define SIGNEDNESS(a) a 
#endif 
class Whatever : public QObject 
{ 
[...] 
signals: 
void someSignal( SIGNEDNESS(a) ); 
[...] 
};

3、构造函数不能用在 signals 或者 slots 声明区域内

的确,将一个构造函数放在 signals 或者 slots 区内有点不可理解,无论如何,不能将它们放在 private slots、protected slots 或者 public slots 区内。下面的用法是不合语法要求的:

class SomeClass : public QObject 
{ 
    Q_OBJECT 
public slots: 
    SomeClass( QObject *parent, const char *name ) 
        : QObject( parent, name ) {}  /* 在槽声明区内声明构造函数不合语法 */
};

4、函数指针不能作为信号或槽的参数

例如,下面的例子中将 void (applyFunction)(QList, void*) 作为参数是不合语法的:

class someClass : public QObject 
{ 
    Q_OBJECT

public slots: 
    void apply(void (*applyFunction)(QList*, void*), char*); /* 不合语法 */
};

你可以采用下面的方法绕过这个限制:

typedef void (*ApplyFunctionType)(QList*, void*); 
class someClass : public QObject 
{ 
    Q_OBJECT

public slots: 
void apply( ApplyFunctionType, char *); 
};

5、 信号与槽不能有缺省参数

既然signal->slot绑定是发生在运行时刻,那么,从概念上讲使用缺省参数是困难的。下面的用法是不合理的:

class SomeClass : public QObject 
{ 
    Q_OBJECT 
public slots: 
void someSlot(int x=100); /* 将 x 的缺省值定义成 100,在槽函数声明中使用是错误的 */
};

6、信号与槽也不能携带模板类参数

如果将信号、槽声明为模板类参数的话,即使 moc 工具不报告错误,也不可能得到预期的结果。 例如,下面的例子中当信号发射时,槽函数不会被正确调用:

public slots: 
void MyWidget::setLocation (pair<int,int> location); 

public signals: 
void MyObject::moved (pair<int,int> location);

但是,你可以使用 typedef 语句来绕过这个限制。如下所示:

typedef pair<int,int> IntPair; 

public slots: 
void MyWidget::setLocation (IntPair location); 

public signals: 
void MyObject::moved (IntPair location);

这样使用的话,你就可以得到正确的结果。

7、嵌套的类不能位于信号或槽区域内,也不能有信号或者槽

例如,下面的例子中,在 class B 中声明槽 b() 是不合语法的,在信号区内声明槽 b() 也是不合语法的。

class A 
{ 
    Q_OBJECT 
public: 
    class B 
   { 
    public slots:
    /* 在嵌套类中声明槽不合语法 */
        void b(); 
    }; 
signals: 
    class B 
   { 
    /* 在信号区内声明嵌套类不合语法 */
    void b(); 
    }: 
};

8、友元声明不能位于信号或者槽声明区内

相反,它们应该在普通 C++ 的 private、protected 或者 public 区内进行声明。下面的例子是不合语法规范的:

class someClass : public QObject 
{ 
    Q_OBJECT 

signals: /* 信号定义区 */
friend class ClassTemplate; /* 此处定义不合语法 */
};

9、QT重复connect出现重复调用现象(connect N次,则每次emit都会执行N次SLOT函数)

修改方法:

 connect(ui.messageButton,SIGNAL(pressed()),this,SLOT(on_messageButton_clicked()));

更改为:

connect(ui.messageButton,SIGNAL(pressed()),this,SLOT(on_messageButton_clicked()),Qt::UniqueConnection);

Qt::UniqueConnection可以防止重复连接。如果当前信号和槽已经连接过了,就不再连接了。

四、Qt VS Tools 信号槽常见问题

1、信号、槽修改不同步运行时connect警告

经常会遇到写完信号、槽绑定后,又因为需求变更修改signal、slot函数。倘若没同步修改connect代码,Qt会输出如下警告。这种情况不处理经常导致一些致命bug。
在这里插入图片描述

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值