3.3 Qt的信号和槽机制
在前面章节的学习中,已经初步使用了Qt的信号和槽,对Qt的信号和槽机制有了一个基本的认识。在此基础上,现在就Qt的信号和槽机制进行深入的阐述。
3.3.1 基本原理
在GUI用户界面中,当用户操作一个窗口部件时,需要其他窗口部件的响应或者能够激活其他的操作。在程序开发中,经常使用回调(callback)机制来实现。所谓回调,就是事先将一个回调函数(callback function)指针传递给某一个处理过程,当这个处理过程得到执行时,回调预先定义好的回调函数以期实现激活其他处理过程的目的。
不同于回调函数机制,Qt提供了信号和槽机制。信号是一个特定的标识;一个槽就是一个函数,与一般的函数不同,槽函数既能够和信号关联,也能够像普通函数一样直接调用。当某个事件出现时,通过发送信号,可以将与之相关联的槽函数激活,即执行槽函数代码。在程序中,使用QObject::connect()函数来将某个信号和某个槽进行关联,而信号和槽之间的真正关联是由Qt的信号和槽机制来实现的。
信号和槽的关联关系可以有几种模式:
● 一个信号和一个槽关联;
● 一个信号和多个槽关联;
● 多个信号和一个槽关联。
一个信号和多个槽关联的情况下,当发出该信号的时候,与该信号关联的各个槽以任意的先后顺序立即执行(即槽函数的执行顺序是随机的,与槽关联的顺序没有关系)。信号和槽机制是完全独立于GUI事件循环的。
此外,信号还可以和信号进行关联。当两个信号关联时,第一个信号的发出将会激活Qt发送第二个信号。
信号和槽通过QObject::connect ( const QObject * sender, const char * signal, const QObject *receiver, const char * method, Qt::ConnectionType type = Qt::AutoCompatConnection ) 函数关联,参数type定义了信号和槽的关联方式,决定一个信号是立即传递到槽还是排队等待以后传递。具体如图3-26所示。Qt使用枚举类型Qt::ConnectionType定义信号和槽的关联方式,有三种:
图3-26 信号和槽的关联
● Qt::DirectConnection,信号发送后立即传递给相关联的槽,只有槽函数执行完毕返回后,发送信号 “emit <信号>”之后的代码才被执行。
● Qt::QueuedConnection,信号发送后排队,直到事件循环(event loop)有能力将它传递给槽;而不管槽函数有没有执行,发送信号 “emit <信号>”之后的代码都会立即得到执行
● Qt::AutoConnection:如果信号和槽在同一个线程,信号发出后,槽函数将立即执行,等同于Qt::DirectConnection;如果信号和槽不在同一个线程,信号将排队,等待事件循环的处理,效果等同于Qt::QueuedConnection。
对于Qt::QueuedConnection方式,Qt元对象系统(meta-object system)必须知道信号/槽的参数类型,否则的话编译器会报错“QObject::connect: Cannot queue arguments of type 'Type'”,要想使用类型Type必须首先通过qRegisterMetType()函数在元对象系统中注册(简单数据类型和Qt定义的数据类型读者无需注册,可以直接使用)。对于不同线程间的通信,将在后面的章节中介绍。
在第1章提到过,宏SIGNAL()和SLOT()返回其参数的C风格的字符串(const char *),因此下面关联信号和槽的两个语句是等同的:
connect(pushButton, SIGNAL(clicked()), this, SLOT(doPushButton())); connect(pushButton, “clicked()”, this, “doPushButton()”);
Qt信号和槽机制的优点是:
● 类型安全的。需要关联的信号和槽的签名必须是等同的,即信号的参数类型和参数个数与接受该信号的槽的参数类型和参数个数相同;不过,一个槽的参数个数是可以少于信号的参数个数的,但缺少的参数必须是信号参数的最后一个或几个参数。如果信号和槽的签名不符,编译器就会报错。
● 松散耦合的。Qt信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无须知道是哪个对象的哪个槽需要接收它发出的信号,它只需要做的是在适当的时间发送适当的信号就可以了,而不需要知道也不必关心它的信号有没有被接收到,更不需要知道是哪个对象的哪个槽接收到了信号;同样地,对象的槽也不知道是哪些信号关联到了自己。而一旦关联信号和槽,Qt就保证了适合的槽得到调用。即使关联的对象在运行时被删除,应用程序也不会出现崩溃。
一个类要想支持信号和槽,必须从QObject或QObject的子类继承。注意,Qt信号和槽机制不支持对模板的使用。
扩展阅读
关于信号/槽机制的效率问题
信号和槽机制增强了对象间通信的灵活性,然而这也损失了一些性能。与回调函数相比较,信号和槽机制有些慢。通常,通过传递一个信号来调用槽函数将会比直接调用非虚函数慢10倍。原因主要有:
● 需要定位接收信号的对象;
● 安全地遍历所有的关联(比如,一个信号关联到多个槽的情况);
● 编组(marshal)/解组(unmarshal)传递的参数;
● 多线程的时候,信号可能需要排队等待。
然而,与创建堆对象的new操作以及删除堆对象的delete操作相比较,信号和槽的代价只是它们很少的一部分。信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的。与信号和槽提供的灵活性和简便性相比,这点性能的损失也是值得的。
3.3.2 设计信号和槽
在学习了Qt信号和槽的基本机制后,现在设计槽函数来响应用户的查找文件操作。
下面,以多继承方式来使用Qt设计器绘制的界面类,实现文件的查找功能。加入自定义槽后的类CFindFileForm,其定义如下所示。
// chapter03/findfile/mulinherit/src/findfileform.h #ifndef _FINDFILEFORM_H_ #define _FINDFILEFORM_H_ #include <QStringList> #include <QDir> #include "ui_findfileform.h" class CFindFileForm : public QWidget, public Ui_FindFileForm { Q_OBJECT public: CFindFileForm(QWidget* = 0); private: QStringList findFiles(const QDir&, const QString&, const QString&); void showFiles(const QDir&, const QStringList&); void tranvFolder(const QDir&, const QString& , const QString ); bool m_bStoped; bool m_bSubfolder; bool m_bSensitive; int m_nCount; private slots: void browse(); void find(); void stop(); void doTxtChange(const QString&); }; #endif
头文件中,包含了头文件<QStringList>,该文件包含了需要的类QString和QStringList的定义。头文件<QDir>包含了类QDir的定义,类QDir提供了对目录结构和目录的访问方法。
在CFindFileForm类的私有区,声明了必需的成员函数和成员变量:
● findFiles()函数实现文件的查找并返回符合条件的文件列表。
● tranvFolder()是一个递归函数,实现对文件夹的递归查找。
● 成员变量m_bStoped记录用户是否单击了“停止”按钮,如果是,m_bStoped为true,否则为false。
● 成员变量m_bSubfolder记录用户是否需要查找子文件夹中的文件,如果需要查找则m_bSubfolder为true,否则为false。
● 成员变量m_bSensitive用于查找包含特定文本信息的文件时,是否区分文本信息的大小写。
如果用户选中了“区分大小写”复选框,则m_bSensitive为true,否则为false。
● m_nCount记录查找到文件的总数。
在“private slots”区,声明了四个槽。其中,browse()槽函数响应用户的单击“浏览...”操作;find()槽函数响应用户的单击“查找”操作;stop()槽函数响应用户的“停止查找”操作;doTxtChange()槽函数响应用户的输入“包含文本”操作。所有的四个槽函数返回类型都是void。当槽函数响应信号而执行时,槽函数的返回类型被忽略掉。但是槽函数作为一般函数使用时(即直接在程序中调用槽函数),槽函数的返回类型是有用的。在这种情况下,声明返回类型为非void的槽函数是必要的。
接下来,看一下类CFindFileForm的实现文件findfileform.cpp,它包含了头文件中定义的所有函数和槽函数的实现。
// chapter03/findfile/mulinherit/src/findfileform.cpp #include <QtGui> #include "findfileform.h" CFindFileForm::CFindFileForm(QWidget* parent) : QWidget(parent), m_bStoped(false), m_nCount(0) { setupUi(this); statusLabel->setText(tr("就绪")); resultLabel->setText(tr("找到0个文件")); nameComboBox->setEditText("*"); dirComboBox->setEditText(QDir::currentPath()); dirComboBox->addItem(QDir::currentPath()); sensitiveCheckBox->setEnabled(false); connect(findPushBtn, SIGNAL(clicked()), this, SLOT(find())); connect(stopPushBtn, SIGNAL(clicked()), this, SLOT(stop())); connect(closePushBtn, SIGNAL(clicked()), this, SLOT(close())); connect(browsePushBtn, SIGNAL(clicked()), this, SLOT(browse())); connect(txtLineEdit, SIGNAL(textChanged(const QString&)), this, SLOT(doTxtChange(const QString&))); }
在类CFindFileFrom的构造函数中,首先在它的初始化列表中对成员变量进行了必要的初始化。接着,初始化Qt窗口部件的初始状态。最后,将Qt窗口部件的信号关联到相应的槽。在行编辑框QLineEdit的文本发生改变的时候,将会发送QLineEdit::textChanged(const QString& text)信号,该信号的参数text是用户输入到行编辑框的新文本。
现在,看一下响应用户“浏览...”操作的槽函数browse()。
void CFindFileForm::browse() { QString dir = QFileDialog::getExistingDirectory(this, tr("选择查找路径"), QDir::currentPath(), QFileDialog::ShowDirsOnly); if (!dir.isEmpty()) { dirComboBox->addItem(dir); dirComboBox->setCurrentIndex(dirComboBox->currentIndex() + 1); } }
函数QFileDialog::getExistingDirectory()打开一个文件对话框,它将返回用户选择的文件系统中存在的路径。静态函数QDir::currentPath()获取应用程序所在的路径,并初始化为文件对话框的当前目录。实参QFileDialog::ShowDirsOnly指示文件对话框只显示目录。
在文件对话框返回的路径非空的情况下,QComboBox::adddItem()将返回的路径添加到“查找位置”下拉框dirComboBox中,并通过QComboBox::setCurrentIndex()设置为下拉框的当前显示内容。
void CFindFileForm::find() { frame->setEnabled(false); findPushBtn->setEnabled(false); stopPushBtn->setEnabled(true); statusLabel->setText(tr("正在搜索...")); resultTableWidget->setRowCount(0); QString fileName = nameComboBox->currentText(); QString txt = txtLineEdit->text(); QString path = dirComboBox->currentText(); m_bSubfolder = subfolderCheckBox->isChecked(); m_bSensitive = sensitiveCheckBox->isChecked(); m_nCount = 0; m_bStoped = false; QDir dir = QDir(path); if (fileName.isEmpty()) fileName = "*"; tranvFolder(dir,fileName, txt); if(m_bStoped) statusLabel->setText(tr("已中止")); else statusLabel->setText(tr("就绪")); findPushBtn->setEnabled(true); stopPushBtn->setEnabled(false); frame->setEnabled(true); }
CFindFileForm::find()槽函数响应用户的单击“查找”操作,实现查找文件的功能。
当用户单击“查找”按钮的时候,首先改变显示窗口部件的状态。容器frame变为不可用时,它的所有子窗口部件的状态将会成为不可用。这样做是为了防止应用程序在进行文件查找的过程中用户再次进行输入操作。这也是使用QFrame容器的主要目的。当“查找”按钮变为不可用时,“停止查找”按钮变为可用,允许查找的过程中用户中断查找。接着设置“状态”标签的显示内容为“正在搜索...”以提示用户已经进入查找,同时清理显示查找结果的表格,设置它的行数为0。
接着,取得要查找的文件名字、包含的文本和查找的路径并保存在相应的变量中;保存“区分大小写”和“含子文件夹”复选框的选择状态。同时将记录查找文件数目的成员变量m_nCount清零,成员变量m_bStoped设置为false,以备查找过程中使用。
查找之前需要获取用户选择的查找路径,因此调用了QDir类的构造函数来构造QDir对象dir,它指向用户选择的目录。
最后判断用户输入的要查找的文件名是否为空。如果用户没有任何输入,那么应用程序将查找所有文件,即将目录下所有的文件列出显示在表格中。
函数tranvFolder()实现真正的查找文件功能。它以用户选择的查找位置、查找的文件名和文件中包含的文本作为输入参数,将最终的查找结果显示在表格中。
在完成查找并显示查找结果后,窗口部件将恢复到查找之前的状态,以便用户能够重新输入查找条件,执行下一个查找操作。
void CFindFileForm::tranvFolder(const QDir& dir, const QString& fileName, const QString txt) { if(m_bSubfolder) { QStringList folders; folders = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); for (int i = 0; i < folders.size(); ++i) { qApp->processEvents(); if (m_bStoped) break; QString strDir = QString("%1/%2").arg(dir.absolutePath()) .arg(folders[i]); tranvFolder(strDir, fileName, txt); } } QStringList files = findFiles(dir, fileName, txt); showFiles(dir, files); m_nCount += files.size(); resultLabel->setText(tr("找到%1个文件").arg(m_nCount)); }
tranvFolder()是一个递归函数,它递归地实现文件的查找(如果用户选择了“包含子文件夹”复选框的话)并将查找结果显示在用户界面上。该函数具有三个形参,分别用来接受用户输入的查找路径、文件名和包含的文本。
首先,该函数判断用户是否选中了“包含子文件夹”复选框,如果选中,该函数将会执行文件的递归查找。在递归查找文件的情况下,QDir::entryList()函数获取当前文件夹下的所有子文件夹的列表并保存在QStringList对象folders中。QDir::entryList()函数具有一个实参,它指定了该函数的过滤器,QDir::Dir表示只获取当前路径下的文件夹;QDir::NoDotAndDotDot表示获取的文件夹列表中不包含“.”(当前目录)和“..”(当前目录的上一级目录)目录。
这样做的目的很简单,防止应用程序永远在当前目录和上一级目录中循环,无法退出。接着应用程序递归的遍历当前目录的子目录列表,进行文件的查找。在这个过程中,调用了QApplication::processEvents()事件处理函数(关于事件处理的内容,将在第11 章详细介绍),以便应用程序即使在查找过程中也能响应用户的“停止查找”或“关闭”操作。最后获取子目录的绝对路径,并将它以及文件名和包含文本传递给递归函数tranvFolder(),进行进一步的查找。
如果用户没有选中“包含子文件夹”复选框或者递归函数已经遍历完了当前目录下的所有子目录,那么tranvFolder() 函数进入对当前文件夹下的文件查找。findFiles() 函数接受当前目录的绝对路径、文件名和包含文本作为参数,实现对当前目录的文件查找,并返回查找到的文件列表。showFiles() 函数进行文件的显示。最后在用户界面上显示当前应用程序查找到符合条件的文件个数。
函数QString::arg()用传递给它的参数代替原字符串中对应的“%1”,以构建一个新的字符串并返回(详见第13章的13.2节)。
QStringList CFindFileForm::findFiles(const QDir &dir, const QString &fileName, const QString &txt) { QStringList files = dir.entryList(QStringList(fileName), QDir::Files | QDir::NoSymLinks); if (txt.isEmpty()) return files; QStringList foundFiles; Qt::CaseSensitivity sensitive = Qt::CaseInsensitive; if(m_bSensitive) sensitive = Qt::CaseSensitive;
函数findFiles()对一个特定的目录实现文件的查找,它具有和tranvFolder()函数相同的参数个数和参数类型,但它返回的是一个查找结果的文件列表。
函数QDir::entryList()获取具有给定文件名的文件列表。它具有两个实参:第一个参数是一个QStringList类型的列表,指定了一个文件名字过滤器;第二个参数指定了另一个过滤器,QDir::Files表示只获取当前目录下的文件的名字(不包含目录),QDir::NoSymLinks表示返回的文件名列表不含系统的符号链接。
接着判断是否区分大小写,并将状态保存下来。
for (int i = 0; i < files.size(); ++i) { qApp->processEvents(); if (m_bStoped) break; QFile file(dir.absoluteFilePath(files[i])); if (file.open(QIODevice::ReadOnly)) { QString line; QTextStream in(&file); while (!in.atEnd()) { if (m_bStoped) break; line = in.readLine(); if (line.contains(txt, sensitive)) { foundFiles << files[i]; break; } } } } return foundFiles; }
对当前目录的所有符合文件名字条件的文件内容进行遍历,看是否包含用户指定的文本。如果文件符合用户的查找要求则将文件名字保存在一个列表中,最后返回查找的结果列表。在这个过程中,用户同样可以终止应用程序对文件的查找。
QDir::absoluteFilePath()函数返回当前目录下一个文件的绝对路径名。该函数不会检查该文件是否真正存在,它只是将给定的文件名和QDir对象具有的绝对路径构建一个新的绝对路径名。因为程序已经保证了该文件是确实存在的,所以不存在检查的问题。该函数的返回结果作为创建一个QFile对象的参数,QFile对象提供了对文件的读写操作。
QFile::open()试着打开文件,QIODevice::ReadOnly参数指定了以只读的方式打开文件。如果能够打开,则返回true,否则返回false。
接着构造一个QTextSteam栈对象in,QTextStream提供了对文本进行读写的操作,然后函数QTextSteam::readLine() 对文件进行行读入操作,函数QString::contains()进行文本的包含判断,直到函数QTextSteam::atEnd()判断已经到了文件尾。
void CFindFileForm::showFiles(const QDir &dir, const QStringList &files) { for (int i = 0; i < files.size(); ++i) { QString strFilePath = dir.absoluteFilePath(files[i]); QFile file(strFilePath); QFileInfo fileInfo(file); qint64 size = fileInfo.size(); QDateTime dateTime = fileInfo.created(); QString strDateTime = dateTime.toString(tr("yyyy MM月dd日hh:mm")); QString strPermission; if(fileInfo.isWritable()) strPermission = ("w"); if(fileInfo.isReadable()) strPermission.append(" r"); if(fileInfo.isExecutable()) strPermission.append(" x"); QTableWidgetItem *fileNameItem = new QTableWidgetItem(strFilePath); fileNameItem->setFlags(Qt::ItemIsEnabled); QTableWidgetItem *sizeItem = new QTableWidgetItem(tr("%1 KB") .arg(int((size + 1023) / 1024))); sizeItem->setTextAlignment(Qt::AlignRight | Qt::AlignVCenter); sizeItem->setFlags(Qt::ItemIsEnabled); QTableWidgetItem* createdItem = new QTableWidgetItem(strDateTime); QTableWidgetItem* permissionItem = new QTableWidgetItem(strPermission); int row = resultTableWidget->rowCount(); resultTableWidget->insertRow(row); resultTableWidget->setItem(row, 0, fileNameItem); resultTableWidget->setItem(row, 1, sizeItem); resultTableWidget->setItem(row, 2, createdItem); resultTableWidget->setItem(row, 3, permissionItem); } }
showFiles()函数在用户界面上显示查找的结果。它具有两个参数:第一个是个QDir类型的参数,它指定了文件所在目录的绝对路径;第二个参数是文件名列表。
QFileInfo对象提供了系统无关的文件信息,包括文件名、文件大小、在文件系统中的位置、访问权限以及是否是一个符号链接等。在此,程序利用它获取文件的大小信息和访问权限信息。
最后,程序将获得的文件的信息显示在用户界面的表格中。
void CFindFileForm::stop() { m_bStoped = true; }
槽函数stop()响应用户的单击“停止”按钮操作,并将用户的意图保存在成员变量m_bStopped中。
void CFindFileForm::doTxtChange(const QString& txt) { if(txt.isEmpty()) sensitiveCheckBox->setEnabled(false); else sensitiveCheckBox->setEnabled(true); }
槽函数doTxtChanged()响应“包含文本”行编辑框对象txtLineEdit的编辑操作,如果txtLineEdit的文本发生了变化,那么将会激发该槽函数的执行。它的作用是判断“包含文本”行编辑框的内容是否为空,如果为空,将“区分大小写”复选框sensitiveCheckBox的状态置为不可用,此时用户不能够作出是否进行“区分大小写”查找的选择;如果“包含文本”行编辑框的内容不为空,则置为可用。
现在对应用程序进行重编译,运行界面如图3-27所示。
图3-27 查找文件运行界面
3.3.3 信号和槽的自动关联
除了能够在程序中手动关联信号和槽之外,Qt的元对象提供了信号和槽的自动关联。对于Qt窗口部件已经提供的信号,如果能够按下面的规则命名槽函数,那么Qt就能够自动进行关联:
void on_<窗口部件名>_<信号名>(<信号参数>);
例如,对于Qt设计器绘制的“查找文件”窗口部件中的“浏览...”按钮browsePushBtn,以及它的单击信号“clicked()”,将原来的槽函数void browse() 修改为on_browsePushBtn_clicked()。修改findfileform.h的声明,定义如下:
class CFindFileForm : public QWidiget, public Ui_FindFileForm { Q_OBJECT …… private slots: void on_browsePushBtn_clicked(); …… };
实现文件findfileform.cpp中的槽如下所示。
void CFindFileForm::on_browsePushBtn_clicked() { …… }
屏蔽CFindFileForm类构造函数中的“浏览...”按钮的信号和槽的关联操作:
CFindFileForm::CFindFileForm(QWidget* parent) : QWidget(parent), m_bStoped(false), m_nCount(0) { setupUi(this); …… // connect(browsePushBtn, SIGNAL(clicked()), this, SLOT(browse())); …… }
编译运行程序,在“查找文件”对话框中单击“浏览...”按钮。OK,应用程序依然能够响应用户的操作。
在大型的Qt应用程序开发中,使用Qt信号/槽的自动关联功能,可以加速应用程序的开发进度。