2、信号与槽的用法
1、信号与槽的关联方式
在Qt中使用信号和槽机制来完成对象之间的协同操作。
简单来说,信号和槽都是函数,比如按下窗口上的一个按钮后想要弹出一个对话框,那么就可以将这个按钮的单击信号和我们定义的槽关联起来,在这个槽中可以创建一个对话框,并且显示它。这样,当单击这个按钮时就会发射信号,进而执行我们的槽来显示一个对话框。
信号与槽的关联方式有3种:使用connect()关联、在设计模式关联、自动关联。
1.1、使用connect()关联
mywidget.h文件写上槽的声明 :
public slots://槽一般使用slots加以修饰,这里使用public slots加以修饰表面这个槽在类外可以被调用 void showChildDialog();
在mywidget.cpp文件中将槽的实现 :
void MyWidget::showChildDialog() { QDialog *dialog = new QDialog(this); dialog->show(); }
在mywidget.cpp文件的MyWidget类的构造函数中使用connect()关联按钮单击信号和自定义的槽如下:
connect(ui->showChildButton, &QPushButton::clicked,this, &MyWidget::showChildDialog);
这里自定义了一个槽,槽一般使用slots关键字进行修饰(Qt 4中必须使用slots关键字进行修饰,Qt 5使用新connect语法时可以不用,为了与一般函数进行区别,建议使用),这里使用了public slots,表明这个槽可以在类外被调用。
clicked()信号在QPushButton类中进行了定义,而connect()是QObject类中的函数,因为我们的类继承自QObject,所以可以直接使用它。
connect()函数中的四个参数分别是:发送信号的对象、发送的信号、接收信号的对象和要执行的槽。
(1)使用信号与槽函数关联
Qt5中对 Qt的信号和槽机制进行了很大的优化,使其更加易于使用。例如,某个类在值改变时发射 valueChanged(QString,QString)信号,需要在槽 showValue(QString)中对改变的值做相应的处理。Qt4中一般这样来关联信号和槽:
connect(sender,SIGNAL(valueChanged(OString,OString)),receiver,SL0T(showValue(OString)));
Qt4的SIGNAL和SLOT两个宏实际是将其参数转换成相应的字符串。在编译之前,Qt 的moc工具从源代码中提取出所需要的元数据,形成一张由使用了 signals 和slots修饰的所有函数组成的字符串表。connect函数将与信号关联起来的槽的字符串,同这张字符串表中的信息进行比较匹配,从而在发出信号时知道需要调用哪个槽函数。这种实现有下面两个问题:
(1)没有编译期检查。由于信号和槽都会被SIGNAL和SLOT宏处理成字符串,字符串的对比是在运行时完成的,并且失去了类型信息。所以,编写Qt4程序时可能会出现编译通过但是运行时原本应该调用的槽函数却没有执行的情况。此时,编译器不能给出任何错误信息,只能在运行时看有没有警告。
(2)无法使用相容类型的参数。由于connect函数使用的是字符串对比,所以槽函数的参数类型的名字必须和信号的完全一致,也必须与头文件中的类型一致。这里的“一致”是严格的字符串意义上的相同,因此,那些使用了typedef或者namespace的类型,即便实际类型是相同的,依然可能由于字符串名字不一样而不能正常工作。
为了解决这两个问题,Qt5提供了一套全新的信号槽语法。前面在Qt4中的关联可以使用下面的方式代替:
connect(sender,&Sender::valueChanged, receiver,&Receiver::showValue);
其中,Sender是发出信号的sender对象的类型,Receiver是接收信号的receiver对象的类型。需要说明,Qt4中的关联方式在Qt5程序中依然可用,不过新的语法有下面几个优点:
(1)支持编译期检查。Qt5新的关联语法可以在编译时进行检查,信号或槽的拼写错误、槽函数参数数目多于信号的参数数目等错误在编译时就能够被发现。
(2)支持相容参数类型的自动转换。使用新的语法不仅支持使用typedef或者命名空间,还支持使用隐式类型转换。例如,当信号参数类型是QString,而槽函数对应的参数类型是QVariant时,那么,在进行信号槽的连接时,QString将被自动转换成 QVariant。这是因为QVariant有一个可以使用QString的隐式构造函数。
(3)允许连接到任意函数。在Qt4中,槽函数只能使用slots关键字修饰的成员函数,而新的语法则通过函数指针直接调用函数,任意成员函数、静态函数或者C++11Lambda表达式都可以作为槽进行关联。(以前的信号槽语法是不受private限制的。槽函数虽然可以被声明为private,但仅在作为普通函数调用时起作用,作为槽连接时,LOT无视private修饰,因为仅作为字符串连接。而新语法无法获取私有函数指针,编译时就会有警报,因而更安全。)
除了可以将普通成员函数作为槽函数,Qt5新语法还允许使用任意类的静态函数。例如,有如下connect语句:
connect(dlg,&StringDialog::stringChanged, &OApplication::quit);
这里希望在对话框的字符串发生改变时,调用QApplication的静态函数quit实现程序的退出。因为Qt4的信号槽机制只能处理成员函数,所以如果要将QApplica-tion::quit函数作为槽函数,则必须使用一个成员函数封装这个静态函数。而在Qt5中,使用新的语法可以将信号直接关联到静态函数。(这里接收信号的receiver对象是this,允许进行省略。)
当信号有重载的情况时,使用Qt5的新语法可能会有一些不方便。例如,QSpin-Box有两个重载的信号:
void valueChanged(int i);
void valueChanged(const QString & text)。
当使用下面的语句进行连接时,
connect(spinBox,&0SpinBox::valueChanged,this,&MainWindow::onSpinBoxValueChanged):
编译器会发出一个错误。因为信号valueChanged有重载,所以使用&.QSpinBox::valueChanged语句获取信号的指针时会有歧义:有两个相同名字的信号。为解决这个问题,一方面,可以选择使用Qt4类型的旧的信号槽连接语法:
connect(spinBox,SIGNAL(valueChanged(int))this,SLoT(onSpinBoxValueChanged(int)))
由于SIGNAL和SLOT两个宏都要求指明参数类型,所以不会出现歧义。但是这样做又失去了编译期检查的优点。为了继续使用Qt5的新语法,需要增加一个显式类型转换:
00bject ::connect(
spinBox,
static_cast<void(QSpinBox::*)(int)>(&QSpinBox::valueChanged),
this,
&MainWindow::onSpinBoxValueChanged);
另外,有些槽函数的参数具有默认值,比如QPushButton的一个槽函数:
void animateClick(int msec=100)
如果有一个自定义组件MyWidget,这个组件有一个不带参数的信号 aSingal()想要连接到QPushButton的这个槽函数,当试图使用新语法这样写时:
connect(myWidget,&MyWidget::aSingalpushButton, &OPushButton::anumateClick);
编译器通常会给出一个错误(错误信息可能会因为编译器版本不同而各异):
The slot requires more arquments than the siqnal provides
大致是说,槽函数所要求的参数个数比信号提供的要多。这在Qt中一般是不允许的。但是这里的情形又有所不同:因为这个函数的参数具有默认值。但是,函数参数的默认值只在直接调用函数时才有效。在取函数地址的时候,编译器是看不到参数默认值的。所以取到的函数指针并不包含这个参数默认值。另一方面,由于这个槽函数的参数具有默认值,所以这个槽函数并不真正需要显式地提供一个参数。这种情况在Qt4中就需要使用合适的槽函数进行封装,在Qt5中也可以这么做。但是,Qt5的信号槽新语法还支持使用C++11 Lambda表达式。因此,还可以选择利用Lambda 表达式达到这一目的。
(2)、使用Lambda 表达式
上面讲述了Qt5全新的信号槽语法,可以看到,Qt5提高了信号槽关联的灵活性,允许使用任意函数作为槽函数。但是如果想更好地执行异步代码,连函数都不想定义,能否在connect关联时直接指定信号发出时执行的代码呢?通过Lambda表达式可以达到这个目的。例如:
connect(dlg,&StringDialog::stringChanged, [=](QString str){
if(str == "qt")
{
ui->stringLabel->setText("hello qt!");
}
else
{
ui->stringLabel->setText( error!);
}
});
这里直接在connect语句中添加了信号发射后要执行的代码,使用参数值区别显示不同的字符串。这种写法就是所谓的lambda表达式。Lambda表达式是C++11新增加的特性。简单来说,Lambda表达式就是匿名函数。在C++中,它以一对方括号开始。这对方括号被称为Lambda表达式引人符。引人符后面可以添加Lambda表达式的返回值类型,之后是参数列表,最后是Lambda表达式的函数体。
不同的Lambda表达式引人符有不同的含义(附表B-1所列)。引入符描述函数体如何“获得”外部变量。这一过程被称为“捕获”。所谓“外部变量”,指的是函数体以外的变量。这些变量需要在引入符可见的作用域有定义。
例如,一个Lambda表达式需要两个参数,以传值方式捕获的a和以传引用方式捕获的str,其返回值是QString类型,那么这个Lambda表达式应该写成:
[a,&str]->Qstring{)
如果需要了解有关 Lambda表达式的更多信息,可参考《C++Primer(第5版)》或其他相关教程。
前面提到,对于带有默认参数的槽函数,可以使用Lambda表达式作为一个匿名函数,从而达到函数封装的目的。依旧以前面提到的函数为例,可以使用下面的代码重写:
connect(myWidget,&MyWidget::aSingal,
[](){pushButton->anumateClick()});
1.2、在设计模式关联
首先添加自定义对话框类MyDialog。在设计模式中向窗口上添加两个Push Button,并且分别更改其显示文本为“进入主界面”和“退出程序”。
点击设计器上方的“编辑信号/槽”图标,或者按下快捷键F4,这时便进入了部件的信号和槽的编辑模式。在“退出程序”按钮上按住鼠标左键,然后拖动到窗口界面上,这时松开鼠标左键。
在弹出的配置连接对话框中,选中下面的“显示从QWidget继承的信号和槽”选项,然后在左边的QPushButton栏中选择信号clicked(),在右边的QDialog栏中选择对应的槽close(),完成后按下“确定”。
1.3、自动关联
在“进入主界面”按钮上右击,在弹出的菜单上选择“转到槽”,然后在弹出的对话框中选择clicked()信号,并按“确定”。这时便会进入代码编辑模式,并且定位到自动生成的on_pushButton_clicked()槽中。在其中添加代码:
void MyDialog::on_pushButton_clicked() { accept(); }
自动关联就是将关联函数整合到槽命名中。
例如on_pushButton_clicked()就是由字符“on”和发射信号的部件对象名,还有信号名组成。这样就可以去掉那个connect()关联函数了。每当pushButton被按下,就会发射clicked()信号,然后就会执行on_pushButton_clicked()槽。
这里accept()函数是QDialog类中的一个槽,对于一个使用exec()函数实现的模态对话框,执行了这个槽,就会隐藏这个模态对话框,并返回QDialog::Accepted值,我们就是要使用这个值来判断是哪个按钮被按下了。与其对应的还有一个reject()槽,它可以返回一个QDialog::Rejected值。其实,前面的“退出程序”按钮也可以关联这个槽。
2、信号与槽的参数的基本规则
(1) 信号参数 ≥ 槽参数
信号参数可以多于槽参数,多余的参数会被忽略,参数类型和顺序必须匹配(从第一个参数开始)
// 信号:void signalWithParams(int a, QString b, bool c); // 槽:void slotWithParams(int a, QString b); connect(sender, &Sender::signalWithParams, receiver, &Receiver::slotWithParams); // 第三个参数 bool c 会被忽略
(2) 信号参数 < 槽参数
需要特殊处理(不能直接连接),通常使用 lambda 表达式提供额外参数
// 信号:void simpleSignal(); // 槽:void complexSlot(int id, QString name); // 直接连接会编译错误 // connect(sender, &Sender::simpleSignal, receiver, &Receiver::complexSlot); // 错误! // 使用 lambda 提供额外参数 connect(sender, &Sender::simpleSignal, receiver, [receiver]() { receiver->complexSlot(123, "default"); // 提供额外参数 });