【C++】 Qt-信号与槽

基本概念

信号(signal)和槽(slot)是Qt框架引以为豪的机制之一,用于完成界面操作的响应,是完成任意两个Qt对象之间的通信机制。

信号:特定条件下发射的事件。例如pushButton最常见的信号是鼠标单击时发射一个clicked信号。

槽:对信号响应的函数。槽就是一个类成员函数可以是任何属性的(public、protected、private),可以带有参数、也可以被直接调用,槽函数与一般的函数的区别:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。

当某个事件发生之后他就会发射一个信号,如果有对象对这个信号感兴趣,将信号和自己的一个函数(称之为槽(slot))绑定来处理这个函数,这个槽函数就会执行,也就是回调。所以槽的本质是一个类成员函数。

所有使用信号与槽的类中,必须有Q_OBJECT这个宏。

添加信号与槽

方法一

在UI界面编辑器中,我们手动从窗口添加一个信号与槽,假设我们想点击一下窗口上的按钮使窗口关闭程序退出。

我们可以先拉出一个Push Button组件,然后切换到第二个工具按钮

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Id0tSgtP-1687728177458)(C++.assets/image-20230613181326607.png)]

拖拽窗口拉出一根线后松开,会出现一个弹出框,左边是信号右边是槽

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RdiKeLfn-1687728177459)(C++.assets/image-20230613181516977.png)]

按需求选中对应的信号和槽后点击ok就可以了

这种方式的信号与槽是通过ui_mainwindow.h中的一行connect代码进行关联链接的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ylfyhuoY-1687728177460)(C++.assets/image-20230613182741254.png)]

方法二

还有一种比较快捷的添加方法,选中某个组件后右键点击转到槽,然后选择一个信号,然后就会在按钮的父窗口mainwindow中自动生成一个槽,也就是说这个信号的处理函数,由mainwindow中自定义函数处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ti7ElxnD-1687728177460)(C++.assets/image-20230613182946532.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oILqaOcO-1687728177461)(C++.assets/image-20230613183002379.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wWXv9Lbu-1687728177461)(C++.assets/image-20230613183022765.png)]

在.h文件中也会有相应的槽函数声明

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h9WyVjP2-1687728177461)(C++.assets/image-20230613183055831.png)]

这种方式添加的信号槽并未直接用到connect函数关联绑定,而是在ui_mainwindow.h的setupUi中按name规则进行绑定的。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1h2n2wC3-1687728177462)(C++.assets/image-20230613183251159.png)]

自定义信号与槽

在QMetaObject::connectSlotsByName(MainWindow);执行后,会自动匹配特定命名规则的槽函数,也就是说我们可以手动添加一个能被自动调用的槽函数。但这种方式并不是长久之计,如果我们手动指定信号与槽的绑定连接,需要用到函数QObject::connect。

//方式一:*(常用推荐)
QObject::connect(
	const QObject *sender,  //发出信号的对象
    const char *signal,     //发送对象发出的具体信号
    const QObject *receiver,//接收信号的对象
    const char *method,     //接收对象在接收信号之后所需要调用的函数
    Qt::ConnectionType type = Qt::AutoConnection  //连接类型
)
    
//方式一:*(常用推荐)
QMetaObject::connection QObject::connect(
	const QObject *sender,  
    PointerToMemberFunction signal,     
    const QObject *receiver,
    PointerToMemberFunction method,   
    Qt::ConnectionType type = Qt::AutoConnection  //连接类型
)   

方式一指定信号和槽分别用宏SIGNAL(a)和SLOT(a),这两个宏最终将信号槽转换为字符串。

connect中信号与槽需要指定函数名,参数列表形参类型但不包含其变量名,不需要返回值。

CheckBox

我们在设计界面向窗口内添加一个Check Box按钮和Label文本标记

然后手动的在主窗口头文件与源文件中声明和定义槽函数,并且在构造函数中对信号-槽进行绑定链接

槽函数声明

void slots_stateChange(int);  //返回类型为 void ,参数应当与信号保持一致

定义

void MainWindow::slots_stateChange(int a){}

连接

    //信号-槽 绑定连接
    connect(ui->checkBox/*信号的发送者(对象)*/,
            SIGNAL(stateChanged(int))/*信号*/,  //SIGNAL 宏 参数 为信号的函数名和参数列表,如果有形参名应当去掉
            this/*接收者(对象)*/,
            SLOT(slots_stateChange(int))/*接收的槽函数*/);

通过测试我们发现槽函数的形参a在勾选时为2,不勾选为0,那么就可以根据这个参数来改变文本标记

void MainWindow::slots_stateChange(int a){
    qDebug()<<"slots_stateChange :"<<a;
    if(a==0){  //不勾选
        ui->label->setText("不勾选");
    }else if(a==2){  //勾选
        ui->label->setText("勾选");
    }
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9o9k3ZTM-1687728177462)(C++.assets/image-20230613191725120.png)]

我们发现参数状态是0,2缺少1。原来1代表的是半选状态。

所以我们可以在构造函数中去设置三态

ui->checkBox->setTristate(true);  //设置三态

这样就可以在槽函数中去对半选状态进行操作了

自定义QMessageBox

对于QMessageBox中提供的静态函数,只是支持已经定义好的一些按钮,却不支持自定义按钮。

所以我们尝试手动构建QMessageBox对话框,在MainWindiw构造函数中创建一个QMessageBox对象,并添加标题、文本和按钮。

    QMessageBox * pMsg = new QMessageBox;
    pMsg->setWindowTitle("title");
    pMsg->setText("我的文本");
    pMsg->addButton("是",QMessageBox::YesRole);
    pMsg->addButton("否",QMessageBox::NoRole);

然后我们可以在QMessageBox中找到对应的信号,所以此时信号发送者和信号还有接收者都有了,我们只需要再创建一个槽函数就可以了。

我们在头文件中去声明一下槽函数,参数与信号保持一致,然后在源文件中定义。这样我们就可以将信号与槽进行绑定链接了。

void slots_buttonClicked(QAbstractButton *button);
void MainWindow::slots_buttonClicked(QAbstractButton *button){
    
}

绑定链接

connect(pMsg,SIGNAL(buttonClicked(QAbstractButton *)),this,SLOT(slots_buttonClicked(QAbstractButton *)));

随后我们要对槽函数进行实现,参数中有一个button,我们可以利用这个参数来封装按钮,这个参数是之前写的按钮的返回值,所以我们在类成员属性中定义两个QPushButton*类型的变量用来接yes和no的返回值,然后由于我们创建QMessagebox时也是在构造函数中new的对象,所以就相当于是临时变量接了一个永久的对象,所以也将这个变量在类成员属性中去声明。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8VBbchEk-1687728177463)(C++.assets/image-20230614150142902.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-poCLM3jL-1687728177463)(C++.assets/image-20230614150154609.png)]

然后就可以根据这个返回值做对应操作了

void MainWindow::slots_buttonClicked(QAbstractButton *button){
    if(button == pYes){
        qDebug()<<"是";
    }else if(button == pNo){
        qDebug()<<"否";
    }
}

最后就是窗口显示,我们想让这个窗口与checkbox进行联系,所以就在checkbox的槽函数中如果勾选就显示窗口,不勾选就隐藏窗口就可以了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-I3jhoZjC-1687728177463)(C++.assets/image-20230614150331911.png)]

测试一下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f1Q46bs8-1687728177464)(C++.assets/image-20230614150406156.png)]

点击是在控制台上也会输出“是”。

自定义信号

在mainwindow.ui设计界面添加一个按钮和一个单行文本输入框,然后直接右键按钮后选择转到槽 直接创建一个发射信号的槽函数。

然后在槽函数中获取编辑框中的文本,然后发射信号并且携带参数QString。

所以我们要自己定义一个信号,定义信号的关键字是signals,不能加访问修饰符,并且只声明不定义

signals:  //关键字修饰,不能加访问修饰符,只声明不定义
    void signals_sendQString(QString);

因为信号不是最终的执行者,就算定义了也用不了,所以干脆就不用定义

发射信号的槽函数,信号函数前面一般加一个emit,为的是告诉看代码的人这里不是正常的函数,而是一个信号,当然不写也没关系。

//发射信号
void MainWindow::on_pb_send_clicked(){
    QString str = ui->lineEdit->text();    //获取编辑框上的文本

    //发射信号,并且携带参数 QString
    emit signals_sendQString(str);
}

现在发送者有了,信号也有了,但是还没有接收者和接收的槽函数

我们决定发送给另一个窗口,现在只有一个mainwindow主窗口,所以要添加一个新的窗口

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D5rneNql-1687728177464)(C++.assets/image-20230614154514984.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2HBtbsOj-1687728177464)(C++.assets/image-20230614154531495.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsZX02YU-1687728177465)(C++.assets/image-20230614154551593.png)]

之后完成就可以了,就会创建出一个新的头文件、源文件以及设计窗口

我们在这个窗口上添加一个lable,用来显示接收到的文本

现在接收者的类有了,但是对象还没有定义,我们在主函数中去定义一下这个对象。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-90sOPTfA-1687728177465)(C++.assets/image-20230614154813865.png)]

我们可以先不显示这个窗口,等到接收到信号之后在显示,那么接收的对象有了,就缺槽函数了

所以在这个窗口类中去声明定义槽函数,注意参数与信号保持一致就可以了,然后在定义时别忘了加类名作用域,否则就变成全局函数了。

public slots:
    void slots_recvQString(QString);
void Dialog::slots_recvQString(QString s){
}

最后就是连接了,我们要在主函数去连接信号与槽,因为信号和槽属于不同类,所以只能在主函数中去进行连接

QObject::connect(&w,SIGNAL(signals_sendQString(QString)),&dia,SLOT(slots_recvQString(QString)));

这里connect前要加QObject,之前不用加是因为mainwindow属于是它的子类,而main函数属于是外人,就不能直接使用。

然后在槽函数中实现显示接收到的文本信号并显示窗口就可以了

void Dialog::slots_recvQString(QString s){
    ui->label->setText(s);  //设置显示的文本
    this->show();
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ayMm9j5d-1687728177465)(C++.assets/image-20230614155524471.png)]

以上我们做的内容就是 通过自定义的信号槽以及手动的去绑定链接,实现了一个窗口到另一个窗口传递信息。

信号与槽多对多

一个信号连接多个槽

在窗口上添加一个水平滑块、一个时间编辑框和一个进度条共三个组件,想要通过水平滑块控制其他两个组件。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nxBXGyTP-1687728177466)(C++.assets/image-20230614231109395.png)]

现在我们是有了信号发出者和信号,也有信号接收者,只差槽函数

所以我们去声明定义分别与两个组件有关的槽函数

    void slots_timeChanged(int);
    void slots_progressChanged(int);

定义就先不写了

有了槽函数之后我们就可以对信号和槽进行绑定了,因为我们是一个信号连接多个槽,所以再绑定时只有槽函数位置不同

	connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_timeChanged(int)));
    connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_progressChanged(int)));

这样信号就与这两个槽函数绑定成功了,剩下的就是对槽函数进行实现了

因为我们水平滑块默认范围是0-99,我们想让他是0-100,所以我们手动设定一下水平滑块范围

   ui->horizontalSlider->setRange(0,100);

因为我们的时间编辑框想设为一天的时间,所以我们定义一个QTime,然后根据信号a的值给其赋值(一个单位代表多少秒)然后向这个组件传入这个时间就可以了。顺便我们也实值一下编辑框的显示格式

    ui->timeEdit->setDisplayFormat("hh-mm:ss");  //设置显示的格式
void MainWindow::slots_timeChanged(int a){
    qDebug()<<"a = "<<a;

    QTime time(0,0);  //定义一个初始时间
    time = time.addSecs(24*6*6*a);  //初始时间加上多少秒

    ui->timeEdit->setTime(time);  //时间组件上设置时间
}

进度条的槽函数时间就很简单了,只需要将整型信号当作进度条的值传入即可

void MainWindow::slots_progressChanged(int a){
    ui->progressBar->setValue(a);  //设置进度条的值
}

这样我们就实现了一个信号绑定多个槽了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d7IoNbUd-1687728177466)(C++.assets/image-20230614234550612.png)]

多个信号连接一个槽

我们在主窗口添加两个push button按钮分别命名为门口开关和床头开关,想要让这两个开关的信号同时控制dialog窗口的显示与隐藏。

现在我们也是有信号发送者、信号和信号接收者,所以我们只需要去信号接收者类中写一个槽函数就可以进行连接了。

void slots_isShow();

有了槽函数之后我们开始连接,因为是两个窗口对象之间的连接,所以要在主函数中实现,不过在主函数中我们要调用主窗口的ui时我们发现ui被锁住了,原因是在类成员属性中我们将ui设为私有了,所以我们可以弄一个接口函数。

public:
    Ui::MainWindow * GetUI(){return ui;}

同时我们要在主函数中加上ui的头文件

#include "ui_mainwindow.h"

现在就可以实现链接了

    QObject::connect(w.GetUI()->pb_door,SIGNAL(clicked()),&dia,SLOT(slots_isShow()));
    QObject::connect(w.GetUI()->pb_bed,SIGNAL(clicked()),&dia,SLOT(slots_isShow()));

最后就是去实现槽函数的功能,我们想要的就是如果窗口显示那么按一下按钮就关闭,如果窗口关闭,那么按按钮就显示

void Dialog::slots_isShow(){
    if(this->isVisible()){  //是否可视
        this->hide();
    }else{
        this->show();
    }
}

现在就实现了只要任意一个信号发出(点击任何一个按钮),这个槽就会被调用(Dialog会显示或隐藏)。

一个信号连接一个信号连接一个槽

在主窗口添加一个SpinBox组件用来输入数字,然后在Dialog窗口上添加一个LCD Number组件,用来显示数字。我们想通过改变主窗口上的数字来同时对dialog上的数字进行更改。

正常思路就是向刚才我们写的主窗口的开关调用dialog窗口的显示一样。但是这里有一个限制,就是不能用ui的公共接口,所以我们需要自己声明一个信号,然后用已有信号连接到自己声明的信号,再用这个信号与槽进行连接。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W4N6LRDY-1687728177466)(C++.assets/image-20230615115942922.png)]

所以先自定义一个信号,参数与spinBox发送的信号保持一致

void signals_transportValue(int);  //信号,只声明

然后在mainwindow的构造函数中对两个信号进行连接

    //主窗口中的 spinBOx 信号 与 主窗口的信号进行连接
    connect(ui->spinBox,SIGNAL(valueChanged(int)),this,SIGNAL(signals_transportValue(int)));

这样我们只需要去主函数中将自定义信号和dialog的槽函数进行连接就可以了

先在dialog中声明一个槽函数

    void slots_setValue(int);

然后连接

    QObject::connect(&w,SIGNAL(signals_transportValue(int)),&dia,SLOT(slots_setValue(int)));

最后去定义一下槽函数就可以了

void Dialog::slots_setValue(int a){
    ui->lcdNumber->display(a);
}

这样我们就实现了信号->自定义信号->槽函数,其实不管多少个信号之间进行传递,最终都要有一个槽函数进行接收

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZXaNdc8W-1687728177467)(C++.assets/image-20230615120418834.png)]

断开连接

在主窗口上增加两个checkbox并命名为断开连接,作用是控制水平滑块和时间组件、进度条之间是否连接

我们可以直接通过转到槽来声明定义这两个状态改变的槽函数

这里断开连接的方式有两种

方法一

当参数为0就是不勾选,那么就重新连接,如果参数为2勾选就断开连接

我们可以在勾选时直接将连接函数改为disconnect,然后不勾选时还是connect

void MainWindow::on_cb_time_stateChanged(int arg1)
{
    if(arg1 == 0){  //不勾选 重新连接
        connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_timeChanged(int)));
    }else if(arg1 == 2){  //勾选 断开连接
        disconnect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_timeChanged(int)));
    }
}

方法二

还有一种方法就是通过返回值来执行断开连接

我们在水平滑块和进度条连接函数前面加一个QMetaObject::Connection类型的返回值,这个返回值放到类成员属性中

    QMetaObject::Connection con;  //连接信息
con = connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_progressChanged(int)));

然后在槽函数中如果勾选了就在disconnect()中填入返回值就可以了,如果没勾选就还是执行一遍连接函数,注意这里返回值不能省略

void MainWindow::on_cb_progress_stateChanged(int arg1)
{
    if(arg1 == 0){  //不勾选 重新连接
        con = connect(ui->horizontalSlider,SIGNAL(valueChanged(int)),this,SLOT(slots_progressChanged(int)));
    }else if(arg1 == 2){  //勾选 断开连接
        disconnect(con);  //直接传递,连接信息
    }
}

判断是否连接成功

返回值的作用还可以判断是否连接成功,如果返回为真就是成功,否则连接失败

    if(con){  //可用于判断是否连接成功
        qDebug()<<"连接成功";
    }else{
        qDebug()<<"连接失败";
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bgDSb1d6-1687728177467)(C++.assets/image-20230615122644504.png)]

猜你喜欢

转载自blog.csdn.net/jia_03/article/details/131389324