Qt中类里为什么要加上Q_OBJECT这个东西呢,原来这是一个Qt定义的宏.它的作用是什么呢? 原来这个宏中包含了Qt中最重要的一个机制:也就是信号和槽的机制.所有的Qt程序一定会使用这个机制!
Q_OBJECT宏有着连接信号与宏的功能,一旦类中没有这个宏定义的话,信号与槽的关联也就消失了
信号和槽机制是 QT 的核心机制,要精通 QT 编程就必须对信号和槽有所了解。信号和槽是一种高级接口,应用于对象之间的通信,它是 QT 的核心特性,也是 QT 区别于其它工具包的重要地方。信号和槽是 QT 自行定义的一种通信机制,它独立于标准的 C/C++ 语言,因此要正确的处理信号和槽,必须借助一个称为 moc(Meta Object Compiler)的 QT 工具,该工具是一个 C++ 预处理程序,它为高层次的事件处理自动生成所需要的附加代码。
在我们所熟知的很多 GUI 工具包中,窗口小部件 (widget) 都有一个回调函数用于响应它们能触发的每个动作,这个回调函数通常是一个指向某个函数的指针。但是,在 QT 中信号和槽取代了这些凌乱的函数指针,使得我们编写这些通信程序更为简洁明了。信号和槽能携带任意数量和任意类型的参数,他们是类型完全安全的,不会像回调函数那样产生 core dumps。
所有从 QObject 或其子类 ( 例如 Qwidget) 派生的类都能够包含信号和槽。当对象改变其状态时,信号就由该对象发射 (emit) 出去,这就是对象所要做的全部事情,它不知道另一端是谁在接收这个信号。这就是真正的信息封装,它确保对象被当作一个真正的软件组件来使用。槽用于接收信号,但它们是普通的对象成员函数。一个槽并不知道是否有任何信号与自己相连接。而且,对象并不了解具体的通信机制。
你可以将很多信号与单个的槽进行连接,也可以将单个的信号与很多的槽进行连接,甚至于将一个信号与另外一个信号相连接也是可能的,这时无论第一个信号什么时候发射系统都将立刻发射第二个信号。总之,信号与槽构造了一个强大的部件编程机制。
信号
当某个信号对其客户或所有者发送的内部发生状态发生改变,信号被一个对象发射。只有定义过这个信号的类及其派生类能够发射这个信号。当一个信号被发射时,预期相关联的槽将被立刻执行,就像一个正常的函数调用一样。信号-槽机制完全独立于任何GUI事件循环。只有当所有的槽返回以后发射函数才返回。如果存在多个槽与某个信号相关联,那么,当这个信号被发射时,这些槽会一个接一个执行,顺序随机、不确定的。
信号的声明是在头文件中进行的,QT的signals关键字之处了信号声明区,随后即可声明自己的信号。例如,下面定义了一些信号:
signals:
void mySignal();
void mySignal(int x);
void mySignalParam(int x,int y);
void mySignal();
void mySignal(int x);
void mySignalParam(int x,int y);
上面的定义中,signals是QT关键字,而非C/C++的。接下来的一行void mySignal()定义了信号mySignal,这个信号没有携带参数;接下来的一行void mySignal() 定义了信号 mySignal,这个信号没有携带参数;接下来的一行 void mySignal(int x) 定义 了重名信号 mySignal,但是它携带一个整形参数,这有点类似于 C++ 中的重载,从形式上讲信号的声明与普通的 C++ 函数是一样的,但是信号却没有函数体定义,另外,信号的返回类型都是 void,不要指望能从信号返回什么有用信息。信号由 moc 自动产生,它们不应该在 .cpp 文件中实现。
链接signal和slot:
QT5之前:
connect(sender, SIGNAL(valueChanged(QString, QString)), receiver, SLOT(updateValue(QString)));
QT实际上利用SIGNAL和SLOT这两个宏,把其后的函数名转换成一个字符串。随后,moc将会扫描全部文件,将所有的signal和slot提取出来做成一个映射表。QObject::connect()函数则会从这个映射表里面找到该字符串,从signal的名字就可以找到slot的名字,因此也就知道了再signal emit的时候,该去调用哪一个slot函数。
Qt 5 之前的 signal/slot 语法的问题
从上面的解释可以看出,Qt 5 之前版本提供的这种语法其实有一些问题:
- 没有编译期检查:因为函数名被处理成字符串,所有的检查都是在运行时完成的。这就是为什么有时会发生编译通过了,但 slot 并没有被调用。此时,你就应该去检查 console 的输出,看看有没有什么 warning 说明 connect 并没有成功。
- 因为处理的是字符串,所以 slot 中的类型名字必须用 signal 的完全一致,而且在头文件中的和实际 connect 语句中的也必须一致。也就是说,如果你使用了 typedef 或者 namespace,connect 就可能不成功(在 Qt 5 之前的版本中,我们当然也可以使用 namespace,但是必须保证头文件中的和 connect 语句中的文本完全一致)。
新语法:使用函数指针
在QT5提供了人一套新的语法。之前的语法依然可以使用,但是现在,我们有了更好的选择:
connect(sender, &Sender::valueChanged, receiver, &Receiver::updateValue)
这个看起来和之前的版本类似,很容易迁移到新的语法。下面我们看看新的语法好处:
编译器检查
如果把 signal 或者 slot 名字编写错误,或者 slot 的参数同 signal 不一致,你会在编译期就获得一个错误。这肯定会在重构、或者修改 signal 或 slot 的名字时节省很多时间。
另一个影响是,Qt 可以利用static_cast返回更友好的错误信息。例如,如果我们少了Q_OBJECT宏,则会有:
#include <QtCore/QtCore>
class Goo : public QObject {
Goo() {
connect(this, &Goo::someSignal, this, &QObject::deleteLater);
}
signals:
void someSignal();
};
其错误信息是:
qobject.h: In member function 'void QObject::qt_check_for_QOBJECT_macro(const T&) const [with T = Goo]':
qobject.h:535:9: instantiated from 'static typename QtPrivate::QEnableIf::ArgumentCount) >= (int)(QtPrivate::FunctionPointer::ArgumentCount)), void*>::Type QObject::connect(const typename QtPrivate::FunctionPointer::Object*, Func1, const typename QtPrivate::FunctionPointer::Object*, Func2, Qt::ConnectionType) [with Func1 = void (Goo::*)(), Func2 = void (QObject::*)(), typename QtPrivate::QEnableIf::ArgumentCount) >= (int)(QtPrivate::FunctionPointer::ArgumentCount)), void*>::Type = void*, typename QtPrivate::FunctionPointer::Object = Goo, typename QtPrivate::FunctionPointer::Object = QObject]'
main.cc:4:68: instantiated from here
qobject.h:353:5: error: void value not ignored as it ought to be
make: *** [main.o] Error 1
参数的自动类型转换
现在,我们不仅可以更好地使用 typedef 或 namespace,而且可以利用隐式类型转换。在下面的例子中,我们的 signal 有一个QString参数,而 slot 需要的是QVariant。在新语法中,QString将被自动转换成QVariant:
class Test : public QObject {
Q_OBJECT
public:
Test() {
connect(this, &Test::someSignal, this, &Test::someSlot);
}
signals:
void someSignal(const QString &);
public:
void someSlot(const QVariant &);
}
链接到任意函数
如果你留心上面的函数,就会发现,我们的signal被链接到了一个public函数,但这个函数并不是slot。Qt的新语法通过函数指针直接调用函数,而不需要moc的特殊处理,但是signal仍然需要。
更进一步,我们可以将signal链接到任意函数:
static void someFunction() {
qDebug() << "pressed";
}
// ... somewhere else
QObject::connect(button, &QPushButton::clicked, someFunction);
这样处理,就可以让你很方便的同 boost 或者 tr1::bind 这样的第三方库协作。
C++11 lambda 表达式
至此之前,我们所有的示例都是基于 C++98. 标准的。但是,如果你的编译器支持 C++11,我相信你一看到“函数指针”这几个字,就一定会想到 C++11 的新特性:Lambda 表达式。现在,Lambda 表达式至少被 MSVC 2010,GCC 4.5,clang 3.1 这几个编译器支持。不过对于后面两个编译器,你需要在编译时加上 -std=c++0x 参数。 现在我们可以用 Lambda 表达式重写了:
void MyWindow::saveDocumentAs() {
QFileDialog *dlg = new QFileDialog();
dlg->open();
QObject::connect(dlg, &QDialog::finished, [=](int result) {
if (result) {
QFile file(dlg->selectedFiles().first());
// ... save document here ...
}
dlg->deleteLater();
});
}
这种语法允许我们更方便地编写异步代码;
2021年即将过去,祝大家2022年开心快乐,心想事成!!!
|