前言
自从Qt5发布以来,我一直在推迟对我一直在做的一个项目升级到Qt5。即使是像这样的版本,从Qt 4.7到Qt 5(没错,跳过了4.8),出于某种原因也不像暗示的那么简单。他们说: “只需改变包含和链接路径, 就会自行编译。” Psht,是正确的。别再上当了。
在我使用Qt工作多年之后,我实现了飞跃,获得巨大的进步.我觉得C++和Qt现在是一起工作的,而不是仅仅帮助您开发更好的C++。我相信信号/槽机制已经在c++ 11 lambda函数中找到了它的灵魂伴侣。
这个信号/槽到底是什么?
如果不使用Qt, 你可能根本就不在乎,但是Qt框架中对象之间的基本通讯机制是由信号(可以发出的事件)和槽(事件处理程序)定义的。
作为快速介绍,使用以下示例(假设setupUi方法创建了三个QPushButton对象和一个QTextEdit对象):
class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
{
// create the widget interface (add the buttons and text box)
this->setupUi(this);
// connect the signal/slots
connect(pushButton, SIGNAL(clicked()), this, SLOT(SetTextOne()));
connect(pushButton_2, SIGNAL(clicked()), this, SLOT(SetTextTwo()));
connect(pushButton_3, SIGNAL(clicked()), this, SLOT(SetTextThree()));
}
public slots:
void SetTextOne(void)
{
textEdit->setText("bonjour");
}
void SetTextTwo(void)
{
textEdit->setText("comment allez vous");
}
void SetTextThree(void)
{
textEdit->setText("pas trop mal, et vous?");
}
};
我们已经将每一个按钮的点击信号链接到这里定义的三个方法上。
在connect方法调用中使用的SIGNAL和SLOT是连接函数名称的宏,出于我们的目的,先让我假设它是魔法。
那么,信号/槽机制有什么问题?
这个没有什么损坏,对吧? 它的工作原理…我猜。我前面谈到的信号/槽宏“魔法”并不是那么神奇。这两个宏实际上都解析为一个字符串。
问题1:
它使用字符串在运行时解析连接。所以,如果你碰巧有一个槽,它接受一个字符串,而信号声明接受一个int,但你不知道它,直到你运行你的应用程序。除非您习惯JavaScript开发,否则这可能会让您措手不及。
问题2:
为什么我必须定义三个方法来做基本相同的事情?
在Qt5前
在Qt5和c++ 11之前,我们可以用QSignalMapper类来做这样的事情:
class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
{
// create the widget interface
this->setupUi(this);
// use the QSignalMapper to pass custom string for each button
// to a single slot
QSignalMapper* mapper = new QSignalMapper();
connect(mapper, SIGNAL(mapped(QString)), this, SLOT(SetText(QString)));
connect(pushButton, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(pushButton, "bonjour");
connect(pushButton_2, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(pushButton_2, "comment allez vous");
connect(pushButton_3, SIGNAL(clicked()), mapper, SLOT(map()));
mapper->setMapping(pushButton_3, "pas trop mal, et vous?");
}
public slots:
void SetString(QString text)
{
textEdit->setText(text);
}
};
使用QSignalMapper类我们去掉了三个槽函数,它们对不同的文本做了基本相同的操作,并用一个函数替换了它。但是你我都知道这感觉有点老套。QSignalMapper就像是一个真正问题的补丁。
添加C++ lambda函数
如果您一直关注c++的发展,您可能知道lambda函数和表达式。如果您不熟悉它们,可以通过搜索找到大量信息,但简单的回答是,它们基本上是内联的、未命名的函数。一般格式为:
[capture](parameters) { body };
其中capture指定在函数声明的作用域中哪些可见的符号对lambda的主体是可见的,parameters是传递给lambda的参数列表,而body是函数的定义。
这对Qt意味着什么?
要考虑的最重要的事情是,它们可以用作槽的函数指针。我们可以像这样连接一个槽:
connect(<pointer to source object>, <pointer to signal>,
<pointer to slot function>);
首先,请注意,我们现在可以将实际指针传递到信号和槽,而不是仅仅使用信号和槽宏(如果需要,您可以仍然可以使用这些宏)。这意味着对connect的连接是在编译时期检查。不再运行程序并发现您使用了int作为槽,但是信号传递了一个字符串。
其次,lambda基本上就是一个函数指针。现在考虑一下这如何改变我们的示例:
class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
{
// create the widget interface
this->setupUi(this);
// connect the signal to lambda (using [=] to capture variables
// used in the body by value)
connect(pushButton, &QPushButton::clicked,
[=]() { this->SetString("bonjour"); });
connect(pushButton_2, &QPushButton::clicked,
[=]() { this->SetString("comment allez vous"); });
connect(pushButton_3, &QPushButton::clicked,
[=]() { this->SetString("pas trop mal, et vous?"); });
}
public slots:
void SetString(QString text)
{
textEdit->setText(text);
}
};
注意: 如果您在Mac上使用Clang,您可能需要在.pro文件中添加“CONFIG += c++11”来启用c++11特性来支持lambda函数。
上面例子与使用QSignalMapper比较。它只是更简洁,更容易理解。然而,我们不需要就此打住。由于我们的槽函数非常简单,而且真正重要的东西(我们正在设置的字符串)是在lambda中,所以甚至没有理由拥有它。
class AMainWindow : public QWidget, public Ui::AMainWindow
{
Q_OBJECT
public:
explicit AMainWindow(QWidget *parent = 0) : QWidget(parent)
{
// create the widget interface
this->setupUi(this);
// connect the signal to lambda (using [=] to capture variables
// used in the body by value)
connect(pushButton, &QPushButton::clicked,
[=]() { textEdit->setText("bonjour"); });
connect(pushButton_2, &QPushButton::clicked,
[=]() { textEdit->setText("comment allez vous"); });
connect(pushButton_3, &QPushButton::clicked,
[=]() { textEdit->setText("pas trop mal, et vous?"); });
}
};
这是一个比我们原来有三个槽的类更优雅的解决方案。
附加说明
当然,并非一切都是完美的。在使用指向函数的指针和lambdas作为槽时,有一些事情需要记住。首先,它有点复杂,因为您必须指定slot类的完整类型(如果您不使用lambda),但是较少的模糊性不会影响到任何人。
但是有两个更大的问题:
(1) 函数指针和连接时不支持默认参数;
(2) 使用lambdas创建的槽在‘receiver’销毁时不会自动断开。
第二个问题不一定是主要问题,因为使用lambda函数的事实表明您并不打算经常连接/断开连接(尽管仍然可以手动连接)。然而,第一个问题可能是很烦人的。考虑这样的写法:
connect(fromObject, &ASomeClass::someSignal,
toObject, &AAnotherClass::slotHandler);
其中信号和槽定义为:
void ASomeClass::someSignal(bool arg = true);
void AAnotherClass::slotHandler() { cout << “handled”; }
即使参数在考虑默认参数时匹配,connect方法也会抛出编译时错误。当然,一个简单的修复方法是这样使用lambda并且忽略信号参数:
connect(object, &ASomeClass::someSignal,
[=](bool arg) { toObject->slotHandler(); });
原文作者:
Brian Poteat;
原文链接:
https://artandlogic.com/2013/09/qt-5-and-c11-lambdas-are-your-friend/