4.2 使用原有的信号和槽

上节第一个例子中,按钮的信号 clicked() 是 Qt 库自带的,而槽函数 FoodIsComing 是手动编写的。Qt 库各种窗体控件都带有预先定义好的信号和槽函数,本节示范使用 Qt 文本编辑控件和标签控件的信号和槽函数,实现信号和槽的一对一关联、一对多关联和多对一关联。在声明匹配的情况下,信号和槽的关联是非常自由的,举吃货的例子,一对一相当 于某客户只买某一家餐馆的饭,一对多相当于某客户同时买了多家餐馆的饭,多对一相当于多个客户都订了某一家餐馆的饭。Qt 的信号和槽函数不仅能实现关联,也能在运行时解除关联,本节最后一个例子示范在程序运行时控制单行编辑控件和标签控件的关联和解除关联。本节内容较多,可以分两次 来学。

4.2.1 文本同步示例

本小节的例子就是在窗口里放置一个单行文本编辑器(QLineEdit)和一个标签控件(QLabel),实现的效果就是当编辑器的内容被编辑时,标 签控件同步显 示编辑控件里的内容。
打开 QtCreator ,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 textshow,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向窗体拖入一个 Line Edit 控件和 Label 控件,拖动这两个控件的边框可以改变它们的大小,修改之后效果如下图所示:
ui
默认这两个控件对象名字分别为 lineEdit 和 label,就用这两个名字,编辑好界面后保存。当 lineEdit 控件被用户编辑时,它会发出信号:
void    textEdited(const QString & text);
信号源头已经有了,接收端是标签控件,我们希望标签控件自动同步修改文本,标签控件自带槽函数:
void    setText(const QString &);
之前我们一直拿这个槽函数当普通成员函数来修改标签控件文本的,它本质是一个槽函数。接收端的槽函数也是 Qt 库自带的,我们需要做的就是把它们关联起来。
只需要在窗体构造函数加一个 connect 函数调用就行了,手动修改后的 widget.cpp 文件内容如下:
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    //关联单行编辑控件的信号到标签控件的槽函数
    connect(ui->lineEdit, SIGNAL(textEdited(QString)), ui->label, SLOT(setText(QString)));
}

Widget::~Widget()
{
    delete ui;
}
这样整个例子的代码编辑就完成了。信号和槽机制有三板斧,源头控件 lineEdit 的信号是 Qt 库自带的,接收端控件 label 的槽函数也是 Qt 库自带的,我们只需要添加 connect 函数调用关联它们就行了。

connect 关联之后,当源头发出信号时,接收端的槽函数就会自动被调用,信号里的参数值也会自动传递给槽函数,这样槽函数就能干自己该干的活了。

SIGNAL 宏包裹的信号名称与 Qt 帮助文档里的声明稍有差异,实际使用时按照代码编辑器根据 "SIGNAL(" 自动提示的信号名称。SLOT 宏包裹的槽函数名称也是类似的,一般都按照代码编辑器自动提示的名称。
代码已经编辑完毕,点击 QtCreator 左下角的运行按钮,在窗体编辑框输入文本,测试例子运行效果:
run
程序刚开始运行的时候,编辑框是空白,标签默认显示 TextLabel 字样。我们在编辑框输入文本,标签控件是完全同步的,如果删除编辑框的文本,标签控件也是同步删除相应的文本。

这个示例非常简单,就上面这些内容。现在有一个问题,怎么知道单行编辑控件、标签控件等有哪些信号和槽函数呢?
最好的办法就是查阅 Qt 帮助文档,在 Qt 助手索引里输入 QLineEdit ,可以查看该类的文档,点击帮助页面右上角“Contents”里面的“Signals”链接就是信号列表,点击“Public Slots”就是可用的槽函数列表:
help
在槽函数列表下方还有该类的父类槽函数声明链接,信号列表下方也是父类的信号声明链接。
对于 QLabel 控件,也可以查它的帮助文档:
help2
编程时遇到不清楚的都可以随时查阅帮助文档,一定要学会用帮助文档,而不是屁大点事就到处瞎问。

4.2.2 一对多关联示例

这一小节示范在窗体里放置一个单行文本编辑控件(QLineEdit)、一个标签控件(QLabel)和一个文本浏览控件(QTextBrowser),在单行文 本编辑控件里的文本被编辑时,标签控件和文本浏览控件都会同步显示新的文本。另外我们自定义一个槽函数,同步打印文本的变化到调试输出面板里。例子其实示范了将单 行文本编辑控件的信号同时关联到三个槽函数,观察相应的效果。
重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 textmulti,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向窗体拖入 Line Edit、Label、Text Browser 控件各一个,并大致调 整一下控件位置和大小,调整之后如下图所示:
ui
这时三个控件对象名称默认为 lineEdit、label、textBrowser,就用默认的名字,编辑好界面之后保存。这时信号的源头是单行文本编辑控件,接收端有标签控件和文本浏览控件。
现在回到 QtCreator 代码编辑模式,打开 widget.h 头文件,添加我们自定义的槽函数声明,用于打印文本到输出面板:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

public slots:   //添加槽函数打印调试信息
    void PrintText(const QString& text);

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
头文件编辑好之后,再编辑 widget.cpp 文件:
#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //关联信号到槽函数
    //接收端是标签控件
    connect(ui->lineEdit, SIGNAL(textEdited(QString)), ui->label, SLOT(setText(QString)));
    //接收端是文本浏览控件
    connect(ui->lineEdit, SIGNAL(textEdited(QString)), ui->textBrowser, SLOT(setText(QString)));
    //接收端是主窗口的 PrintText 
    connect(ui->lineEdit, SIGNAL(textEdited(QString)), this, SLOT(PrintText(QString)));
}

Widget::~Widget()
{
    delete ui;
}

void Widget::PrintText(const QString &text)
{
    qDebug()<<text; //打印到调试输出面板
}
widget.cpp 文件开头增加了头文件包含 <QDebug>。在构造函数里,增加了三行 connect 函数调用:
第一个是将 lineEdit 的编辑信号关联到 label 的设置文本槽函数;
第二个是将 lineEdit 的编辑信号关联到 textBrowser 的设置文本槽函数;
第三个是将 lineEdit 的编辑信号关联到主窗体的 PrintText 槽函数。
PrintText 槽函数的定义代码非常简单,就是将获取的 text 文本打印到调试输出面板。虽然 PrintText 函数定义和声明里的参数看起来有点复杂,又是 const,又是 &,在关联函数位置只需要按照 QtCreator 编辑器的代码补全功能自动提示的槽函数名称即可。比如
connect(ui->lineEdit, SIGNAL(textEdited(QString)), this, SLOT(PrintText(QString)));
槽函数 PrintText 参数里只需要类型名 QString 就够了,不需要加变量名 text 的,信号 textEdited 的参数也是类似的。
如果用 Qt5 新式的关联语法,就如下面示例的这句:
connect(ui->lineEdit, &QLineEdit::textEdited, this, &Widget::PrintText);

代码编辑好之后,点击 QtCreator 运行按钮,就可以测试例子运行的效果了:
run
三个关联函数都是正常工作的,单行编辑控件文本被编辑之后,窗体里的标签控件和文本浏览控件都是同步显示的,在输出面板也是可以看到文本同步打印的。

4.2.3 多对一关联示例

本小节回到吃货的例子,有三个顾客 Anderson、Bruce、Castiel 都要订饭,分别对应三个按钮,点击一个按钮,就会弹出给该顾客送饭的消息。注意这个例子只使用一个槽函数,而三个顾客名称是不一样的,弹窗时显示的消息不一样,这需要一些 技巧,下面我们开始这个示例的学习。
重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 multiconsumer,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向窗体拖三个按钮:
第一个按钮对象名称设为 pushButtonAnderson,显示文本为 Anderson;
第二个按钮对象名称设为 pushButtonBruce,显示文本为 Bruce;
第三个按钮对象名称设为 pushButtonCastiel,显示文本为 Castiel。
界面编辑好之后如下图所示:
ui
编辑好界面之后保存。这样三个信号的源头就设置好了,下面需要编写接收它们信号的槽函数。我们回到 QtCreator 的代码编辑模式,打开头文件 widget.h,向类里添加槽函数声明:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

public slots:   //添加槽函数进行弹窗
    void FoodIsComing();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
FoodIsComing 就是我们需要添加的槽函数,添加好声明之后,下面再向 widget.cpp 添加代码:
#include "widget.h"
#include "ui_widget.h"
#include <QMessageBox>
#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //三个按钮的信号都关联到 FoodIsComing 槽函数
    connect(ui->pushButtonAnderson, SIGNAL(clicked()), this, SLOT(FoodIsComing()));
    connect(ui->pushButtonBruce, SIGNAL(clicked()), this, SLOT(FoodIsComing()));
    connect(ui->pushButtonCastiel, SIGNAL(clicked()), this, SLOT(FoodIsComing()));
}

Widget::~Widget()
{
    delete ui;
}

void Widget::FoodIsComing()
{
    //获取信号源头对象的名称
    QString strObjectSrc = this->sender()->objectName();
    qDebug()<<strObjectSrc; //打印源头对象名称

    //将要显示的消息
    QString strMsg;
    //判断是哪个按钮发的信号
    if( "pushButtonAnderson" == strObjectSrc )
    {
        strMsg = tr("Hello Anderson! Your food is coming!");
    }
    else if( "pushButtonBruce" == strObjectSrc )
    {
        strMsg = tr("Hello Bruce! Your food is coming!");
    }
    else if( "pushButtonCastiel" == strObjectSrc )
    {
        strMsg = tr("Hello Castiel! Your food is coming!");
    }
    else
    {
        //do nothing
        return;
    }
    //显示送餐消息
    QMessageBox::information(this, tr("Food"), strMsg);
}
widget.cpp 里面首先添加了 <QMessageBox> 和 <QDebug> 两个头文件包含。
在构造函数里,我们将三个按钮的 clicked() 信号都关联到 FoodIsComing() 槽函数了,这就是多个信号关联到同一个槽函数的示范。然后槽 函数该如何编写呢?
因为我们要区分这个三个客户的名称,这就要在程序运行时判断是哪个按钮发出的信号。
当一个信号触发了它关联的槽函数时,可以在该槽函数里通过 sender() 函数来获取发送信号的源头对象,该函数返回源头信号指针,sender() 函数的声明如下:
QObject * QObject::​sender() const
并且它是个 [protected] 保护类型的函数,在该类及其派生类之外是不能访问的。该函数返回通用基类 QObject * 指针,可以通过 QObject 类对象的 objectName() 函数获取该对象的名称。
在 FoodIsComing 函数内,先获取发送信号的源头对象名称,并打印到输出面板。
然后根据对象名称进行匹配:
如果源头对象名称是 pushButtonAnderson,那么设置显示给 Anderson 的文本消息;
如果源头对象名称是 pushButtonBruce,那么设置显示给 Bruce 的文本消息;
如果源头对象名称是 pushButtonCastiel,那么设置显示给 Castiel 的文本消息;
最后如果三个都不符合,那就直接返回,不进行弹窗。
设置好文本消息之后,就可以用 QMessageBox::information 函数显示相应的文本消息框了。

注意在 if-else 条件判断里,三个按钮对象名称字符串("pushButton***")没有使用 tr 函数封装,因为这三个名字是在 ui 文件里预 先规定好的,不能随便修改,也不用做翻译,所以直接用了三个裸的英文名字符串。

编辑好代码之后,点击 QtCreator 的运行按钮,测试示例的效果:
run

4.2.4 解除关联示例

对于源端的信号和接收端的槽函数,不仅可以进行关联,在关联之后,如果不需要用到它们的关联关系了,可以使用 disconnect 函数解除之前的关联关系。disconnect 函数就是 connect 函数的逆向过程,它们二者的参数是差不多的,对于旧式语法:
bool QObject::​disconnect(const QObject * sender, const char * signal, const QObject * receiver, const char * method)
新式语法:
bool QObject::​disconnect(const QObject * sender, PointerToMemberFunction signal, const QObject * receiver, PointerToMemberFunction method)
disconnect 函数返回值表明解除过程是否正确执行。
除了上述两种解除关联语法,还可以直接针对 connect 函数返回的连接对象进行解除关联,比如:
//头文件声明连接对象
QMetaObject::Connection m_conn;
......
//关联时保存返回值,将信号关联到 lambda 表达式, lambda 表达式是 C++11 新特性
 m_conn = connect(ui->pushButtonSignal, &QPushButton::clicked, []() { qDebug()<<100; } );
......
//解除关联时使用对象 m_conn
disconnect(m_conn);

下面我们还是以单行文本编辑控件和标签控件为例,示范在程序运行时实现关联和解除关联。
重新打开 QtCreator,新建一个 Qt Widgets Application 项目,在新建项目的向导里填写:
①项目名称 disconn,创建路径 D:\QtProjects\ch04,点击下一步;
②套件选择里面选择全部套件,点击下一步;
③基类选择 QWidget,点击下一步;
④项目管理不修改,点击完成。
建好项目之后,打开窗体 widget.ui 文件,进入设计模式,向窗体里面拖入一个单行编辑控件、一个标签控件、两个按钮控件,如下图所示:
ui
单行编辑控件和标签控件都用默认的属性不修改,而对于两个按钮:
左边的按钮对象名称修改为 pushButtonConn,显示文本设置为“关联”;
右边的按钮对象名称修改为 pushButtonDisconn,显示文本设置为“解除关联” 。
我们关注的单行编辑控件信号和标签控件槽函数的关联和解除关联,对于按钮的信号,我们使用自动生成槽函数办法,在设计模式右击按钮,在右键菜单里点击“转到 槽...”,为两个按钮的 clicked() 都添加各自的槽函数:
slot
选择 clicked 信号,添加对应的槽函数:
slot2
添加一个按钮的槽函数后,回到设计模式,再添加第二个按钮 clicked 信号的槽函数。添加完两个槽函数之后,widget.h 自动变为:
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_pushButtonConn_clicked();

    void on_pushButtonDisconn_clicked();

private:
    Ui::Widget *ui;
};

#endif // WIDGET_H
头文件不需要修改,就用上面默认生成的代码即可。下面我们开始编辑 widget.cpp 文件,实现单行编辑控件信号和标签控件槽函数的关联与解除关联:
#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    //进行关联,按钮的槽函数可以当作普通函数来调用,实现关联
    on_pushButtonConn_clicked();
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_pushButtonConn_clicked()
{
    //关联
    connect(ui->lineEdit, SIGNAL(textEdited(QString)), ui->label, SLOT(setText(QString)));

    //调整按钮可用性
    ui->pushButtonConn->setEnabled(false);      //已经关联,禁用关联按钮
    ui->pushButtonDisconn->setEnabled(true);    //已经关联,只有解除关联按钮可用
}

void Widget::on_pushButtonDisconn_clicked()
{
    //解除关联
    disconnect(ui->lineEdit, SIGNAL(textEdited(QString)), ui->label, SLOT(setText(QString)));

    //调整按钮可用性
    ui->pushButtonConn->setEnabled(true);       //没有关联,只有关联按钮可用
    ui->pushButtonDisconn->setEnabled(false);   //没有关联,解除关联按钮禁用
}
关联按钮对应的槽函数为 on_pushButtonConn_clicked,解除关联按钮对应的槽函数为 on_pushButtonDisconn_clicked。在构造函数里面,on_pushButtonConn_clicked 是作为普通成员函数调用了一次,这个槽函数实现了单行编辑控件和标签控件的文本同步,并且对界面里的两个按钮可用性进行了调整。
首先看看 on_pushButtonConn_clicked 函数里的三句代码的意思:
①connect 函数将单行编辑控件的 textEdited(QString) 信号关联到标签控件的 setText(QString) 槽函数,当单行编辑控件的文本被用户修改时,标签控件槽函数会被自动调用,根据传递的文本字符串设置标签控件文本,自动与单行编辑控件文本同步。
②按钮控件的 setEnabled 函数接收一个 bool 参数,如果为 true,按钮就可用,能被点击;如果参数为 false,按钮被禁用,无法点击。在实现关联之后,就不需要 pushButtonConn 按钮可用,所以把它禁用了;
③因为已经关联了,所以需要把 pushButtonDisconn 设为可用状态,这样就能够在以后实现解除关联。

再看看 on_pushButtonDisconn_clicked 槽函数里的三句代码,这三句和 on_pushButtonConn_clicked 里面一一对应的,都是逆过程。disconnect 函数语法和 connect 函数语法差不多,就是多了 dis 三个字母而已。disconnect 函数可以把之前的关联关系解除掉,这样当单行编辑控件文本被用户编辑时,标签控件是没任何变动的。

代码就这些,点击运行按钮看看运行效果,默认启动时是处于关联状态,文本会同步变化:
run1

然后我们点击“解除关联”按钮,再编辑单行编辑控件里的内容,标签控件里的文本是不会变化的:
run2

本节的四个示例内容就是这些,这里使用的信号都是 Qt 控件里面预先定义好的,这些信号可以通过查阅 Qt 帮助文档获知更详细的信息。下一节我们开始自己定义信号和相应的槽函数,学会使用自定义的信号和槽机制在各个对象之间进行通信。



prev
contents
next